逆向webpack打包的h5st签名参数:从模块图到运行时上下文
发布时间:2026/11/2 11:48:20 锦皓数字建站

简介这份资源面向具备一定前端基础、希望深入理解移动端加密参数生成机制的爬虫学习者聚焦某东平台以 webpack 方式打包的 h5st 逆向分析场景提供一套可对照复现的完整代码案例。压缩包共 2 个文件以 Python 与 JavaScript 源码为主分别承担请求逻辑与前端加密逻辑的还原整体约 159KB体量轻便便于快速阅读与本地调试。目前已有 858 人学习下载说明该方向在爬虫实战中关注度较高。读者可从中获得 webpack 模块加载与调用链路的分析思路、h5st 参数生成过程的代码级还原以及前后端脚本如何配合完成一次完整请求的参考实现适合作为理解加密参数构造、提升逆向调试能力的实战素材也便于在此基础上做二次分析与排错验证。1. 签名参数不是玄学h5st 到底在防什么打开某电商 H5 页面的网络面板你会看到每个关键接口都挂着一个叫h5st的参数长度动辄几百字符肉眼完全不可读。很多人第一反应是「加密了得找解密函数」于是顺着调用栈一路翻翻到一堆混淆过的 JS 就卡住了。这里有个反直觉的结论h5st 的核心难点从来不是「加密算法有多强」而是「它把哪些运行时状态揉进了签名里」——时间戳、页面指纹、请求体摘要、随机数、版本号这些东西按特定顺序拼接后再做哈希任何一项对不上服务端直接判定签名无效。你就算把哈希函数抄下来缺了运行时上下文照样算不出能过校验的值。这也是为什么 webpack 打包的 H5 站点让逆向的人格外头疼。webpack 会把模块拆成 chunk用__webpack_require__做运行时加载函数名被压缩成单字母字符串常量被抽到独立的数组里做偏移索引。你看到的不是一段线性的加密逻辑而是一张模块依赖图。想搞懂 h5st 的生成机制本质上是想搞懂「这张图里哪个模块负责拼签名、它从哪些模块取运行时数据、这些数据在什么时机被写入」。搞清这条链路你才能理解为什么单纯复制一个函数跑不通也才能判断哪些参数可以固定、哪些必须实时采集。对做爬虫的人来说这是一道绕不开的坎踩过一次就明白签名参数不是黑匣子它是一份被刻意打散的运行时快照。2. webpack 运行时模块图为什么让逆向翻车2.1 从__webpack_require__看模块加载的真实顺序webpack 打包产物最外层通常是一个 IIFE内部维护一个modules对象和一个installedModules缓存。所有业务代码都被包成function(module, exports, __webpack_require__)的形式塞进modules。页面初始化时入口模块被__webpack_require__(entryId)拉起之后每个模块第一次被引用时才执行执行结果缓存起来。这个「懒执行 缓存」的机制意味着签名模块不一定在页面加载时就跑它可能在你触发某个请求时才被 require 进来。常见的实现思路是签名逻辑会被单独打成一个 chunk通过__webpack_require__.e(chunkId)动态加载。你在 DevTools 里看到的调用栈往往是从业务模块跳到__webpack_require__.e再跳到 chunk 里的签名函数。如果只盯着最上层那个函数看会误以为签名是同步算出来的实际上它前面还有一段异步加载。理解这一点很关键断点要下在 chunk 加载完成之后否则你拿到的__webpack_require__缓存里根本没有签名模块。// webpack 运行时简化结构用于理解模块注册与加载 (function(modules) { var installedModules {}; function __webpack_require__(moduleId) { if (installedModules[moduleId]) { return installedModules[moduleId].exports; // 命中缓存直接返回 } var module installedModules[moduleId] { i: moduleId, l: false, exports: {} }; modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); module.l true; return module.exports; } // 动态 chunk 加载返回 Promise __webpack_require__.e function(chunkId) { return new Promise(function(resolve) { // 实际会插入 script 标签加载对应 chunk 文件 resolve(); }); }; __webpack_require__(0); // 启动入口模块 })({ 0: function(module, exports, __webpack_require__) { /* 入口 */ }, 1: function(module, exports, __webpack_require__) { /* 签名模块 */ } });上面这段是 webpack 运行时的骨架installedModules是缓存表__webpack_require__.e负责异步拉 chunk。参数moduleId是打包时分配的整数chunkId对应拆分出来的文件编号。实际产物里这些名字会被压缩但结构不变。逆向时我一般会先在控制台把__webpack_require__.c缓存对象打印出来看哪些模块已经被实例化再顺着exports找签名相关的函数名。这一步能省掉大量在混淆代码里瞎翻的时间。2.2 字符串数组与偏移索引常量为什么搜不到webpack 配合压缩工具后会把代码里所有字符串常量抽到一个大数组里原位置替换成_0x1a2b[3]这样的索引访问。你搜h5st搜不到搜sign也搜不到因为它们在数组里躺着访问时靠下标。常见的实现思路是这个数组还会做一次「旋转」——启动时执行一段代码把数组元素循环移位让静态分析时看到的索引和运行时实际取到的值对不上。这就是为什么直接读源码里的_0x1a2b[3]得到的字符串可能是错的。破解这个的手段不复杂在数组定义之后、旋转逻辑执行之后下断点把数组整个 dump 出来再写个小脚本把索引映射还原。我一般会在控制台执行类似JSON.stringify(_0x1a2b)把数组存下来然后对照代码里的索引逐个替换。这一步做完混淆代码的可读性会提升一大截签名逻辑里那些关键字段名、拼接分隔符、哈希前的固定盐值就都浮出来了。血泪经验是别急着读逻辑先把常量表还原否则你会在错误的字符串上浪费半天。2.3 运行时上下文签名依赖的页面状态从哪来h5st 这类签名参数除了算法本身还依赖一批运行时采集的值。常见的包括当前时间戳毫秒级、一个随机数、页面 URL 的某些片段、浏览器指纹UA、屏幕尺寸、时区等、请求体的哈希摘要、以及一个版本标识。这些值在签名函数执行时被读取拼成一个原始串再做哈希。问题在于这些值的采集点分散在不同模块里有的在页面初始化时写入全局对象有的在发请求前临时计算。理解这条链路的方法是在签名函数入口下断点看它从哪些变量取值再反查这些变量在哪赋值。常见的实现思路是站点会维护一个全局的window.__INITIAL_STATE__或类似的运行时对象签名模块从里面读版本号和指纹时间戳和随机数则现场生成。请求体摘要需要你传入和实际发送一致的 body这一点最容易翻车——很多人签名算对了但 body 序列化方式和服务端预期不一致摘要对不上照样被拒。所以逆向 h5st 不是找到一个函数就完事而是要把「哪些值固定、哪些值实时算、body 怎么序列化」这三件事对齐。3. h5st 生成链路从参数拼接到哈希输出3.1 原始串的拼接顺序与分隔符签名参数的可读部分通常是明文拼接的比如时间戳;随机数;版本号;指纹;body摘要用分号或竖线分隔然后整体做一次哈希再把哈希结果和明文按某种格式组合成最终的h5st值。拼接顺序是硬约定服务端按同样顺序重算顺序错一位就失败。常见的实现思路是明文部分会保留在最终参数里方便服务端取用哈希部分附在后面做校验。所以你看到的长字符串前半段其实是可读的只是被编码过。逆向时我一般会先把最终参数按分隔符拆开观察每一段的长度和字符集判断哪段是时间戳、哪段是随机数、哪段是哈希。时间戳是 13 位数字随机数长度固定哈希段通常是十六进制或 Base64。拆清楚结构再回头找拼接代码定位速度会快很多。这里的关键认知是h5st 不是单一加密输出它是「明文载荷 校验摘要」的组合体理解这个结构比理解哈希算法本身更重要。3.2 哈希函数的选型与调用方式哈希部分常见的选型是 MD5 或 SHA 系列具体用哪个要看站点实现。webpack 产物里这类哈希函数往往以独立模块存在输入是拼接好的原始串输出是十六进制字符串。调用方式上有的站点直接调库有的自己实现了一份精简版。自己实现的版本反而好办逻辑直白调库的版本需要确认库的版本和参数比如是否加盐、编码方式。// 签名链路示意拼接 - 哈希 - 组合非真实站点代码 function buildSign(ctx) { var ts Date.now(); // 毫秒时间戳实时生成 var nonce Math.random().toString(36).slice(2, 10); // 随机串 var ver ctx.version; // 从运行时对象读取 var fp ctx.fingerprint; // 页面指纹初始化时写入 var bodyHash md5(JSON.stringify(ctx.body)); // 请求体摘要序列化方式要对齐 var raw [ts, nonce, ver, fp, bodyHash].join(;); // 拼接顺序是硬约定 var sign md5(raw); // 哈希输出 return raw ; sign; // 明文 摘要组合 }这段示意代码把链路讲清楚了ts和nonce现场生成ver和fp来自运行时上下文bodyHash依赖请求体的序列化结果raw按固定顺序拼接最后哈希并组合。参数上ctx.body必须和实际发送的 body 完全一致JSON.stringify的键顺序、空格处理都会影响摘要。实际站点里这些函数名是混淆的但数据流就是这个形状。定位到这段逻辑后剩下的工作是把ctx里每个字段的来源摸清确认哪些能固定、哪些必须每次重算。3.3 版本号与指纹为什么固定值会失效版本号看起来是个常量但它往往和站点前端发版绑定服务端会校验版本号是否在允许范围内。你把某次抓到的版本号写死短期内能用站点一发版就失效。指纹同理它可能包含 UA、屏幕分辨率、时区、语言等服务端不一定逐项校验但某些字段变了会触发风控。常见的实现思路是指纹采集逻辑会在页面加载时执行一次结果存进运行时对象签名时直接读。这里的设计取舍值得琢磨站点为什么要把版本号和指纹揉进签名因为这两项能有效区分「真实浏览器里的正常请求」和「脚本批量构造的请求」。版本号保证你用的是当前前端逻辑指纹保证请求来自一个有状态的页面环境。对逆向的人来说这意味着纯算法层面的复现不够还得模拟出一个合理的运行时上下文。我的做法是把指纹采集逻辑单独抽出来跑一遍拿到和真实页面一致的值再喂给签名函数而不是拍脑袋填默认值。4. 避坑与排查签名算对了为什么还是被拒4.1 现象本地算出的 h5st 长度对但服务端返回校验失败原因通常是拼接顺序或分隔符和服务端预期不一致。混淆代码里分隔符可能是从字符串数组取的你还原常量表时漏了一项拼出来的原始串结构就错了。解决方法是把最终参数按分隔符拆开逐段比对长度和字符集再对照还原后的常量表确认分隔符。我一般会在签名函数返回前打印raw和抓包得到的明文段做逐字符对比差异一眼就能看出来。4.2 现象时间戳和随机数都实时生成了仍然偶发失败原因多半是请求体摘要不一致。JSON.stringify对键顺序敏感你构造的对象键顺序和页面实际发送的不一样摘要就不同。另外有些站点会对 body 做额外处理比如去掉空值、统一编码你没跟上就会翻车。解决办法是直接抓取页面实际发送的 body 原文用它算摘要而不是自己重新序列化。这一步的教训是签名链路里任何一处「我以为一样」都可能是失败点。4.3 现象断点打上了但签名函数不执行原因是签名模块是异步 chunk 加载的断点下早了模块还没被 require 进来。解决办法是在__webpack_require__.e或 chunk 加载完成的回调里下断点等模块实例化后再跟进签名函数。另一个可能是签名逻辑被放在了 Worker 里主线程断点抓不到需要在 Worker 的源码面板里单独下断点。4.4 现象常量表还原后索引仍然对不上原因是数组做了运行时旋转你 dump 的时机在旋转之前或之后不一致。解决办法是在旋转逻辑执行完毕后立即 dump或者在控制台直接读取运行时数组的当前状态而不是读源码里的初始定义。确认方法是随便取一个索引用运行时数组取值和代码里预期值比对一致了再批量还原。4.5 现象指纹字段填了默认值请求被风控拦截原因是服务端对指纹做了合理性校验UA 和屏幕尺寸、时区不匹配会触发异常判定。解决办法是把指纹采集逻辑完整跑一遍拿到和真实浏览器环境自洽的一组值而不是东拼西凑。这里没有捷径指纹的各项之间有关联性单独改一项反而更容易暴露。5. 设计取舍为什么这类签名机制值得从运行时层面理解把 h5st 拆到这个程度会发现它的防护思路并不神秘用 webpack 打散代码结构用字符串数组和旋转抬高静态分析成本用运行时上下文把签名和页面状态绑定用版本号制造时效性。这套组合的取舍很清晰——它不追求密码学意义上的不可破解而是追求「让批量复现的成本高于收益」。哈希算法是公开的拼接规则一旦被还原也不难复刻真正起作用的是那些必须实时采集的运行时值。这也解释了为什么单纯抄一个签名函数往往跑不通。函数只是链路的一环它依赖的上下文分散在模块图各处依赖的 body 序列化方式藏在业务代码里依赖的版本号随发版变化。理解这一点逆向的重心就从「找算法」转向「还原数据流」先 dump 常量表再定位签名模块再顺着变量反查采集点最后把整条链路在本地复现并和抓包结果对齐。这个顺序比盲目读混淆代码高效得多。验证方法上我习惯做两件事一是把本地算出的raw明文段和抓包得到的明文段逐字符比对确认拼接结构一致二是用同一组输入多次调用签名函数观察哪些段变化、哪些段稳定变化的段就是实时生成的稳定的段就是上下文读取的。这两步做完链路基本就摸清了。至于指纹和版本号这类会随环境变化的字段我的做法是保留采集逻辑而不是写死值宁可多跑一步采集也不留一个随时会失效的常量。从那以后我每次碰这类 webpack 打包的签名参数都强制先 dump 常量表、再打印模块缓存、最后才读逻辑顺序不乱。这套习惯帮我省下了大量在混淆代码里打转的时间也让我少踩了几次「算法抄对了但上下文错了」的坑。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。