Webpack打包环境下的H5ST逆向:从签名原理到代码落地的完整拆解

📅 发布时间:2026/10/13 5:49:19
Webpack打包环境下的H5ST逆向:从签名原理到代码落地的完整拆解
简介面向爬虫工程师与逆向学习者的一份实战代码资源围绕某东平台基于webpack打包的H5ST签名算法展开提供一套完整、可运行的逆向破解方案。压缩包为zip格式内部共包含2个文件整体体积仅159KB1个Python脚本负责请求参数的串联与调用流程控制1个JavaScript文件还原了webpack模块中的核心加密逻辑二者配合可直接验证H5ST生成结果也可稍作改造后嵌入自己的爬虫工程文件数量虽少但将Python调度与JS加密核心分离层次清晰便于按需修改。目前已有855人学习下载适合具备一定JavaScript基础、希望突破webpack逆向门槛的开发者。通过这套代码能够直观获得从定位加密入口、扣取关键模块、补全浏览器环境到最终生成有效H5ST参数的完整链路同时可以借鉴作者对某东加密参数的处理方式与调试思路减少重复分析时间。对于其他采用webpack构建、存在类似签名机制的站点这套方法同样具有迁移参考价值整体上是一份聚焦完整调用链路的逆向实战资料。1. 实战篇某东 Webpack 打包的 H5ST 逆向完整代码到底怎么落地先说结论H5ST 是某东电商前端用于风控和签名校验的一套动态令牌体系名字里的“H5”指它跑在 H5 页面环境“ST”可以理解为签名令牌Sign Token。你搜到的“webpack 方式”指的是它的生产代码经过 Webpack 打包混淆后你拿到的是 bundle 文件而不是源码逆向的第一步就是把这层 Webpack 壳子解开。这套东西适合谁做电商数据采集、商品价格监控、订单状态跟踪的从业者以及想搞明白前端风控系统工作原理的安全测试人员。如果你只是想随便抓个页面数据H5ST 确实是一道绕不过去的坎但它不是黑匣子只要按步骤拆是可以稳定跑通的。很多人一上来就搜“完整代码”以为拿到一段 JS 就能直接复制跑通结果发现要么版本过期要么缺依赖环境。这篇笔记我会按实际落地的顺序讲清楚 Webpack 打包的 JS 怎么定位入口、怎么还原算法、怎么补环境、怎么验证签名有效性以及我踩过的坑。全文不涉及任何非法用途只讲技术原理与代码调试方法。2. 先搞懂 H5ST 的生成链路它不是一个简单函数而是一套状态机2.1 H5ST 在请求里长什么样哪个字段是它某东 H5 页面的接口请求头里你会看到x-api-eid-token、x-api-request-id这类字段但真正的核心签名是x-api-h5st。这个值是一长串经过 URL 编码的字符串格式通常是版本号.时间戳.随机数.签名值.扩展参数这样分段拼出来的具体段数会随版本变化。我之前分析过的版本大致结构是1.0.时间戳.随机数.签名.算法版本这里的签名不是简单的 MD5 或 SHA1而是基于一套自定义的 HMAC 类算法密钥由前面的时间戳、随机数和一些固定字节共同推导。换句话说服务端校验的不只是最终签名还会校验时间戳与随机数的合法性所以你在逆向时不能只抄签名函数必须把整个生成过程完整还原。2.2 Webpack 打包产物怎么找入口从window.webpackJsonp和模块 ID 入手某东的 H5 静态资源经过 Webpack 打包后所有业务逻辑都被拆成 chunk 和 module。Webpack 4 时代常见的是window.webpackJsonp数组Webpack 5 则变成webpackChunkxxx。你打开 DevTools 的 Sources 面板全局搜webpackJsonp或webpackChunk能定位到加载器代码。但更快的办法是搜特征字符串。H5ST 在代码里通常有英文关键词比如h5st、x-api-h5st、sign、token以及一些固定的报错文本。用 DevTools 的全局搜索直接搜这些字符串定位到包含业务逻辑的那个 chunk再从 chunk 里找 Webpack 模块数组逐个看模块导出。常见做法是先搜h5st字符串找到赋值语句再在赋值语句附近打断点刷新页面触发请求调用栈会直接把你带到生成签名的核心函数。这比静态分析快很多也是我每次都优先用的方法。2.3 动态调试环境用本地代理替换线上 JS打断点看调用栈静态看代码容易晕因为混淆后的变量名全是_0x开头控制流也拍平了。这时候我一般会在本地起一个代理工具把线上某个 JS 文件替换成本地副本副本里在关键位置打上debugger或console.log。替换文件的操作逻辑是// 本地替换脚本示例跑在代理工具的自定义规则里 if (url.includes(h5st) url.includes(.js)) { return localFile(/path/to/local/h5st.js); }这样做的意义在于你可以随意修改本地 JS而线上页面加载的是你的副本不会触发服务端日志异常。调试时在生成签名的函数入口打 Condition Breakpoint条件可以写arguments.length 2减少断点命中次数。参数说明url.includes(h5st)是为了精确匹配包含 h5st 文件名的资源避免误伤其他脚本。localFile指向你本地保存的 JS 文件路径代理工具会把本地文件内容当作响应体返回。2.4 H5ST 的算法核心从 36 位随机数与时间戳推导密钥拿到核心函数后你会发现H5ST 的签名本质是这样一个流程timestamp Date.now() random 36位随机字符串 message 业务参数 timestamp random key 自定义派生函数(timestamp, random, 固定密钥字节) sign HmacSHA256(message, key).toString() h5st 1.0. timestamp . random . sign .算法版本这个过程最刁钻的地方是密钥派生函数。它不是直接用随机数做密钥而是先对随机数做一轮自定义变换再与一个固定字节数组拼接最后再哈希。我见过好几个版本的实现派生逻辑都不同所以网上流传的代码你直接拿去跑大概率签名不通过。3. 用 Webpack 方式还原完整代码从模块提取到环境补全3.1 提取 Webpack 模块把混淆代码还原成可读模块当你定位到含 H5ST 逻辑的 chunk 后需要把目标模块从 Webpack 的模块数组里抠出来。Webpack 打包后的模块数组形如(function(modules) { // webpack runtime })([ function(module, exports, require) { // 模块0 }, function(module, exports, require) { // 模块1 } ])手动抠模块容易出错我一般写一个 Node 脚本按模块 ID 提取函数体然后保存成独立文件// extract.js const fs require(fs); const code fs.readFileSync(./h5st.chunk.js, utf-8); // 用正则粗略切出模块数组 const match code.match(/\[(function\(module, exports, require\)[\s\S]*?)\]\)/); if (match) { const modules match[1]; fs.writeFileSync(./modules.js, modules); console.log(模块数组已提取); }这段代码做的事很简单读取原始 chunk 文件用正则找到模块数组的起始位置把整个数组内容截出来保存。正则的匹配范围是从第一个function(module, exports, require)到最后一个])覆盖所有模块。注意这只是一个粗提取如果 chunk 里有嵌套的数组或对象字面量里也包含])正则可能提前截断。遇到这种情况就手动调整匹配结束位置或者用括号匹配算法来切。3.2 补全运行环境window、document、navigator等浏览器对象提取出来的模块是依赖浏览器环境的直接丢进 Node 跑会报window is not defined。你需要补一个最小环境// env.js global.window global; global.self global; global.navigator { userAgent: Mozilla/5.0 }; global.document { createElement: () ({ getContext: () ({}) }), documentElement: { style: {} } }; global.location { href: https://www.jd.com/ }; global.localStorage { getItem: () null, setItem: () {} };这里的关键点navigator.userAgent会影响某些环境检测逻辑如果签名结果不对试试改成你抓包时实际的 UA。document对象只用到了createElement因为 H5ST 可能通过 canvas 指纹生成辅助参数但你只调签名主流程时不一定走那段逻辑。localStorage用来缓存状态某些版本的 H5ST 会读取本地存储里的设备标识。3.3 构造调用入口导出核心签名函数并验证入参格式模块还原后需要找到模块的导出函数。Webpack 模块内部通常长这样function(module, exports, require) { var _0xabc function() { // 混淆逻辑 }; module.exports _0xabc; }你只需要在提取出来的模块代码末尾追加一句导出语句module.exports _0xabc;然后在 Node 里这样调用const h5stFactory require(./module_xx.js); const h5st h5stFactory(); const result h5st.generate({ method: GET, url: /api/product/detail, body: skuId10001 }); console.log(result);入参格式不是固定的不同版本 H5ST 的generate方法接收对象字段不同。我建议先打断点把线上实际调用时的参数对象 JSON.stringify 打出来再对照着补你的调用代码。这一步是血泪经验别自己猜字段名。4. H5ST 逆向避坑指南3 个最容易翻车的环节4.1 版本号与算法版本不匹配导致签名直接 403现象本地生成的 h5st 看起来格式正确但发到线上接口返回 403 或invalid h5st。原因某东的 H5ST 有多个算法版本不同页面可能加载不同版本的脚本。你逆向的是 A 版本但请求的接口要求 B 版本签名。解决抓包时看线上脚本文件名里的版本号或时间戳比如h5st-20240601.js这种命名确保你分析的 chunk 和线上页面加载的一致。同时检查你生成的 h5st 字符串最后一个分段是否与线上一致。4.2 补环境时缺失 Canvas 指纹导致随机数生成异常现象签名能生成但连续调用多次返回的随机数部分出现规律性重复。原因某些版本用 Canvas 绘制内容计算设备指纹作为随机数生成的种子之一。Node 环境没有 Canvas代码走了 fallback 分支fallback 算法可能只依赖时间戳导致快速调用时随机数撞车。解决认真看生成随机数那段代码找到它依赖的浏览器 API在 Node 里 mock 掉。Canvas 的 mock 可以这样写global.document.createElement function(tag) { if (tag canvas) { return { getContext: () ({ measureText: (text) ({ width: text.length * 10 }), fillText: () {}, getImageData: () ({ data: new Array(400).fill(128) }) }), width: 300, height: 150 }; } return { getContext: () ({}) }; };这里面measureText返回的宽度会影响绘制结果getImageData返回的像素数据会被用来计算哈希。mock 得越接近真实浏览器生成的指纹越稳定。4.3 时间戳偏差导致的校验失败现象本机时间与服务器时间相差超过几秒签名直接被拒。原因服务端校验时间戳范围防止重放攻击。你本机时间如果不准或者用new Date()生成时间戳时被某东的代码做了偏移校正就会出现偏差。解决不要在调用时手动传入时间戳让代码自己生成。但如果你发现线上请求头里有x-api-eid-token这类带时间信息的字段可以抓下来看看时间偏移量手动在本地环境里设置// 如果服务器时间比本地快 5 秒 Date.now () realNow 5000;注意这个偏移量不是固定的建议每次抓包都重新确认。这也是为什么不要轻易改代码里的时间生成逻辑宁可让代码自己走完流程。5. 把签名算法封装成独立服务本地验证与持续更新策略5.1 最小可用封装输入 URL 和参数输出完整请求头当你跑通签名生成后下一步是封装成可复用模块。我一般这样组织// h5st-service.js const { generateH5st } require(./core/h5st-core); function buildHeaders({ method, url, body, cookie }) { const h5st generateH5st({ method, url, body }); return { x-api-h5st: h5st, Content-Type: application/json, Cookie: cookie }; } module.exports { buildHeaders };这里generateH5st是从逆向代码里封装的同步函数buildHeaders负责把它包装成请求头。这样业务代码不需要关心签名细节只要传请求参数就行。5.2 验证签名是否有效用真实接口做回归测试签名算法改过之后必须验证。我通常写一个测试脚本用固定的 URL 和参数跑 100 次检查三点node test.js --count 100 --url /api/product/detail --body skuId10001检查项状态码是否全部为 200出现 403 直接失败。生成的 h5st 串中时间戳部分是否单调递增如果出现乱序说明随机数或时间处理有问题。连续 100 次签名是否不重复重复则说明随机数池不够。5.3 持续更新策略关注脚本版本变化定期抓包比对H5ST 算法会不定期更新可能是一个月一次也可能是大促前强制升级。你需要做的是每次抓包时留意x-api-h5st的版本段是否变化。保存线上加载的 JS 文件名与大小一旦发现新文件立即拉下来对比。用 diff 工具对比新旧 chunk重点关注生成密钥和随机数的部分。我习惯在本地维护一个版本清单表格记录日期、脚本文件名、h5st 格式、算法版本。这样更新时能快速定位改动点。6. 进阶技巧用自动化工具监控签名变化提前适配最后分享一个我一直在用的技巧写一个定时任务每天抓一次某东 H5 页面的静态资源列表自动下载 JS 文件并检查其中是否包含h5st关键词如果有新文件就触发 diff 告警。# monitor.py import requests import re import hashlib def check_h5st_script(html): scripts re.findall(rscript[^]src([^]), html) for src in scripts: if h5st in src.lower(): content requests.get(src).text digest hashlib.md5(content.encode()).hexdigest() print(f{src} - {digest})这个脚本的输出结果会告诉你哪些文件发生了变化你只需要对比 hash 不同的文件即可。注意脚本里的正则只匹配了普通的 script 标签如果页面用动态加载方式插入脚本你需要额外监听网络请求。我的习惯是每周跑一次这个监控跑完把结果发到工作群里看到有变化就手动拉新代码分析。H5ST 逆向不是一次性的工作它更像是在跟版本迭代赛跑。希望这个流程能帮你在自己的项目里少走弯路稳定跑通签名生成。本文还有配套的精品资源点击获取