ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

webpack逆向补环境全流程:定位anti-content签名模块与浏览器环境模拟

webpack逆向补环境全流程:定位anti-content签名模块与浏览器环境模拟 简介一份以Webpack补环境为核心的拼多多anti_content参数逆向学习资料适合具备JavaScript基础、希望进阶逆向工程的前端开发者或安全学习者。资料针对拼多多web端接口中的anti_content签名机制梳理了利用Webpack配置与JavaScript环境补丁绕过校验的完整思路。压缩包共含2个文件——一个Python脚本与一个JavaScript文件体积仅约40KB方便快速查看与复用。Python脚本主要负责启动辅助流程或与调试环境对接JavaScript文件则集中体现了Webpack模块加载、补环境注入和逆向调用等关键代码。目前已有431人学习下载对于想弄懂此类平台复杂参数生成逻辑的读者这份资料能提供从环境模拟、模块分析方法到Python/JS协作的实操参考帮助缩短自行摸索的周期并加深对补环境技术的理解。1. webpack 拼多多 anti-content 补环境这是 js 逆向学习最典型的一条线爬虫工程师打开拼多多 Web 端在开发者工具里随便点开一个接口大概率会在请求参数里看到anti-content这个字段。想顺着字符串搜索找到加密函数却发现代码是 webpack 打包产物模块 ID、__webpack_require__、自执行函数层层嵌套根本无从下手。把代码拖到 Node 里跑又立刻被window is not defined、document is not defined拍在脸上。这三件事——定位 webpack 模块、补环境、生成 anti-content 签名——恰好是 js 逆向入门绕不开的一条主线。下面这套流程按最小可行方式拆开怎么把签名模块从产物里抠出来怎么在 Node 里把浏览器环境补起来以及补环境代理为什么经常失效、翻车之后怎么定位。2. 从 webpack 产物中定位 anti-content模块结构与入口查找Webpack 打包产物不像手写代码那样按文件目录排列。它把所有模块塞进一个数组或对象再用统一的加载函数按 ID 取用。如果不先理解这个骨架拿到手几百 KB 的压缩 JS 就是一团乱麻。这一章的落点是在产物里准确找到 anti-content 相关代码并搭出一个能独立运行的最小框架。后面的补环境工作全都建立在这个最小框架之上。2.1 webpack 产物骨架模块数组与__webpack_require__的内存寻址webpack 在生产模式下打包产物比开发模式简洁得多可读性也差得多。常见结构是每个源文件被编译成一个“模块工厂函数”存放在以模块 ID 为 key 的对象里另外提供一个__webpack_require__用来按 ID 加载模块。如果站点还开启了代码压缩和模块合并scope hoisting / concatenateModules许多小模块会被内联进一个大函数模块边界被打散字符串搜索时上下文会更碎。这是 webpack 打包优化配置对逆向分析最直接的影响优化开得越狠产物越“平”可搜索的特征越少。先看一个删掉业务代码后的骨架真实产物再复杂核心也就是这几行// webpack 产物最小骨架示意 var modules { 0: function (module, exports, __webpack_require__) { var signer __webpack_require__(12); exports.build function (obj) { return signer.sign(obj); }; }, 12: function (module, exports, __webpack_require__) { exports.sign function (obj) { // 真实产物里这里是一大段压缩代码 return step1: (obj.timestamp || ); }; }, }; var cache {}; function __webpack_require__(id) { if (cache[id]) return cache[id].exports; var module (cache[id] { exports: {} }); modules[id].call(module, module, module.exports, __webpack_require__); return module.exports; } // 入口加载模块 0 并调用 console.log(__webpack_require__(0).build({ timestamp: Date.now() }));这个骨架说明三件事。第一模块 ID 不一定是递增数字production 模式压缩后可能是短字符串反查时不要默认从 1 开始数。第二__webpack_require__做了模块缓存同一个模块被多处依赖时不会重复执行工厂函数这是“环境只补一次”能够成立的前提。第三模块间依赖是运行时通过 require 按 ID 查找而不是直接引用全局变量所以抠出单个模块时必须把整个 modules 对象和__webpack_require__一起搬走否则模块内部依赖会全部断掉。还有一种常见变体异步加载 chunk。主入口里只有webpackJsonpCallback和 chunk 加载函数业务模块在独立文件里。这种产物里直接搜 anti-content 往往命中在子 chunk 文件里需要先确定主入口调用了哪个 chunk ID再单独分析对应文件分析思路跟处理单文件产物完全一样。2.2 用字符串反查定位从 anti-content 到加密函数拿到产物后第一件事不是通读而是搜字符串。anti-content大概率出现在两个位置一是作为对象 key 被拼进请求体二是被压缩工具改名成短变量后真实 key 写在某个字符串常量里。我习惯先搜带引号的完整字符串减少误命中。import re js open(pdd.js, r, encodingutf-8, errorsignore).read() for m in re.finditer([\]anti-content[\], js): start max(0, m.start() - 300) end min(len(js), m.end() 300) print( match at %d % m.start()) print(js[start:end])命中处的上下文里通常能看到类似anti-content: n[XX]的赋值。n[XX]就是签名函数被压缩后的调用位置。接下去反查XX这个 key 是在哪个模块里定义回到 modules 对象里在这个 key 所在的模块工厂函数字符串里继续搜。如果压缩工具把属性名也全部短化比如n[a1]就搜a1在模块工厂函数里出现的位置顺着赋值语句往上找函数入口。这一阶段容易误入歧途的地方是直接复制整个产物到本地运行然后在海量 DOM 操作报错里打转。正确做法是只保留“模块表 入口调用”把无关模块删掉用最小脚本逐步加载。看到一个报错解决一个比一次性挑战整个产物要快得多。2.3 把目标模块抠出来最小可运行脚本的搭建假设通过 2.2 的反查已经确定签名入口模块 ID 为 12这一步就把模块表按原样搬进本地文件再写一个精简版 require 让它能被调用。// 最小运行脚本只加载需要的模块 const modules { /* 从 pdd.js 里复制整个 modules 对象 */ }; const cache {}; function __webpack_require__(id) { if (cache[id]) return cache[id].exports; const module (cache[id] { exports: {} }); modules[id].call(module, module, module.exports, __webpack_require__); return module.exports; } // 假设签名入口是模块 12 const signer __webpack_require__(12); console.log(signer.sign({ timestamp: 1700000000000 }));运行这段脚本常见的报错有两种。一是xxx is not defined说明模块表漏了依赖模块回到 2.1 步骤把缺失 ID 的模块补进来。二是window is not defined或document is not defined说明加密模块在加载阶段就引用了浏览器全局对象此时正式进入补环境流程。注意报错位置通常不在你调用的入口函数里而在模块工厂函数顶部——webpack 打包时很多模块会在初始化阶段执行var doc window.document这类语句因此补环境必须在模块加载前完成否则每次 require 都会挂掉。3. 补环境的原理与实操在 Node 里把浏览器环境“演”出来补环境是 js 逆向学习里的分水岭概念。外行以为要把 window、document、navigator 整套实现一遍内行知道只需要让加密代码“演”得不报错并且补出来的属性值和类型能被服务端认可。这一章按“先原理、再实操”的顺序把补环境从零到能用的过程写清楚。3.1 补环境的运行逻辑不是抄浏览器是演到不报错先理解报错为什么会发生。Node.js 本身不提供 window、document 这类浏览器 API加密代码却习惯性地直接访问它们。补环境就是在 Node 的全局对象上“种”出这些 API 的替身让加密代码在替身上正常走完逻辑。补环境的范围怎么定我的习惯是三步先补最外层全局对象window、document、navigator、location让脚本能跑起来再根据下一次报错补齐细节最后做一次浏览器环境对比把会参与签名计算的属性值校准到和真实浏览器一致。这里有一条重要原则能少补就少补。补得越多伪造痕迹越多越容易被服务端从环境指纹上识别出脚本痕迹。常见做法是抓一个浏览器真实环境导出一份 JSON 配置来补而不是凭记忆手写几百行模仿代码。反正服务端校验的是“像不像浏览器”不是“功能全不全”。3.2 第一轮补环境从报错驱动把 window、document、navigator 补出来给出一个最基础的骨架能覆盖大多数模块加载阶段的全局引用。代码在每个属性后面标注了为什么需要这个值避免新手照抄一堆用不上的属性// 补环境骨架按需添加不要照抄一堆没用的属性 global.window global; global.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, language: zh-CN, languages: [zh-CN, zh], webdriver: false, }; global.document { cookie: , referrer: , addEventListener: function () {}, removeEventListener: function () {}, createElement: function (tag) { // 节点类型基础属性要带上指纹计算常从节点上取值 return { nodeName: String(tag).toUpperCase(), style: {} }; }, documentElement: { style: {} }, body: { appendChild: function () {} }, }; global.location { protocol: https:, hostname: mobile.yangkeduo.com, href: https://mobile.yangkeduo.com/, pathname: /, };这段代码里有三个参数需要重点校准。navigator.userAgent必须和目标浏览器保持一致它几乎必然参与 UA 指纹计算填错直接导致签名不一致。document.createElement返回的对象要带nodeName、style这类基础属性因为加密代码可能从节点上取这些值拼进指纹字符串。location.hostname要填实际请求的域名签名校验偶尔会带上 referrer 或 origin域名写错也会被服务端识别出来。第一轮补完报错会向前推进变成xxx is not a function或xxx is undefined这类具体问题。每修一处就回到报错现场看它到底要什么值不要提前补后面才可能用到的东西。3.3 原型链补环境从“补实例”升级到“补类”有些模块不直接访问document而是在代码深处new Image()或new HTMLImageElement()。如果只补了空 documentnew Image()会直接抛Image is not defined。这类场景就要从“给实例补属性”升级到“给类补原型”。// 原型链补环境先定义类再把类挂到全局 function HTMLImageElement() {} Object.defineProperty(HTMLImageElement.prototype, nodeName, { value: IMG, writable: true, configurable: true, }); HTMLImageElement.prototype.addEventListener function () {}; HTMLImageElement.prototype.setAttribute function (k, v) {}; global.HTMLImageElement HTMLImageElement; global.Image function (w, h) { const img new HTMLImageElement(); img.width w || 0; img.height h || 0; return img; };这里用Object.defineProperty而不是直接赋值HTMLImageElement.prototype.nodeName IMG原因是直接赋值会让 nodeName 变成可枚举属性。真实浏览器里 nodeName 是原型上的不可枚举属性检测代码用Object.keys遍历原型链时多出来的可枚举项会直接暴露环境被改过。原型链补环境的核心不是“属性多不多”而是“像不像”。另一个经验补原型链时尽量往专用类上补比如只给HTMLImageElement补属性不要顺手在Object.prototype上挂一堆自定义字段。后者是排查灾难因为所有对象都会被污染服务端一旦检测某个对象的自有属性数量整个环境都会翻车。3.4 用 Proxy 接管属性读取补环境代理失效的原因补环境补到最后总会有漏网属性此时很多方案会写一个 Proxy把全局对象的属性读取统一接管没命中的属性返回一个函数或空对象避免代码因取不到值直接抛错。// 用 Proxy 兜底而非完全替代显式定义 const handler { get(target, prop, receiver) { if (prop in target) return Reflect.get(target, prop, receiver); if (typeof prop string) { const fallback function () {}; fallback.toString () function () { [native code] }; fallback.valueOf () undefined; return fallback; } return undefined; }, has(target, prop) { return true; // 让prop in window同样返回 true }, getOwnPropertyDescriptor(target, prop) { if (prop in target) { return Object.getOwnPropertyDescriptor(target, prop); } return { configurable: true, enumerable: false, writable: true, value: undefined, }; }, }; global.window new Proxy(globalThis, handler);这段代码看起来能把所有兜底逻辑处理好但实际场景里“补环境代理失效”往往不是 get 没生效而是另外三个原因。第一加密代码判断属性是否存在时可能用prop in window这个操作走的是 has 陷阱而不是 get 陷阱漏掉 has 就会让所有属性都判定为不存在。第二Object.getOwnPropertyDescriptor(window, prop)直接拿属性描述符既不经过 get 也不经过 has需要单独实现。第三也是最隐蔽的一点兜底函数被当作对象继续访问其上的方法时函数本身没有这些方法调用链还是会断。比如代码里window.someApi.init()兜底返回的函数没有init属性执行到window.someApi.init()依然报错。所以 Proxy 只适合兜底冷门属性主力仍要靠显式定义关键对象二者结合才能少踩坑。4. 补环境避坑代理失效、prototype 检测与 source map 报错处理补环境方案能不能过最终取决于服务端认不认。这一章整理四个高频翻车点每条按“现象、原因、解决”展开都是实际排查中反复遇到的场景。照着这个顺序检查能少走不少弯路。4.1 补环境代理失效get 返回了值代码却走了 undefined 分支现象日志里 get 陷阱被频繁触发说明代理在工作但签名结果和浏览器里对不上甚至明显走了“不支持”逻辑分支。原因多数代理只实现了 get漏掉 has 和 getOwnPropertyDescriptor。加密代码判断某个 API 是否存在有三种常见写法typeof window.xxx ! undefined、xxx in window、Object.getOwnPropertyDescriptor(window, xxx)。第一种会被 get 陷阱拦到后两种完全绕过 get。尤其in表达式在 JS 里非常常见漏掉 has 陷阱等于让所有属性都是“不存在”代码自然走进错误分支。解决把上一节给出的 handler 补全get、has、getOwnPropertyDescriptor 三个陷阱一起实现。接着做验证写一段十行测试脚本分别用typeof、in、getOwnPropertyDescriptor访问关键属性三个结果必须和浏览器里一致否则继续修。4.2 could not read source map for webpack://meai.web/node_modules/ 报错怎么处理现象把线上的 webpack JS 拉下来放到本地或分析工具里时控制台抛出could not read source map for webpack://meai.web/node_modules/xxx.js这类报错。报错指向 node_modules 里的模块路径看起来像缺依赖容易误导新手去补 node_modules。原因webpack 产物末尾通常带着//# sourceMappingURLxxx.js.map注释生产环境不会把 map 文件随包发布工具按注释去找当然找不到。这个报错跟签名逻辑无关也不影响 JS 执行纯粹是注释指向了一个不存在的文件。线上包里拿不到 map 文件想通过 source map 还原变量名的路是堵死的老老实实用字符串搜索和断点定位更实际。解决本地分析时先把 sourceMappingURL 注释整行删掉或者用编辑器插件忽略 map 文件。不要花时间去找 map。这个经验对任何 webpack 打包的站点产品都适用。4.3 原型链补环境被检测toString 的破绽现象补完环境本地运行一切正常anti-content 能算出来但提交到真实请求里服务端返回风控提示。对比浏览器真实值后发现某些对象的方法实现特征异常。原因检测脚本常执行Function.prototype.toString.call(obj)来判断某个方法是不是原生实现。补进去的document.createElement、window.addEventListener是普通 JS 函数toString会输出完整的函数源码而浏览器原生方法输出的是function () { [native code] }。一旦检测发现字符串不是以[native code]结尾就判定环境被改过。解决所有补进去的方法统一改写 toString让它返回function () { [native code] }并且要在函数定义后立即改写const fakeAddListener function () {}; fakeAddListener.toString () function () { [native code] };另外能少重写就少重写。比如某个 API 在签名流程中根本不参与计算宁可不补也不给它一个假的实现因为每个假实现都多一个被检测的破绽。4.4 navigator 和 UA补了但和服务端不匹配现象navigator.userAgent填了最新版 Chrome 的 UA其它常见属性也都有但签名一直不过。把浏览器里的 navigator 整个导出再补进去结果还是一样。原因不参与指纹的不只是 userAgent。screen.width、screen.height、devicePixelRatio、timezoneOffset、plugins、webdriver都在参与环境指纹计算。UA 对上了但其它值还是 Node 默认状态指纹就不一致。比如真实浏览器里navigator.webdriver是 undefined补环境脚本如果随手给了个 false检测方一比对就发现问题。解决在真实浏览器控制台执行JSON.stringify(Object.entries(navigator))拿到完整键值对再按这份清单逐项补。补完后写一个断言脚本把 Node 里的 navigator 和运行Object.entries(navigator)的结果做 diff改到除动态字段外全量一致。这一步是补环境参数调试里最花时间的部分也是能不能让服务端认你的分水岭。5. 验证与边界怎么确认补出来的 anti-content 真的能用很多人在补环境上花了大量精力却忽略了一个关键动作验证。补环境跑通只是第一步跑出来的签名和浏览器真实请求里的是否一致需要专门设计对比方法。这一章从验证方法、动态因子识别、浏览器对比三个角度把这套流程的最后一段补完。5.1 双端对比让浏览器和 Node 各算一次逐字段对齐从浏览器抓一个真实请求把请求体里的动态字段抽出来在 Node 里用同一份输入去生成 anti-content然后对比。这是最直接的验证方式。// 用一份真实请求体验证补环境结果 const fs require(fs); const { getAntiContent } require(./pdd_env.js); // capture.json 来自浏览器复制{ url, data, anti-content } const capture JSON.parse(fs.readFileSync(capture.json, utf-8)); const params Object.assign({}, capture.data); const output getAntiContent(params); console.log(node :, output); console.log(browser:, capture[anti-content]); console.log(length :, output.length, capture[anti-content].length); console.log(match :, output capture[anti-content]);对比时看三个信息。长度不同大概率是环境指纹输出不一致优先回第 4 章排查 navigator 等属性。前缀不同问题多半出在签名参与字段的取值方式上。前缀一致但后段不一致重点检查时间戳和随机数是否同步更新。注意 anti-content 里通常含时间戳或随机数直接要求全等不现实可以抓两个间隔极短的请求观察动态部分的变化规律再做替换验证。5.2 动态因子识别法哪些字段参与签名哪些不参与如果双端对比每次都不一样先别急着怀疑环境用“单字段替换法”确认哪些字段真正参与签名。抓两个真实请求 B1 和 B2把时间戳等可能变化的字段归零逐个替换观察 anti-content 是否变化。下面是一份示意记录表操作anti-content 变化结论修改 timestamp变化参与签名修改 pageSn变化参与签名修改 pageSize不变不参与签名修改扩展字段 ext_a变化参与签名识别过程中有两个边界坑要注意。第一时间戳精度有些实现取秒有些取毫秒比较时要把两包的 timestamp 归一化再测否则结论会误判。第二部分字段不参与签名计算但服务端会单独校验改动它们同样会导致请求失败。因此“签名参与”和“服务端校验参与”是两个维度不能混为一谈。排错时先确认哪一层在报错再决定改哪里的代码。5.3 把浏览器当黑匣子校准补环境参数的三个技巧第一个技巧是从浏览器控制台导出现有属性而不是靠记忆补。navigator、screen、location、performance这些对象每一组都执行一次Object.entries()导出再与 Node 里的对应对象做按字段比对。补环境脚本与真实环境的差异会一目了然不需要反复试错。第二个技巧是使用“复制为 fetch”保留完整请求链路。浏览器里点一下页面可能会同时触发多个请求复制完整请求能帮你分辨当前 anti-content 是哪个页面动作生成的。单独复制一个请求体容易漏掉请求头或 URL 参数导致补出来的环境在重放时整体错位。第三个技巧是动态字段不要写死。随机数的长度、字符集、是否带大写都要以抓包观察为准。补环境脚本里生成随机数时改成与抓包格式一致而不是随便用Math.random().toString(36)凑数。格式对了签名后续部分才能稳定对齐。6. 用 vm 模块给补环境加隔离避免环境串味与多开脏数据补环境代码一直挂在 global 上短期跑通没问题一旦并发请求或长期运行就会栽跟头。两个任务同时执行时A 任务改写了 navigator.languageB 任务又改了一部分两边签名全部乱套。我的做法是用 Node 的 vm 模块把补环境代码和签名代码一起丢进沙箱每次调用创建全新 context跑完即弃。// 用 vm 隔离补环境避免全局污染 const vm require(vm); const fs require(fs); const envCode fs.readFileSync(./env.js, utf-8); // 补环境代码 const signCode fs.readFileSync(./sign.js, utf-8); // 从 webpack 抠出的签名模块 function makeAntiContent(params) { const sandbox { params: { ...params }, result: }; vm.createContext(sandbox); vm.runInContext(envCode signCode, sandbox, { filename: sandbox.js }); return sandbox.result; } console.log(makeAntiContent({ timestamp: Date.now() }));这段代码的核心是sandbox.params用了展开复制避免签名代码在沙箱里修改外部对象后下一轮调用还带着上一轮的痕迹。每次 makeAntiContent 都从干净状态开始不存在全局串味问题。代价是createContext和runInContext有固定开销高频调用时可以用一个 context 池预创建几个沙箱轮流复用。我早期做 webpack 逆向时图省事把所有补环境代码挂在 global 下结果凌晨两点排查一个“同一函数同一入参、两次输出不同”的玄学问题最后发现是上一个任务改了 navigator.language。从那以后我再不把补环境放进共享全局作用域每次请求都用 vm 重新隔离。逆向这条路留退路比炫技重要。希望帮到你。本文还有配套的精品资源点击获取
返回列表