饿了么H5端sign与bxet参数逆向:JS混淆破解与环境模拟实战

发布时间:2026/7/22 11:39:03
饿了么H5端sign与bxet参数逆向:JS混淆破解与环境模拟实战 1. 项目概述一次典型的Web端加密参数逆向之旅最近在分析一些主流应用的H5端时饿了么的登录和部分核心接口的加密参数引起了我的兴趣。具体来说是名为bxet和sign的两个参数。这几乎是现代Web应用安全对抗的标配场景前端通过JavaScript生成一串“密码”随请求发送给服务器服务器用同样的逻辑验证以此判断请求是否合法、是否被篡改。逆向这两个参数本质上是在理解前端的安全逻辑这对于安全研究、合规测试乃至学习现代Web加密技术都很有价值。整个过程就像在解一个由JavaScript编写的动态谜题你需要找到生成这些参数的代码片段插桩定位然后模拟出代码运行所需的所有环境环境补全最终在独立的环境中复现整个生成逻辑。这次实战的目标很明确不依赖浏览器环境纯用Node.js或Python脚本稳定生成出与饿了么H5端完全一致的bxet和sign参数。这不仅仅是“找到算法”更是对前端混淆、环境检测等对抗手段的一次系统性拆解。无论你是从事爬虫开发遇到了加密壁垒还是做安全审计需要评估前端代码强度亦或是单纯对JavaScript逆向感兴趣这次从“插桩”到“环境补全”的完整流程都能提供一套可复用的方法论。2. 逆向目标与核心思路拆解2.1 目标参数bxet与sign的定位与作用首先得弄清楚我们要对付的是什么。通过浏览器开发者工具的网络请求面板很容易在饿了么H5端通常是h5.ele.me域名下的登录或关键API请求中找到这两个参数。它们通常出现在请求头或请求体里。sign参数非常常见它的作用是签名。前端会将请求的某些部分比如URL路径、查询参数、时间戳、请求体等按照一定规则拼接成一个字符串然后通过一个密钥可能硬编码在JS里也可能动态获取和特定的哈希算法如MD5、SHA系列或自定义算法计算出一个摘要值这就是sign。服务器收到后用同样的规则和密钥计算一遍如果结果一致就认为请求是合法且未被篡改的。它的核心是防篡改和身份验证。bxet这个参数名看起来像是自定义的经过分析它很可能是一个包含时间戳、随机数或会话特定标识的令牌用于防重放攻击。也就是说同一个bxet不能被重复使用服务器会记录或校验其有效性。它可能由sign算法生成也可能是另一套独立的逻辑但通常也会和当前时间、设备或用户的一些指纹信息绑定。我们的逆向工作就是要找到生成这两个字符串的那几行核心JavaScript代码并理解其依赖的所有输入和上下文环境。2.2 逆向总体策略插桩定位与环境模拟面对经过混淆、压缩、可能还有代码保护的JavaScript文件直接阅读是不现实的。我们的策略分为两步走这也是目前应对这类问题最有效的方法论。第一步是插桩定位。我们不会去大海捞针般地搜索整个JS文件。而是利用浏览器强大的开发者工具在关键位置“埋下探测器”。具体来说就是使用“XHR/Fetch断点”功能。在开发者工具的Sources面板中我们可以针对包含特定关键词如sign或bxet的URL请求设置断点。当浏览器发起这类网络请求时代码执行会立即暂停。此时调用栈会清晰地展示出是哪个JavaScript函数最终发起了这个包含加密参数的请求。我们顺着调用栈向上回溯就能一步步逼近生成这些参数的函数。在这个过程中我们还需要熟练使用“Hook”技术比如通过重写XMLHttpRequest.prototype.send或fetch方法在请求发出前拦截并打印参数这能帮助我们更早地捕获到参数生成后的原始状态。第二步是环境补全。找到生成函数只是开始。这些函数往往严重依赖浏览器环境比如window、document、navigator对象以及一些特定的Web API。当我们将这段代码抠出来试图在Node.js这样的无头环境中运行时会报大量的“xxx is not defined”错误。环境补全就是为我们的脚本伪造一个足以“骗过”加密代码的浏览器环境。这不仅仅是定义几个空对象那么简单我们需要仔细分析原代码中到底用到了环境的哪些属性、哪些方法这些方法被调用时的行为是什么然后一一用JavaScript模拟实现。有时候一些复杂的属性如navigator.userAgent或方法如window.crypto.getRandomValues需要精确还原。这个过程考验的是耐心和细致程度。3. 实操第一步动态插桩与关键代码定位3.1 浏览器开发者工具的高级用法工欲善其事必先利其器。现代浏览器的开发者工具是我们逆向的“手术刀”。打开饿了么H5页面后按F12进入开发者工具。首先清空网络请求记录然后进行一次触发目标API的操作比如点击登录。在网络面板中找到目标请求通常是api或passport子域名下的请求仔细查看其请求头Headers和负载Payload确认bxet和sign的位置。记住这个请求的URL特征比如路径中包含login或sign关键字。接下来进入Sources面板。在右侧的“XHR/Fetch Breakpoints”处点击“”号添加一个断点。由于我们不知道精确的URL可以输入一个部分路径如sign。这样任何URL中包含sign字符串的请求都会被断住。这是一种非常高效的过滤方式。注意有些应用可能会使用动态生成的URL或者将参数放在请求体中。此时使用关键词断点可能不灵。备用方案是使用“Event Listener Breakpoints”勾选“XHR” - “readystatechange” 或 “load” 事件但这会断住所有XHR请求噪音较大需要结合调用栈仔细筛选。3.2 调用栈分析与算法函数追踪设置好断点后再次触发请求。浏览器会立刻暂停并高亮显示在发起网络请求的那一行代码上。此时注意力要转向右侧的“Call Stack”调用栈面板。调用栈展示了从当前断点位置一路回溯到最初入口的函数调用链。最上面是当前暂停的位置通常是send或fetch越往下则越是底层、越是接近业务逻辑和加密逻辑的地方。我们的目标不是看最顶层的库代码而是寻找属于饿了么业务域的、名字可能被混淆但结构相对清晰的函数。点击调用栈中的不同层级观察代码区的变化。寻找那些将bxet和sign作为参数赋值或计算的代码行。通常在发起请求之前会有一个函数负责组装所有请求参数其中就包括计算签名。这个函数可能就是我们的目标。找到可疑函数后不要急于跳出。可以在该函数体的第一行打上断点然后取消全局的XHR断点重新触发请求。这样代码会直接在我们关心的函数入口处暂停。接下来使用“Step Over”F10、“Step Into”F11逐行执行并观察右侧“Scope”面板中局部变量的变化。重点关注那些参与字符串拼接、调用CryptoJS方法、或执行类似md5、hex操作的代码行。实操心得逆向时浏览器的“Watch”表达式功能非常好用。你可以将疑似计算sign的表达式比如e t r添加到Watch中实时查看其值的变化。当最终生成的sign值出现时对比Watch中的中间值能快速验证你的猜想。3.3 代码提取与初步格式化一旦锁定了生成bxet或sign的核心函数可能是一个几十行到几百行的函数就需要将其提取出来。在Sources面板中找到这个函数所在的JS文件右键选择“Save as…”保存到本地。或者更直接的方法是在代码区选中这个函数及其所有依赖的内部函数通过查看调用关系复制到一个新的文本文件中。复制出来的代码通常是压缩混淆过的变量名都是a, b, c, d可读性极差。我们需要使用工具进行初步的格式化。这里推荐使用浏览器开发者工具自带的“Pretty print”功能那个{}图标。如果代码已经提取到本地可以使用在线工具或VS Code插件如Prettier进行格式化。格式化后代码结构会清晰很多虽然变量名还是无意义的但if/else、for循环、函数调用等结构一目了然为后续分析打下了基础。4. 核心逆向技术JavaScript混淆分析与算法还原4.1 常见混淆手段识别与应对饿了么作为大厂其前端代码必然经过了高级的混淆处理。识别这些手段是逆向的前提。标识符混淆这是最基本的将函数名、变量名替换为短字符。应对方法不要尝试理解每个a、b的意思而是关注数据流和控制流。这个变量从哪里来传递给了哪个函数最终影响了哪个输出控制流平坦化这是最强的混淆之一。它将原本顺序执行的代码块打乱放入一个巨大的switch-case或while循环中由一个“分发器”来控制下一个执行哪个块。代码看起来像一个状态机极其难读。应对方法寻找“分发器”变量通常是一个自增的索引并尝试手动或使用工具如de4js进行反平坦化还原出原本的代码执行顺序。字符串加密代码中出现的明文字符串如API路径、密钥会被加密存储在运行时通过一个解密函数动态还原。你会在代码中看到大量的atob()、fromCharCode或自定义的解密函数调用。应对方法找到解密函数将其提取出来在Node.js中运行它就能得到所有明文字符串。这是关键一步因为密钥往往就藏在其中。环境依赖检测代码会检查window、document、navigator等对象是否存在或检测某些属性是否被重写。如果环境不对可能进入错误的逻辑分支或直接报错。这就是我们下一步“环境补全”要解决的核心问题。4.2 算法逻辑静态分析技巧面对格式化后的代码我们需要像侦探一样梳理逻辑。以下是一些实用技巧从输出倒推输入我们已经知道了sign的最终值从网络请求中捕获。在代码中搜索这个值的生成位置。它很可能是一个哈希值32位或64位十六进制字符串。向上查找看它是哪个变量的toString(‘hex’)或类似操作的结果。定位加密库搜索CryptoJS、MD5、SHA、Hmac等关键词。如果使用了CryptoJS通常会看到CryptoJS.MD5(…).toString()这样的调用。确认使用的具体算法。分析参数组装找到计算哈希的输入数据。它通常是由多个部分拼接而成的一个字符串。仔细分析拼接的每一个部分可能是固定的字符串、时间戳、随机数、请求体JSON的字符串化、甚至是另一个哈希值。用注释在代码中标记出每一部分的来源和含义。bxet的特殊性bxet可能是一个基于时间戳和随机数的编码结果也可能是一个独立的哈希。分析其生成逻辑看它是否依赖于设备指纹、用户令牌等。注意事项在分析过程中强烈建议使用一个简单的Node.js脚本将你怀疑的核心函数复制进去并尝试用你推测的输入参数运行它。通过不断调整输入和对照真实请求的输出可以快速验证你的分析是否正确。这是一个“假设-验证”的循环过程。4.3 关键常量与密钥的提取算法逻辑理清后最关键的就是提取那些“常量”。这包括加密密钥如果使用了HMAC算法必然有一个密钥。它可能以加密字符串的形式硬编码在代码里需要你用找到的解密函数去解密。盐值在拼接字符串时可能会加入固定的“盐值”以增加复杂度。固定路径或参数名参与签名的URL路径或参数名列表。魔术数字一些用于位运算或计算的固定数值。将这些常量提取出来妥善保存。它们是复现算法的“配方”中的固定配料。5. 攻坚克难浏览器环境模拟与补全实战5.1 Node.js中缺失的Web API模拟当我们把分析好的算法函数放到Node.js中运行时第一个拦路虎就是环境缺失。Node.js没有window、document、navigator等浏览器对象。解决方案是“造假”。我们不需要实现完整的Web标准只需要实现算法函数所用到的那部分。创建一个名为env.js的文件开始补环境// env.js - 模拟浏览器全局对象 global.window global; // 简单地将window指向global global.document { documentElement: { clientWidth: 1920, clientHeight: 1080 }, // 根据报错信息按需添加其他属性 createElement: function() { return {}; }, getElementsByTagName: function() { return []; } }; 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, // 模拟一个常见的UA platform: Win32, language: zh-CN, // 可能用到的其他属性 appVersion: 5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, vendor: Google Inc. }; global.location { href: https://h5.ele.me, protocol: https:, host: h5.ele.me }; // 模拟 localStorage 和 sessionStorage (通常只是简单的键值对象) global.localStorage { _data: {}, getItem: function(key) { return this._data[key] || null; }, setItem: function(key, value) { this._data[key] value; }, removeItem: function(key) { delete this._data[key]; } }; global.sessionStorage { ...global.localStorage }; // 简单复制然后在你的主算法脚本开头通过require(‘./env.js’)引入这个环境文件。5.2 复杂对象与方法的精细模拟有些环境依赖没那么简单。例如算法可能用到了window.crypto或window.crypto.getRandomValues来生成随机数。在Node.js中我们可以用crypto模块来模拟。// 在 env.js 中继续补充 const nodeCrypto require(crypto); global.crypto { getRandomValues: function(array) { // 用Node.js的crypto.randomFillSync填充传入的TypedArray const bytes nodeCrypto.randomBytes(array.length); for (let i 0; i array.length; i) { array[i] bytes[i]; } return array; }, // 模拟 subtle 对象如果用到Web Cryptography API的话可能性较低但需留意 subtle: {} };另一个常见难点是window.btoa和window.atobBase64编码解码。Node.js中没有但可以轻松模拟global.btoa function(str) { return Buffer.from(str, binary).toString(base64); }; global.atob function(b64Encoded) { return Buffer.from(b64Encoded, base64).toString(binary); };实操心得补环境是一个“运行-报错-补充”的迭代过程。不要试图一次性补全所有。先运行你的算法脚本看它报什么错说哪个对象或属性未定义然后就去env.js里补上。补的时候尽量让模拟的属性或方法返回一个符合预期的类型值如对象、函数、字符串即使功能不完整有时也能骗过简单的typeof检测。5.3 对抗环境检测与反调试技巧高级的混淆代码会进行主动的环境检测和反调试。检测开发者工具有些代码会检测window.outerHeight与window.innerHeight的差值控制台打开会影响这个值或者重写console.log等。在我们的Node.js环境中这些检测通常不成立但需要确保我们的模拟对象不会因为缺少某些属性而触发异常。如果算法代码里有明显的if (typeof window.console ! ‘undefined’)之类的检测确保你的模拟对象结构与之匹配。检测代码执行时间通过Date.now()计算函数执行时间如果过短说明可能是在无头环境或模拟环境中运行则返回错误结果。应对方法在Node.js中我们很难模拟真实浏览器的执行速度。如果遇到可能需要分析其时间阈值并考虑在关键函数入口和出口人为增加延迟不优雅但有时有效或者更彻底地尝试通过修改本地保存的JS代码绕过这个时间检测逻辑。Function构造函数与eval混淆代码可能使用new Function(‘return this’)()来获取全局对象或者用eval执行动态代码。在Node.js的vm模块或补全的window上下文中需要确保this的指向正确。面对极其复杂的对抗终极手段是使用一个“接近真实”的环境来执行代码比如Puppeteer或Playwright。它们可以启动一个完整的、受控的Chromium浏览器。你可以将关键的、难以模拟的JS代码注入到页面上下文中执行并获取结果。这相当于“打不过就加入”但代价是运行效率远低于纯Node.js脚本。在本项目中我们的目标是纯Node.js实现因此会优先选择补全环境这条路只有在遇到无法逾越的、极度依赖浏览器渲染引擎的检测时才考虑降级使用无头浏览器方案。6. 算法复现与参数生成脚本编写6.1 基于Node.js的加密算法实现假设我们已经成功提取了算法逻辑并确定sign是使用MD5算法对特定字符串进行哈希得到的。在Node.js中我们不需要重新实现MD5直接使用内置的crypto模块即可。首先将分析得到的参数拼接逻辑翻译成Node.js代码。例如如果原逻辑是// 伪代码源自混淆后的JS分析 function generateSign(params, timestamp, secretKey) { var str params.join() timestamp secretKey; var hash CryptoJS.MD5(str).toString().toUpperCase(); return hash; }那么对应的Node.js实现如下const crypto require(crypto); function generateSign(params, timestamp, secretKey) { // 1. 参数排序与拼接 (根据实际分析结果调整) // 假设需要按键名排序后拼接成 k1v1k2v2 格式 const sortedParams Object.keys(params).sort().map(key ${key}${params[key]}).join(); // 2. 组装待签名字符串 (根据实际分析结果调整拼接顺序和分隔符) const signString ${sortedParams}${timestamp}${secretKey}; // 3. 计算MD5 (如果原代码是hex输出且大写) const hash crypto.createHash(md5); hash.update(signString, utf8); // 注意编码可能是utf8或latin1 const sign hash.digest(hex).toUpperCase(); return sign; } // 示例使用 const mockParams { phone: 13800138000, code: 123456 }; const mockTimestamp Date.now().toString(); const extractedSecretKey 从JS中提取出的密钥; // 替换为真实密钥 const sign generateSign(mockParams, mockTimestamp, extractedSecretKey); console.log(生成的sign:, sign);对于bxet如果它是一段Base64编码的字符串可能包含时间戳和随机数function generateBxet(timestamp, nonce) { const payload { t: timestamp, n: nonce, // ... 可能还有其他字段 }; const payloadStr JSON.stringify(payload); // Node.js中使用Buffer进行Base64编码 const bxet Buffer.from(payloadStr).toString(base64); // 注意有时Base64需要做URL安全处理替换/为-_ return bxet.replace(/\/g, -).replace(/\//g, _).replace(/$/, ); }6.2 时间戳、随机数与请求逻辑的同步在Web端时间戳通常是客户端当前的毫秒时间戳。服务器会校验这个时间戳如果与服务器时间相差太大比如超过5分钟请求会被拒绝。因此我们的脚本生成的时间戳必须是当前的、准确的时间。const timestamp Date.now(); // 毫秒时间戳 // 有时需要的是秒级时间戳 const timestampInSeconds Math.floor(Date.now() / 1000);随机数nonce用于防止重放。它需要是全局唯一或短时间内唯一。在Node.js中我们可以使用crypto.randomBytes生成。const crypto require(crypto); function generateNonce(length 16) { // 生成指定长度的随机字节并转为十六进制字符串 return crypto.randomBytes(length).toString(hex); } const nonce generateNonce();在组装最终请求时务必严格按照分析出的顺序先计算依赖时间戳和随机数的bxet然后将bxet和其他参数一起按照规则参与sign的计算。最后将bxet、sign、timestamp等参数一同放入请求头或请求体中。6.3 构建完整的请求示例将以上所有部分组合起来形成一个完整的、可发送请求的示例。这里以使用axios库为例const axios require(axios); const crypto require(crypto); // 假设我们已经有了 generateSign 和 generateBxet 函数 async function makeElemeRequest() { // 1. 准备基础参数 const timestamp Date.now(); const nonce generateNonce(); const secretKey YOUR_EXTRACTED_SECRET_KEY; // 替换 const apiPath /api/v1/login; // 替换为目标API路径 // 2. 生成 bxet const bxet generateBxet(timestamp, nonce); // 3. 准备请求参数 const requestBody { phone: 13800138000, code: 123456, bxet: bxet, timestamp: timestamp, // ... 其他必要参数 }; // 4. 生成 sign (注意sign的生成可能不包含sign自身) // 根据分析可能需要将 requestBody 的某些字段和 bxet、timestamp等一起签名 const sign generateSign({ ...requestBody, // 可能还需要包含 path 或其他固定参数 path: apiPath, }, timestamp, secretKey); // 5. 将sign加入请求参数或请求头 requestBody.sign sign; // 或者放在headers里 // 6. 发送请求 try { const response await axios.post(https://h5.ele.me${apiPath}, requestBody, { headers: { Content-Type: application/json, User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., // 可能还有其他固定headers } }); console.log(请求成功:, response.data); } catch (error) { console.error(请求失败:, error.response?.data || error.message); } } makeElemeRequest();7. 调试、验证与问题排查实录7.1 本地运行与线上请求对比验证脚本写好后最重要的步骤是验证。最直接的方法是与浏览器发出的真实请求进行对比。抓取真实请求在浏览器中清空网络记录执行一次操作抓取包含bxet和sign的请求。将请求的完整URL、Headers尤其是User-Agent、Content-Type、请求体全部记录下来。隔离变量在本地脚本中固定所有变量。将时间戳timestamp、随机数nonce设置为与抓取的请求中完全一致的值。其他所有参数也照搬。运行脚本执行你的Node.js脚本生成bxet和sign。逐字段对比将你生成的bxet、sign与抓取到的真实值进行比对。如果完全一致恭喜你算法复现成功如果不一致进入排查环节。7.2 常见不一致原因与排查步骤当生成的值与真实值不符时不要慌张这是逆向的常态。按照以下步骤系统排查第一步检查输入是否绝对一致。这是最常见的问题。确保你的脚本用于生成签名的每一个字符都与浏览器端完全一致。时间戳是毫秒还是秒是字符串还是数字抓包工具里看到的是什么格式脚本里就要用什么格式。参数顺序参与拼接的参数是否按照字母顺序或某种特定顺序排序多一个空格、少一个符号都会导致MD5结果天差地别。编码问题字符串编码是UTF-8还是Latin-1在Node.js的crypto.update()中明确指定。有时需要将字符串转为Buffer时指定编码。隐藏参数是否有一些隐式的、不在请求体里但参与签名的参数比如URL的路径部分/api/v1/login或查询参数?foobar。JSON字符串化如果参数是对象需要JSON.stringify。那么字符串化时键的顺序是否稳定默认的JSON.stringify键序可能不稳定虽然通常按定义顺序但规范未强制。安全起见可以自己实现一个按字母序序列化对象的函数。此外JSON.stringify是否去掉了空格默认会浏览器端用的JSON.stringify行为是否一致第二步检查算法细节。哈希算法你确定是MD5吗有没有可能是MD5之后又做了二次处理如截取部分字符、再哈希一次输出格式哈希结果是十六进制hex吗是大写还是小写是否是Base64编码密钥使用如果是HMAC密钥是否正确密钥是直接拼接还是作为HMAC的密钥使用crypto.createHmac(‘md5’, key)的用法是否正确第三步检查环境差异。补全的环境属性你的env.js是否模拟了所有算法依赖的浏览器属性某个属性返回的值类型如navigator.userAgent是字符串是否与真实浏览器一致可以尝试在浏览器控制台打印出算法函数中用到的所有环境变量值然后在你的脚本里硬编码这些值确保完全一致。全局变量算法是否依赖某个在浏览器全局作用域下定义的变量这个变量是否在你的Node.js脚本中被正确声明7.3 问题排查工具箱与技巧日志大法在你的算法函数中在每一个关键步骤后用console.log打印出中间变量的值。然后在浏览器的Sources面板中在相同的位置使用debugger或console.log对比两边的输出。这是定位差异最有效的方法。差分对比将你生成的待签名字符串和浏览器端的待签名字符串保存到两个文本文件使用diff工具或在线对比工具进行逐字对比很容易发现哪里多了个空格、哪里少了字符。简化测试如果原始算法很复杂尝试构造一个最简测试用例。例如固定所有参数只让一个参数变化看输出是否符合预期。这有助于排除干扰。社区与工具如果遇到无法解决的控制流平坦化或混淆可以尝试使用开源的JavaScript反混淆工具如de4js、jsnice等它们有时能提供更可读的代码。也可以在相关的技术社区搜索“饿了么 sign 逆向”等关键词看看是否有前人分享过经验但需要注意时效性和具体版本的差异。整个逆向过程就是一场与代码作者之间的精密博弈。从模糊的请求参数出发通过动态调试定位代码静态分析理解逻辑模拟环境克服障碍最终成功复现算法。每一步都需要耐心、细致和对JavaScript运行机制的深刻理解。当你第一次用自己的脚本成功生成出被服务器接受的bxet和sign时那种成就感便是对这份努力最好的回报。记住逆向的终极目的不是为了破坏规则而是为了理解系统运行的本质这份理解力在任何技术领域都是无价的财富。