行业资讯

JS逆向实战:猿人学第五题环境检测破解与Proxy补环境技术详解

发布时间:2026/8/1 3:54:18
JS逆向实战:猿人学第五题环境检测破解与Proxy补环境技术详解 1. 项目概述猿人学第五题的挑战与价值最近在JS逆向的圈子里猿人学这个练习平台可以说是无人不知。它就像是我们逆向工程师的“木人巷”从易到难一步步考验着你的真功夫。今天要聊的第五题绝对是这个平台里一个标志性的分水岭。它不像前几题那样可能只是简单的参数加密或者ob混淆第五题开始真正引入了“反混淆”和“环境检测”这两个核心对抗点。很多朋友卡在这里不是因为算法有多复杂而是被它精心布置的“环境陷阱”给绕晕了。题目要求我们获取到特定页面的数据但你会发现直接运行它的核心JS代码要么报错要么返回空值仿佛有一堵无形的墙。这背后正是它在检测你的代码执行环境是否是一个真实的浏览器。为什么这个案例如此经典且值得深究因为它模拟了当前主流网站尤其是数据密集型应用如电商、票务、内容聚合平台最常用的前端保护策略之一通过检测关键浏览器环境对象如window、document、navigator以及它们的方法属性来判断JS代码是否在预期的浏览器中运行还是在Node.js或纯JS引擎这类“非浏览器环境”下被分析执行。我们搜索“js逆向 补环境proxy代理”时会发现大量讨论都围绕此展开。掌握攻克此题的方法就等于拿到了一把解开许多类似网站反爬机制的通用钥匙。接下来我将带你完整走一遍我的逆向分析过程从抓包定位、代码反混淆、核心逻辑分析到最关键的一步——如何精准、高效地“补环境”最终成功拿到数据。2. 核心思路与逆向入口定位面对任何JS逆向题目第一步永远是观察网络请求。打开题目页面通过浏览器开发者工具的Network面板很容易找到一个返回了题目要求数据的接口请求。查看这个请求的Headers你会发现它携带了一个关键的签名参数比如可能叫做sign、token或s每次请求值都不同。这个参数就是服务器验证请求合法性的钥匙也是我们逆向分析的核心目标——找到生成这个参数的JavaScript代码逻辑。2.1 请求分析与代码搜索通常生成动态参数的代码会直接放在前端JS文件中。我们在Network面板的Initiator列点击这个请求可以快速跳转到发起该请求的调用栈最终定位到具体的JS文件及行号。另一种更通用的方法是在Sources面板对所有JS文件进行全局搜索快捷键CtrlShiftF搜索那个签名参数的参数名如sign或者其值的一部分。猿人学第五题的代码通常经过了一定程度的混淆但关键的函数调用或字符串常量可能依然可见。找到疑似代码后我们需要设置断点进行动态调试。在可能生成参数的函数入口、或是对网络请求API如fetch、XMLHttpRequest的封装处打上断点然后重新触发请求比如点击查询按钮。当断点命中时通过Call Stack调用堆栈一步步向上回溯就能找到最核心的参数生成函数。2.2 反混淆与代码逻辑梳理第五题的JS代码通常不是简单的变量名缩短而是采用了诸如“控制流平坦化”和“字符串加密”等混淆技术。代码看起来会有大量的switch-case语句在一个大循环里分支跳转令人眼花缭乱。这时直接阅读是低效的。我的策略是优先尝试使用自动化工具进行初步反混淆。可以将关键代码片段复制出来使用在线的JS反混淆工具注意安全使用离线或可信工具更佳进行处理。处理后的代码虽然可能仍不完美但可读性会大幅提升变量名和函数逻辑会更清晰。如果自动化工具效果不佳就需要进行“半手动”分析。核心是找到字符串解密函数和调度器。混淆代码中通常会有一个数组存放所有真实的字符串和一个函数负责在运行时解密并返回。同时控制流平坦化的核心是一个“分发器”它根据一个状态变量来决定执行哪一块真实的逻辑代码。通过调试我们可以逐步记录下每个case块执行后对最终结果即签名参数的贡献从而逆向拼凑出完整算法。注意在动态调试时务必留意console面板是否有错误或警告输出。猿人学第五题的环境检测代码很可能在检测失败时通过console.log输出一些提示信息这是定位检测点的宝贵线索。3. 环境检测原理深度剖析与应对策略当我们费尽千辛万苦似乎找到了生成签名的核心函数并尝试在Node.js环境下单独执行它时往往会遭遇滑铁卢函数报错或者返回的结果与浏览器中运行的结果完全不同。这就是遇到了“环境检测”。3.1 环境检测的常见手段这类检测的本质是判断代码是否运行在一个“真实的浏览器环境”中。检测点五花八门但归根结底是检查一些在Node.js等非浏览器环境中不存在、或存在但属性不同的全局对象及其方法。以下是第五题中可能出现的几种典型检测检测全局对象window和document这是最基础的检测。在浏览器中window是顶层对象document是其属性。在Node.js中默认不存在这两个对象。代码中可能会直接引用window.location、document.cookie或document.getElementById如果这些对象是undefined代码就会抛出异常或走入错误分支。检测特定对象的方法或属性即使补上了window和document空对象检测还会深入。例如document.createElement(div)调用这个方法并检查返回的DOM元素是否具有标准的属性和方法。navigator.userAgent、navigator.plugins、navigator.language检查浏览器指纹信息。screen.width/height检查屏幕分辨率。window.localStorage或window.sessionStorage检查Web存储API。检测函数toString()后的结果这是一个高级技巧。在浏览器中原生函数的toString()方法返回的是函数体的原生代码字符串通常格式为function name() { [native code] }。而在Node.js中如果我们用JS模拟了一个函数其toString()结果是我们定义的JS代码。检测代码可能会通过Function.prototype.toString.call(someFunction)来检查某个关键API如document.getElementById是否是“原生函数”。检测原型链与构造函数检查对象的原型链是否完整、构造函数是否正确。例如检查通过document.createElement(canvas)创建的canvas元素的getContext方法是否存在以及调用它返回的对象类型。3.2 通用应对策略补环境知道了检测原理我们的对策就是“补环境”——在Node.js等执行环境中模拟创建出这些浏览器特有的对象、属性和方法让目标JS代码“以为”自己正运行在浏览器里。补环境有两种主要思路笨办法但有效缺啥补啥。通过调试让代码运行起来当它报错“Cannot read property xxx of undefined”时就在执行环境的全局对象上补上这个xxx属性。一层一层地补直到代码能完整执行。这种方法直接但效率低且可能漏掉一些不报错但会走入错误分支的隐式检测。推荐策略使用Proxy进行全局拦截与监控。这是处理此类问题的“神器”。我们可以在Node.js中用一个Proxy对象来包装全局对象比如global或者包装我们创建的模拟window对象。Proxy可以拦截对该对象的所有属性读取(get)、设置(set)等操作。通过它我们可以清晰地看到目标代码在运行过程中尝试访问了哪些我们未曾预料到的属性从而无一遗漏地发现所有环境检测点。// 示例使用Proxy监控全局对象属性访问 let handler { get: function(target, prop, receiver) { console.log([GET] 尝试访问属性: ${String(prop)}, typeof prop); // 如果属性不存在可以返回一个自定义的Proxy或特定值 if (!(prop in target)) { console.warn( 属性 ${String(prop)} 不存在正在创建模拟值); // 可以返回一个函数或新的Proxy对象来应对深层调用 target[prop] new Proxy(() {}, { apply: function(target, thisArg, argumentsList) { console.log([CALL] 调用了模拟函数 ${String(prop)}参数:, argumentsList); // 根据prop决定返回什么例如对于document.getElementById可以返回一个模拟的DOM元素 return {}; }, get: function(target, propKey) { console.log([GET on Function] 访问函数属性: ${String(propKey)}); return undefined; } }); } return Reflect.get(...arguments); }, set: function(target, prop, value) { console.log([SET] 设置属性: ${prop} , value); return Reflect.set(...arguments); } }; // 将global对象用Proxy包装 global new Proxy(global, handler); // 或者创建一个模拟的window对象 let window new Proxy({}, handler); global.window window; // 让全局可以访问到window运行目标代码控制台会输出所有被访问的属性。那些我们没预先定义、但被代码访问到的属性就是我们需要去补充模拟的关键点。根据输出我们可以有针对性地、由浅入深地构建我们的模拟环境。4. 猿人学第五题环境构建实战理论讲完我们进入实战。假设通过反混淆和调试我们找到了一个名为generateSign的核心函数它依赖了window、document和navigator。4.1 基础环境搭建首先在Node.js项目中我们创建一个补环境的JS文件例如env.js。// env.js const vm require(vm); // 可选用于创建更隔离的沙箱环境 // 1. 创建模拟的window对象并使其成为全局对象的属性 let window { navigator: {}, document: {}, location: {}, screen: {}, localStorage: {}, sessionStorage: {}, // 其他常见属性... }; // 2. 补全navigator的常见属性根据题目调试输出调整 window.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, platform: Win32, language: zh-CN, languages: [zh-CN, zh], cookieEnabled: true, // 注意navigator.plugins 和 navigator.mimeTypes 在Chrome中已弃用但可能被检测 plugins: [], mimeTypes: [], }; // 3. 补全document对象及其关键方法 window.document { cookie: , // 初始为空可根据需要设置 documentElement: {}, createElement: function(tagName) { console.log(document.createElement called with tag: ${tagName}); // 返回一个模拟的DOM元素对象 return { tagName: tagName.toUpperCase(), style: {}, getContext: function(type) { // 如果是canvas模拟getContext return type 2d ? {} : null; }, // 其他DOM元素属性和方法... }; }, getElementById: function(id) { // 通常返回null或一个模拟元素 return null; }, // 其他方法... }; // 4. 将window设为全局对象 global.window window; global.document window.document; global.navigator window.navigator; global.location window.location; global.screen window.screen; // 5. 处理Function.prototype.toString的检测 // 这是一个关键点我们需要让模拟函数的toString返回[native code] const originalToString Function.prototype.toString; Function.prototype.toString function() { // 可以定义一个列表列出哪些函数需要伪装成原生函数 // 如果这个函数是我们模拟的浏览器API就返回[native code] // 这里示例如果函数名包含‘getElementById’或‘createElement’则伪装 if (this.name (this.name.includes(getElementById) || this.name.includes(createElement))) { return function ${this.name}() { [native code] }; } // 否则返回原来的结果 return originalToString.call(this); }; console.log(基础浏览器环境模拟完成。);4.2 使用Proxy进行精细化监控与补充基础环境可能还不够。我们将上面的window对象用Proxy包装以发现遗漏的检测。// env_proxy.js function createWindowProxy() { let realWindow { // ... 同上放置所有我们已知的基础属性和方法 ... navigator: { /* ... */ }, document: { /* ... */ }, // ... }; let handler { get(target, prop) { // 记录所有访问特别是我们未定义的 if (!(prop in target)) { console.log([环境检测发现] window.${prop.toString()} 被访问当前为 undefined。); // 动态补上一个空对象或函数防止报错但可能不够 // 更好的做法是分析这个prop的用途再模拟 target[prop] new Proxy({}, { get(obj, subProp) { console.log([环境检测发现] window.${prop.toString()}.${subProp.toString()} 被访问。); return undefined; } }); } return target[prop]; }, set(target, prop, value) { // console.log(设置 window.${prop} , value); target[prop] value; return true; } }; return new Proxy(realWindow, handler); } global.window createWindowProxy(); global.document global.window.document; global.navigator global.window.navigator; // ... 其他全局绑定运行目标JS代码观察控制台输出。你会看到一串[环境检测发现]的日志。比如可能会输出window.outerWidth、window.performance、document.defaultView等。这些就是我们需要进一步补充的检测点。4.3 针对特定检测点的补充根据Proxy的日志我们逐一补充window.outerWidth/outerHeight模拟屏幕外尺寸通常设为与screen.width一致。realWindow.outerWidth 1920; realWindow.outerHeight 1080;window.performance这是一个复杂的对象但很多检测只关心performance.timing或performance.now()。我们可以模拟一个简化版。realWindow.performance { timing: {}, now: function() { return Date.now(); } };document.defaultView在浏览器中document.defaultView就是window。realWindow.document.defaultView realWindow;navigator.webdriver这是一个重要的反爬检测点。在真实的浏览器中通常是undefined或false而在自动化工具如Selenium控制下可能为true。务必将其设为undefined或false。realWindow.navigator.webdriver undefined;4.4 集成与执行核心函数环境补全后我们就可以将目标网站的核心JS代码通常是那个经过反混淆的、包含generateSign函数的代码块引入我们的Node.js环境。// main.js require(./env_proxy.js); // 引入我们构建的环境 // 假设核心JS代码已经保存为字符串 coreCode const coreCode // 这里是反混淆后的猿人学第五题核心JS代码 function generateSign(data) { // ... 复杂的计算逻辑其中会用到 window, document 等对象 ... var someValue window.navigator.userAgent; var another document.createElement(canvas); // ... return calculatedSign; } // 可能还会定义其他辅助函数或变量 ; // 在一个沙盒中执行这段代码避免污染全局 const scriptContext { window: global.window }; const vmContext vm.createContext(scriptContext); vm.runInContext(coreCode, vmContext); // 现在vmContext中应该有了 generateSign 函数 const sign vmContext.generateSign({ /* 传入必要的参数 */ }); console.log(计算得到的签名:, sign);运行node main.js观察结果。如果环境补得足够好generateSign函数将顺利执行并返回一个有效的签名。将这个签名填入我们之前抓包看到的请求参数中用Python的requests库或Node.js的axios发起请求就能成功获取到题目要求的数据。5. 常见问题排查与实战心得即使按照上述步骤操作你也可能会遇到各种问题。这里分享一些我踩过的坑和排查技巧。5.1 问题排查清单问题现象可能原因排查思路与解决方案函数执行报错xxx is not a function环境对象中对应的属性虽然存在但其值不是函数或函数内部又调用了不存在的子属性。1. 使用Proxy的get拦截器确认访问路径。2. 确保模拟的函数返回了正确类型如document.createElement应返回一个对象。3. 对于复杂API如canvas.getContext(‘2d’)需要模拟返回对象具有相应方法如fillText,measureText。函数执行无报错但返回结果与浏览器不一致1. 环境检测导致代码走了不同的逻辑分支。2. 模拟的环境属性值如userAgent、屏幕尺寸与浏览器中不同影响了算法。3. 算法依赖了时间戳、随机数等动态值。1.仔细对比在浏览器调试器中在生成签名的函数里多打几个断点记录下关键变量的值如navigator.userAgent、window.screen.width、Date.now()的值。然后在Node.js环境中确保这些值完全一致。2.Hook关键函数在Node.js环境中可以重写Math.random、Date.now等函数使其返回浏览器中记录下的固定值确保结果可复现。3.检查隐式分支代码中可能有if (typeof window ! ‘undefined’)或if (‘ontouchstart’ in window)这样的检测确保你的模拟环境让这些条件判断的结果与浏览器一致。Proxy监控到大量未知属性访问补不过来代码可能进行了一次性的全局属性枚举或深度检测。1. 不必追求100%补全所有属性。很多属性访问只是探测只要不导致代码报错或走入错误分支可以忽略。关键是保证代码主逻辑通路畅通。2. 对于频繁访问的深层对象如window.chrome、window.WebGLRenderingContext可以一次性补一个结构大致正确的空对象或Proxy。补了环境后在Node里运行成功但签名仍被服务器拒绝1.签名算法依赖了请求本身的上下文如URL、请求头特别是User-Agent、Referer。2.时间戳同步问题签名可能包含了服务器时间窗验证。3.还有未发现的环境检测导致算法内部某处计算有细微差别。1.完全模拟请求用Node.js发起请求时Headers尤其是User-Agent要与浏览器完全一致。签名函数可能需要location.href在Node中需要手动设置window.location.href为目标URL。2.时间戳处理服务器可能检查时间戳是否在合理范围内。确保Node.js环境的时间与网络时间同步或者直接使用从浏览器请求中提取的时间戳值。3.差分调试这是终极手段。在浏览器和Node.js中在签名函数的每一步计算后都打印出中间变量的值进行逐行对比找到第一个出现差异的地方那里就是问题所在。5.2 实操心得与技巧最小化补环境原则不要一开始就试图构建一个完整的浏览器环境。从空对象开始利用Proxy监控只补充代码实际访问到的属性和方法。这能大大减少工作量并让环境更干净。优先使用静态补全对于Proxy监控到的、确定需要的属性最好在模拟对象如realWindow中静态定义好而不是每次都通过Proxy动态创建。静态定义的属性描述符configurable,writable等更接近真实浏览器能通过一些更严格的检测。Function.prototype.toString是灵魂对于关键的原生API如document.createElement一定要处理好其toString方法使其返回[native code]。很多强检测都会检查这个。注意属性描述符有些检测会用Object.getOwnPropertyDescriptor来检查属性的configurable、writable等特性。在Node中用简单赋值创建的属性其描述符默认都是true而浏览器中很多原生属性是不可配置、不可写的。如果遇到因此导致的检测需要使用Object.defineProperty来精确模拟。保持耐心与细致JS逆向尤其是反混淆和补环境是一个需要极大耐心和细致观察的工作。每一个错误信息、每一个Proxy日志都是通往成功的线索。养成记录每一步操作和现象的习惯。攻克猿人学第五题的过程本质上是一次完整的前端逆向工程实战训练。它系统地锻炼了代码静态分析、动态调试、反混淆理解以及浏览器环境模拟的能力。掌握了这套方法你再面对大多数带有环境检测的JS加密时就不会再感到无从下手了。记住核心思路就是定位关键代码 - 理解其逻辑 - 识别环境检测 - 精准模拟环境 - 集成验证。剩下的就是经验的积累和耐心的比拼了。