小程序环境指纹采集与blackbox加密生成逆向分析实战
2026/8/5 8:54:13 网站建设 项目流程

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.sendfetch来拦截所有网络请求;重写Crypto.subtle或常见的加密库函数(如crypto-js的方法)。
    • Proxy对象:这是一个更强大的工具,可以拦截对目标对象的各种操作,包括属性读取、函数调用等,非常适合Hook一些复杂的API。
  • 辅助分析工具
    • 浏览器开发者工具(DevTools):这是我们的主战场。Network面板监控请求,Sources面板下断点、查看调用栈,Console面板执行Hook代码。
    • Fiddler/Charles:作为网络代理,可以辅助查看和修改HTTPS请求/响应,特别是在小程序真机调试时必不可少。
    • 自定义Hook脚本:我们将编写一个集成了多种Hook方法的脚本,在分析开始时注入到小程序上下文中。

2.3 整体分析流程设计

我们的行动路线图如下:

  1. 环境准备与启动:配置好微信开发者工具,确保目标小程序能正常运行。
  2. 初步侦察:打开Network面板,清空记录,然后启动小程序。观察最先发起的几个请求,寻找名称或参数中包含getinfoinitconfig等关键词的请求。这就是我们的第一个切入点。
  3. 注入Hook脚本:在Sources面板或通过Snippet功能,将我们编写的通用Hook脚本注入到页面中。脚本会立即开始工作,打印出被访问的环境属性和发起的网络请求。
  4. 定位getinfo处理逻辑:通过Hook,我们能抓到getinfo请求的返回数据。接下来,在Network面板中找到这个请求,在Initiator(发起者)一栏点击查看调用栈,通常能直接跳转到发起该请求的JavaScript代码位置。即使代码被混淆,这个位置也是至关重要的起点。
  5. 逆向blackbox生成过程:以getinfo的响应为起点,通过代码回溯(查看调用栈)和正向跟踪(单步调试),结合对加密函数(如JSON.stringify,btoa, 或特定加密算法函数)的Hook,一步步分析原始环境数据是如何被加工、组装,最终加密成blackbox的。
  6. 数据流建模与验证:将分析出的采集项、处理逻辑、加密算法和密钥(或密钥生成逻辑)整理成文档或代码。最后,尝试脱离原小程序环境,用Python或Node.js编写一个模拟程序,生成一个blackbox,并与原程序生成的进行对比验证。

注意:整个分析过程必须在合法的授权范围内进行,仅用于学习安全技术和提升防御能力。切勿将其用于攻击、破坏或侵犯他人合法权益的活动。

3. 实操过程:从环境准备到关键逻辑定位

3.1 环境搭建与初步网络抓包

首先,打开微信开发者工具,导入或打开目标小程序项目。在调试器中选择“Sources”和“Network”面板。在Network面板中,勾选“Preserve log”(保留日志),防止页面跳转时请求记录被清除。

刷新小程序。一瞬间,Network面板会被请求填满。我们需要快速过滤出关键请求。在Filter(过滤器)中输入关键词,如“info”、“init”、“get”、“config”。很快,我发现了一个名为https://xxx.com/api/getinfo?t=123456789的请求。这极有可能就是我们要找的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字段里的内容,尤其是tokenconfig,很可能在后续生成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”列,它会显示一个调用栈。点击调用栈中最上面一个属于小程序代码的文件(通常不是VMxxxwasm文件,而是那个被混淆的主js文件),它会直接跳转到Sources面板中发起这个网络请求的那一行代码。

通常,这里是一个wx.requestuni.request(取决于小程序框架)的调用。我们在这行代码上打一个断点。然后再次刷新小程序。代码会在这里暂停。

现在,我们进入了一个关键的调试阶段。不要急着执行下一步,而是查看右侧的Call Stack(调用栈)面板。这里展示了从当前断点开始,向上回溯的函数调用链。我们需要逐层向上查看,寻找处理getinfo响应和可能触发blackbox生成的逻辑。

通常,处理响应的代码会在successcomplete回调函数里。在Call Stack中点击上一层函数,查看它的上下文。我们的目标是找到这样一个地方:它接收了getinforesponse.data,然后开始调用一系列函数来处理这些数据,并最终调用了一个可能是generateBlackboxcollectFingerprintencryptData的函数。

在这个过程中,我们Hook的JSON.stringify和加密函数可能会被触发,控制台会输出相应的日志,这为我们提供了宝贵的线索。例如,如果我们看到JSON.stringify被调用,参数是一个包含了navigatorscreen等属性的大对象,那么这里很可能就是在组装原始指纹数据。

实操心得:在高度混淆的代码中,函数名可能都是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),然后回车。这样可以漂亮地打印出完整的对象结构。

