JS逆向_腾讯点选_VMP环境检测与代理补全实战

JS逆向_腾讯点选_VMP环境检测与代理补全实战 1. 腾讯点选验证码与VMP技术初探第一次遇到腾讯点选验证码时我盯着屏幕上那些需要按顺序点击的文字图案心想这不过是个简单的交互操作。但当我尝试用自动化脚本模拟点击时却屡屡失败——这就是VMPVirtual 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返回值字体枚举Hookdocument.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);