2026/10/12 1:58:28

补环境版x-s算法实战:JS逆向签名自动化与403排查

补环境版x-s算法实战:JS逆向签名自动化与403排查 简介这份资源面向需要研究某红书x-s参数生成机制的开发者与安全学习者提供纯JavaScript补环境版本的加密算法实现并配套Python通过execjs调用JS的完整接口调用Demo可用于接口调试、参数复现与算法学习等场景。压缩包共2个文件包含1个js算法文件与1个py调用脚本整体约59KB体积轻量、结构清晰便于快速集成到现有Python项目中测试验证。目前已有1891人学习下载说明该方案在社区中具备一定参考价值。读者可获得独立可运行的x-s参数生成逻辑、完整的调用测试示例以及接口请求演示能够对照代码理解补环境思路与参数构造流程减少自行逆向调试的时间成本遇到问题时还可联系作者沟通适合具备一定JavaScript与Python基础、希望深入理解加密参数生成细节的读者参考使用。1. 补环境版 x-s 算法到底解决了什么从一次接口 403 说起上周帮朋友排查一个数据采集脚本目标站点是大家熟悉的那种图文社区。脚本逻辑没问题请求头也带全了但跑个十来条就开始返回 403换 IP 也没用。抓包一看问题出在x-s这个请求头上——它是前端 JS 在发请求前动态算出来的跟时间戳、URL 路径、请求体甚至 Cookie 里的某些字段都绑死。你直接复制浏览器里那串值过几秒就失效你想在 Node 里复现发现那坨 JS 被混淆得亲妈都不认识还夹着一堆浏览器环境检测。这个资源就是冲这个场景来的一份补环境版本的x-s加密算法实现当前可用8.28 更新。所谓补环境就是不硬逆混淆代码而是用 Node 或 Python 把 JS 运行所需的浏览器对象window、document、navigator、location 等补齐让原始 JS 在服务端跑起来从而拿到合法签名。它适合两类人一是做数据采集、需要稳定过签的工程师二是想学 JS 逆向里“补环境”这套方法论的安全方向从业者。如果你还在用 selenium 硬拖页面或者手动复制签名这份东西能让你把签名环节彻底自动化。2. 补环境的核心原理为什么不用硬逆混淆2.1 硬逆 vs 补环境两条路线的取舍拿到一段混淆 JS传统做法是硬逆——把混淆还原、把控制流捋直、把加密函数抠出来用 Python 重写。这条路走通了一次后续维护成本极高对方一更新你之前抠的逻辑全废得重新来一遍。而且x-s这类签名往往不是单一算法它是多个函数嵌套调用中间还掺了环境取值你抠到一半发现某个值来自window.performance或者document.cookie又得回头补。补环境的思路反过来我不动你的混淆代码我造一个假的浏览器环境让你的代码以为自己在浏览器里跑。JS 引擎还是那个 JS 引擎混淆逻辑原封不动执行我只需要在它伸手要window、要navigator的时候递一个我构造好的对象过去。这样对方更新算法只要环境检测逻辑没大改我这份补环境代码基本不用动顶多补几个新属性。代价是你要对浏览器对象模型足够熟知道哪些属性会被检测、返回值该长什么样。这也是为什么补环境版本比硬逆版本更“耐用”——它抗更新的能力来自环境层的稳定性而不是算法层的复刻精度。2.2 补环境要补哪些对象一张对照表不是所有浏览器对象都要补补多了反而容易露馅。常见做法是按需补先跑一遍看报什么错缺什么补什么。下面这张表是我一般会优先覆盖的几类对象关键属性常见检测点补法要点windowinnerWidth/innerHeight、outerWidth、screenX/Y屏幕尺寸是否合理给一组常见分辨率别用 0navigatoruserAgent、platform、webdriver、pluginswebdriver 是否为 falsewebdriver 必须为 falseplugins 给空数组或伪造documentcookie、createElement、getElementsByTagNamecreateElement 返回对象是否完整至少支持 canvas、div 的创建locationhref、protocol、host与请求 URL 是否一致动态传入目标 URL别写死screenwidth、height、colorDepth与 window 尺寸是否匹配和 window 保持同一套分辨率performancenow、timingnow 返回值是否单调递增用 Date.now 模拟注意别返回固定值这张表不是让你一次全补上而是给你一个排查顺序。实际跑的时候JS 报Cannot read property xxx of undefined你就顺着栈找到是哪个对象缺了属性补上再跑。补环境是个迭代过程别指望一次成型。2.3 用 Node 起一个最小补环境骨架下面这段是补环境的最小骨架用 Node 的vm模块把目标 JS 放进一个沙箱里跑沙箱的全局对象就是我们伪造的浏览器环境。先看代码const vm require(vm); const fs require(fs); // 读取目标混淆 JS const targetCode fs.readFileSync(./target.js, utf-8); // 构造伪造的浏览器环境 const fakeWindow { innerWidth: 1920, innerHeight: 1080, outerWidth: 1920, outerHeight: 1080, screenX: 0, screenY: 0, navigator: { userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, platform: Win32, webdriver: false, plugins: [], languages: [zh-CN, zh], }, location: { href: https://example.com/explore, protocol: https:, host: example.com, }, document: { cookie: , createElement: function (tag) { // 简单返回一个带基础属性的对象够用即可 return { tagName: tag.toUpperCase(), style: {}, setAttribute: function () {}, getContext: function () { return null; }, }; }, getElementsByTagName: function () { return []; }, }, performance: { now: function () { return Date.now(); }, }, }; // 让 window 指向自身很多代码会检测 window.window window fakeWindow.window fakeWindow; fakeWindow.self fakeWindow; fakeWindow.top fakeWindow; // 创建沙箱上下文 const sandbox { window: fakeWindow, document: fakeWindow.document, navigator: fakeWindow.navigator, location: fakeWindow.location, performance: fakeWindow.performance, console: console, Date: Date, Math: Math, JSON: JSON, }; vm.createContext(sandbox); // 执行目标 JS让它把签名函数挂到 window 上 vm.runInContext(targetCode, sandbox); // 调用签名函数传入实际参数 const sign sandbox.window._sign || sandbox.window.sign; if (typeof sign ! function) { throw new Error(未找到签名函数检查目标 JS 挂载点); } const result sign(GET, /api/sns/web/v1/homefeed, ); console.log(x-s:, result);逻辑说明vm.createContext创建了一个隔离的 JS 执行上下文sandbox里的属性就是这个上下文里的全局变量。目标 JS 执行时它访问window.navigator.userAgent拿到的就是我们伪造的值。fakeWindow.window fakeWindow这行很关键很多混淆代码会检测window.window是否指向自身不补这个直接报错。参数说明innerWidth/innerHeight给 1920/1080 是常见桌面分辨率别给 0 或者太离谱的值webdriver必须为false这是最基础的检测点location.href要和你实际请求的 URL 保持一致否则签名里的路径参数对不上。sign函数的调用参数方法、路径、请求体要根据目标 JS 实际暴露的接口来传不同版本可能不一样跑之前先确认挂载点。3. 从零跑通一次签名环境补齐与参数对齐3.1 定位签名入口别急着补先看它要什么拿到一份混淆 JS别上来就补环境。先做一件事找到签名函数在哪、它接收什么参数、返回什么格式。常见做法是在目标 JS 里搜x-s或者sign关键字看它被赋值的地方。如果搜不到就在浏览器里打断点看请求发出前x-s是从哪个函数返回的。找到入口后把那个函数单独抠出来跑看它报什么错。报错信息就是你的补环境清单。比如报window is not defined你就补window报navigator.userAgent is undefined你就补navigator.userAgent。这个过程可能要来回好几轮但每一轮你都知道自己缺什么比盲目硬逆效率高得多。我一般会写一个循环跑一次捕获错误解析错误信息里的属性名自动往环境对象里塞一个默认值再跑。这样几轮下来环境骨架就出来了。当然自动塞的值可能不对后面再根据检测逻辑微调。3.2 用 Python 调 Node 补环境打通采集链路补环境代码用 JS 写最自然但你的采集主逻辑可能是 Python。常见做法是用 Python 的subprocess调 Node 脚本把签名结果拿回来。下面是一个可复用的封装import subprocess import json def get_xs(method, url_path, payload): 调用 Node 补环境脚本获取 x-s 签名 method: HTTP 方法如 GET / POST url_path: 请求路径如 /api/sns/web/v1/homefeed payload: 请求体字符串GET 时传空 node_script ./sign_server.js # 把参数以 JSON 形式传给 Node避免命令行转义问题 args json.dumps({ method: method, path: url_path, payload: payload, }) result subprocess.run( [node, node_script, args], capture_outputTrue, textTrue, timeout10, # 超时保护避免 Node 卡死拖垮采集 ) if result.returncode ! 0: raise RuntimeError(f签名脚本执行失败: {result.stderr}) return result.stdout.strip() if __name__ __main__: xs get_xs(GET, /api/sns/web/v1/homefeed) print(x-s , xs)逻辑说明subprocess.run启动 Node 进程执行sign_server.js参数通过 JSON 字符串传递避免路径里的特殊字符被 shell 解析。capture_outputTrue捕获标准输出和错误timeout10是必须加的——补环境脚本偶尔会因为某个属性没补全陷入死循环没有超时保护会把整个采集进程拖死。参数说明method和path必须和实际请求完全一致大小写、斜杠都不能差因为签名里通常会把这些拼进去做哈希。payload是请求体原文POST 请求时传入GET 传空字符串。sign_server.js里要做的事就是接收参数、调用补环境后的签名函数、把结果打到标准输出。对应的sign_server.js骨架const vm require(vm); const fs require(fs); const args JSON.parse(process.argv[2]); const targetCode fs.readFileSync(./target.js, utf-8); // ... 这里放前面构造 fakeWindow 和 sandbox 的代码 ... vm.createContext(sandbox); vm.runInContext(targetCode, sandbox); const sign sandbox.window._sign; const result sign(args.method, args.path, args.payload); process.stdout.write(result);这样 Python 侧只管发请求签名的事全交给 Node职责清晰出问题也好定位——签名失败就单独跑 Node 脚本调试不用把整个采集流程拉起来。3.3 参数对齐时间戳、Cookie 与 URL 编码补环境跑通不代表签名就有效。x-s这类签名通常和请求的多个字段绑定最常见的是时间戳、Cookie 里的a1和web_session、以及 URL 的编码形式。你补环境算出来的签名如果和实际发请求时用的参数对不上服务端一样拒绝。时间戳这块签名内部一般会取Date.now()你补环境时用的Date是 Node 原生的和浏览器一致这块通常没问题。但要注意签名结果里可能带时间戳服务端会校验时间窗口别拿一个几分钟前的签名去请求。Cookie 是重灾区。很多签名会把 Cookie 里的a1值拼进去做哈希你补环境时document.cookie是空的算出来的签名自然不对。常见做法是在调用签名函数前把真实 Cookie 塞进document.cookie// 在调用 sign 之前注入真实 Cookie sandbox.document.cookie a1你的a1值; web_session你的session值;URL 编码也要注意。你传给签名函数的path如果是编码后的签名内部可能又编码一次导致双重编码。我一般会先传原始路径试不行再试编码后的对比服务端返回来判断。4. 避坑与排查补环境路上最容易翻车的五个点4.1 现象签名能算出来但请求返回 403原因签名结果和请求实际参数不匹配。最常见的是 Cookie 没注入或者注入的 Cookie 和发请求时用的不是同一套。另一个可能是时间戳偏差太大服务端校验时间窗口。解决先把签名函数单独跑打印出它内部用到的时间戳和 Cookie 值和你发请求时实际带的值逐项对比。确认一致后再检查 URL 路径是否完全一致包括查询参数。如果还不行用浏览器抓一个真实请求把它的x-s和你算的放一起对比长度和字符集看是不是算法版本不对。4.2 现象Node 脚本跑着跑着内存暴涨然后崩掉原因补环境时给某个属性返回了会无限增长的对象或者签名函数内部有循环引用导致 GC 回收不掉。常见于document.createElement返回的对象被反复缓存。解决给createElement返回的对象加一个简单的缓存上限或者每次返回新对象不缓存。另外在 Node 启动参数里加--max-old-space-size512限制内存崩了至少不会拖垮整机。如果签名调用频率高考虑把 Node 脚本做成常驻服务用进程间通信代替反复启动既省内存又快。4.3 现象本地跑正常放到服务器上签名就失效原因服务器环境和你本地的 Node 版本、时区、甚至 CPU 架构不同导致Date.now()返回值或者浮点数运算精度有差异。有些签名算法对浮点数敏感不同平台算出来最后几位不一样。解决统一 Node 版本用 Docker 固定运行环境。时区设成Asia/Shanghai避免Date相关计算偏差。如果还不行在签名函数入口把关键中间值打出来和本地对比定位是哪一步开始分叉。4.4 现象目标 JS 更新后补环境脚本报一堆 undefined原因对方更新了环境检测逻辑新增了属性检测或者改了签名函数的挂载点。解决别慌补环境的好处就在这里。重新跑一遍看新报错是什么属性补上就行。如果挂载点变了在沙箱里遍历window的所有属性找到那个返回函数的更新调用入口。一般这种更新半小时内能搞定比硬逆重抠快得多。4.5 现象签名函数调用返回 undefined 但不报错原因签名函数是异步的或者它依赖某个初始化流程没走完。有些混淆代码会把签名函数挂在一个 Promise 后面你直接调的时候它还没挂上去。解决在沙箱里等一小会儿再调或者监听window上某个标志位。更稳妥的做法是看目标 JS 有没有暴露初始化完成的回调有就等回调没有就轮询检查签名函数是否存在存在再调。5. 进阶把补环境做成可维护的签名服务补环境脚本写成一坨过两周自己都看不懂。我一般会把它拆成三层环境层、加载层、接口层。环境层负责构造window、document这些对象每个对象一个模块改哪个补哪个加载层负责读目标 JS、创建沙箱、执行代码接口层暴露一个干净的签名函数接收 method/path/payload返回签名字符串。这样目标 JS 更新你只需要动环境层里对应的模块。再进一步把 Node 脚本做成 HTTP 服务Python 侧用 requests 调本地端口拿签名。好处是 Node 进程常驻省去反复启动的开销签名速度从几百毫秒降到几毫秒。下面是一个极简的服务实现const http require(http); const vm require(vm); const fs require(fs); // 启动时加载一次目标 JS构造好沙箱 const targetCode fs.readFileSync(./target.js, utf-8); const sandbox buildSandbox(); // 你的环境构造逻辑 vm.createContext(sandbox); vm.runInContext(targetCode, sandbox); const server http.createServer((req, res) { let body ; req.on(data, chunk { body chunk; }); req.on(end, () { try { const { method, path, payload } JSON.parse(body); const sign sandbox.window._sign; const xs sign(method, path, payload || ); res.writeHead(200, { Content-Type: text/plain }); res.end(xs); } catch (e) { res.writeHead(500); res.end(e.message); } }); }); server.listen(3000, () { console.log(签名服务已启动监听 3000 端口); });Python 侧调用就变成import requests def get_xs(method, path, payload): resp requests.post(http://127.0.0.1:3000/sign, json{ method: method, path: path, payload: payload, }, timeout5) resp.raise_for_status() return resp.text这样采集主流程和签名服务解耦签名服务挂了不影响采集脚本本身重启一下就行。验证方法也简单拿一个已知有效的请求用你的签名服务算一遍和浏览器抓包的值对比一致就说明环境补对了。不一致就逐项排查环境属性。从那以后我每次拿到新的混淆 JS都强制先跑一遍最小补环境骨架把报错清单列出来再动手不再上来就硬逆。这个习惯帮我省了至少一半的调试时间。希望帮到你。本文还有配套的精品资源点击获取