我观察到的采集项通常包括以下几大类:

  1. 浏览器/环境基础信息
    • userAgent: 用户代理字符串。
    • language: 浏览器语言。
    • platform: 操作系统平台。
    • deviceMemory: 设备内存(GB)。
    • hardwareConcurrency: CPU逻辑核心数。
  2. 屏幕与显示信息
    • screenWidth,screenHeight: 屏幕分辨率。
    • availWidth,availHeight: 可用屏幕区域。
    • colorDepth,pixelDepth: 颜色深度。
    • devicePixelRatio: 设备像素比。
  3. 时区与时间信息
    • timezoneOffset: 时区偏移(分钟)。
    • timezone: 时区名称(如Asia/Shanghai)。
    • timestamp: 当前时间戳(可能包含服务器时间差计算)。
  4. Canvas与WebGL指纹
    • canvasFp: 通过绘制Canvas并toDataURL()得到的一个哈希值,极其稳定且唯一。
    • webglVendor,webglRenderer: WebGL厂商和渲染器信息。
  5. 音频指纹
    • audioFp: 通过AudioContext分析音频信号处理差异得到的哈希。
  6. 字体列表:通过测量特定字符的渲染宽度来推测已安装的字体。
  7. getinfo返回的特定数据
    • serverToken: 从getinfo响应中获取的令牌。
    • config: 服务器下发的配置,可能包含加密算法标识、版本号、盐值(salt)等。

4.2 数据处理与组装流程

采集到原始数据后,并不会直接加密。通常会经过以下步骤:

  1. 数据标准化:将所有值转换为字符串或数字,处理可能的undefinednull
  2. 排序与序列化:为了确保同一环境每次生成的输入一致,会对采集到的对象按键名进行排序(通常是字典序),然后使用JSON.stringify序列化成字符串。这里就是我们之前HookJSON.stringify能抓到的地方。
  3. 添加噪声与混淆:可能会在序列化后的字符串前后拼接一些固定的或随机的字符串(盐值),或者对某些字段进行简单的变换(如异或、Base64编码等)。
  4. 计算摘要(哈希):对上一步得到的字符串计算哈希(如MD5、SHA-1、SHA-256)。哈希值通常作为中间结果或最终blackbox的一部分。这里可能会Hook到crypto.subtle.digest或类似函数。
  5. 核心加密:将哈希结果(或整个处理后的字符串)使用对称加密算法(如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。即使keyCryptoJS.lib.WordArray对象,我们也可以调用key.toString()来查看其16进制表示。记录下这个key和对应的IV(如果有)。然后,我们可以尝试用Python的pycryptodome库,使用相同的keyIV、模式和填充,对相同的明文进行加密,看结果是否与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; }

deriveKeyFromTokenderiveIVFromToken的函数实现,取决于我们的逆向结果。可能很简单,比如key = CryptoJS.MD5(serverToken).toString().substr(0, 16);也可能比较复杂,涉及多次哈希和拼接。

5.3 完整流程验证与问题排查

  1. 获取实时数据:运行我们的小程序分析环境,从Network面板中复制一次真实的getinfo请求的响应数据(serverToken,config)。
  2. 运行模拟器:将上述真实数据输入我们的模拟程序,生成一个blackbox_sim
  3. 触发真实请求:在小程序环境中,同时触发一次真实的blackbox上报请求,从Network面板或Hook日志中捕获真实的blackbox_real
  4. 对比:比较blackbox_simblackbox_real
    • 如果完全一致:恭喜,大功告成!
    • 如果长度相同但内容不同:很可能加密的keyIVmodepadding分析有误,或者哈希前的字符串处理(如排序规则、盐值)有细微差别。
    • 如果长度都不同:可能连算法都判断错了,或者数据采集项有遗漏/多余。

常见问题排查表

问题现象可能原因排查方向
模拟结果长度与真实结果不同加密算法判断错误;数据序列化格式不对(如多了空格)。检查加密函数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可能会升级,采用更复杂的对抗技术:

  1. 代码虚拟化与混淆:核心逻辑被转换成自定义的字节码在虚拟机中执行。应对思路:重点Hook虚拟机与外界交换数据的接口函数;或者尝试寻找并dump出虚拟机解释执行后的“还原”代码(如果存在)。
  2. WebAssembly (WASM) 加密:加密算法用WASM实现,逆向难度大增。应对思路:使用调试器对WASM模块进行调试;使用wasm2c等工具将其转换为C代码再分析;或者直接Hook JavaScript与WASM模块交互的import/export函数。
  3. 环境检测与反调试:检测开发者工具、识别Hook行为、检测代码执行时间异常等。应对思路:使用更隐蔽的Hook方法(如Proxy);在合适的时机(如代码加载前)注入Hook脚本;使用debugger;语句反反调试,或通过代理工具修改响应来绕过检测代码。
  4. 动态密钥与算法:密钥不再固定或简单派生,而是每次由服务端下发一个随机数或算法标识,客户端动态选择加密算法。应对思路:需要更精细地分析getinfo返回的config,并Hook所有可能的加密算法入口点。

6.3 最后的建议

这类分析工作是对耐心、细心和逻辑推理能力的综合考验。它没有一成不变的公式,每个案例都有其独特性。最重要的不是记住某个特定小程序的破解方法,而是掌握“动态分析、数据追踪、假设验证”这一套方法论。从简单的案例开始,逐步挑战更复杂的保护,你的逆向工程能力会在解决一个又一个具体问题的过程中得到实质性的提升。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询