小程序环境指纹采集与blackbox加密生成逆向分析实战

发布时间:2026/8/5 8:54:34
小程序环境指纹采集与blackbox加密生成逆向分析实战 1. 项目概述一次针对小程序环境指纹的深度采集与分析最近在分析一个小程序时遇到了一个挺有意思的挑战。这个小程序的核心业务逻辑被一个名为“某盾”的版本为1.7.1的安全组件保护着。它会在启动时执行一系列环境检测收集设备指纹并将这些信息加密后发送到服务端进行校验。如果校验不通过核心功能就无法使用或者返回的数据是加密的乱码。我们的目标很明确理解并复现这个环境指纹的采集过程从最开始的getinfo接口调用到最终生成那个被称为blackbox的加密数据包从而为后续的逆向分析、数据模拟或风控策略研究打下基础。这不仅仅是“绕过”那么简单更重要的是理解其背后的安全设计思路和实现细节。这个过程本质上是一次对客户端环境信息采集技术的逆向工程。它适合对移动端安全、JavaScript逆向、网络协议分析感兴趣的朋友无论是安全研究员、爬虫工程师还是对前端混淆和加密技术好奇的开发者都能从中获得一些实操层面的启发。你需要对JavaScript特别是ES5/ES6有基本了解熟悉浏览器开发者工具特别是Sources和Network面板并且对常见的加密算法如AES、RSA、哈希有概念性的认识。不用担心我们会一步步拆解把每个环节都讲透。2. 核心思路与技术方案选型面对这样一个被加固的小程序直接硬啃混淆后的代码效率极低。我们的核心思路是“动态分析为主静态分析为辅”通过观察小程序运行时的行为来定位关键代码和逻辑。2.1 为什么选择动态Hook作为突破口静态分析面对高度混淆、压缩且可能被虚拟化保护的代码时往往事倍功半。而动态分析则是在代码实际执行时进行观察能够看到最真实的函数调用、参数传递和返回值。对于getinfo这类显然是向某个接口发起请求获取初始数据的行为以及blackbox这种最终产出的加密结果通过Hook网络请求和特定的加密函数可以快速定位到关键代码段。2.2 技术栈与工具链选择运行时环境为了获得最大的分析灵活性和工具支持我们优先选择在PC端使用微信开发者工具或兼容的浏览器环境运行小程序。这允许我们使用强大的浏览器开发者工具和第三方Hook脚本。核心Hook工具我们将主要依赖浏览器原生支持的JavaScript Hook技术。具体来说会用到Object.defineProperty: 用于Hook对象属性如window.navigator,window.screen的访问。重写原生函数如XMLHttpRequest.prototype.send和fetch来拦截所有网络请求重写Crypto.subtle或常见的加密库函数如crypto-js的方法。Proxy对象这是一个更强大的工具可以拦截对目标对象的各种操作包括属性读取、函数调用等非常适合Hook一些复杂的API。辅助分析工具浏览器开发者工具DevTools这是我们的主战场。Network面板监控请求Sources面板下断点、查看调用栈Console面板执行Hook代码。Fiddler/Charles作为网络代理可以辅助查看和修改HTTPS请求/响应特别是在小程序真机调试时必不可少。自定义Hook脚本我们将编写一个集成了多种Hook方法的脚本在分析开始时注入到小程序上下文中。2.3 整体分析流程设计我们的行动路线图如下环境准备与启动配置好微信开发者工具确保目标小程序能正常运行。初步侦察打开Network面板清空记录然后启动小程序。观察最先发起的几个请求寻找名称或参数中包含getinfo、init、config等关键词的请求。这就是我们的第一个切入点。注入Hook脚本在Sources面板或通过Snippet功能将我们编写的通用Hook脚本注入到页面中。脚本会立即开始工作打印出被访问的环境属性和发起的网络请求。定位getinfo处理逻辑通过Hook我们能抓到getinfo请求的返回数据。接下来在Network面板中找到这个请求在Initiator发起者一栏点击查看调用栈通常能直接跳转到发起该请求的JavaScript代码位置。即使代码被混淆这个位置也是至关重要的起点。逆向blackbox生成过程以getinfo的响应为起点通过代码回溯查看调用栈和正向跟踪单步调试结合对加密函数如JSON.stringify,btoa, 或特定加密算法函数的Hook一步步分析原始环境数据是如何被加工、组装最终加密成blackbox的。数据流建模与验证将分析出的采集项、处理逻辑、加密算法和密钥或密钥生成逻辑整理成文档或代码。最后尝试脱离原小程序环境用Python或Node.js编写一个模拟程序生成一个blackbox并与原程序生成的进行对比验证。注意整个分析过程必须在合法的授权范围内进行仅用于学习安全技术和提升防御能力。切勿将其用于攻击、破坏或侵犯他人合法权益的活动。3. 实操过程从环境准备到关键逻辑定位3.1 环境搭建与初步网络抓包首先打开微信开发者工具导入或打开目标小程序项目。在调试器中选择“Sources”和“Network”面板。在Network面板中勾选“Preserve log”保留日志防止页面跳转时请求记录被清除。刷新小程序。一瞬间Network面板会被请求填满。我们需要快速过滤出关键请求。在Filter过滤器中输入关键词如“info”、“init”、“get”、“config”。很快我发现了一个名为https://xxx.com/api/getinfo?t123456789的请求。这极有可能就是我们要找的getinfo。点击这个请求查看其详细信息Headers注意它的Request Headers特别是Cookie,User-Agent, 以及是否有自定义的Header如X-Sign。Preview/Response查看服务器返回的数据。通常是一个JSON对象。我看到的返回结构大致如下{ code: 0, data: { key1: value1, key2: value2, token: encrypted_token_string_here, config: { sampleRate: 1, blackboxVersion: 1.0 } } }这个data字段里的内容尤其是token和config很可能在后续生成blackbox时被用作参数或密钥的一部分。3.2 注入通用Hook脚本为了不遗漏任何细节我们在Sources面板的Snippets标签页中新建一个脚本文件比如叫universal_hook.js内容如下(function() { use strict; // 1. Hook 控制台日志防止其被重写 var originalLog console.log; console.log function(...args) { originalLog.apply(console, [[Hook], ...args]); // 也可以在这里将日志发送到自己的服务器用于远程分析 }; // 2. Hook 网络请求 (XMLHttpRequest) var originalXHROpen XMLHttpRequest.prototype.open; var originalXHRSend XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open function(method, url, async) { this._requestMethod method; this._requestUrl url; return originalXHROpen.apply(this, arguments); }; XMLHttpRequest.prototype.send function(body) { console.log([XHR Hook] ${this._requestMethod} ${this._requestUrl}, body); // 重点监听响应 this.addEventListener(load, function() { if (this._requestUrl.includes(getinfo)) { console.log([XHR Response Hook] ${this._requestUrl}, this.responseText); } if (this._requestUrl.includes(blackbox)) { console.log([XHR Response Hook] ${this._requestUrl}, this.responseText); } }); return originalXHRSend.apply(this, arguments); }; // 3. Hook 环境属性访问 var hookedProperties [userAgent, platform, language, deviceMemory, hardwareConcurrency]; hookedProperties.forEach(prop { var originalDescriptor Object.getOwnPropertyDescriptor(Navigator.prototype, prop); if (originalDescriptor originalDescriptor.get) { Object.defineProperty(Navigator.prototype, prop, { get: function() { var value originalDescriptor.get.call(this); console.log([Navigator Hook] Accessed: ${prop} ${value}); return value; }, configurable: true }); } }); // Hook screen Object.defineProperty(window.screen, width, { get: function() { var value this._width || 1920; // 假设值 console.log([Screen Hook] Accessed: width ${value}); return value; }, configurable: true }); // 类似地 Hook height, availWidth, availHeight, colorDepth, pixelDepth // 4. Hook 加密相关函数 (以 crypto-js 为例实际中可能是其他库或原生API) if (window.CryptoJS) { var originalAESEncrypt CryptoJS.AES.encrypt; CryptoJS.AES.encrypt function(message, key, cfg) { console.log([Crypto Hook] AES.encrypt called, { message, key: key.toString(), cfg }); var result originalAESEncrypt.apply(this, arguments); console.log([Crypto Hook] AES.encrypt result:, result.toString()); return result; }; } // 5. Hook JSON.stringify 和 JSON.parse (常用于数据序列化) var originalStringify JSON.stringify; JSON.stringify function(value, replacer, space) { console.log([JSON Hook] stringify called with:, value); return originalStringify.apply(this, arguments); }; console.log([Universal Hook] 注入成功); })();保存后右键点击这个snippet选择“Run”。如果控制台输出[Universal Hook] 注入成功并且后续操作开始出现[Hook]开头的日志说明脚本生效了。3.3 定位getinfo响应处理与blackbox生成入口重新触发小程序的初始化流程或刷新页面。在控制台中我们期待看到[XHR Response Hook] ...getinfo...的日志其中包含了服务器返回的数据。关键的一步来了在Network面板中找到那个getinfo请求点击其右侧的“Initiator”列它会显示一个调用栈。点击调用栈中最上面一个属于小程序代码的文件通常不是VMxxx或wasm文件而是那个被混淆的主js文件它会直接跳转到Sources面板中发起这个网络请求的那一行代码。通常这里是一个wx.request或uni.request取决于小程序框架的调用。我们在这行代码上打一个断点。然后再次刷新小程序。代码会在这里暂停。现在我们进入了一个关键的调试阶段。不要急着执行下一步而是查看右侧的Call Stack调用栈面板。这里展示了从当前断点开始向上回溯的函数调用链。我们需要逐层向上查看寻找处理getinfo响应和可能触发blackbox生成的逻辑。通常处理响应的代码会在success或complete回调函数里。在Call Stack中点击上一层函数查看它的上下文。我们的目标是找到这样一个地方它接收了getinfo的response.data然后开始调用一系列函数来处理这些数据并最终调用了一个可能是generateBlackbox、collectFingerprint或encryptData的函数。在这个过程中我们Hook的JSON.stringify和加密函数可能会被触发控制台会输出相应的日志这为我们提供了宝贵的线索。例如如果我们看到JSON.stringify被调用参数是一个包含了navigator、screen等属性的大对象那么这里很可能就是在组装原始指纹数据。实操心得在高度混淆的代码中函数名可能都是a,b,c,d。不要试图去理解每一个变量而是关注数据流。找到getinfo的响应数据假设是变量res被赋值给了谁比如var e res.data然后跟踪这个变量e后续被传递到了哪些函数中b(e),c(e)。结合Hook日志比如Hook到了加密函数被调用且参数包含了e中的某个值就能快速定位到核心加密逻辑的位置。4. 深度逆向blackbox的生成逻辑拆解假设通过上述方法我们定位到了一个关键函数我们暂且称它为function n(t)它接收一个对象t包含了大量环境信息并返回一个加密后的字符串blackbox。4.1 数据采集项分析首先我们需要弄清楚传入function n(t)的对象t到底包含了哪些属性。在function n的入口处打上断点当程序停在这里时在Console面板中输入JSON.stringify(t, null, 2)然后回车。这样可以漂亮地打印出完整的对象结构。我观察到的采集项通常包括以下几大类浏览器/环境基础信息userAgent: 用户代理字符串。language: 浏览器语言。platform: 操作系统平台。deviceMemory: 设备内存GB。hardwareConcurrency: CPU逻辑核心数。屏幕与显示信息screenWidth,screenHeight: 屏幕分辨率。availWidth,availHeight: 可用屏幕区域。colorDepth,pixelDepth: 颜色深度。devicePixelRatio: 设备像素比。时区与时间信息timezoneOffset: 时区偏移分钟。timezone: 时区名称如Asia/Shanghai。timestamp: 当前时间戳可能包含服务器时间差计算。Canvas与WebGL指纹canvasFp: 通过绘制Canvas并toDataURL()得到的一个哈希值极其稳定且唯一。webglVendor,webglRenderer: WebGL厂商和渲染器信息。音频指纹audioFp: 通过AudioContext分析音频信号处理差异得到的哈希。字体列表通过测量特定字符的渲染宽度来推测已安装的字体。getinfo返回的特定数据serverToken: 从getinfo响应中获取的令牌。config: 服务器下发的配置可能包含加密算法标识、版本号、盐值(salt)等。4.2 数据处理与组装流程采集到原始数据后并不会直接加密。通常会经过以下步骤数据标准化将所有值转换为字符串或数字处理可能的undefined或null。排序与序列化为了确保同一环境每次生成的输入一致会对采集到的对象按键名进行排序通常是字典序然后使用JSON.stringify序列化成字符串。这里就是我们之前HookJSON.stringify能抓到的地方。添加噪声与混淆可能会在序列化后的字符串前后拼接一些固定的或随机的字符串盐值或者对某些字段进行简单的变换如异或、Base64编码等。计算摘要哈希对上一步得到的字符串计算哈希如MD5、SHA-1、SHA-256。哈希值通常作为中间结果或最终blackbox的一部分。这里可能会Hook到crypto.subtle.digest或类似函数。核心加密将哈希结果或整个处理后的字符串使用对称加密算法如AES进行加密。加密密钥可能来源于getinfo返回的token或由token与本地某些固定值派生而来。这里是我们Hook加密函数如CryptoJS.AES.encrypt的主要战场。通过单步调试F10和观察变量变化我们可以还原出这个流程。例如原始数据对象 t - JSON.stringify(排序后的t) - 字符串S 字符串S salt - 新字符串 S 计算 SHA256(S) - 哈希值 H 使用 AES-128-CBC 加密 H密钥K来自serverToken - 密文 C 将密文C进行Base64编码 - 最终 blackbox 字符串4.3 加密算法与密钥分析这是最核心也最具挑战性的一步。我们需要确定加密算法是AES、DES、RSA还是自定义算法通过Hook通用加密库的函数调用和观察输入输出数据的长度、特征可以判断。例如AES加密后的数据长度通常是16字节的倍数。加密模式与填充是ECB、CBC、CFB填充是PKCS#7吗这需要更细致的分析有时可以通过尝试常见的组合来验证。密钥Key和初始向量IV它们是如何生成的是硬编码在代码里还是由getinfo返回的token动态生成常见的方式是对token进行某种哈希或拼接固定字符串后再取指定位数。一个实用的技巧在加密函数被调用时我们Hook到的日志会打印出key。即使key是CryptoJS.lib.WordArray对象我们也可以调用key.toString()来查看其16进制表示。记录下这个key和对应的IV如果有。然后我们可以尝试用Python的pycryptodome库使用相同的key、IV、模式和填充对相同的明文进行加密看结果是否与blackbox匹配。如果匹配就验证了我们的分析。注意事项在实际操作中加密部分可能被进一步混淆比如算法被拆分成多个小函数或者使用WebAssembly来执行加密以增加逆向难度。如果遇到WASM就需要使用WASM分析工具如wasm2c或动态调试WASM模块。5. 模拟复现与验证分析完成后我们需要将理论转化为代码进行模拟复现这是检验分析成果的唯一标准。5.1 构建数据采集模拟器使用Node.js或Python模拟我们分析出的所有采集项。对于Canvas、WebGL、Audio等浏览器特有API在无头浏览器如Puppeteer中运行采集代码是最可靠的。但对于基础信息可以用库来模拟。// Node.js 模拟示例 (部分) const crypto require(crypto); function collectFingerprint(serverToken, serverConfig) { const fp { // 模拟环境信息 userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., language: zh-CN, platform: Win32, deviceMemory: 8, hardwareConcurrency: 16, screenWidth: 1920, screenHeight: 1080, devicePixelRatio: 1, timezoneOffset: -480, timezone: Asia/Shanghai, timestamp: Date.now(), // 注意Canvas/WebGL/Audio指纹需要浏览器环境生成这里用模拟值或从真实环境抓取一次后固定 canvasFp: simulated_canvas_fp_hash, webglVendor: Google Inc. (NVIDIA), webglRenderer: ANGLE (NVIDIA, ...), // 集成服务器数据 serverToken: serverToken, config: serverConfig }; return fp; }5.2 实现数据处理与加密流水线严格按照逆向出来的流程编写代码function generateBlackbox(fpData) { // 1. 键名排序并序列化 const orderedKeys Object.keys(fpData).sort(); const orderedObj {}; orderedKeys.forEach(key { orderedObj[key] fpData[key]; }); const jsonString JSON.stringify(orderedObj); // 2. 添加盐值 (假设我们从分析中得知盐值是固定的 _salt_) const saltedString jsonString _salt_; // 3. 计算SHA256哈希 const hash crypto.createHash(sha256).update(saltedString).digest(hex); // 4. AES加密 (假设是CBC模式PKCS7填充Key和IV来自serverToken的派生) const key deriveKeyFromToken(fpData.serverToken); // 需要实现deriveKeyFromToken const iv deriveIVFromToken(fpData.serverToken); // 需要实现deriveIVFromToken const cipher crypto.createCipheriv(aes-128-cbc, key, iv); let encrypted cipher.update(hash, utf8, base64); encrypted cipher.final(base64); return encrypted; }deriveKeyFromToken和deriveIVFromToken的函数实现取决于我们的逆向结果。可能很简单比如key CryptoJS.MD5(serverToken).toString().substr(0, 16)也可能比较复杂涉及多次哈希和拼接。5.3 完整流程验证与问题排查获取实时数据运行我们的小程序分析环境从Network面板中复制一次真实的getinfo请求的响应数据serverToken,config。运行模拟器将上述真实数据输入我们的模拟程序生成一个blackbox_sim。触发真实请求在小程序环境中同时触发一次真实的blackbox上报请求从Network面板或Hook日志中捕获真实的blackbox_real。对比比较blackbox_sim和blackbox_real。如果完全一致恭喜大功告成如果长度相同但内容不同很可能加密的key、IV、mode或padding分析有误或者哈希前的字符串处理如排序规则、盐值有细微差别。如果长度都不同可能连算法都判断错了或者数据采集项有遗漏/多余。常见问题排查表问题现象可能原因排查方向模拟结果长度与真实结果不同加密算法判断错误数据序列化格式不对如多了空格。检查加密函数Hook日志确认算法和模式。对比模拟与真实环境下JSON.stringify的输入对象是否完全一致。模拟结果与真实结果部分相似密钥或IV错误加密模式或填充方式错误。验证Key和IV的生成逻辑。尝试不同的常见模式CBC, ECB和填充PKCS7, ZeroPadding。哈希值阶段就不同数据采集项不一致排序规则错误盐值错误或遗漏。逐项对比模拟与真实环境采集的数据。确认对象键的排序是升序还是降序。检查盐值是否动态变化。无法触发加密函数Hook加密可能由WebAssembly执行或使用了非常冷门的库。在Network面板查看是否有.wasm文件加载。在Sources面板搜索WebAssembly.instantiate等关键字。6. 经验总结与高级对抗思路完成一次完整的分析后我积累了一些宝贵的经验和对于更高级对抗的思考。6.1 核心经验与避坑指南动态Hook是利器在逆向高度混淆的前端代码时静态分析如同大海捞针而动态Hook能让你直接看到代码在“做什么”。优先Hook网络、环境API和加密相关函数。关注数据流而非控制流不要试图理解每一行混淆的代码。紧紧抓住“输入数据如getinfo响应”和“输出数据blackbox”在代码中追踪它们是如何被传递和变换的。善用调用栈Call Stack这是从运行时行为定位到源代码位置的最快途径。保持环境一致性分析时的小程序版本、浏览器环境、系统时间等尽量与你要模拟的环境保持一致。某些指纹如时间戳具有时效性。记录与验证每一步的分析结论尤其是推测的算法、密钥、盐值都要尽快通过编写小段测试代码来验证。不要等到全部分析完再验证那样排查问题的范围太大。6.2 面对更高级保护的可能策略“某盾”或其他安全SDK可能会升级采用更复杂的对抗技术代码虚拟化与混淆核心逻辑被转换成自定义的字节码在虚拟机中执行。应对思路重点Hook虚拟机与外界交换数据的接口函数或者尝试寻找并dump出虚拟机解释执行后的“还原”代码如果存在。WebAssembly (WASM) 加密加密算法用WASM实现逆向难度大增。应对思路使用调试器对WASM模块进行调试使用wasm2c等工具将其转换为C代码再分析或者直接Hook JavaScript与WASM模块交互的import/export函数。环境检测与反调试检测开发者工具、识别Hook行为、检测代码执行时间异常等。应对思路使用更隐蔽的Hook方法如Proxy在合适的时机如代码加载前注入Hook脚本使用debugger;语句反反调试或通过代理工具修改响应来绕过检测代码。动态密钥与算法密钥不再固定或简单派生而是每次由服务端下发一个随机数或算法标识客户端动态选择加密算法。应对思路需要更精细地分析getinfo返回的config并Hook所有可能的加密算法入口点。6.3 最后的建议这类分析工作是对耐心、细心和逻辑推理能力的综合考验。它没有一成不变的公式每个案例都有其独特性。最重要的不是记住某个特定小程序的破解方法而是掌握“动态分析、数据追踪、假设验证”这一套方法论。从简单的案例开始逐步挑战更复杂的保护你的逆向工程能力会在解决一个又一个具体问题的过程中得到实质性的提升。