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

发布时间:2026/7/4 12:16:18

JS逆向实战:链式补环境破解某东h5st签名加密 1. 项目概述逆向某东最新版5.2.3的“补环境”实战最近在逆向某东搜索接口时遇到了一个典型的“环境检测”难题。目标接口的h5st参数其生成逻辑被封装在一个复杂的JavaScript函数中直接扣取代码在Node.js环境下运行会因为缺少浏览器环境如window、document、Element等对象而报错。这就是所谓的“环境检测”也是逆向工程中一个绕不开的坎。我这次的目标就是针对某东最新版版本号指向5.2.3的加密逻辑完成一套完整的“补环境”方案让扣出来的JS代码能在Node.js中稳定运行并生成有效的h5st签名。简单来说“补环境”就是为Node.js这个“服务器环境”伪造一个足以“骗过”JavaScript代码的“浏览器环境”。这不仅仅是创建一个空对象那么简单它涉及到对浏览器原生对象如Window、Document、Element及其复杂原型链的精确模拟。如果补得不够像代码要么直接报错要么生成的签名长度、格式看起来都对但服务器一校验就失败这就是典型的“环境检测”没过。这次逆向的核心就是通过“链式补环境”的方法从根源上解决这个问题构建一个足以通过检测的“虚拟浏览器”。2. 逆向目标与核心挑战解析2.1 目标接口与h5st参数这次的目标非常明确某东商品搜索列表页的异步数据接口。当你打开某东搜索页面输入关键词后页面会通过Ajax请求加载商品列表这个请求中就携带了一个关键的加密参数——h5st。这个参数是一长串由分号分隔的字符串包含了时间戳、随机值、签名等多种信息是服务器验证请求合法性、防止爬虫的核心手段。从抓包分析来看h5st的生成逻辑完全在前端JavaScript中完成。这意味着如果我们想在Python等后端语言中模拟请求获取数据就必须复现这套JS加密逻辑。最直接的方法就是把生成h5st的JavaScript代码“扣”出来放到Node.js中执行然后由Python去调用。然而扣出来的代码往往无法直接运行因为它依赖浏览器提供的全局对象和API。2.2 核心挑战环境检测与原型链校验直接运行扣出的JS代码通常会遇到如下报错ReferenceError: window is not definedReferenceError: document is not definedTypeError: Cannot read properties of undefined (reading xxx)初级解决方案是简单地在Node.js的global对象上挂载window {}和document {}。对于早期或防护较弱的网站这招可能管用。但像某东这样的大型平台其反爬策略早已升级。它不仅仅检查window、document这些对象是否存在还会深入检查它们的类型、构造函数以及完整的原型链。例如代码可能会执行Object.prototype.toString.call(document.createElement(div))期望返回[object HTMLDivElement]而不是[object Object]。或者它会检查element instanceof Element、element.__proto__ HTMLElement.prototype等关系。如果我们只是用普通对象{}来模拟这些检查全部会失败导致加密逻辑走入错误分支或者生成一个无效的签名。因此本次逆向的难点不在于找到加密入口通常通过搜索h5st关键字或sign等函数名可以定位而在于如何构建一个足够逼真的浏览器环境让扣出来的加密代码“相信”自己正在浏览器中运行从而执行正确的加密路径。3. 补环境的核心思路与框架设计3.1 从“对象模拟”到“原型链构建”传统的“补环境”是缺什么补什么报window补window报document补document。这种方法在应对深度检测时力不从心。我们需要转变思路不是模拟对象而是模拟“类”和“继承关系”。浏览器的DOM是一个复杂的继承体系。HTMLDivElement继承自HTMLElementHTMLElement继承自ElementElement继承自NodeNode继承自EventTarget最终继承自Object。document是HTMLDocument的实例而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函数就是一个“侦察兵”。它的原理是利用Proxy或Object.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 链式补环境的实施步骤整个补环境过程是一个“运行 - 报错/监控 - 补全 - 再运行”的循环基础骨架搭建先补上最顶层的window对象并将其self、top、window属性指向自身模拟浏览器全局作用域。构建核心原型链使用createConstructor按照EventTarget-Node-Element-HTMLElement的顺序构建基础DOM类。补全document对象document不是一个简单的对象它是HTMLDocument的实例。因此需要先创建Document和HTMLDocument构造函数然后用new HTMLDocument(...)来实例化window.document。同时要根据监控信息补全document.all、document.documentElement、document.body、document.cookie等关键属性。处理动态创建的元素加密代码中可能会动态创建script、canvas等元素。我们需要在Document.prototype.createElement方法中做拦截当创建特定标签时返回我们补全好的对应元素实例如new HTMLScriptElement并确保其parentNode等属性也符合预期。迭代与细化通过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对象 // 将globalThisNode.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去补全对应的类或原型方法。注意事项某东的加密代码可能会检测navigator、screen、performance等对象。你需要根据监控输出同样使用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中我们不再需要execjs或subprocess来调用复杂的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_urlhttp://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_asciiFalse) 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, jsonrequest_payload, timeout10 # 设置超时 ) 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, page1, area1_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( appidsearch-pc-java, function_idpc_search_searchWare, body_dictbody, uuid1745841960334352365307, # 示例实际需要动态生成或从cookie获取 x_api_eid_tokenjdd03QJJ7DOUYP7T5O2IKSRFQANXZJYHALCU3ECRYXYVSULUXN7DODVWDAGUVYK2WLOTISQ3XQ7U7G5PP57CC2QFRLQFA5AAAAAMYYUR7TVIAAAAACORWVTOFTVSAUQX # 示例 ) # 发起真正的搜索请求 api_url https://api.m.jd.com/api response requests.get( api_url, paramsparams, headersheaders, cookiescookies ) 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(白酒, page1) if result: print(f搜索成功获取到 {len(result.get(data, {}).get(wareList, []))} 条商品数据)这种架构将复杂的JS环境补全和加密逻辑隔离在Node.js服务中Python端只需关注业务逻辑和HTTP调用大大降低了耦合度和维护成本。6. 常见问题排查与实战技巧6.1 签名生成了但请求还是被拒绝这是补环境过程中最常见的问题。可能的原因和排查思路如下环境检测未完全通过这是最可能的原因。即使生成了h5st如果环境有破绽服务器端可能通过其他暗桩检测出来并拒绝。排查方法在Node.js中运行加密代码时打开所有watch监控仔细查看控制台输出。有没有访问某个属性返回了undefined有没有调用某个方法抛出了异常这些静默的错误可能导致加密逻辑走入非预期分支。重点检查navigator.userAgent、navigator.platform、screen.width/height、performance.now()、Date构造函数、Math.random等。有些网站会检测这些值的合理性和一致性。例如userAgent说是Chrome但navigator.platform却是Linux这可能就会被怀疑。Cookie或Token问题h5st的生成可能依赖于cookie中的某些值如pinId,_pst,thor等或请求头中的x-api-eid-token。如果你在Node.js环境中使用的cookie字符串是过期的、无效的或者eid-token不正确生成的签名自然无效。解决确保从浏览器复制的是登录后、当前有效的完整Cookie。并且注意某些Cookie如thor有效期很短可能需要定期更新。参数构造错误传递给加密函数的参数不正确。例如body参数在加密前需要做SHA256哈希但你可能传了原始JSON字符串或者哈希算法不一致。核对用浏览器的开发者工具在加密函数入口处打断点记录下传入参数的确切值。然后在你的Node.js代码中用同样的输入去生成h5st对比两者结果是否完全一致。时间戳同步问题h5st中包含了时间戳t。如果服务器时间和你本地Node.js服务的时间相差太大比如超过几分钟签名可能会因“过期”而被拒绝。解决确保服务器时间同步。在生成h5st时可以使用从某东服务器响应头中获取的服务器时间如果有或者确保你的机器开启了NTP时间同步。补环境过于“干净”浏览器环境有很多“杂质”比如window对象上挂载了大量的属性、方法、第三方库变量。我们的补环境为了简洁往往只补了必要的。有时目标代码会通过遍历window的属性或检查某些特定全局变量是否存在来做检测。技巧可以在补完基础环境后适当“污染”一下window对象添加一些常见的浏览器全局变量比如$,jQuery,Vue(设为undefined)或者添加一些无意义的数字属性让Object.keys(window).length看起来不那么“假”。6.2 如何高效调试补环境过程分层监控不要一开始就给所有对象加watch信息太多会淹没重点。可以先监控window和document。运行后看代码最先缺什么补上。然后再运行缺什么补什么。像“剥洋葱”一样一层层推进。利用浏览器控制台在浏览器Sources面板中找到加密函数在可能进行环境检测的地方如if (window.xxx)、Object.prototype.toString.call(yyy)下条件断点。当断点触发时记录下此时浏览器环境中该属性的完整值和类型然后在Node.js中精确复现。对比执行路径在浏览器和Node.js中分别单步执行加密函数。观察在关键的分支判断处if/else,switch两者的执行路径是否一致。如果不一致一定是环境差异导致的立刻检查该分支判断所依赖的环境变量。最小化测试用例不要每次都跑完整的加密流程。可以写一个小的测试函数只测试某个特定的环境检测点。例如专门测试document.createElement(‘div’) instanceof Element在你的补环境中是否返回true。6.3 关于“版本号5.2.3”标题中的“最新版5.2.3”很可能指的是某东前端加密库的版本号。不同版本的加密逻辑和环境检测强度可能不同。这意味着时效性本文提供的补环境方案是针对特定时间点、特定版本加密逻辑的。某东可能会更新其加密算法或增加新的环境检测点。针对性当你遇到不同版本时补环境的核心思路和框架createConstructor,watch是通用的但需要补的具体对象、属性、方法可能需要调整。务必使用watch工具重新分析。维护将补环境代码模块化。把通用的createConstructor和watch函数放在一个基础工具模块。把针对某东特定版本的补丁如特定的Document.prototype.createElement实现放在另一个模块。这样当加密库升级时你主要更新后者即可。6.4 性能与优化补环境会创建大量Proxy和构造函数可能影响性能。在开发调试阶段可以开启所有监控但在生产环境运行服务时应该移除所有watch包装Proxy的拦截开销不小。缓存实例对于像document、navigator这种全局单例创建一次后缓存起来不要每次调用加密函数都重新创建。精简补丁只保留加密代码真正用到的属性和方法。通过watch确定哪些是必需的哪些从未被访问可以安全移除。考虑使用更轻量的对象模拟方式替代Proxy或者在确定环境稳定后将补环境代码“固化”成一个纯净的、不依赖动态代理的JS文件。补环境是一场与反爬机制斗智斗勇的持久战。它没有一劳永逸的解决方案需要的是对JavaScript原型链的深刻理解、耐心细致的调试技巧以及一套像createConstructor和watch这样好用的工具。掌握了这套方法你就能应对大多数基于浏览器环境检测的JS逆向挑战。

相关新闻