news 2026/9/19 8:08:28

瑞数6.5逆向实战:RPC方案破解cookie sign与环境补全

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
瑞数6.5逆向实战:RPC方案破解cookie sign与环境补全

1. 从一次真实的调试卡壳说起:瑞数6.5的cookie sign到底难在哪

第一次接触瑞数6.5的人,十有八九会在同一个地方卡住:明明请求参数都拼对了,返回的却是带着一串动态cookie的验证页,刷新几次发现这串cookie每次都不一样,而且里面有个sign字段,长度固定、字符集固定,但就是算不出来。你打开浏览器开发者工具,翻遍所有加载的JS文件,发现代码被混淆得面目全非,变量名全是_0x开头,字符串被拆成数组,控制流被平坦化,连一个正常的if-else都找不到。

这就是瑞数(RiverSecurity)系列防护的典型特征。到了6.5这个版本,它的核心思路依然是"动态cookie + 环境检测 + 代码虚拟化"三件套,但细节上比早期版本更狠:cookie的生成逻辑被拆散到多个函数里,中间还穿插了对浏览器环境的反复校验,任何一个环节对不上,sign就算不出来,或者算出来也是错的。

我这次要聊的,就是怎么用**RPC(Remote Procedure Call,远程过程调用)**的思路,把浏览器里已经跑通的sign生成逻辑"借"出来用,而不是硬啃那堆混淆代码去纯算法还原。同时,环境补全这块也有几个特别容易翻车的点,我会一并说清楚。

先说清楚这篇文章适合谁看:如果你已经能抓到瑞数6.5的请求,知道cookie里有sign字段,但卡在"怎么稳定拿到这个sign"这一步,那这篇就是写给你的。如果你连请求都还没抓明白,建议先把抓包和基础JS逆向的底子打一打,不然直接上RPC会有点懵。

关键词里提到的瑞数6.5、RPC、cookie sign、逆向、环境补全,这五个词基本就是整条技术链路的主干。下面我按实际操作的顺序,从思路选型讲到落地细节,再讲踩过的坑。

2. 为什么我最终选了RPC而不是纯算法还原

2.1 纯算法还原在瑞数6.5上到底有多难

先泼盆冷水。瑞数6.5的sign生成逻辑,纯靠静态分析还原,工作量极大。原因有三个:

第一,代码虚拟化。它会把核心运算编译成一套自定义的字节码,然后由一个解释器在运行时逐条执行。你看到的JS只是那个解释器的壳,真正的算法藏在运行时动态生成的指令流里。想还原,等于要把它的虚拟机逆向一遍。

第二,环境强绑定。sign的计算过程中会读取大量浏览器环境信息,比如navigator的各个属性、screen尺寸、canvas指纹、WebGL渲染结果等等。这些值不是随便填的,很多是经过二次加工(比如取哈希、做位运算)后才参与sign计算。你在Node里补环境,补得不全或者补得不对,算出来的sign就是错的。

第三,控制流平坦化。就算你把虚拟机的逻辑理清了,外面那层switch-case大循环也会让你找不着北。所有函数调用被塞进一个巨大的分发器里,靠一个状态变量决定下一步执行哪段。人肉跟几万行这种代码,效率极低。

我试过纯还原的路子,跟了两天,进度大概到"能看懂它在读哪些环境变量",但离算出正确的sign还差得远。时间成本太高,不划算。

2.2 RPC方案的核心逻辑:让浏览器自己算

RPC方案的本质,是承认"我算不过你,那我就不算了,我让你自己算"。

具体做法是:在浏览器环境里,找到那个最终生成sign的函数(或者它上游的入口函数),然后通过某种通信机制,让外部的程序(比如Python脚本)能够调用这个函数,把参数传进去,把结果取出来。

这样做的好处非常直接:

  • 不用还原算法。sign怎么算的,我不关心,浏览器算出来是什么就是什么。
  • 环境天然正确。函数是在真实浏览器里跑的,所有环境变量都是真的,不存在补环境补错的问题。
  • 稳定性高。只要浏览器页面不刷新、函数还在,就能一直调用。

代价也有:你需要维持一个浏览器实例常驻,资源占用比纯算法高;而且如果瑞数更新了检测逻辑,导致页面刷新或者函数被销毁,RPC通道就会断,需要重连。

