JS逆向实战:链式补环境破解某东h5st签名加密

1. 项目概述:逆向某东最新版5.2.3的“补环境”实战

最近在逆向某东搜索接口时,遇到了一个典型的“环境检测”难题。目标接口的h5st参数,其生成逻辑被封装在一个复杂的JavaScript函数中,直接扣取代码在Node.js环境下运行,会因为缺少浏览器环境(如windowdocumentElement等对象)而报错。这就是所谓的“环境检测”,也是逆向工程中一个绕不开的坎。我这次的目标,就是针对某东最新版(版本号指向5.2.3)的加密逻辑,完成一套完整的“补环境”方案,让扣出来的JS代码能在Node.js中稳定运行并生成有效的h5st签名。

简单来说,“补环境”就是为Node.js这个“服务器环境”伪造一个足以“骗过”JavaScript代码的“浏览器环境”。这不仅仅是创建一个空对象那么简单,它涉及到对浏览器原生对象(如WindowDocumentElement)及其复杂原型链的精确模拟。如果补得不够像,代码要么直接报错,要么生成的签名长度、格式看起来都对,但服务器一校验就失败,这就是典型的“环境检测”没过。这次逆向的核心,就是通过“链式补环境”的方法,从根源上解决这个问题,构建一个足以通过检测的“虚拟浏览器”。

2. 逆向目标与核心挑战解析

2.1 目标接口与h5st参数

这次的目标非常明确:某东商品搜索列表页的异步数据接口。当你打开某东搜索页面,输入关键词后,页面会通过Ajax请求加载商品列表,这个请求中就携带了一个关键的加密参数——h5st。这个参数是一长串由分号分隔的字符串,包含了时间戳、随机值、签名等多种信息,是服务器验证请求合法性、防止爬虫的核心手段。

从抓包分析来看,h5st的生成逻辑完全在前端JavaScript中完成。这意味着,如果我们想在Python等后端语言中模拟请求获取数据,就必须复现这套JS加密逻辑。最直接的方法就是把生成h5st的JavaScript代码“扣”出来,放到Node.js中执行,然后由Python去调用。然而,扣出来的代码往往无法直接运行,因为它依赖浏览器提供的全局对象和API。

2.2 核心挑战:环境检测与原型链校验

直接运行扣出的JS代码,通常会遇到如下报错:

  1. ReferenceError: window is not defined
  2. ReferenceError: document is not defined
  3. TypeError: Cannot read properties of undefined (reading 'xxx')

初级解决方案是简单地在Node.js的global对象上挂载window = {}document = {}。对于早期或防护较弱的网站,这招可能管用。但像某东这样的大型平台,其反爬策略早已升级。它不仅仅检查windowdocument这些对象是否存在,还会深入检查它们的类型构造函数以及完整的原型链

例如,代码可能会执行Object.prototype.toString.call(document.createElement('div')),期望返回[object HTMLDivElement],而不是[object Object]。或者,它会检查element instanceof Elementelement.__proto__ === HTMLElement.prototype等关系。如果我们只是用普通对象{}来模拟,这些检查全部会失败,导致加密逻辑走入错误分支,或者生成一个无效的签名。

因此,本次逆向的难点不在于找到加密入口(通常通过搜索h5st关键字或sign等函数名可以定位),而在于如何构建一个足够逼真的浏览器环境,让扣出来的加密代码“相信”自己正在浏览器中运行,从而执行正确的加密路径。

3. 补环境的核心思路与框架设计

3.1 从“对象模拟”到“原型链构建”

传统的“补环境”是缺什么补什么,报windowwindow,报documentdocument。这种方法在应对深度检测时力不从心。我们需要转变思路:不是模拟对象,而是模拟“类”和“继承关系”

浏览器的DOM是一个复杂的继承体系。HTMLDivElement继承自HTMLElementHTMLElement继承自ElementElement继承自NodeNode继承自EventTarget,最终继承自ObjectdocumentHTMLDocument的实例,而HTMLDocument继承自DocumentDocument继承自Node

我们的补环境框架,就是要用代码在Node.js中重建这套继承体系。核心工具是一个自定义的createConstructor函数,它能够创建具有正确原型链关系的“构造函数”。这样,当我们用new HTMLDivElement()创建的“元素”,其__proto__链就能完美匹配浏览器中的真实对象。

3.2 关键工具函数:createConstructor与watch

