ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

瑞数6.5防护逆向实战:RPC方案实现sign生成与环境补全

瑞数6.5防护逆向实战:RPC方案实现sign生成与环境补全 1. 瑞数6.5防护机制与逆向整体思路拆解瑞数6.5这套防护体系做过Web逆向的人应该都不陌生。它最让人头疼的地方不在于算法本身有多复杂而在于它把核心逻辑藏在一层又一层的动态混淆和环境检测里。你打开浏览器开发者工具看到的是一堆看起来毫无规律的变量名和函数调用想直接静态分析基本等于大海捞针。我这次拿到的目标站点用的是瑞数6.5版本核心需求很明确请求头里的cookie中有一个sign字段每次请求都会变化服务端会校验这个sign的合法性。如果sign不对直接返回403或者一个假的页面数据。这个sign的生成逻辑就藏在瑞数加载的那段混淆JS里。1.1 为什么选择RPC方案而不是纯算法还原面对这种场景通常有两条路可以走。第一条路是纯算法还原把混淆JS里的核心逻辑一点点抠出来用Python或者Node.js重新实现一遍。第二条路就是RPC方案让浏览器里的JS自己算我们通过RPC调用把结果取出来。纯算法还原的好处是脱离浏览器也能跑速度快、资源占用低。但瑞数6.5的混淆强度很高变量名动态生成、字符串加密、控制流平坦化这些手段全用上了再加上它还有环境检测你还原出来的算法放到Node.js里跑很可能因为环境差异导致结果不对。我之前试过硬啃某版本的瑞数算法光是理清那个虚拟机式的指令分发逻辑就花了两天最后跑出来的结果还是和浏览器有偏差。RPC方案就务实多了。它的核心思路是既然浏览器能算出正确的sign那我直接让浏览器算我只需要把结果拿出来用就行。这样做的好处是省去了算法还原的巨大工作量而且不用担心环境检测的问题因为代码就是在真实浏览器环境里跑的。缺点也很明显你需要维持一个浏览器实例资源占用比纯算法方案高而且如果目标站点有频率限制RPC调用的速度可能跟不上。综合评估下来对于瑞数6.5这种级别的防护RPC方案的投入产出比明显更高。特别是当你需要快速拿到数据、而不是做一个长期稳定的采集系统时RPC几乎是唯一的选择。1.2 RPC通信的基本架构设计RPC的全称是Remote Procedure Call翻译过来就是远程过程调用。在这个场景里我们的架构是这样的浏览器端注入一段我们自己的JS代码这段代码负责调用瑞数的sign生成函数然后把结果通过WebSocket发送出去。服务端也就是我们的Python脚本通过WebSocket接收结果同时也可以主动发起调用请求。为什么用WebSocket而不是HTTP因为RPC调用需要双向通信。服务端要能主动告诉浏览器“帮我算一个sign”浏览器算完之后要能把结果推回来。HTTP是请求-响应模式服务端没法主动推送用轮询又太浪费资源。WebSocket建立一条长连接之后双方可以随时互发消息天然适合RPC场景。整个数据流是这样的Python端发起调用请求 - WebSocket发送到浏览器 - 浏览器执行JS函数 - 拿到sign结果 - 通过WebSocket回传 - Python端收到结果。整个过程延迟主要取决于浏览器执行JS的时间通常在几十毫秒到几百毫秒之间。1.3 环境补全的必要性分析瑞数6.5有一个很关键的特性它会在代码执行过程中检测当前环境的各种属性。比如navigator.userAgent、window.screen、document.cookie等等。如果你用的是无头浏览器比如Puppeteer的headless模式这些属性可能和真实浏览器有差异瑞数就会识别出来然后返回一个假的sign或者直接不执行核心逻辑。所以环境补全这一步是绕不过去的。所谓环境补全就是在浏览器加载页面之前通过注入脚本的方式把那些容易被检测的属性改成看起来正常的值。比如把webdriver属性设为false把plugins数组填充一些假数据把chrome对象补上等等。这一步做得好不好直接决定了你的RPC方案能不能跑通。我见过很多人卡在这一步代码写得没问题但就是因为环境没补全瑞数死活不返回正确的sign。2. 核心细节解析与实操要点2.1 瑞数6.5的cookie sign生成位置定位要搞定sign第一步是找到它在哪生成的。瑞数6.5的代码经过混淆之后函数调用关系非常隐蔽。我的做法是先在浏览器里正常访问目标页面然后在Sources面板里全局搜索“sign”这个关键词。注意瑞数可能会把sign这个字符串拆开或者加密所以直接搜可能搜不到。更有效的方法是Hook cookie的set操作。在页面加载之前注入一段脚本重写document.cookie的setter当有代码往cookie里写sign的时候就能拿到调用栈顺着调用栈往上找就能定位到生成函数。// Hook cookie setter let originalCookieSetter Object.getOwnPropertyDescriptor(Document.prototype, cookie).set; Object.defineProperty(document, cookie, { set: function(val) { if (val.indexOf(sign) ! -1) { debugger; // 在这里断点查看调用栈 } return originalCookieSetter.call(this, val); }, get: function() { return Object.getOwnPropertyDescriptor(Document.prototype, cookie).get.call(this); } });这段代码注入之后当瑞数往cookie里写sign时会自动断在debugger那一行。然后你在Call Stack里往上翻找到第一个不是瑞数混淆代码的调用那个位置通常就是生成sign的入口函数。注意瑞数可能会检测debugger语句如果发现代码里有debugger它会改变执行逻辑。所以实际使用的时候建议用条件断点或者把debugger换成console.log输出调用栈。2.2 环境补全的关键参数清单环境补全涉及到的属性很多我整理了一份核心清单这些是瑞数6.5重点检测的检测项正常值示例补全方式navigator.webdriverfalseObject.defineProperty重定义navigator.plugins包含PDF Viewer等构造假数组navigator.languages[zh-CN, zh]直接赋值window.chrome包含runtime对象构造假对象navigator.hardwareConcurrency8直接赋值navigator.deviceMemory8直接赋值WebGL vendorGoogle Inc.重写getParameterNotification.permissiondefault重写这些属性不是随便填个值就行需要和你的User-Agent保持一致。比如你用的是Windows的UA那navigator.platform就应该是Win32hardwareConcurrency也不能太离谱一般4或者8比较常见。2.3 RPC通信协议的设计要点RPC通信协议的设计直接影响到调用的稳定性和效率。我的设计是这样的请求消息格式{ id: 唯一标识, method: sign, params: { url: 当前页面URL, data: 需要签名的数据 } }响应消息格式{ id: 对应请求的id, result: 生成的sign值, error: null }id字段用来匹配请求和响应因为WebSocket是异步的可能同时有多个调用在飞。method字段指定要调用的方法名params传递参数。响应里带上同样的idPython端收到之后根据id找到对应的回调。提示实际使用中建议加一个超时机制。如果发出请求后30秒还没收到响应就认为这次调用失败重新发起或者报错。瑞数有时候会因为环境检测触发而卡住不返回没有超时机制的话整个流程就挂死了。2.4 浏览器注入脚本的编写技巧注入脚本的时机很关键。必须在瑞数的代码执行之前完成注入否则环境补全就来不及了。用Puppeteer的话可以通过page.evaluateOnNewDocument方法这个方法会在页面加载任何脚本之前执行。await page.evaluateOnNewDocument(() { // 环境补全代码 Object.defineProperty(navigator, webdriver, { get: () false }); // 补全plugins const fakePlugins [ {name: PDF Viewer, filename: internal-pdf-viewer}, {name: Chrome PDF Viewer, filename: internal-pdf-viewer}, {name: Chromium PDF Viewer, filename: internal-pdf-viewer} ]; Object.defineProperty(navigator, plugins, { get: () fakePlugins }); // 补全chrome对象 window.chrome { runtime: {}, loadTimes: function() {}, csi: function() {} }; });这段代码会在每个新页面加载时自动执行确保环境补全在瑞数代码之前生效。3. 实操过程与核心环节实现3.1 浏览器环境搭建与配置我用的方案是Puppeteer加上一个修改版的Chromium。为什么不用原版Chromium因为原版有一些特征太明显了比如navigator.webdriver默认就是true虽然可以通过注入脚本改掉但有些检测是在更底层做的注入脚本可能覆盖不到。我推荐使用puppeteer-extra配合puppeteer-extra-plugin-stealth插件这个插件已经帮你处理了大部分常见的环境检测点。安装命令npm install puppeteer puppeteer-extra puppeteer-extra-plugin-stealth启动浏览器的配置const puppeteer require(puppeteer-extra); const StealthPlugin require(puppeteer-extra-plugin-stealth); puppeteer.use(StealthPlugin()); const browser await puppeteer.launch({ headless: false, // 先用有头模式调试稳定后再改无头 args: [ --no-sandbox, --disable-setuid-sandbox, --disable-blink-featuresAutomationControlled, --window-size1920,1080 ], executablePath: /path/to/your/chrome });headless建议先设为false这样你能看到浏览器实际在干什么方便调试。等整个流程跑通之后再改成true或者用新的headless模式。3.2 WebSocket服务端搭建Python端我用的是websockets库轻量且好用。安装pip install websockets服务端核心代码import asyncio import websockets import json import uuid class RPCClient: def __init__(self): self.ws None self.pending {} async def connect(self, uri): self.ws await websockets.connect(uri) asyncio.create_task(self._listen()) async def _listen(self): async for message in self.ws: data json.loads(message) req_id data.get(id) if req_id in self.pending: future self.pending.pop(req_id) if not future.done(): future.set_result(data) async def call(self, method, params, timeout30): req_id str(uuid.uuid4()) future asyncio.get_event_loop().create_future() self.pending[req_id] future await self.ws.send(json.dumps({ id: req_id, method: method, params: params })) try: result await asyncio.wait_for(future, timeouttimeout) return result.get(result) except asyncio.TimeoutError: self.pending.pop(req_id, None) raise Exception(fRPC调用超时: {method})这段代码实现了基本的RPC客户端功能包括连接管理、请求发送、响应匹配和超时处理。3.3 浏览器端RPC响应逻辑实现浏览器端需要注入一段脚本监听WebSocket消息收到调用请求后执行对应的函数然后把结果发回去。const ws new WebSocket(ws://localhost:8765); ws.onmessage async (event) { const request JSON.parse(event.data); const { id, method, params } request; try { let result; if (method sign) { // 调用瑞数的sign生成函数 result await generateSign(params.url, params.data); } ws.send(JSON.stringify({ id: id, result: result, error: null })); } catch (e) { ws.send(JSON.stringify({ id: id, result: null, error: e.message })); } };generateSign函数需要你根据之前定位到的瑞数函数来具体实现。通常瑞数会把生成函数挂在一个全局对象下面你需要找到那个对象和对应的方法名。3.4 完整调用流程串联把上面这些环节串起来完整的流程是这样的启动Python WebSocket服务端监听8765端口启动Puppeteer浏览器注入环境补全脚本和RPC响应脚本浏览器访问目标页面等待瑞数代码加载完成Python端发起RPC调用请求生成sign浏览器端收到请求调用瑞数函数生成sign浏览器端把sign通过WebSocket回传Python端拿到sign构造请求头发送业务请求实测下来从发起RPC调用到拿到sign平均耗时在200毫秒左右。如果目标站点有频率限制可以在Python端加一个请求队列控制调用节奏。注意瑞数6.5有时候会在页面加载后过一段时间才初始化sign生成函数。如果你在页面刚加载完就发起RPC调用可能会失败。建议在调用之前先轮询检查目标函数是否存在确认存在后再发起调用。4. 常见问题与排查技巧实录4.1 RPC调用超时问题排查“cannot finish rpc call in 30 seconds”这个报错是我遇到最多的问题。原因通常有三个一是浏览器端根本没收到请求二是收到了但执行出错没返回三是返回了但Python端没匹配上。排查步骤首先检查WebSocket连接是否正常。在Python端打印连接状态在浏览器端监听ws.onopen和ws.onerror事件。如果连接都没建立后面的事就不用谈了。其次检查浏览器端是否收到了消息。在ws.onmessage里加console.log看看有没有输出。如果没有说明消息没发出去检查Python端的send逻辑。最后检查响应匹配。在Python端的_listen方法里打印收到的每一条消息看看id是否对得上。有时候浏览器端返回的id格式和Python端发送的不一致导致匹配失败。4.2 环境补全被检测的典型表现环境补全没做好的话瑞数会有一些典型表现。最常见的是sign生成函数不执行或者执行了但返回一个固定值。你可以在浏览器控制台里手动调用那个函数看看返回什么。如果返回undefined或者一个明显不对的值基本可以确定是环境问题。另一个表现是页面加载后不断刷新或者跳转到一个空白页。这是瑞数在检测到异常环境后主动做的干扰。排查环境问题的方法用puppeteer-extra-plugin-stealth插件先跑一遍如果问题解决了说明是环境检测。然后逐个关闭stealth插件的子功能定位到具体是哪个检测点没过。4.3 sign值不稳定的原因分析有时候你会发现同样的参数两次调用生成的sign不一样。这是正常的因为瑞数的sign通常包含时间戳和随机数。服务端校验的时候会检查时间戳的有效期一般是几分钟。所以你的sign生成之后要尽快使用不要缓存太久。但如果sign完全没法通过校验那可能是参数不对。瑞数的sign生成函数通常需要传入当前页面的URL、请求参数、时间戳等信息。你需要确认传入的参数和服务端期望的一致。最可靠的方法是在浏览器里Hook那个函数看看正常请求时传了什么参数。4.4 常见问题速查表问题现象可能原因解决方案RPC调用超时WebSocket未连接检查端口和防火墙sign返回undefined环境检测未通过补全navigator和window属性sign校验失败参数不匹配Hook函数查看实际参数页面不断刷新被识别为自动化工具使用stealth插件浏览器崩溃内存不足限制并发数定期重启浏览器调用速度慢浏览器性能瓶颈使用有头模式关闭不必要的标签页4.5 提升稳定性的几个实操心得第一个心得是浏览器实例要复用。不要每次调用都开一个新浏览器那样开销太大。维持一个浏览器实例开一个标签页专门跑RPC服务其他标签页用来执行业务请求。第二个心得是加心跳检测。WebSocket连接可能因为各种原因断开加一个定时ping机制发现连接断了就自动重连。第三个心得是做好错误重试。RPC调用失败很正常关键是失败之后能自动重试。我一般设置重试3次每次间隔1秒。如果3次都失败再报错出来人工处理。第四个心得是监控浏览器状态。浏览器可能会因为内存泄漏或者页面崩溃而变得不可用。定期检查浏览器是否还在响应如果连续多次调用失败就重启浏览器。async def call_with_retry(self, method, params, max_retries3): for i in range(max_retries): try: return await self.call(method, params) except Exception as e: if i max_retries - 1: raise await asyncio.sleep(1)这个重试逻辑看起来简单但实际用起来能解决80%的偶发失败问题。4.6 关于瑞数版本升级的应对策略瑞数不是一成不变的它会不定期更新版本。6.5之后可能还有6.6、7.0。每次升级都可能改变检测点和函数命名。所以你的RPC方案要设计得足够灵活把环境补全、函数定位、参数构造这些环节解耦升级的时候只需要改对应的部分。我的做法是把环境补全脚本单独放在一个文件里函数定位逻辑也单独封装。这样瑞数升级的时候我只需要更新这两个文件RPC通信框架不用动。另外建议定期关注瑞数的更新动态。如果你发现之前能用的方案突然失效了先别急着改代码去目标站点看看是不是瑞数升级了。确认升级之后按照之前的定位流程重新走一遍通常半天之内就能恢复。4.7 性能优化与资源控制RPC方案最大的成本是浏览器资源。一个Chrome实例大概占500MB到1GB内存如果你需要高并发开多个浏览器实例很快就把内存吃满了。优化的思路有几个一是用轻量级的浏览器内核比如用playwright的chromium而不是完整版Chrome。二是控制并发数不要一次性发起太多RPC调用用队列控制节奏。三是定期重启浏览器释放内存碎片。我实测下来一个4核8G的机器跑2到3个浏览器实例比较合适。每个实例每秒能处理5到10次RPC调用整体吞吐量足够大多数场景使用。如果确实需要更高的并发可以考虑分布式方案把RPC服务部署到多台机器上Python端做负载均衡。不过这就涉及到更复杂的架构设计了一般项目用不上。4.8 调试技巧与工具推荐调试RPC方案的时候有几个工具特别好用。第一个是Chrome DevTools的Network面板可以看WebSocket的帧数据确认消息有没有正常收发。第二个是Python端的日志把每次调用的请求和响应都打出来方便对比。我还写了一个简单的测试页面可以在浏览器里手动触发RPC调用用来验证浏览器端的逻辑是否正确。这个页面就是一个按钮点击之后调用generateSign函数然后把结果显示出来。调试的时候先用这个页面确认浏览器端没问题再去调Python端。另外推荐用wireshark抓包看看WebSocket的底层通信虽然大部分时候用不上但遇到诡异问题的时候能帮你排除网络层面的干扰。整个方案跑通之后我最大的体会是RPC方案的核心不在于技术有多难而在于细节的把控。环境补全少补一个属性、WebSocket消息格式差一个字段、超时时间设得太短都可能导致整个流程失败。所以每一步都要仔细验证不要想当然。
返回列表