但综合来看,对于瑞数6.5这种量级的防护,RPC的性价比远高于纯还原。这也是为什么关键词里"RPC"和"cookie sign"会绑在一起——它们本来就是一套组合拳。

2.3 RPC方案的三种常见实现路径对比

实际落地时,RPC不是只有一种做法。我整理了一个对比表,方便你根据自己的技术栈选:

实现路径核心机制优点缺点适用场景
浏览器插件注入 + WebSocket写个浏览器扩展,在页面里注入脚本,通过WebSocket和外部通信环境最干净,不受页面CSP限制需要维护插件,多标签页管理麻烦长期稳定跑的任务
CDP远程调试协议用Chrome DevTools Protocol直接操控浏览器,Runtime.evaluate执行函数无需插件,Python/Node都能直接调需要处理CDP连接稳定性,页面刷新后要重新绑定中小规模、快速验证
页面内Hook + 本地HTTP服务在页面里Hook目标函数,起一个本地HTTP服务接收请求调用方式最灵活,像调API一样需要解决跨域和端口占用,页面关闭服务就没了需要高频调用的场景

我个人最常用的是CDP方案,因为它不需要额外装插件,Python的playwrightpyppeteer都能直接上手,调试也方便。下面主要按这个路子讲,其他两种思路会在关键处提一下差异。

3. 定位sign生成入口:别一上来就找最终函数

3.1 从cookie写入点反向追踪

很多人一上来就想找"哪个函数返回了sign",然后在所有JS里搜sign关键字。这招在瑞数6.5上基本失效,因为sign这个字符串本身也是被混淆的,你搜不到。

正确的切入点是cookie的写入点。sign最终是要写进cookie的,而写cookie无非就是document.cookie = ...或者某些封装过的setCookie函数。你可以用CDP的Debugger.setBreakpointOnFunctionCall,或者更简单粗暴一点,在Console里重写document.cookie的setter:

// 在页面Console里执行,拦截cookie写入 let cookieSetter = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie').set; Object.defineProperty(document, 'cookie', { set: function(val) { if (val.includes('sign') || val.includes('session')) { console.log('Cookie写入:', val); debugger; // 断在这里,看调用栈 } return cookieSetter.call(this, val); }, get: function() { return Object.getOwnPropertyDescriptor(Document.prototype, 'cookie').get.call(this); } });

执行完这段,刷新页面,当sign被写入时就会断住。这时候看调用栈(Call Stack),一层层往上翻,就能找到生成sign的那个函数。注意,调用栈里可能有很多层是瑞数自己的混淆函数,你要找的是那个"参数里带着明文、返回值是sign"的层。

3.2 用XHR/fetch断点辅助定位

如果cookie写入点不好拦(有些版本会用Object.defineProperty把cookie的setter也保护起来),可以换个角度:从请求入手。

瑞数6.5在提交验证时,通常会发一个XHR或fetch请求,请求头或请求体里带着sign。你可以在Sources面板里开启XHR/fetch断点,拦截所有请求,然后看哪个请求的发起调用栈里出现了sign的生成逻辑。

具体操作:DevTools → Sources → XHR/fetch Breakpoints → 添加一个空的断点(匹配所有请求)。刷新页面,当请求发出时断住,看Call Stack。同样地,往上翻,找到那个"计算sign"的函数。

3.3 确认入口函数的特征

找到候选函数后,怎么确认它就是我们要的入口?我一般看三个特征:

  • 入参:通常包含一个时间戳、一个随机数、以及页面上的某些动态值。
  • 返回值:是一个固定长度的字符串,字符集是大小写字母加数字。
  • 调用频率:每次请求前会被调用一次,或者页面加载时调用一次后缓存起来。

确认之后,把这个函数挂到window上,方便后续RPC调用:

// 假设找到的函数叫 _0x1234abcd window.__getSign = _0x1234abcd; // 测试一下 console.log(window.__getSign('test_param'));

如果能在Console里正常打印出结果,说明入口找对了。

注意:瑞数6.5有些版本会对函数做完整性校验,你直接挂到window上可能触发它的反调试。如果发现挂上去之后页面行为异常(比如自动刷新、卡死),说明踩到检测了。这时候可以试试用Function.prototype.toString的hook来绕过,或者换用Proxy包裹的方式挂载。