整个补环境框架建立在两个核心函数之上:

1. createConstructor:构造器工厂这个函数是环境模拟的基石。它的作用是动态创建一个符合浏览器规范的构造函数。

function createConstructor(constructorName, enableStrictMode, propertiesList = [], prototypeMethods = {}, parentConstructorName = null) { // ... 内部实现 // 1. 处理严格模式调用验证(防止非法new) // 2. 实现原型继承(如果指定了parentConstructorName) // 3. 挂载原型方法 // 4. 将构造函数注册到window对象上 window[constructorName] = Constructor; return Constructor; }
  • constructorName: 要创建的构造函数名,如"HTMLElement"
  • enableStrictMode: 是否启用严格模式。启用后,只有传入特定令牌(如"ljc")才能成功实例化,这可以防止目标代码意外地、错误地调用我们的构造函数。
  • propertiesList: 实例属性列表。可以是简单的字符串数组,用于定义属性名;也可以是数组,第二个元素是属性描述符,用于定义getter/setter。
  • prototypeMethods: 要添加到该构造函数prototype上的方法对象,例如document.createElement
  • parentConstructorName: 父构造函数的名称,用于实现原型链继承。

通过这个函数,我们可以像搭积木一样,从Object开始,一层层构建出EventTarget->Node->Element->HTMLElement->HTMLDivElement这样的完整链条。

2. watch:环境监控器在补环境过程中,我们常常不知道下一步该补什么。watch函数就是一个“侦察兵”。它的原理是利用ProxyObject.defineProperty,对一个对象进行包装。当目标代码访问这个对象的任何属性或方法时,watch函数会打印出访问路径,让我们一目了然地看到代码在“寻找”什么。

// 伪代码逻辑 function watch(obj, name) { return new Proxy(obj, { get(target, property) { console.log(`[Watch] ${name} 被访问了属性: ${String(property)}`); // 递归包装返回的值,实现深度监控 const value = target[property]; if (value && typeof value === 'object') { return watch(value, `${name}.${String(property)}`); } return value; } }); } // 使用 window.document = watch({}, “document”);

当运行扣出的JS代码时,如果遇到document.body.appendChild的调用,控制台就会输出[Watch] document 被访问了属性: body,接着输出[Watch] document.body 被访问了属性: appendChild。这样,我们就精准地知道了需要补全document.body对象以及它的appendChild方法。

3.3 链式补环境的实施步骤

整个补环境过程是一个“运行 -> 报错/监控 -> 补全 -> 再运行”的循环:

  1. 基础骨架搭建:先补上最顶层的window对象,并将其selftopwindow属性指向自身,模拟浏览器全局作用域。
  2. 构建核心原型链:使用createConstructor,按照EventTarget->Node->Element->HTMLElement的顺序构建基础DOM类。
  3. 补全document对象document不是一个简单的对象,它是HTMLDocument的实例。因此需要先创建DocumentHTMLDocument构造函数,然后用new HTMLDocument(...)来实例化window.document。同时,要根据监控信息,补全document.alldocument.documentElementdocument.bodydocument.cookie等关键属性。
  4. 处理动态创建的元素:加密代码中可能会动态创建scriptcanvas等元素。我们需要在Document.prototype.createElement方法中做拦截,当创建特定标签时,返回我们补全好的对应元素实例(如new HTMLScriptElement),并确保其parentNode等属性也符合预期。
  5. 迭代与细化:通过watch函数监控运行过程,发现一个缺失的属性或方法,就溯源其所属的构造函数,然后通过createConstructor补到对应的prototype上。这是一个需要耐心和细心的过程。

实操心得:不要试图一次性补全所有浏览器API,那是不可能的。我们的目标是“够用就行”。只补那些加密代码实际用到的部分。watch函数是达成这一目标的关键,它能帮你快速聚焦到真正需要补的环境点上,避免做无用功。

4. 针对某东5.2.3版本的具体补环境实现

4.1 加密入口定位与代码扣取

首先,通过浏览器的开发者工具,在网络请求中定位到携带h5st的请求。在发起该请求的JavaScript代码处下断点,可以追踪到h5st的生成函数。在某东的案例中,通常与一个名为_$sdnmd的函数或window.PSign.sign方法相关。

扣取代码时,不能只扣一个函数。需要沿着调用栈向上查找,将依赖的函数、变量、对象定义都一并扣出。关键是要找到加密函数的载体对象。在本案例中,加密逻辑位于一个ParamsSign(或类似名称)的类实例方法中。因此,我们需要扣取这个类的定义,并在Node.js环境中正确地实例化它。

扣取后的核心代码结构通常如下:

// 这是扣出来的、依赖浏览器环境的加密核心函数 function _$sdnmd(params) { // ... 复杂的加密逻辑,其中会访问 window, document, location, 创建DOM元素等 // 例如:var div = document.createElement('div'); // var isElement = div instanceof Element; return h5stString; } // 这是一个封装类 function ParamsSign(options) { this.appId = options.appId; // ... 其他初始化 } ParamsSign.prototype._$sdnmd = _$sdnmd; // 在浏览器中,可能会这样挂载和调用 // window.PSign = new ParamsSign({...}); // window.PSign._$sdnmd(params);

我们的任务,就是让这段代码在Node.js中,不修改其内部逻辑的情况下,能够成功执行并返回正确的h5st

4.2 环境补全的详细步骤与代码

以下是根据某东5.2.3版本加密代码的依赖,进行的链式补环境实现。请将以下代码放在你扣取的JS源码之前执行。

步骤一:注入工具函数与搭建最顶层环境

// ============== 1. 监控工具函数 ============== function watch(obj, name) { // 这里使用Proxy实现深度监控,实际应用中可根据兼容性选择defineProperty const handler = { get(target, property, receiver) { console.log(`[环境监控] ${name} 尝试获取属性: ${String(property)}`); const value = Reflect.get(target, property, receiver); // 如果获取到的是对象或函数,继续包装以便深度监控 if (value && typeof value === 'object' && property !== '__proto__' && property !== 'constructor') { return watch(value, `${name}.${String(property)}`); } return value; }, set(target, property, value, receiver) { console.log(`[环境监控] ${name} 设置属性: ${String(property)} =`, value); return Reflect.set(target, property, value, receiver); } }; return new Proxy(obj, handler); } // ============== 2. 构造函数工厂(核心) ============== function createConstructor(constructorName, enableStrictMode, propertiesList = [], prototypeMethods = {}, parentConstructorName = null) { const instancesData = new WeakMap(); // 使用WeakMap存储实例私有数据,更安全 const Constructor = function(element, propertySetter, validationToken) { // 严格模式校验 if (enableStrictMode && validationToken !== 'ljc') { throw new TypeError(`Illegal constructor: ${constructorName}`); } // 调用父类构造函数(实现继承) if (parentConstructorName && globalThis[parentConstructorName]) { globalThis[parentConstructorName].call(this, element, null, 'ljc'); } // 设置实例属性 const instanceProps = element && typeof element === 'object' ? { ...element } : {}; instancesData.set(this, instanceProps); // 应用属性设置器 if (propertySetter && typeof propertySetter === 'function') { propertySetter(this); } // 将propertiesList中的简单属性挂载到实例上 propertiesList.forEach(prop => { if (Array.isArray(prop)) { // 自定义属性描述符,如 ['all', {get: function(){...}}] Object.defineProperty(this, prop[0], prop[1]); } else if (typeof prop === 'string') { // 简单属性名,初始化为undefined if (!(prop in instanceProps)) { this[prop] = undefined; } } }); }; // 设置原型链继承 if (parentConstructorName && globalThis[parentConstructorName]) { Constructor.prototype = Object.create(globalThis[parentConstructorName].prototype); Constructor.prototype.constructor = Constructor; } // 设置对象的 toStringTag,用于 Object.prototype.toString.call 检测 Object.defineProperty(Constructor.prototype, Symbol.toStringTag, { value: constructorName, configurable: true }); // 添加原型方法 Object.entries(prototypeMethods).forEach(([methodName, methodFunc]) => { Constructor.prototype[methodName] = methodFunc; }); // 将构造函数挂载到全局对象 globalThis[constructorName] = Constructor; return Constructor; } // ============== 3. 构建全局Window对象 ============== // 将globalThis(Node.js全局对象)伪装成window Object.defineProperty(globalThis, 'window', { get() { return globalThis; }, set(val) { globalThis = val; }, configurable: true, enumerable: true }); const window = globalThis; // 设置window的自身引用 window.self = window.top = window.window = window; // 创建大写的Window构造函数(类) createConstructor('Window', true, [], {}); // 将小写的window实例的原型指向大Window的原型,建立继承关系 Object.setPrototypeOf(window, Window.prototype); // 为window实例设置正确的toStringTag Object.defineProperty(window, Symbol.toStringTag, { value: 'Window', configurable: true });

步骤二:构建DOM核心原型链这是补环境中最关键的一环,必须严格按照浏览器的继承顺序来构建。

// ============== 4. 构建DOM基础原型链 ============== // 顺序:EventTarget -> Node -> Element -> HTMLElement createConstructor('EventTarget', true, [], { addEventListener: function() {}, removeEventListener: function() {}, dispatchEvent: function() { return false; } }); createConstructor('Node', true, ['nodeType', 'nodeName', 'childNodes', 'parentNode'], { appendChild: function() { return this; }, removeChild: function() { return this; }, hasChildNodes: function() { return false; } }, 'EventTarget'); createConstructor('Element', true, ['tagName', 'className', 'id', 'children', 'innerHTML'], { getAttribute: function() { return null; }, setAttribute: function() {}, getElementsByTagName: function() { return []; } }, 'Node'); createConstructor('HTMLElement', true, ['title', 'lang', 'dir', 'hidden'], { click: function() {}, focus: function() {}, blur: function() {} }, 'Element');

步骤三:补全Document及其相关对象document对象是环境检测的重灾区,需要精细处理。

// ============== 5. 补全Document对象链 ============== // Document 继承自 Node createConstructor('Document', true, [], { createElement: function(tagName) { console.log(`[Document] 尝试创建元素: <${tagName}>`); // 根据标签名返回对应的元素实例 switch(tagName.toLowerCase()) { case 'div': if (!globalThis.HTMLDivElement) { createConstructor('HTMLDivElement', true, [], {}, 'HTMLElement'); } return new HTMLDivElement(null, null, 'ljc'); case 'script': if (!globalThis.HTMLScriptElement) { createConstructor('HTMLScriptElement', true, ['src', 'type', 'async'], { // 某东加密可能会检查script的某些属性 }, 'HTMLElement'); } const script = new HTMLScriptElement(null, null, 'ljc'); // 关键:某东代码可能会访问 script.parentNode,需要指向一个head或body if (!globalThis.HTMLHeadElement) { createConstructor('HTMLHeadElement', true, [], {}, 'HTMLElement'); } // 这里简化处理,将parentNode设为一个模拟的head元素 script.parentNode = new HTMLHeadElement(null, null, 'ljc'); return script; case 'canvas': if (!globalThis.HTMLCanvasElement) { createConstructor('HTMLCanvasElement', true, ['width', 'height'], { getContext: function() { // 返回一个模拟的CanvasRenderingContext2D return { fillRect: function(){}, strokeRect: function(){}, fillText: function(){} }; } }, 'HTMLElement'); } return new HTMLCanvasElement(null, null, 'ljc'); default: // 对于其他标签,返回一个基本的HTMLElement实例 return new HTMLElement(null, null, 'ljc'); } }, createEvent: function(type) { return { type, initEvent: function(){} }; }, querySelector: function() { return null; }, querySelectorAll: function() { return []; }, getElementsByTagName: function(name) { return []; } }, 'Node'); // HTMLDocument 继承自 Document createConstructor('HTMLDocument', true, [], {}, 'Document'); // HTMLAllCollection (document.all 的类型) createConstructor('HTMLAllCollection', true, ['length'], { item: function(index) { return null; }, namedItem: function(name) { return null; } }); // HTMLHtmlElement (document.documentElement 的类型) createConstructor('HTMLHtmlElement', true, [], {}, 'HTMLElement'); // HTMLBodyElement (document.body 的类型) createConstructor('HTMLBodyElement', true, [], { appendChild: function() { return this; } }, 'HTMLElement');

步骤四:实例化并装配完整的document对象现在,用我们创建好的类来构造一个“逼真”的document对象。

// ============== 6. 实例化 document 对象 ============== // 注意:这里使用 new HTMLDocument 来创建,确保 instanceof 检测通过 window.document = new HTMLDocument({ // document.all 是一个特殊的类数组对象 all: new HTMLAllCollection(null, null, 'ljc'), // document.documentElement 通常是 <html> 元素 documentElement: new HTMLHtmlElement(null, null, 'ljc'), // document.body 是 <body> 元素 body: new HTMLBodyElement(null, null, 'ljc'), // cookie 需要根据实际情况填写,可以从浏览器复制,但注意隐私和安全 cookie: '你的某东cookie字符串,可从浏览器复制', // 其他可能被检测的属性 characterSet: 'UTF-8', compatMode: 'CSS1Compat', // location 对象也需要模拟 location: { href: 'https://search.jd.com/', protocol: 'https:', host: 'search.jd.com', hostname: 'search.jd.com', port: '', pathname: '/', search: '', hash: '', origin: 'https://search.jd.com' } }, null, 'ljc'); // 将 location 对象也挂载到 window 上 window.location = window.document.location;

步骤五:启动监控与运行测试在补完上述基础环境后,可以暂时用watch函数包装关键对象,运行扣取的JS代码,观察还缺什么。

// 启动监控(调试时打开,正式运行时关闭以提高性能) // window = watch(window, 'window'); // window.document = watch(window.document, 'document'); // 现在,可以尝试运行你扣取的加密函数了 // 例如,假设加密主函数是 getH5st function getH5st(params) { // 这里是你扣取的、依赖浏览器环境的原始加密代码 // 它内部会调用 window.PSign._$sdnmd 或类似方法 // ... } // 导出函数供Node.js调用 module.exports = { getH5st };

运行测试代码。控制台会输出一系列[环境监控][Document]日志。根据这些日志,你会看到代码在尝试访问哪些尚未定义的属性或方法,然后针对性地用createConstructor去补全对应的类或原型方法。

注意事项:某东的加密代码可能会检测navigatorscreenperformance等对象。你需要根据监控输出,同样使用createConstructor或直接赋值的方式补全它们。例如:

window.navigator = { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...', platform: 'Win32', language: 'zh-CN', // ... 其他属性 }; window.screen = { width: 1920, height: 1080, colorDepth: 24, pixelDepth: 24 };

5. 与后端集成:构建可调用的H5ST生成服务

补环境的最终目的是让加密代码在Node.js中运行起来。一个优雅的方案是将它封装成一个HTTP服务,这样任何语言(如Python)都可以通过简单的网络请求来获取h5st

5.1 使用Express.js搭建服务

首先,确保你的项目目录下已经初始化了Node.js项目 (npm init -y),并安装了Express (npm install express)。

创建一个server.js文件:

const express = require('express'); const app = express(); const port = 3000; // 中间件:解析JSON请求体 app.use(express.json()); // 引入我们补好环境并封装好的加密模块 // 假设我们的加密主逻辑写在一个叫 `jd_h5st.js` 的文件里,并导出了一个 `getH5st` 函数 const { getH5st } = require('./jd_h5st.js'); // 定义一个生成h5st的API端点 app.post('/api/gen_h5st', (req, res) => { try { const params = req.body; // 参数校验 if (!params.appid || !params.functionId || !params.body) { return res.status(400).json({ code: -1, message: '缺少必要参数: appid, functionId, body' }); } console.log(`[服务端] 收到请求,appid: ${params.appid}, functionId: ${params.functionId}`); // 调用加密函数 const result = getH5st(params); // 返回结果 res.json({ code: 0, message: 'success', data: { h5st: result.h5st, t: result.t, // 通常加密函数也会返回时间戳t // 可能还有其他字段 } }); } catch (error) { console.error('[服务端] 生成h5st时出错:', error); res.status(500).json({ code: -2, message: '内部服务错误', error: error.message }); } }); // 健康检查端点 app.get('/health', (req, res) => { res.json({ status: 'ok', service: 'jd-h5st-generator' }); }); app.listen(port, () => { console.log(`✅ H5ST生成服务已启动,监听 http://localhost:${port}`); });

5.2 Python客户端调用示例

在Python中,我们不再需要execjssubprocess来调用复杂的JS文件,只需一个简单的HTTP请求。

import requests import hashlib import time def sha256_hash(data: str) -> str: """计算字符串的SHA-256哈希值,用于处理body参数""" return hashlib.sha256(data.encode('utf-8')).hexdigest() class JDH5STClient: def __init__(self, server_url='http://localhost:3000'): self.server_url = server_url def get_h5st(self, appid: str, function_id: str, body_dict: dict, **extra_params): """ 获取某东接口的h5st参数 Args: appid: 接口appid,如 'search-pc-java' function_id: 功能ID,如 'pc_search_adv_Search' body_dict: 请求体参数字典 extra_params: 其他可能需要的参数,如client, clientVersion等 Returns: dict: 包含h5st、t等完整请求参数 """ # 1. 将body字典转换为JSON字符串并计算SHA256 import json body_json = json.dumps(body_dict, separators=(',', ':'), ensure_ascii=False) body_hash = sha256_hash(body_json) # 2. 构造请求到Node.js服务的参数 request_payload = { "appid": appid, "functionId": function_id, "body": body_hash, "client": extra_params.get("client", "pc"), "clientVersion": extra_params.get("clientVersion", "1.0.0"), "t": int(time.time() * 1000) # 当前时间戳 } # 3. 调用本地服务 try: resp = requests.post( f"{self.server_url}/api/gen_h5st", json=request_payload, timeout=10 # 设置超时 ) resp.raise_for_status() result = resp.json() if result['code'] != 0: raise Exception(f"服务返回错误: {result['message']}") h5st_data = result['data'] # 4. 组装最终请求参数 final_params = { "appid": appid, "t": h5st_data.get('t', request_payload['t']), "client": "pc", "clientVersion": "1.0.0", "uuid": extra_params.get("uuid", ""), # 需要从cookie或其他地方获取 "functionId": function_id, "body": body_json, # 注意这里是原始的JSON字符串,不是hash "x-api-eid-token": extra_params.get("x-api-eid-token", ""), # 需要从cookie解析 "h5st": h5st_data['h5st'] } return final_params except requests.exceptions.RequestException as e: print(f"请求H5ST服务失败: {e}") raise except Exception as e: print(f"处理H5ST结果失败: {e}") raise def search_products(self, keyword, page=1, area="1_72_55652_0"): """示例:搜索商品""" # 你的某东Cookie,需要从浏览器获取 cookies = { '__jda': '...', '__jdv': '...', 'pin': '...', # ... 其他cookie } headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...', 'Referer': 'https://search.jd.com/', # ... 其他headers } # 构造请求体 body = { "enc": "utf-8", "keyword": keyword, "page": page, "area": area, "s": 1 } # 获取加密参数 params = self.get_h5st( appid="search-pc-java", function_id="pc_search_searchWare", body_dict=body, uuid="1745841960334352365307", # 示例,实际需要动态生成或从cookie获取 x_api_eid_token="jdd03QJJ7DOUYP7T5O2IKSRFQANXZJYHALCU3ECRYXYVSULUXN7DODVWDAGUVYK2WLOTISQ3XQ7U7G5PP57CC2QFRLQFA5AAAAAMYYUR7TVIAAAAACORWVTOFTVSAUQX" # 示例 ) # 发起真正的搜索请求 api_url = "https://api.m.jd.com/api" response = requests.get( api_url, params=params, headers=headers, cookies=cookies ) if response.status_code == 200: return response.json() else: print(f"请求搜索接口失败: {response.status_code}") return None # 使用示例 if __name__ == "__main__": client = JDH5STClient() # 先启动Node.js服务:node server.js result = client.search_products("白酒", page=1) if result: print(f"搜索成功,获取到 {len(result.get('data', {}).get('wareList', []))} 条商品数据")

这种架构将复杂的JS环境补全和加密逻辑隔离在Node.js服务中,Python端只需关注业务逻辑和HTTP调用,大大降低了耦合度和维护成本。

6. 常见问题排查与实战技巧

6.1 签名生成了,但请求还是被拒绝?

这是补环境过程中最常见的问题。可能的原因和排查思路如下:

  1. 环境检测未完全通过:这是最可能的原因。即使生成了h5st,如果环境有破绽,服务器端可能通过其他暗桩检测出来并拒绝。

    • 排查方法:在Node.js中运行加密代码时,打开所有watch监控,仔细查看控制台输出。有没有访问某个属性返回了undefined?有没有调用某个方法抛出了异常?这些静默的错误可能导致加密逻辑走入非预期分支。
    • 重点检查navigator.userAgentnavigator.platformscreen.width/heightperformance.now()Date构造函数、Math.random等。有些网站会检测这些值的合理性一致性。例如,userAgent说是Chrome,但navigator.platform却是Linux,这可能就会被怀疑。
  2. Cookie或Token问题h5st的生成可能依赖于cookie中的某些值(如pinId,_pst,thor等)或请求头中的x-api-eid-token。如果你在Node.js环境中使用的cookie字符串是过期的、无效的,或者eid-token不正确,生成的签名自然无效。

    • 解决:确保从浏览器复制的是登录后当前有效的完整Cookie。并且注意,某些Cookie(如thor)有效期很短,可能需要定期更新。
  3. 参数构造错误:传递给加密函数的参数不正确。例如,body参数在加密前需要做SHA256哈希,但你可能传了原始JSON字符串,或者哈希算法不一致。

    • 核对:用浏览器的开发者工具,在加密函数入口处打断点,记录下传入参数的确切值。然后在你的Node.js代码中,用同样的输入去生成h5st,对比两者结果是否完全一致。
  4. 时间戳同步问题h5st中包含了时间戳t。如果服务器时间和你本地Node.js服务的时间相差太大(比如超过几分钟),签名可能会因“过期”而被拒绝。

    • 解决:确保服务器时间同步。在生成h5st时,可以使用从某东服务器响应头中获取的服务器时间(如果有),或者确保你的机器开启了NTP时间同步。
  5. 补环境过于“干净”:浏览器环境有很多“杂质”,比如window对象上挂载了大量的属性、方法、第三方库变量。我们的补环境为了简洁,往往只补了必要的。有时,目标代码会通过遍历window的属性或检查某些特定全局变量是否存在来做检测。

    • 技巧:可以在补完基础环境后,适当“污染”一下window对象,添加一些常见的浏览器全局变量,比如$,jQuery,Vue(设为undefined),或者添加一些无意义的数字属性,让Object.keys(window).length看起来不那么“假”。

6.2 如何高效调试补环境过程?

  1. 分层监控:不要一开始就给所有对象加watch,信息太多会淹没重点。可以先监控windowdocument。运行后,看代码最先缺什么,补上。然后再运行,缺什么补什么。像“剥洋葱”一样一层层推进。

  2. 利用浏览器控制台:在浏览器Sources面板中,找到加密函数,在可能进行环境检测的地方(如if (window.xxx)Object.prototype.toString.call(yyy))下条件断点。当断点触发时,记录下此时浏览器环境中该属性的完整值类型,然后在Node.js中精确复现。

  3. 对比执行路径:在浏览器和Node.js中分别单步执行加密函数。观察在关键的分支判断处(if/else,switch),两者的执行路径是否一致。如果不一致,一定是环境差异导致的,立刻检查该分支判断所依赖的环境变量。

  4. 最小化测试用例:不要每次都跑完整的加密流程。可以写一个小的测试函数,只测试某个特定的环境检测点。例如,专门测试document.createElement(‘div’) instanceof Element在你的补环境中是否返回true

6.3 关于“版本号5.2.3”

标题中的“最新版5.2.3”很可能指的是某东前端加密库的版本号。不同版本的加密逻辑和环境检测强度可能不同。这意味着:

  • 时效性:本文提供的补环境方案是针对特定时间点、特定版本加密逻辑的。某东可能会更新其加密算法或增加新的环境检测点。
  • 针对性:当你遇到不同版本时,补环境的核心思路和框架(createConstructor,watch)是通用的,但需要补的具体对象、属性、方法可能需要调整。务必使用watch工具重新分析。
  • 维护:将补环境代码模块化。把通用的createConstructorwatch函数放在一个基础工具模块。把针对某东特定版本的补丁(如特定的Document.prototype.createElement实现)放在另一个模块。这样当加密库升级时,你主要更新后者即可。

6.4 性能与优化

补环境会创建大量Proxy和构造函数,可能影响性能。在开发调试阶段可以开启所有监控,但在生产环境运行服务时,应该:

  • 移除所有watch包装Proxy的拦截开销不小。
  • 缓存实例:对于像documentnavigator这种全局单例,创建一次后缓存起来,不要每次调用加密函数都重新创建。
  • 精简补丁:只保留加密代码真正用到的属性和方法。通过watch确定哪些是必需的,哪些从未被访问,可以安全移除。
  • 考虑使用更轻量的对象模拟方式替代Proxy,或者在确定环境稳定后,将补环境代码“固化”成一个纯净的、不依赖动态代理的JS文件。

补环境是一场与反爬机制斗智斗勇的持久战。它没有一劳永逸的解决方案,需要的是对JavaScript原型链的深刻理解、耐心细致的调试技巧,以及一套像createConstructorwatch这样好用的工具。掌握了这套方法,你就能应对大多数基于浏览器环境检测的JS逆向挑战。