资讯动态

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

发布时间:2026/9/19 8:08:31 来源:尧图企业网站定制
1. 从一次真实的调试卡壳说起瑞数6.5的cookie sign到底难在哪第一次接触瑞数6.5的人十有八九会在同一个地方卡住明明请求参数都拼对了返回的却是带着一串动态cookie的验证页刷新几次发现这串cookie每次都不一样而且里面有个sign字段长度固定、字符集固定但就是算不出来。你打开浏览器开发者工具翻遍所有加载的JS文件发现代码被混淆得面目全非变量名全是_0x开头字符串被拆成数组控制流被平坦化连一个正常的if-else都找不到。这就是瑞数RiverSecurity系列防护的典型特征。到了6.5这个版本它的核心思路依然是动态cookie 环境检测 代码虚拟化三件套但细节上比早期版本更狠cookie的生成逻辑被拆散到多个函数里中间还穿插了对浏览器环境的反复校验任何一个环节对不上sign就算不出来或者算出来也是错的。我这次要聊的就是怎么用**RPCRemote 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的playwright或pyppeteer都能直接上手调试也方便。下面主要按这个路子讲其他两种思路会在关键处提一下差异。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如果你习惯用Nodeplaywright的Node版也一样好用。核心逻辑是通的。4.2 连接已打开的浏览器实例RPC方案的关键是复用已经跑通的页面而不是每次重新打开。所以第一步是让Playwright连接到一个已经启动的Chrome实例。启动Chrome时带上远程调试端口# Windows chrome.exe --remote-debugging-port9222 --user-data-dirC:\temp\chrome_debug # Mac /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port9222 --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(fwindow.__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), timeout5000) 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( headlessFalse, # 瑞数对headless检测很严建议用有头模式 args[ --disable-blink-featuresAutomationControlled, --no-sandbox, --disable-infobars, --start-maximized, ] )--disable-blink-featuresAutomationControlled这个参数最关键它能去掉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.outerWidth和window.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(fwindow.__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 break7. 让RPC通道更稳的几个工程化技巧7.1 封装一个带重连的SignClient类把上面所有逻辑封装成一个类用起来会舒服很多class SignClient: def __init__(self, cdp_urlhttp://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, retry3): 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第{i1}次调用失败: {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(targetself._run, daemonTrue) 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] fERROR: {e} def submit(self, task_id, param): self.task_queue.put((task_id, param)) def get_result(self, task_id, timeout10): 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方案的上限取决于你的工程能力而不是逆向能力。把重连、队列、监控这三件事做好比多还原一个算法细节有价值得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价