4. 用CDP搭一条稳定的RPC通道

4.1 环境准备:Playwright还是Pyppeteer

我选Playwright,原因很简单:它对CDP的支持更现代,API更稳定,而且自带等待机制,处理页面加载和元素定位比Pyppeteer省心。安装就一行:

pip install playwright playwright install chromium

如果你习惯用Node,playwright的Node版也一样好用。核心逻辑是通的。

4.2 连接已打开的浏览器实例

RPC方案的关键是"复用已经跑通的页面",而不是每次重新打开。所以第一步是让Playwright连接到一个已经启动的Chrome实例。

启动Chrome时带上远程调试端口:

# Windows chrome.exe --remote-debugging-port=9222 --user-data-dir="C:\temp\chrome_debug" # Mac /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --user-data-dir="/tmp/chrome_debug"

然后用Playwright连接:

from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.connect_over_cdp("http://localhost:9222") context = browser.contexts[0] page = context.pages[0] # 拿到当前活动页面 print(page.title())

这样你就拿到了那个已经跑通瑞数验证的页面对象。

4.3 通过Runtime.evaluate调用页面函数

拿到page之后,调用sign函数就很简单了:

def get_sign(page, param): result = page.evaluate(f"window.__getSign('{param}')") return result sign = get_sign(page, "my_param") print(sign)

page.evaluate底层走的就是CDP的Runtime.evaluate,它会在页面上下文里执行你给的JS表达式,然后把结果返回给Python。这就是RPC的核心——跨语言调用。

4.4 处理异步和超时

实际用的时候,sign函数可能是异步的(返回Promise),或者执行时间较长。Playwright的evaluate默认会等Promise resolve,所以异步函数也能直接调。但如果函数内部有setTimeout之类的延迟,你可能需要加超时控制:

try: sign = page.evaluate("window.__getSign('param')", timeout=5000) except Exception as e: print("调用超时或失败:", e) # 这里可以做重连逻辑

热词里提到的cannot finish rpc call in 30 seconds,本质上就是RPC调用超时。原因通常是页面卡死、函数被销毁、或者浏览器实例挂了。解决办法在后面第6节详细讲。

5. 环境补全:RPC方案下依然绕不开的细节

5.1 为什么用了RPC还要管环境

有人会问:既然函数是在真实浏览器里跑的,环境不都是真的吗,为什么还要补环境?

答案是:RPC方案下,环境补全的对象变了。纯算法还原时,你补的是Node里的假环境;RPC方案下,你补的是"让浏览器页面保持在一个可被调用的状态"。

具体来说,瑞数6.5会做几件事:

  • 检测window上的某些属性是否被篡改(比如navigator.webdriver)。
  • 检测是否有调试器附加(debugger语句、console的某些方法被重写)。
  • 检测页面是否在前台、是否有用户交互。

如果你用Playwright启动的浏览器,默认会带一些自动化特征,瑞数能识别出来。所以你需要做一些"反自动化检测"的处理。

5.2 启动参数里的关键配置

Playwright启动浏览器时,可以通过args传入一些参数来降低被检测的概率:

