1. 瑞数6.5防护机制与逆向思路拆解
瑞数6.5这套防护体系,做过Web逆向的人应该都不陌生。它最让人头疼的地方不在于算法本身有多复杂,而在于它把整个加密链路拆散到了多个环节里——cookie的生成、请求参数的签名、动态代码的注入,每一层都有独立的校验逻辑。你单独搞定某一层没用,必须把整条链路串起来才能拿到可用的请求凭证。
1.1 瑞数6.5的核心防护层次
先理清楚瑞数6.5到底在哪些位置做了防护。根据我实际调试的经验,它主要分为三个层面:
第一层是动态代码混淆。页面首次加载时,服务端返回的HTML中会嵌入一段经过高度混淆的JavaScript代码。这段代码的执行结果会生成一个初始cookie,通常命名为类似ssxmod_itna这样的形式。这个cookie不是固定值,它跟当前时间戳、浏览器指纹、页面内容都有关系。
第二层是cookie sign签名。这是本文重点要解决的部分。初始cookie生成后,后续每个请求都需要携带一个经过签名的cookie值。这个签名过程涉及多个参数的拼接、加密和编码,最终生成一个动态变化的sign值。瑞数6.5在这一层用了自定义的加密算法,不是标准的MD5或AES,而是经过改造的变种算法。
第三层是请求参数校验。除了cookie之外,请求的URL参数、请求头、请求体都可能参与签名计算。服务端会校验这些参数是否被篡改,任何一个字段不匹配都会导致请求被拒绝。
三层防护是联动的。你只破解第一层拿到初始cookie,后续请求照样会被拦截。必须把签名逻辑也还原出来,才能维持一个可用的会话。
1.2 为什么选择RPC方案而不是纯算法还原
面对瑞数6.5的cookie sign,通常有两种思路:
- 纯算法还原:把混淆的JS代码完全逆向,用Python或Java重新实现签名算法。
- RPC调用:不还原算法,直接让浏览器环境执行原始JS代码,通过远程调用的方式获取签名结果。
纯算法还原听起来更“彻底”,但实际操作中坑非常多。瑞数6.5的混淆代码里大量使用了浏览器环境特有的API,比如canvas指纹、WebGL信息、AudioContext特征等。你在Node.js里模拟这些环境,工作量巨大且容易遗漏。更麻烦的是,瑞数会定期更新混淆策略,每次更新你都得重新逆向一遍。
RPC方案的核心思路是:既然浏览器能正常执行这段JS并生成正确的签名,那我为什么不直接“借用”浏览器的执行能力?通过RPC(Remote Procedure Call,远程过程调用),我在Python端发起请求,实际签名计算交给浏览器里的JS函数完成。这样既避免了环境模拟的复杂性,又能在瑞数更新时快速适配——只要浏览器还能正常访问页面,RPC就能继续工作。
注意:RPC方案的前提是你能够在一个可控的浏览器环境中加载目标页面,并且能够定位到生成签名的核心函数。如果页面有严格的反调试机制,需要先过掉反调试才能进行后续操作。
1.3 RPC方案的整体架构设计
一个完整的RPC方案包含三个核心组件:
浏览器端(JS环境):负责加载目标页面、执行瑞数的混淆代码、暴露签名函数供外部调用。这里通常用Playwright或Puppeteer来控制浏览器,通过page.exposeFunction或WebSocket将签名函数暴露出去。
通信层:负责Python端和浏览器端之间的消息传递。常见方案有WebSocket、HTTP接口、或者直接利用Playwright的evaluate方法。WebSocket方案更灵活,适合高频调用场景。
Python端(业务逻辑):负责发起实际请求、调用RPC获取签名、组装最终请求参数。这部分就是你的爬虫或自动化脚本的主体。
整个数据流是这样的:Python端需要签名时,通过通信层发送请求参数到浏览器端;浏览器端调用瑞数的签名函数,将计算结果返回;Python端拿到签名后,拼接到请求中发送给目标服务器。
这个架构的好处是解耦彻底。浏览器端只管执行JS,Python端只管业务逻辑,两边通过标准协议通信。即使瑞数更新了签名算法,只要浏览器端能正常执行,Python端代码完全不用改。
2. 环境补全的核心技巧与实操要点
环境补全是RPC方案能否成功的关键。很多人卡在这一步:浏览器里手动访问页面一切正常,但用自动化工具打开就各种报错。问题往往出在浏览器环境被检测到了。
2.1 浏览器指纹的检测与规避
瑞数6.5会检测大量的浏览器特征来判断当前环境是否“真实”。根据我的测试,以下几个维度是重点检测对象:
| 检测维度 | 检测内容 | 规避方法 |
|---|---|---|
| WebDriver标志 | navigator.webdriver是否为true | 启动参数添加--disable-blink-features=AutomationControlled |
| 插件信息 | navigator.plugins长度和内容 | 注入伪造的插件列表 |
| 语言设置 | navigator.languages是否为空 | 设置合理的语言数组 |
| Canvas指纹 | Canvas渲染结果的哈希值 | 使用真实浏览器或注入噪声 |
| WebGL信息 | GPU厂商和渲染器信息 | 伪造为常见显卡型号 |
| 屏幕参数 | 分辨率、色深、可用区域 | 设置为常见分辨率 |
| 时区偏移 | Date.getTimezoneOffset() | 设置为目标地区时区 |
这些检测项不是孤立的,瑞数会把它们组合起来计算一个综合指纹。任何一项异常都可能导致后续签名计算被污染。
实际操作中,我推荐使用Playwright的addInitScript方法,在页面加载前注入补丁脚本。这样瑞数的代码执行时,读到的已经是被修正过的环境信息。
// 在页面加载前注入的补丁脚本示例 Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); Object.defineProperty(navigator, 'plugins', { get: () => [ { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer' }, { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' }, { name: 'Native Client', filename: 'internal-nacl-plugin' } ] }); Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh', 'en'] });这段代码看起来简单,但效果立竿见影。我实测下来,不加补丁的情况下,瑞数会在页面加载后3秒内触发一次校验失败,直接跳转到验证页面。加上补丁后,页面能稳定停留并正常执行签名逻辑。
2.2 反调试机制的绕过策略
瑞数6.5内置了反调试机制,常见的手段包括:
debugger死循环:代码中插入debugger语句,配合setInterval不断触发。你打开开发者工具就会陷入无限断点。
console检测:检测console.log是否被重写,或者检测开发者工具是否打开。
时间差检测:记录代码执行的时间差,如果发现执行时间异常长(说明有人在单步调试),就触发异常流程。
函数toString检测:检测关键函数是否被Hook,通过Function.prototype.toString的返回值来判断。
绕过这些反调试,核心思路是“让代码以为自己在正常执行”。具体操作上:
对于debugger死循环,可以在页面加载前重写Function.prototype.constructor,拦截debugger语句的构造。或者更简单粗暴的方式——直接使用--remote-debugging-port启动浏览器,通过CDP协议操作,不打开开发者工具界面。
对于console检测,可以在注入脚本中保留原始的console对象引用,但对外暴露一个代理对象。这样瑞数检测到的console是“正常”的,但实际输出被重定向了。
对于时间差检测,这个比较难完全规避。我的经验是尽量使用无头模式,减少人为操作带来的时间波动。如果必须用有头模式,操作要快,避免在关键代码段停留太久。
提示:反调试绕过是一个持续对抗的过程。瑞数会更新检测手段,你的绕过策略也需要跟着调整。建议把补丁脚本模块化,方便后续维护和更新。
2.3 签名函数的定位与暴露
环境补全之后,下一步是找到生成cookie sign的核心函数。这个过程需要一些技巧,因为瑞数的代码是混淆过的,函数名都是无意义的字符。
定位方法一:Hook关键API。瑞数生成签名时必然会用到document.cookie的set操作。你可以在注入脚本中重写document.cookie的setter,打印调用栈,就能找到签名函数的调用位置。
// Hook document.cookie的setter const originalCookieSetter = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie').set; Object.defineProperty(document, 'cookie', { set: function(val) { console.log('Cookie set:', val); console.trace(); // 打印调用栈 return originalCookieSetter.call(this, val); }, get: function() { return Object.getOwnPropertyDescriptor(Document.prototype, 'cookie').get.call(this); } });定位方法二:搜索特征字符串。瑞数的签名结果通常有固定的格式特征,比如特定的前缀或长度。你可以在Sources面板中全局搜索这些特征,找到生成位置。
定位方法三:XHR断点。在发起请求的位置打断点,回溯调用栈,找到签名函数的入口。
找到签名函数后,需要把它暴露给外部调用。最简单的方式是把它挂载到window对象上:
// 假设签名函数名为 _sign,混淆后可能是 a1b2c3 window.__getSign = function(params) { return _sign(params); };然后在Python端通过Playwright的evaluate方法调用:
sign_result = page.evaluate('window.__getSign(arguments[0])', params)如果签名函数依赖特定的上下文(比如需要先执行某些初始化代码),你需要确保在调用签名函数之前,页面已经完成了初始化。通常的做法是等待页面加载完成,并且手动触发一次正常的请求流程,让瑞数完成环境初始化。
3. 完整实操流程与核心环节实现
前面讲了原理和准备工作,这一部分进入实战环节。我会按照实际操作的顺序,把每个步骤拆开来讲。
3.1 浏览器环境搭建与初始化
首先需要搭建一个可控的浏览器环境。我推荐使用Playwright,相比Puppeteer,它的API更稳定,对多浏览器的支持也更好。
pip install playwright playwright install chromium安装完成后,创建一个浏览器启动脚本:
from playwright.sync_api import sync_playwright def create_browser(): playwright = sync_playwright().start() browser = playwright.chromium.launch( headless=False, # 首次调试建议用有头模式 args=[ '--disable-blink-features=AutomationControlled', '--disable-dev-shm-usage', '--no-sandbox', '--disable-web-security', '--disable-features=IsolateOrigins,site-per-process' ] ) context = browser.new_context( viewport={'width': 1920, 'height': 1080}, user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', locale='zh-CN', timezone_id='Asia/Shanghai' ) return playwright, browser, context启动参数里的--disable-blink-features=AutomationControlled是关键,它能去掉navigator.webdriver标志。--disable-web-security在某些跨域场景下有用,但生产环境慎用。
创建context时,viewport、user_agent、locale、timezone_id这些参数都要设置合理。瑞数会读取这些信息参与指纹计算,设置不当会导致签名结果异常。
3.2 注入补丁脚本与页面加载
浏览器创建好后,在打开目标页面之前,先注入补丁脚本:
def inject_patches(context): patch_script = """ // 去除webdriver标志 Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); // 伪造插件列表 Object.defineProperty(navigator, 'plugins', { get: () => [ { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer' }, { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' }, { name: 'Native Client', filename: 'internal-nacl-plugin' } ] }); // 设置语言 Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh', 'en'] }); // 伪造WebGL信息 const getParameter = WebGLRenderingContext.prototype.getParameter; WebGLRenderingContext.prototype.getParameter = function(parameter) { if (parameter === 37445) { return 'Intel Inc.'; } if (parameter === 37446) { return 'Intel Iris OpenGL Engine'; } return getParameter.call(this, parameter); }; // Hook cookie setter,用于定位签名函数 const originalCookieSetter = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie').set; Object.defineProperty(document, 'cookie', { set: function(val) { if (val.includes('sign') || val.includes('ssxmod')) { console.log('Cookie set:', val); console.trace(); } return originalCookieSetter.call(this, val); }, get: function() { return Object.getOwnPropertyDescriptor(Document.prototype, 'cookie').get.call(this); } }); """ context.add_init_script(patch_script)补丁脚本通过add_init_script注入,它会在每个页面加载前自动执行。这样瑞数的代码运行时,读到的已经是被修正过的环境。
注入完成后,打开目标页面:
def load_page(context, url): page = context.new_page() page.goto(url, wait_until='networkidle') # 等待瑞数初始化完成 page.wait_for_timeout(5000) return pagewait_until='networkidle'确保页面网络请求基本完成。额外的5秒等待是给瑞数执行初始化代码留时间。这个时间可以根据实际情况调整,如果页面加载快,3秒也够。
3.3 签名函数的定位与RPC暴露
页面加载完成后,打开开发者工具(有头模式下),在Console中查看之前Hook打印的调用栈。你会看到类似这样的输出:
Cookie set: ssxmod_itna=xxx; path=/ at setCookie (eval at <anonymous>:1:1) at _sign (eval at <anonymous>:1:1) at a1b2c3 (eval at <anonymous>:1:1) ...从调用栈中,你能找到签名函数的入口。瑞数的函数名通常是随机字符,比如a1b2c3。找到后,把它暴露到window上:
def expose_sign_function(page): # 假设签名函数名为 a1b2c3,需要根据实际情况替换 page.evaluate(""" window.__originalSign = a1b2c3; window.__getSign = function(params) { return window.__originalSign(params); }; """)如果签名函数需要特定的参数格式,你需要在Python端构造好再传进去。通常瑞数的签名函数接收一个对象,包含URL、请求方法、时间戳等信息。
更稳妥的方式是直接Hook签名函数的返回值。在补丁脚本中添加:
// 在补丁脚本中添加 const originalSign = window.a1b2c3; window.a1b2c3 = function() { const result = originalSign.apply(this, arguments); console.log('Sign result:', result); window.__lastSignResult = result; return result; };这样每次签名函数被调用时,结果都会被记录下来。Python端只需要读取window.__lastSignResult即可。
3.4 Python端RPC调用与请求组装
浏览器端准备好后,Python端的工作就简单了。核心逻辑是:触发一次页面上的正常请求,让瑞数完成签名,然后读取签名结果。
def get_sign_via_rpc(page, url, method='GET', params=None): # 方式一:直接调用暴露的签名函数 sign_result = page.evaluate(""" (args) => { return window.__getSign(args); } """, {'url': url, 'method': method, 'params': params}) # 方式二:触发页面请求,读取Hook记录的签名结果 # page.evaluate("window.__triggerRequest()") # page.wait_for_timeout(1000) # sign_result = page.evaluate("window.__lastSignResult") return sign_result拿到签名后,组装最终请求:
import requests def make_request(page, target_url): sign = get_sign_via_rpc(page, target_url) headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Cookie': f'ssxmod_itna={sign["cookie"]}', 'X-Sign': sign['x_sign'], 'Referer': 'https://target-site.com/' } response = requests.get(target_url, headers=headers) return response这里的关键是Cookie和签名的拼接方式。瑞数6.5的签名结果通常包含多个部分,需要分别放到Cookie和请求头中。具体格式需要根据实际抓包分析确定。
注意:RPC调用有频率限制。瑞数的签名函数内部可能有调用计数,短时间内大量调用会触发异常。建议在Python端做请求限速,或者定期重启浏览器环境。
4. 常见问题与排查技巧实录
RPC方案在实际运行中会遇到各种问题,这一部分整理了我踩过的坑和对应的解决方案。
4.1 签名结果为空或格式异常
现象:调用签名函数返回空值,或者返回的结果格式跟预期不符。
排查思路:
首先检查页面是否完全加载。瑞数的初始化是异步的,如果页面还没加载完就调用签名函数,可能拿到空值。解决方法是增加等待时间,或者监听特定的网络请求完成后再调用。
其次检查环境补丁是否生效。在Console中执行navigator.webdriver,如果返回true或undefined以外的值,说明补丁没生效。检查add_init_script是否在页面创建前调用。
最后检查签名函数的参数格式。瑞数的签名函数对参数格式有严格要求,参数类型错误或缺少字段都会导致返回空值。建议在浏览器端打印参数和返回值,对比正常请求时的差异。
速查表:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 返回undefined | 函数未暴露或页面未加载完 | 检查暴露代码,增加等待时间 |
| 返回空字符串 | 参数格式错误 | 打印参数对比正常请求 |
| 返回结果长度不对 | 环境指纹被污染 | 检查补丁脚本,重启浏览器 |
| 调用报错 | 函数上下文丢失 | 使用apply绑定正确上下文 |
4.2 RPC调用超时或连接断开
现象:Python端调用RPC时超时,或者浏览器端突然断开连接。
排查思路:
超时通常是因为浏览器端执行时间过长。瑞数的签名函数内部可能有复杂的计算,如果环境不完整,计算会卡住。检查浏览器Console是否有报错信息。
连接断开可能是浏览器崩溃或页面跳转导致。瑞数检测到异常时会强制跳转页面,导致之前的函数引用失效。解决方法是监听页面跳转事件,在跳转后重新注入和暴露函数。
另外,Playwright的evaluate方法有默认超时时间(30秒)。如果签名计算确实需要较长时间,可以通过page.set_default_timeout()调整。
4.3 签名结果被服务端拒绝
现象:拿到了签名,但请求发送后被服务端返回403或验证页面。
排查思路:
这种情况通常是签名结果虽然生成了,但跟当前请求不匹配。瑞数的签名是跟请求参数绑定的,URL、请求方法、请求体任何一个不同,签名结果都应该不同。
检查Python端发送请求时的参数是否跟签名时传入的参数完全一致。特别注意URL的编码方式、参数的顺序、请求头的完整性。
另一个常见原因是Cookie的拼接方式不对。瑞数6.5的Cookie有多个字段,每个字段的拼接顺序和分隔符都有讲究。建议用抓包工具对比正常请求和你的请求,逐字段核对。
提示:瑞数6.5的签名结果有时效性,通常几分钟内有效。如果签名生成后长时间未使用,需要重新获取。
4.4 浏览器环境被检测的应对策略
现象:页面加载后直接跳转到验证页,或者签名函数不执行。
排查思路:
这说明环境补全不够彻底。瑞数6.5的检测维度很多,除了前面提到的WebDriver、插件、语言、WebGL之外,还可能检测:
Notification.permission的返回值navigator.hardwareConcurrency(CPU核心数)navigator.deviceMemory(设备内存)screen.availWidth/availHeight(屏幕可用区域)Intl.DateTimeFormat().resolvedOptions().timeZone(时区)
建议在补丁脚本中把这些都设置成常见值。另外,使用真实的浏览器用户数据目录(user_data_dir)也能提高环境真实性。
如果以上都做了还是被检测,可能是IP问题。瑞数会记录访问IP的历史行为,如果IP被标记过,即使环境完美也会被拦截。这种情况下需要更换出口IP。
4.5 性能优化与稳定性提升
RPC方案在生产环境运行时,性能和稳定性是需要重点考虑的。
浏览器复用:不要每次请求都新建浏览器,维护一个浏览器实例池,多个请求共享。但要注意,同一个浏览器实例的签名结果可能被服务端关联,高并发场景下建议每个实例绑定一个独立的会话。
签名缓存:如果签名有时效性(比如5分钟),可以在有效期内缓存签名结果,避免重复调用RPC。但要注意签名跟请求参数的绑定关系,不同参数的签名不能混用。
异常重试:RPC调用失败时要有重试机制。重试前先检查浏览器状态,如果页面已跳转或崩溃,需要重新初始化环境。
日志记录:记录每次RPC调用的参数、返回值、耗时,方便排查问题。特别是签名结果被拒绝时,日志能帮你快速定位是哪个环节出了问题。
import time import logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def get_sign_with_retry(page, params, max_retries=3): for i in range(max_retries): try: start = time.time() result = page.evaluate('window.__getSign(arguments[0])', params) elapsed = time.time() - start logger.info(f'Sign success, elapsed: {elapsed:.2f}s') return result except Exception as e: logger.warning(f'Sign attempt {i+1} failed: {e}') if i == max_retries - 1: raise time.sleep(1) return None这套重试机制在实际运行中很实用。瑞数的签名函数偶尔会因为环境波动返回异常值,重试一次通常就能成功。
5. 方案扩展与长期维护建议
RPC方案跑通之后,还有一些值得深入优化的方向。
5.1 从单机到分布式的扩展
单机RPC方案适合小规模采集,但如果需要大规模并发,就需要考虑分布式架构。核心思路是把浏览器环境池化,通过消息队列分发签名任务。
具体做法是:维护一组浏览器实例,每个实例注册到Redis中。Python端需要签名时,从Redis获取一个可用的浏览器实例地址,通过WebSocket发送签名请求。浏览器端处理完后,把结果返回并释放实例。
这种架构下,浏览器实例可以部署在多台机器上,签名能力可以水平扩展。但要注意会话隔离问题——不同业务线的请求最好使用不同的浏览器实例,避免Cookie互相污染。
5.2 瑞数版本更新时的适配策略
瑞数会不定期更新版本,更新后签名算法和检测逻辑都可能变化。RPC方案的优势在这里体现得很明显:只要浏览器还能正常访问页面,你只需要重新定位签名函数并更新暴露代码即可,Python端逻辑完全不用动。
为了快速适配,建议把浏览器端的代码模块化:
- 环境补丁模块:负责注入各种补丁脚本
- 函数定位模块:负责找到签名函数并暴露
- 通信模块:负责与Python端交互
每次瑞数更新,只需要调整环境补丁和函数定位这两个模块。通信模块和Python端保持稳定。
5.3 替代方案对比与选型建议
RPC不是唯一方案,实际选型时要根据场景权衡:
| 方案 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| RPC调用 | 适配快,无需还原算法 | 依赖浏览器,资源消耗大 | 中小规模,算法复杂 |
| 纯算法还原 | 性能高,可分布式 | 逆向工作量大,更新需重做 | 大规模,算法稳定 |
| 混合方案 | 兼顾性能和适配性 | 架构复杂 | 大规模,算法频繁更新 |
我的建议是:先用RPC方案快速跑通业务流程,验证可行性。如果业务量确实很大,再考虑逐步还原核心算法,把RPC作为兜底方案。这样既保证了开发效率,又为后续优化留了空间。
5.4 合规使用提醒
最后说一点重要的:这类技术方案应该用于合法的自动化测试、数据采集等场景。在实际使用中,要遵守目标网站的服务条款,控制请求频率,避免对目标服务器造成压力。技术本身是中性的,关键在于怎么用。
我在实际项目中,通常会设置合理的请求间隔,模拟真实用户的访问行为。这样既能拿到需要的数据,又不会触发风控,是一种可持续的方案。