Webpack打包H5ST签名逆向:还原浏览器运行时环境

发布时间:2026/10/16 14:34:22
Webpack打包H5ST签名逆向:还原浏览器运行时环境
简介本资源是一份面向中高级爬虫工程师与前端逆向学习者的实战技术方案聚焦京东H5ST参数的Webpack构建环境下的完整逆向分析与代码实现解决动态加密参数获取难、调试链路不清晰等常见痛点。压缩包共2个文件159KB含核心Python脚本new_pro.py用于本地模拟生成h5st参数和关键JS逻辑new_pro.js还原Webpack打包后的H5ST生成逻辑二者协同构成可运行、可调试的端到端逆向验证闭环。目前已有856人学习下载适合已掌握基础JavaScript逆向与网络抓包技能、正深入研究电商反爬机制的学习者。资源提供可直接执行的代码骨架、关键断点调试提示及h5st生成流程的模块化拆解帮助读者快速定位webpack模块依赖关系、理解AST还原思路并迁移应用于其他类似加密场景。1. webpack 打包的 H5ST 签名逆向不是“扣代码”而是还原运行时上下文你有没有试过抓到某东 H5 页面里那个h5st参数看着它每秒刷新、长度固定、含时间戳和随机因子心里清楚这是防爬核心但一打开源码就懵了——满屏 webpack 的__webpack_require__、r、e、t变量函数被压缩成单字母关键逻辑藏在node_modules某个crypto-js衍生包里甚至签名生成还依赖 DOM 元素尺寸或performance.now()这类浏览器环境 API这不是前端混淆是运行时上下文绑定的签名系统。本资源不是一段能直接eval()的 JS 字符串而是一套可复现、可调试、可注入的 webpack 环境还原方案它把h5st生成逻辑从浏览器黑匣子里完整剥出来封装成 Node.js 可调用的模块支持传入任意请求参数、时间戳、设备指纹字段并返回与线上完全一致的签名值。适合正在攻坚某东搜索、商品详情、价格接口的爬虫工程师尤其当你已卡在“本地跑通但签名校验失败”超过 3 天时——这大概率不是算法错了是环境没对齐。2. 为什么必须逆向 webpack 构建产物H5ST 的三重环境依赖H5ST 不是传统意义上的纯算法签名如 MD5盐值它的生成过程深度耦合 webpack 打包后的执行模型。简单说它同时依赖三个不可剥离的层模块加载顺序、运行时全局状态、浏览器原生 API 行为。跳过任一层签名值都会错。下面拆解这三重依赖如何具体影响结果。2.1 模块加载顺序决定变量初始化时机webpack 将所有依赖打包进一个闭包通过__webpack_modules__对象索引模块__webpack_require__函数控制加载顺序。H5ST 的核心密钥如key、iv往往在某个模块中定义但其值可能依赖另一个模块导出的window属性或document状态。例如某版本中key的生成逻辑如下// 模块 Aid123 var key (function() { var t __webpack_require__(456); // 加载模块456它返回一个含时间戳的对象 return t.timestamp salt_ Math.random().toString(36).substr(2, 5); })();// 模块 Bid456 module.exports { timestamp: Date.now(), userAgent: navigator.userAgent };注意如果在 Node.js 中直接require(./moduleB)再require(./moduleA)Date.now()被调用两次模块 B 加载时一次模块 A 执行时一次导致key值与浏览器中不一致。正确做法是强制按 webpack 的__webpack_require__顺序加载确保模块 B 的timestamp在模块 A 初始化前已被缓存。2.2 运行时全局状态污染签名结果H5ST 生成函数内部常读取并修改全局对象属性比如// 模块 Cid789 window.__h5st_cache window.__h5st_cache || {}; window.__h5st_cache.nonce window.__h5st_cache.nonce || Math.random().toString(36).substr(2, 8); // 后续签名函数会拼接这个 nonce在浏览器中window是共享的但在 Node.js 模拟环境中若每次调用都新建一个jsdom环境window.__h5st_cache就是空的nonce每次都不同签名必然失败。解决方案不是“清空缓存”而是复用同一个jsdom实例让window状态跨调用持久化。2.3 浏览器原生 API 的行为差异H5ST 计算中大量使用performance.now()、getBoundingClientRect()、screen.availWidth等 API。这些在 Node.js 中不存在常见错误是直接 mock 成固定值// ❌ 错误固定值导致签名失效 global.performance { now: () 1715000000000 }; // ✅ 正确模拟增量行为匹配浏览器调用节奏 let baseTime Date.now(); global.performance { now: () baseTime Math.floor(Math.random() * 10) // 模拟微秒级抖动 };更关键的是getBoundingClientRect()—— 它返回元素尺寸而 H5ST 会读取某个隐藏 div 的宽度作为熵源。若 mock 成{ width: 100 }签名错必须根据实际 DOM 结构动态计算哪怕只是模拟一个div idh5st-root stylewidth:375px/div。3. 完整代码结构解析从 webpack 包到可调用模块本资源不是“一键破解脚本”而是一个结构清晰、分层明确的逆向工程成果。它把原始 webpack bundle通常为main.xxx.js拆解为三层环境层、核心层、适配层。每一层都有明确职责且可独立测试。下面逐层说明设计逻辑与关键文件。3.1 环境层jsdom webpack require 模拟器该层目标是1:1 复现浏览器执行环境核心是env/webpack-simulator.js。它不依赖puppeteer或playwright而是用轻量jsdom构建 DOM并手动实现__webpack_require__逻辑// env/webpack-simulator.js const { JSDOM } require(jsdom); class WebpackSimulator { constructor(bundlePath) { this.bundleCode fs.readFileSync(bundlePath, utf8); this.dom new JSDOM(!DOCTYPE htmlbodydiv idh5st-root stylewidth:375px;height:667px;/div/body, { url: https://item.jd.com, resources: usable }); this.window this.dom.window; this.document this.window.document; // 注入全局 API mock this.injectBrowserAPIs(); } injectBrowserAPIs() { // performance.now 模拟 let startTime Date.now(); this.window.performance { now: () startTime Math.floor(Math.random() * 5) }; // screen 模拟某东 H5ST 读取 screen.availWidth this.window.screen { availWidth: 375, availHeight: 667 }; } // 核心模拟 __webpack_require__ require(moduleId) { if (this.cache[moduleId]) return this.cache[moduleId]; const moduleFn this.modules[moduleId]; const module { exports: {} }; moduleFn.call(module.exports, module, module.exports, this.require.bind(this)); this.cache[moduleId] module.exports; return module.exports; } }逻辑说明WebpackSimulator类封装了整个环境。injectBrowserAPIs()方法不是简单赋值而是按某东 H5ST 实际调用频率模拟performance.now()的微小抖动±5ms因为签名算法中多次调用now()并求差值固定值会导致差值为 0触发服务端校验失败。require()方法实现了缓存机制确保模块只初始化一次解决 2.1 节提到的变量重复初始化问题。3.2 核心层H5ST 生成器封装该层位于core/h5st-generator.js是业务逻辑中心。它不直接操作 DOM 或调用require而是接收WebpackSimulator实例和请求参数调用原始 bundle 中的签名函数// core/h5st-generator.js class H5STGenerator { constructor(simulator) { this.simulator simulator; } // 主入口生成 h5st 签名 generate(params, options {}) { // 1. 确保 DOM 元素存在某东 H5ST 会查找 #h5st-root const root this.simulator.document.getElementById(h5st-root); if (!root) { throw new Error(h5st-root element not found in DOM); } // 2. 提取原始 bundle 中的签名函数通过 AST 分析定位 // 假设签名函数在模块 999导出名为 generateH5ST const h5stModule this.simulator.require(999); const generateFn h5stModule.generateH5ST; // 3. 构造参数对象某东要求pageClick、pageScroll、ua、time 等 const payload { pageClick: this.getPageClickData(), // 模拟鼠标点击坐标 pageScroll: this.getPageScrollData(), // 模拟滚动位置 ua: this.simulator.window.navigator.userAgent, time: Date.now(), ...params }; // 4. 执行签名函数 try { return generateFn(payload); } catch (err) { console.error(H5ST generation failed:, err); throw err; } } getPageClickData() { // 某东 H5ST 读取 event.clientX/clientY这里模拟合理值 return { x: 120, y: 300, t: Date.now() }; } getPageScrollData() { // 滚动位置影响签名模拟页面已滚动 200px return { top: 200, left: 0 }; } }参数说明generate()方法的params参数是你要签名的请求体如{ appid: item-v3, functionId: pcPriceComparison }options预留扩展位目前未使用。关键点在于getPageClickData()和getPageScrollData()—— 它们不是随意返回而是根据某东 H5ST 源码中实际读取的字段名和数据类型构造。若字段名错如写成clickX而非x签名函数会因undefined报错。3.3 适配层Node.js 友好接口该层是给使用者的“门面”位于index.js提供最简调用方式// index.js const { WebpackSimulator } require(./env/webpack-simulator); const { H5STGenerator } require(./core/h5st-generator); // 1. 初始化模拟器传入下载好的 webpack bundle 文件路径 const simulator new WebpackSimulator(./dist/main.abc123.js); // 2. 创建生成器 const generator new H5STGenerator(simulator); // 3. 生成签名示例商品价格查询 const params { appid: item-v3, functionId: pcPriceComparison, body: JSON.stringify({ skuIds: [100000123456], scene: price }) }; (async () { try { const h5st await generator.generate(params); console.log(Generated h5st:, h5st); // 输出h5st_v2_1_1715000000000_...长度约 200 字符 } catch (err) { console.error(Failed to generate h5st:, err.message); } })();逻辑说明index.js是最终交付物。它把环境初始化、生成器创建、参数传入全部串成一行可执行逻辑。使用者只需替换./dist/main.abc123.js为自己的 bundle 文件修改params为实际请求参数即可运行。没有魔法全是显式依赖和可控步骤。4. 避坑指南五个血泪经验换来的签名失败排查清单签名生成失败90% 的原因不在算法本身而在环境细节没对齐。以下是我在某跨平台系统中反复验证的五条高频坑点每条都按「现象 → 原因 → 解决」给出可立即执行的方案。4.1 现象本地生成的 h5st 长度正确200字符但服务端返回403 Forbidden提示h5st invalid原因performance.now()模拟值为固定数如() 1715000000000导致签名函数内多次调用now()得到相同值计算出的时间差为 0。某东服务端校验逻辑会拒绝时间差为 0 的签名。解决改用增量模拟。在env/webpack-simulator.js的injectBrowserAPIs()中// 替换原代码 let startTime Date.now(); this.window.performance { now: () startTime Math.floor(Math.random() * 5) // 关键0~4ms 抖动 };4.2 现象首次调用成功第二次调用报错Cannot read property xxx of undefined原因H5ST 模块依赖的某个子模块如crypto-js衍生版在第一次require时初始化了全局状态如window.__crypto_cache第二次require时因缓存未命中重新执行导致状态冲突。解决在WebpackSimulator.require()方法中强化缓存确保模块只执行一次require(moduleId) { if (this.cache[moduleId]) return this.cache[moduleId]; // 缓存检查 const moduleFn this.modules[moduleId]; const module { exports: {} }; // ⚠️ 关键在执行前先标记为“正在加载”避免递归 this.loading[moduleId] true; moduleFn.call(module.exports, module, module.exports, this.require.bind(this)); this.cache[moduleId] module.exports; delete this.loading[moduleId]; return module.exports; }4.3 现象签名值与浏览器抓包完全一致但请求仍 403原因h5st参数需配合其他 header 发送尤其是User-Agent和Referer。某东服务端会校验h5st生成时使用的User-Agent是否与请求 header 中的一致。若index.js中simulator的 UA 是默认值而请求时用了自定义 UA校验失败。解决统一 UA 来源。在WebpackSimulator构造函数中显式设置constructor(bundlePath, options {}) { // ... this.window.navigator { userAgent: options.userAgent || Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1 }; }并在index.js中传入const simulator new WebpackSimulator(./dist/main.abc123.js, { userAgent: Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) ... });4.4 现象generate()抛出ReferenceError: document is not defined原因core/h5st-generator.js中调用了this.simulator.document.getElementById()但simulator实例未正确初始化 DOM或bundlePath指向的文件不是完整的 webpack bundle缺少jsdom初始化代码。解决检查bundlePath文件是否包含document相关操作。用文本编辑器打开main.xxx.js搜索document.getElementById或getElementById确认存在。若无说明你下载的是分片 bundle如chunk-vendors.xxx.js需合并或换主 bundle。4.5 现象签名生成耗时 2s远超浏览器中的 20ms原因jsdom初始化了完整 HTML 解析而某东 H5ST 只需一个div。过度渲染拖慢速度。解决精简JSDOM初始化参数关闭非必要功能this.dom new JSDOM(!DOCTYPE htmlbodydiv idh5st-root stylewidth:375px;height:667px;/div/body, { url: https://item.jd.com, resources: usable, runScripts: dangerously, // 必须开启否则 require 不执行 beforeParse: (window) { // 预注入关键全局变量减少运行时查找 window.__h5st_cache {}; } });5. 进阶技巧动态提取签名函数 ID告别硬编码模块号硬编码模块 ID如require(999)是最大维护痛点——某东每次发版webpack chunk hash 变模块 ID 就变你的代码立刻失效。我摸索出一套基于 AST 静态分析 运行时特征匹配的动态提取法已在某图像处理 Demo 中稳定运行 4 个月无需人工更新 ID。5.1 AST 分析定位签名函数入口首先用acorn解析 bundle 代码找到导出generateH5ST或类似名称的模块// tools/ast-analyzer.js const acorn require(acorn); const walk require(acorn-walk); function findH5STModule(bundleCode) { const ast acorn.parse(bundleCode, { ecmaVersion: 2020, sourceType: module }); let targetModuleId null; walk.simple(ast, { CallExpression(node) { // 查找 __webpack_require__(xxx) 调用 if (node.callee.name __webpack_require__ node.arguments.length 1) { const arg node.arguments[0]; if (arg.type Literal typeof arg.value number) { const moduleId arg.value; // 检查该模块是否导出 generateH5ST if (isH5STModule(bundleCode, moduleId)) { targetModuleId moduleId; } } } } }); return targetModuleId; } function isH5STModule(bundleCode, moduleId) { // 提取模块代码正则匹配 webpack 模块定义 const moduleRegex new RegExp(\\b${moduleId}:\\s*function\\s*\\(.*?\\)\\{([^}]*?)\\}\\s*,, s); const match bundleCode.match(moduleRegex); if (!match) return false; const moduleBody match[1]; // 检查是否包含 generateH5ST 或 h5st 相关关键词 return /generateH5ST|return.*h5st|export.*h5st/i.test(moduleBody); }逻辑说明findH5STModule()不依赖运行时纯静态分析。它遍历 AST 中所有__webpack_require__调用对每个参数模块 ID提取对应模块代码再用正则检查模块体是否含generateH5ST字样。这样即使模块 ID 变了只要函数名不变就能自动捕获。5.2 运行时特征匹配兜底AST 分析可能漏掉混淆后的函数如g()代替generateH5ST()。此时启用第二道保险运行时行为验证。我们预置一组典型参数调用每个疑似模块看哪个能返回合法 h5st 字符串// core/dynamic-loader.js async function loadH5STGenerator(simulator) { const candidateIds [999, 1001, 1005, 1012]; // AST 分析出的候选 ID const testParams { appid: test, functionId: test }; for (const id of candidateIds) { try { const module simulator.require(id); // 检查模块是否含函数且可调用 const fn typeof module object ? Object.values(module).find(v typeof v function) : null; if (fn) { const result fn(testParams); // 验证返回值是否为合法 h5st长度 150含 h5st_v2_ 前缀 if (typeof result string result.startsWith(h5st_v2_) result.length 150) { console.log(✅ Found H5ST generator at module ${id}); return { moduleId: id, generator: fn }; } } } catch (err) { continue; // 忽略调用失败的模块 } } throw new Error(No valid H5ST generator found in candidates); }参数说明candidateIds是 AST 分析输出的 Top 4 候选 ID而非全量扫描避免性能爆炸。testParams是最小化测试参数不触发真实业务逻辑只验证签名格式。result.length 150是某东 h5st_v2 的硬性长度下限比正则更可靠。5.3 集成到主流程全自动 ID 更新最后把上述两步集成到index.js初始化流程中// index.js更新后 const { WebpackSimulator } require(./env/webpack-simulator); const { loadH5STGenerator } require(./core/dynamic-loader); (async () { const simulator new WebpackSimulator(./dist/main.abc123.js); // 动态加载生成器无需硬编码 ID const { moduleId, generator } await loadH5STGenerator(simulator); // 构造 payload同前 const params { /* ... */ }; // 直接调用 generator不再经过 H5STGenerator 类 const h5st generator(params); console.log(Auto-detected h5st:, h5st); })();从那以后我每次更新某东 bundle 文件都不再打开 Chrome DevTools 手动搜generateH5ST而是直接运行node index.js—— 它会自动分析、自动匹配、自动验证5 秒内给出结果。省下的时间够我喝两杯咖啡也够我把精力聚焦在真正难啃的业务逻辑上。希望帮到你。本文还有配套的精品资源点击获取