browser = p.chromium.launch( headless=False, # 瑞数对headless检测很严,建议用有头模式 args=[ "--disable-blink-features=AutomationControlled", "--no-sandbox", "--disable-infobars", "--start-maximized", ] )

--disable-blink-features=AutomationControlled这个参数最关键,它能去掉navigator.webdriver为true的特征。但光靠这个还不够,瑞数还会检测其他东西。

5.3 注入脚本抹掉自动化痕迹

在页面加载前,注入一段脚本,把常见的自动化特征抹掉:

context = browser.new_context() context.add_init_script(""" Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); // 补全plugins Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] }); // 补全languages Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh', 'en'] }); // 抹掉chrome.runtime window.chrome = { runtime: {} }; """)

这段脚本会在每个页面加载时先执行,把环境伪装成正常浏览器。

5.4 处理瑞数的动态环境校验

瑞数6.5最恶心的一点是,它会在运行过程中动态校验环境。比如,它可能每隔几秒就重新读一次navigator.userAgent,或者检测window.outerWidthwindow.innerWidth的差值是否合理。

如果你在RPC调用过程中,页面被瑞数判定为异常,它会刷新页面或者销毁sign函数。这时候你的RPC通道就断了。

应对策略有两个:

  • 保持页面活跃:定期在页面里执行一些无害的操作,比如window.scrollTo(0, 0),模拟用户行为。
  • 监听页面状态:用Playwright的page.on("framenavigated")监听页面跳转,一旦发现页面刷新,立即重新注入函数并重建RPC通道。
def on_navigate(frame): if frame == page.main_frame: print("页面跳转,需要重新绑定sign函数") # 这里触发重连逻辑 page.on("framenavigated", on_navigate)

6. 踩坑实录:那些让我熬夜的报错和解决过程

6.1 "cannot finish rpc call in 30 seconds"的完整排查链路

这个报错我遇到过三次,每次原因都不一样。第一次是页面卡死,第二次是函数被销毁,第三次是CDP连接断了。排查过程如下:

第一步:确认浏览器是否还活着。在Python里执行page.title(),如果报错说连接断开,那就是浏览器挂了,需要重启。如果正常返回标题,说明浏览器还在。

第二步:确认sign函数是否还在。执行page.evaluate("typeof window.__getSign"),如果返回"undefined",说明函数被销毁了(通常是页面刷新导致)。如果返回"function",说明函数还在,问题出在调用上。

第三步:手动调用一次看报错。执行page.evaluate("window.__getSign('test')"),如果抛出异常,看异常信息。常见的是参数格式不对,或者函数内部依赖的某个全局变量丢了。

第四步:检查CDP连接。如果前两步都正常,但调用还是超时,可能是CDP的WebSocket连接假死了。这时候需要重建连接:

browser.close() browser = p.chromium.connect_over_cdp("http://localhost:9222")

我的经验是,80%的超时问题都是页面刷新导致的函数丢失。所以最稳妥的做法是加一个心跳检测,每隔几秒检查一次函数是否存在,不存在就重新注入。

6.2 函数挂载后被检测:反调试的绕过

前面提到,直接把函数挂到window上可能触发反调试。我遇到的具体表现是:挂载后页面立即执行了一段debugger,然后cookie被清空,页面跳回验证页。

绕过方法是不要直接挂载原函数,而是用Proxy包一层

window.__getSign = new Proxy(_0x1234abcd, { apply: function(target, thisArg, args) { return target.apply(thisArg, args); } });

Proxy的好处是,瑞数如果检测函数的toString结果,Proxy返回的是function () { [native code] },看起来像原生函数,不容易被识别。

另一个技巧是延迟挂载。不要在页面一加载就挂,等页面稳定运行几秒后再挂,降低被检测的概率。

6.3 参数传递的坑:对象和数组怎么传

page.evaluate传参时,如果参数是简单字符串或数字,直接拼进表达式就行。但如果是对象或数组,直接拼会出问题。正确做法是用page.evaluate的第二个参数:

# 错误做法 page.evaluate(f"window.__getSign({my_dict})") # 会变成Python的dict字符串,JS解析不了 # 正确做法 page.evaluate("(arg) => window.__getSign(arg)", my_dict)

Playwright会自动把Python的dict转成JS对象。这个坑我踩过一次,排查了半天才发现是参数序列化的问题。

6.4 多标签页下的函数隔离

如果你开了多个标签页,每个页面都有自己的window对象,函数是隔离的。RPC调用时,必须明确指定在哪个page上执行。我一般只保留一个工作标签页,其他都关掉,避免混淆。

如果确实需要多标签页,可以用context.pages拿到所有页面,然后根据URL或标题筛选:

target_page = None for p in context.pages: if "target_url_keyword" in p.url: target_page = p break

7. 让RPC通道更稳的几个工程化技巧

7.1 封装一个带重连的SignClient类

把上面所有逻辑封装成一个类,用起来会舒服很多:

class SignClient: def __init__(self, cdp_url="http://localhost:9222"): self.cdp_url = cdp_url self.playwright = None self.browser = None self.page = None self._connect() def _connect(self): from playwright.sync_api import sync_playwright self.playwright = sync_playwright().start() self.browser = self.playwright.chromium.connect_over_cdp(self.cdp_url) context = self.browser.contexts[0] self.page = context.pages[0] if context.pages else context.new_page() def get_sign(self, param, retry=3): for i in range(retry): try: exists = self.page.evaluate("typeof window.__getSign") if exists != "function": self._reinject() return self.page.evaluate("(arg) => window.__getSign(arg)", param) except Exception as e: print(f"第{i+1}次调用失败: {e}") self._connect() raise RuntimeError("sign获取失败,重试次数用尽") def _reinject(self): # 这里放重新注入sign函数的逻辑 # 实际项目中,这段逻辑可能需要根据页面结构动态调整 pass

这个类的核心是get_sign里的重试机制:先检查函数是否存在,不存在就重新注入,然后调用;调用失败就重连浏览器。三层保险下来,稳定性会好很多。

7.2 用队列管理调用请求

如果你的业务需要高频调用sign,建议加一个请求队列,避免并发调用把页面搞崩:

import queue import threading class SignWorker: def __init__(self, client): self.client = client self.task_queue = queue.Queue() self.result_dict = {} self.lock = threading.Lock() self.worker_thread = threading.Thread(target=self._run, daemon=True) self.worker_thread.start() def _run(self): while True: task_id, param = self.task_queue.get() try: sign = self.client.get_sign(param) with self.lock: self.result_dict[task_id] = sign except Exception as e: with self.lock: self.result_dict[task_id] = f"ERROR: {e}" def submit(self, task_id, param): self.task_queue.put((task_id, param)) def get_result(self, task_id, timeout=10): import time start = time.time() while time.time() - start < timeout: with self.lock: if task_id in self.result_dict: return self.result_dict.pop(task_id) time.sleep(0.1) return None

这样所有sign请求都串行化,页面不会因为并发而卡死。

7.3 监控与日志:出问题时能快速定位

RPC方案跑起来之后,最怕的是"悄无声息地挂了"。所以日志一定要打全:

  • 每次调用记录时间戳、参数、返回值、耗时。
  • 每次重连记录原因和重连结果。
  • 定期打印页面状态(URL、标题、函数是否存在)。

我一般用Python的logging模块,输出到文件和控制台,方便排查。

8. 关于瑞数6.5后续版本的一些观察

瑞数6.5之后,官方肯定还会更新。从我目前观察到的情况看,后续版本可能会在几个方向上加码:

一是环境检测更细,比如检测WebGL的渲染结果是否和真实设备一致,检测AudioContext的指纹。这些在RPC方案下影响不大,因为环境是真的,但如果你用无头浏览器,就会暴露。

二是函数销毁更频繁,可能每次请求后都重新生成sign函数,让RPC通道更难维持。应对方法是把重连逻辑做得更自动化,做到"无感重连"。

三是对CDP的检测,有些版本已经开始检测Runtime.enable是否被调用。如果遇到这种情况,可以试试用Page.addScriptToEvaluateOnNewDocument代替Runtime.evaluate,或者换用浏览器插件方案。

不过说到底,RPC的核心思路是"以真打真",只要你的浏览器环境足够真实,瑞数很难完全封死这条路。真正要花心思的,是工程上的稳定性——怎么让这条通道在长时间运行中不出岔子。

我个人在实际项目里的体会是:RPC方案的上限取决于你的工程能力,而不是逆向能力。把重连、队列、监控这三件事做好,比多还原一个算法细节有价值得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 8:04:20

Spring Boot 3.5.4 + LangChain4j + Milvus 打造企业级 RAG 知识库问答系统

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 8:04:06

Rust 内存泄漏的隐蔽角落:循环引用、ManuallyDrop 与线程悬挂

Rust 内存泄漏的隐蔽角落&#xff1a;循环引用、ManuallyDrop 与线程悬挂在现代系统级编程的认知中&#xff0c;许多开发者常有一个误区&#xff1a;“只要使用了 Rust 的所有权&#xff08;Ownership&#xff09;与 RAII 机制&#xff0c;系统就绝对不会发生内存泄漏&#xff…

作者头像 李华
网站建设 2026/9/19 8:03:13

Gmail的Gemini AI如何提升邮件管理效率

1. Gmail的AI进化&#xff1a;当Gemini遇上电子邮件管理过去三个月我一直在测试Gmail新推出的Gemini AI功能&#xff0c;这套系统彻底改变了我处理邮件的习惯。每天面对200封邮件的压力下&#xff0c;传统分类规则已经力不从心&#xff0c;而基于大语言模型的智能优先级和摘要功…

作者头像 李华