ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

webpack逆向实战:解开h5st签名生成核心流程

webpack逆向实战:解开h5st签名生成核心流程 简介某东平台webpack方式的h5st逆向破解完整代码是一份面向爬虫开发者和前端安全研究者的实战资源聚焦移动端h5st参数的生成机制示范如何从webpack打包产物中定位核心加密逻辑并通过补环境、断点调试等方式还原整体流程最终用Python和JS协同完成参数生成适合已经掌握基础爬虫技能、正在寻求突破加密参数瓶颈的读者。压缩包共2个文件包含1个Python脚本和1个JS脚本文件总量仅159KB体积精巧便于逐行阅读和二次修改Python脚本负责请求逻辑与参数拼接JS脚本负责加密核心两者配合可直接用于目标站点的数据采集。目前已有852人学习下载内容具有较好的参考性。借助该代码包读者既能获得一份完整可运行的逆向代码也能学习到webpack类加密站点的通用分析方法从而举一反三地迁移到其他类似场景同时应牢记逆向学习需合规使用仅限技术交流与安全测试。1. 为什么说h5st逆向的突破口在webpack做某东Web端接口的人几乎都会撞上同一个拦截点页面里明明一次请求就带上了签名参数换成自己的脚本后同样的URL和请求体却直接被风控打回。盯着Network面板看多出来的那个参数就是h5st。这个签名的生成逻辑被打包进webpack构建产物里分布在若干异步chunk中直接搜字符串搜不到直接调函数也调不到。标题里说的webpack方式逆向本质是先把webpack的模块运行时还原出来再从模块堆里定位算法、复现签名。这篇文章解决的是三件事第一看懂h5st在webpack产物里的存在形态第二用静态搜索加动态Hook把核心模块从混淆代码里捞出来第三在Node.js里补一个能跑的环境把完整代码落地成可调用的签名函数。适合正在做接口自动化、电商数据采集或Web前端安全验证的工程师。下面按我实际操作的顺序来写。2. 拆开某东h5st的签名结构先读懂再动手2.1 抓包里常见的h5st字段形态h5st在请求里通常以一个下划线拼接的字符串出现形如4.0_3f2a1c_1720000000000_8f9d3a2b...。不同版本前缀和字段数量不一样但核心构成一般可以分成四段算法标识、随机因子、生成时间戳、签名内容。这个串本身不复杂复杂的是它生成的依赖链。我在分析时习惯先做一个字段拆解表格把每一段对应到抓包值再倒推它是怎么来的。分段示例命名说明4.0版本号算法版本v3/v4常见3f2a1c随机数/指纹标识本次签名的随机种子1720000000000生成时间毫秒级Unix时间戳8f9d3a2b...签名摘要由参数、时间戳、密钥计算而来先确定这个串里的时间戳是否和请求时间吻合再确认随机因子是否会随每次调用变化。如果随机因子稳定不变说明它是设备指纹的压缩值如果每次都变说明只是防重放的nonce。这一步判断决定了后面要不要去追指纹算法。2.2 webpack产物为什么让签名隐形某东的Web端构建产物用的是webpack并且做过打包优化配置把公共依赖和异步路由拆成了大量小chunk。签名算法不会出现在入口文件里而是被包装成模块藏在某个异步chunk的模块数组中。webpack的核心机制是一个自执行函数内部维护__webpack_require__来加载模块。所有模块都被包裹成函数塞进一个对象数组里。由于模块ID往往是数字或压缩后的短字符串直接搜h5st、signature这类关键词基本搜不到算法本体。此外打包时开启了代码压缩和变量混淆函数名、变量名都被替换为短名称。即使定位到模块函数里面的逻辑也很难直接阅读。所以第一步不是读混淆代码而是先把模块边界画出来。2.3 h5st的典型生成链路从功能上拆h5st的生成链路通常包含四个环节收集参数参与签名的请求参数、时间戳、随机数生成指纹读取UA、屏幕、Canvas等浏览器环境信息构造待签名串把参数按固定顺序拼接执行摘要算法对拼好的字符串做HMAC或MD5类运算输出签名。其中环境指纹部分最依赖宿主浏览器也是后续在Node.js里最容易报错的地方。很多模块在Node.js下缺了window或document就直接抛异常原因就是这里。我的处理顺序是先不碰指纹生成段优先把算法主流程抠出来用固定的指纹串喂进去跑通。等到签名对不上时再回头处理指纹部分。这样能减少环境依赖带来的干扰。3. 定位签名核心模块从静态搜索到动态Hook三板斧3.1 用静态特征把目标压缩到几十个模块打开抓包工具找到任意一条带h5st的请求复制整个请求URL和POST body。然后去webpack产物里搜这些业务参数名比如appkey、timestamp、sign等。签名算法必然要读取这些变量名或字符串常量因此这些特征是天然的入口。我一般会用Chrome的Sources面板做全局搜索或直接把chunk文件下载到本地用grep扫grep -o appkey[^,}]* ./static/js/*.js | head -20 grep -l h5st ./static/js/*.js提示搜h5st可能搜不到因为变量名被混淆了但业务参数名通常保留因为服务端要按名字取参。如果连参数名也被映射成短名就退一步搜算法特征比如HmacSHA256、MD5、CryptoJS、createHash。命中后打开对应文件通过webpack的模块括号匹配把包含算法调用的模块函数范围标记出来。这一轮的目标不是读懂逻辑而是把候选文件从几百个缩小到个位数。3.2 用Hook验证签名生成的真实时机静态搜索只能定位文件真正确认模块位置要靠运行时观察。签名生成后一定会经过JSON.stringify或赋值操作。在浏览器里注入Hook脚本可以捕获待签名字符串和结果值。// hook JSON.stringify捕获包含sign/h5st的对象序列化内容 const originStringify JSON.stringify; JSON.stringify function (...args) { const target args[0]; if (target typeof target object) { try { const keys Object.keys(target); if (keys.some(k k.includes(sign) || k.includes(h5st) || k.includes(st))) { console.log([Hook JSON.stringify], JSON.parse(JSON.stringify(target))); debugger; } } catch (e) {} } return originStringify.apply(this, args); };这段脚本的逻辑是拦截所有JSON.stringify调用检查被序列化对象的键名是否和签名相关。第一次运行后触发一次页面内的正常请求脚本会在签名生成的位置断住。断住后看调用栈就能看到是哪个模块函数调用的序列化。另一种方式是Hookwindow对象上某个已知的赋值操作。如果h5st最终被写到一个请求头或某个全局变量上可以在defineProperty层面捕获写入// hook 全局对象上的sign属性写入 const originDefineProperty Object.defineProperty; Object.defineProperty function (obj, prop, descriptor) { if (prop.toLowerCase prop.toLowerCase().includes(sign)) { console.log([Hook defineProperty], prop, descriptor.value); debugger; } return originDefineProperty.call(this, obj, prop, descriptor); };Hook阶段的产出是一个模块ID和一个调用点而不是最终代码。拿到调用栈后顺着栈里的函数名进到Sources面板右键选择Pretty print恢复缩进后再定位当前函数。3.3 把依赖链和ModuleID拉出来webpack模块之间用__webpack_require__互相引用。核心签名模块往往会require一些辅助模块比如指纹生成、字符编码、加密库。这些依赖模块可能分散在多个chunk文件里。我确定核心模块后会在控制台查看webpack运行时暴露的内部对象// webpack 5 中模块对象通常挂在 __webpack_require__.m // 通过模块ID查找对应函数体 const mod __webpack_require__.m[3972]; console.log(mod.toString().slice(0, 500));浏览器控制台里执行后能看到该模块的函数体内容。如果看不到说明模块在当前chunk加载完成后才注册需要在Network面板确认chunk文件名后再执行上面命令。对于目标模块记录下它引用的所有__webpack_require__(xxx)模块ID逐个执行上面的查找命令形成一张依赖清单。依赖清单里既有加密算法库也有环境探测函数。保存这组模块ID后续扣代码时只需要把这些模块函数复制出来不需要整包拷贝。4. 完整的webpack方式h5st复现代码补环境、调参数、跑通4.1 在Node.js里重建webpack运行时扣出来的模块函数默认依赖webpack的运行环境。在浏览器里__webpack_require__内嵌在自执行函数中无法直接在Node.js环境调用。常见做法是自己写一个mini版的运行时把扣出的模块函数喂进去。下面是我常用的一个加载器骨架用于在Node.js里加载webpack模块// mini-webpack-runtime.js // 作用模拟webpack运行时加载扣出来的模块函数 const modules { // 从浏览器控制台复制出来的模块ID到函数体的映射 // 3972: function(module, exports, __webpack_require__) { ... } }; const cache {}; function __webpack_require__(moduleId) { if (cache[moduleId]) { return cache[moduleId].exports; } const module { exports: {} }; cache[moduleId] module; modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); return module.exports; } // 入口调用 module.exports { modules, __webpack_require__ };这段代码的逻辑很简单维护一个模块缓存调用过的模块不会重复执行模块函数内部通过__webpack_require__去加载其他模块。把浏览器里拿到的模块ID和函数体粘贴进modules对象即可。参数说明moduleId要和浏览器里的数字或短ID保持一致否则会报Cannot find modulemodule.exports初始为空对象模块内部一般会往它上面挂东西依赖链里如果出现__webpack_require__.ab、__webpack_require__.p这类公共路径属性也要一并补上最简单的方式是把这些属性直接挂到函数上。以一个实际的扣码场景为例核心签名模块可能依赖一个叫utils的辅助模块。需要把utils的模块函数也复制进modules对象否则运行时会因为找不到xxx模块中断。4.2 补window与document等宿主环境某东h5st的签名算法里只要涉及指纹或环境探测就会访问浏览器全局对象。在Node.js里这些对象不存在需要做一层轻量stub。我一般不用jsdom这类重型库而是先跑一次看报错点缺什么补什么遇到ReferenceError: xxx is not defined就在脚本顶部补一个同名变量。这样能保证最小依赖。// env-stub.js // 在Node.js里提供最小可用环境 global.window global; global.window.navigator { userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..., language: zh-CN, platform: Win32, vendor: Google Inc. }; global.navigator global.window.navigator; global.window.document { documentElement: { getAttribute: () null }, createElement: () ({ getContext: () null, style: {} }), getElementsByTagName: () [], cookie: }; global.document global.window.document; global.location { href: https://order.example.com/checkout, host: order.example.com }; global.screen { width: 1920, height: 1080, colorDepth: 24 };这段补的是最常见的五个全局对象window、navigator、document、location、screen。很多签名函数内部只用到了navigator.userAgent和document.cookie补完这两项就能跑过环境检测。注意有的版本会深度遍历document对象属性逐层展开getElementById、querySelector等方法。如果报错指向document的某个方法就在上面代码里补对应函数返回合理默认值即可。4.3 可复现的h5st签名调用示例完成运行时和环境补丁后可以封装一个对外可用的签名函数。下面的示例假设签名模块暴露为getH5ST// sign.js const { modules, __webpack_require__ } require(./mini-webpack-runtime); require(./env-stub); // 确定入口模块ID提前在浏览器里定位到的核心模块 const SIGN_MODULE_ID 3972; const getH5ST __webpack_require__(SIGN_MODULE_ID); function buildH5st(params) { const t Date.now(); // 参与签名的请求参数对象字段顺序与页面一致 const signData { appKey: params.appKey, timestamp: t, body: params.body || , token: params.token || }; // 调用核心模块生成签名 const h5st getH5ST(signData); return { h5st: h5st, timestamp: t, version: 4.0 }; } module.exports { buildH5st };这个脚本的调用逻辑是传入业务参数和时间戳让签名模块生成最终签名字符串。SIGN_MODULE_ID的值需要在浏览器定位阶段确认不同版本号不一样替换成实际值即可。参数说明appKey是固定的应用标识从页面源码或抓包里的公共参数里拿timestamp建议统一用Date.now()生成避免与页面时间戳差异过大body是参与签名的请求体原串如果抓包看到签名结果是基于请求体计算的必须把请求体序列化字符串原样传入。跑通后的验证方式是把生成的h5st放回抓包工具里替换掉原请求的h5st值重放一次。返回200说明签名内容正确返回风控错误则继续检查时间戳和指纹部分。5. 避坑h5st逆向中常见的五个翻车现场5.1 全局搜索h5st结果什么都搜不到现象把chunk文件全部拉下来搜索h5st字段名结果零命中。原因签名模块内部把结果赋给了变量字段名通过对象字面量动态生成或者在字符串拼接中完成导致源码里没有完整的h5st字样。解决放弃搜字段名改搜业务参数名和算法特征。优先搜appKey、timestamp、sign、HmacSHA256、MD5等。这类名称无法混淆因为服务端要对齐。命中后查看所在模块的引用关系再回到运行时Hook。5.2 扣出来的模块在Node.js里运行直接抛错现象粘贴完模块函数后执行加载器报ReferenceError: window is not defined或者Cannot read properties of undefined。原因签名模块依赖了浏览器全局对象但Node.js环境下不存在。解决先用最小stub补window和navigator再按报错信息逐层补齐。Console里抛错一定要看完整堆栈堆栈比报错文本更有用。补环境时不要一次性补全套否则后面排查分不清是环境问题还是算法问题。5.3 签名算出来了但请求仍被拦截现象用自己生成的h5st替换到抓包里服务端返回风控拦截。原因服务端不只校验签名本身还校验签名请求里的时间戳、随机数、请求体hash是否与签名内容一致。某一个字段对不上就会整包拦截。解决重新核对待签名串的拼接顺序。很多情况下是字段顺序按字典序排序而自己按了页面参数顺序拼接导致签名字符串与算法预期不一致。解决办法是回到Hook断点在JSON.stringify处打印完整的待签名对象对比字段顺序逐字段修正自己的构造参数。5.4 算法版本一变之前的代码直接失效现象隔几周后同一段代码生成的h5st在线上开始报错抓包发现字段结构变化了。原因平台升级了签名版本模块ID和算法入口都变了。解决把旧的模块映射表存档重新执行一次静态搜索加Hook定位找到新的签名模块ID。基于webpack方式的逆向我一般会保留每次的模块ID清单和chunk文件版本号换版后对照差异替换核心模块即可运行框架不用改。5.5 时间戳细微偏差导致签名无效现象本地生成签名后立即重放请求偶尔成功偶尔失败失败时提示签名超时。原因签名里嵌入的timestamp与请求发生时服务端接收到的时间差太大。本地时钟不准或签名生成时间与请求发送时间不一致。解决签名函数里统一使用同一份时间戳签名生成和请求发送之间不要有跨秒操作。如果脚本里有重试逻辑重试时重新生成签名不要把旧签名复用到底。另一个方法是检查时间戳位数h5st时间戳通常为13位毫秒级如果错用10位秒级也会被判定无效。6. 签名还原后如何验证是对的参数一致性检查与行为层确认签名能跑通并不代表逆向完成我一般会做三层验证逐层排查潜在隐患。验证层级检查内容通过标准参数层同一参数下签名是否可重放抓包原值与还原值长度、字段结构一致请求层替换h5st后请求是否成功服务端返回业务数据而非风控拦截环境层连续多次请求是否稳定不同参数生成的签名均能通过第一层是基础验证确认算法整体正确第二层是关键验证确认签名能在真实请求里被接受第三层是稳定性验证排除掉指纹模块偶发异常问题。如果第三层失败说明环境指纹的生成逻辑还没完全还原需要回到env-stub.js里补充更多浏览器特征值。我习惯在完成代码后写一个自检脚本批量测试多次生成的签名观察字段是否各段独立变化// verify.js const { buildH5st } require(./sign); for (let i 0; i 5; i) { const result buildH5st({ appKey: demo, body: page1count20 }); console.log(result.timestamp, result.h5st); // 手动确认时间戳递增、签名段每次变化、版本段固定 }这里会看到每次输出的随机因子和签名段都会变化只有版本段不变说明签名函数在按预期工作。如果连续多次签名完全一致说明随机因子没参与计算防重放能力弱这种签名即使能通过一次请求也很容易在后续被风控识别出异常。最后说一个我的习惯每次做完一个版本的签名还原我会把模块ID和依赖清单截图存档并在代码注释里标明对应的chunk文件名。线上版本一更新直接比对清单就能知道变动的范围不用从头再挖一遍。希望帮到你。本文还有配套的精品资源点击获取
返回列表