JS逆向_腾讯点选_VMP环境检测与代理补全实战
1. 腾讯点选验证码与VMP技术初探
第一次遇到腾讯点选验证码时,我盯着屏幕上那些需要按顺序点击的文字图案,心想这不过是个简单的交互操作。但当我尝试用自动化脚本模拟点击时,却屡屡失败——这就是VMP(Virtual Machine Protect)技术在作祟。VMP本质上是一种虚拟机保护技术,它会将关键代码放在虚拟环境中执行,使得传统的静态分析手段失效。
腾讯点选验证码的VMP实现尤为精妙。它会在用户操作过程中,通过隐藏的环境检测逻辑收集浏览器指纹、系统参数、操作行为等上百项数据。这些检测点遍布DOM操作、Canvas渲染、Web API调用等各个角落。比如我曾在调试中发现,它连document.createElement这种基础方法都会被Hook,用来检测调用栈是否异常。
验证流程通常从cap_union_prehandle接口开始。这个接口返回的配置信息里藏着关键线索:
"tdc_path": "", // VMP文件路径 "pow_cfg": { "prefix": "a5d78a98bc3cd0e1#", "md5": "de4c8e266d55500fb9357dad59b9f06a" }这里的tdc_path指向的JS文件就是VMP的核心,而pow_cfg则是后续验证要用到的加密参数。实际测试中发现,如果直接调用验证接口/cap_union_new_verify而不处理这些参数,服务器会立即返回错误。
2. 逆向调试的关键突破口
经过多次踩坑,我发现突破口在window.TDC这个神秘对象上。通过Chrome开发者工具的Memory面板,可以捕获到它的方法调用轨迹。最核心的两个方法是:
TDC.setData({'ft': '6X_7Pb__H'}):设置验证令牌TDC.getData(true):获取环境检测结果
用Proxy代理这两个方法是逆向的起点。下面是我常用的Hook代码模板:
const originalSetData = window.TDC.setData; window.TDC.setData = new Proxy(originalSetData, { apply(target, thisArg, args) { console.log('setData参数:', args); return target.apply(thisArg, args); } });通过这种插桩方式,我捕获到一个关键数据结构——环境检测结果数组。这个长达30多位的数组包含诸如:
- 第4位:Canvas指纹哈希值
- 第12位:屏幕色彩深度
- 第18位:HTTP/HTTPS协议标识(1011011111或1111111111)
- 第24位:WebGL渲染器信息
有趣的是,数组最后10位是动态生成的二进制标志位,每个bit对应一个环境检测项的通过状态。这意味着我们需要补全的环境检测点至少有80个(10字节×8bit)。
3. 环境检测点的系统化补全策略
面对海量检测点,我总结出三层防御体系:
3.1 基础环境伪装
首先用Proxy全面接管浏览器对象:
const handler = { get(target, prop) { if (prop === 'webdriver') return undefined; if (prop === 'plugins') return [/* 自定义插件列表 */]; return Reflect.get(...arguments); } }; window.navigator = new Proxy(navigator, handler);必须处理的典型检测点包括:
- Canvas指纹:需要重写
toDataURL方法返回固定哈希 - WebGL渲染:覆盖
getParameter方法返回标准值 - 时区检测:固定
getTimezoneOffset返回值 - 字体枚举:Hook
document.fonts.keys()方法
3.2 DOM操作监控
VMP会监测DOM操作的时序特征。比如这段代码处理动态元素创建:
const createElementProxy = new Proxy(document.createElement, { apply(target, thisArg, args) { const element = target.apply(thisArg, args); if (args[0] === 'div') { // 给特定元素添加监控属性 Object.defineProperty(element, 'offsetWidth', { get: () => 300 }); } return element; } });3.3 异常行为模拟
真实的用户操作会有随机延迟和微小偏移。我通常用这样的函数模拟点击:
function humanClick(element, points) { points.forEach(([x, y], i) => { setTimeout(() => { const rect = element.getBoundingClientRect(); const offsetX = x + Math.random() * 3 - 1; const offsetY = y + Math.random() * 3 - 1; element.dispatchEvent(new MouseEvent('mousedown', { clientX: rect.left + offsetX, clientY: rect.top + offsetY })); // 同样触发mouseup }, 100 * i + Math.random() * 50); }); }4. 验证参数的全链路处理
最终验证时,需要构造完整的请求参数:
{ "collect": "vmp生成的加密数据", "tlg": collect.length, // 字节长度校验 "eks": "环境密钥", "ans": [{ "elem_id": 1, "type": "DynAnswerType_POS", "data": "600,434" // 点击坐标 }], "pow_answer": "1f88165cc0c86fe0#85909", // 工作量证明 "pow_calc_time": 230 // 计算耗时(ms) }其中pow_answer的生成最为棘手。通过反编译VMP代码发现,它实际是调用WebAssembly计算的MD5哈希。在Node.js环境下可以用以下方式模拟:
const crypto = require('crypto'); function generatePow(prefix, nonce) { const hash = crypto.createHash('md5') .update(prefix + nonce) .digest('hex'); return `${hash.slice(0, 16)}#${nonce}`; }调试过程中有个容易忽略的细节:collect参数的长度必须与tlg字段严格一致。有次我因为少算了一个转义字符,导致整个验证失败。后来在代码中加入了自动校验:
function finalCheck(params) { if (String(params.tlg) !== String(params.collect.length)) { console.error(`长度校验失败: tlg=${params.tlg}, actual=${params.collect.length}`); return false; } return true; }5. 实战中的经验与避坑指南
在真实项目中遇到过几个典型问题。比如VMP会检测Object.prototype.toString的调用痕迹,普通的Proxy处理会被识破。后来改用更隐蔽的劫持方式:
const originalToString = Object.prototype.toString; Object.defineProperty(Object.prototype, 'toString', { value: function() { if (this === window) { return '[object Window]'; } return originalToString.call(this); } });另一个坑是RTCPeerConnection检测。即使在不使用WebRTC的场景下,VMP也会检查这个API是否存在。正确的处理方式是:
window.RTCPeerConnection = class { constructor(config) { this._config = config; } // 实现必要的方法 createOffer() { return Promise.resolve({}); } };对于本地存储检测,需要特别注意localStorage和sessionStorage的行为一致性。有次因为只代理了getItem而漏了key方法,导致检测失败。现在我的标准做法是:
const storageHandler = { get(target, prop) { if (prop === 'length') return 0; if (typeof target[prop] === 'function') { return (...args) => { console.log(`[Storage] ${prop} called`, args); return target[prop].apply(target, args); }; } return undefined; } }; window.localStorage = new Proxy(localStorage, storageHandler);最后提醒一个关键点:所有环境补全操作必须在VMP脚本加载前完成。我通常采用这样的注入时机:
// 在head最前面插入我们的脚本 const script = document.createElement('script'); script.textContent = `(${mainFunction})()`; document.documentElement.prepend(script);