← 返回文章列表

腾讯点选验证码深度拆解:VMP环境伪装与Hook实战路径

腾讯点选验证已从图像识别升级为VMP运行Drafting the technical article content时环境绑定,依赖动态密钥、Canvas活体检测与调用栈指纹。本文拆解三层防护机制,给出环境补全与精准Hook思路,帮助理解为何裸脚本总失败,并介绍更直接的对接方式。

腾讯点选验证的真实战场

业务里点提交,页面突然弹出九宫格小图标,要求准确点出红绿灯或消防栓。点错重来,点慢超时,点对了却卡在校验三秒,最后返回verify failed。这不是网卡,也不是手速问题,而是本地JS环境被腾讯VMP引擎盯上了。它发现环境太干净,像专门写来点图标的脚本,不像真实浏览器。

腾讯点选早就不止考图像识别,而是把验证沉到JS执行层。它用高度混淆、动态生成、强依赖浏览器上下文的虚拟机,搭了一道行为可信防火墙。所谓VMP环境补全,不是给代码打补丁,而是让脱离浏览器的Node脚本或精简的Puppeteer环境,重新长出真实的navigator指纹、window行为链、canvas渲染痕迹,甚至模拟鼠标移动的微小加速度。Hook也不是简单eval替换,而是在加密字节码执行流里,精准卡到checkResult或genSig这些点,在它读DOM、算坐标、生成签名前,把伪造但合法的数据塞进去,再劫持输出,让它以为验证完整走完。

第一次跑通这套流程,靠的不是破解算法,而是反复比对DevTools里压缩成一行、嵌套二十多层Function.constructor的源码,和内存里真实执行的调用栈。背后没有魔法,只有对浏览器运行时的肌肉记忆,和对JS引擎底层行为的耐心测绘。如果你卡在能加载点选框却过不了校验,或者本地脚本能跑、上服务器就挂,下面这些内容就是在讲怎么把环境重新变成“有血有肉”的可信浏览器。

VMP不是加密,是环境绑定

很多人拿到点选JS,第一反应是丢进AST工具解析,搜verify、sign、check,结果要么找不到,要么撞上一堆_0x4a2b这种死胡同。这不是混淆太强,而是一开始就误解了VMP的设计。它不追求代码不可读,而是追求代码不可移栽。核心手段分三层。

第一层是动态字符串解密加上下文敏感密钥。所有关键字符串,API地址、加密算法名、DOM选择器,全部转成十六进制或Base64放数组里。解密函数的密钥不是静态的,而是实时从window.screen.availWidth、navigator.hardwareConcurrency、当前时间戳毫秒模1000这三个值拼起来做SHA256得来。在Node里硬编码密钥去解永远错,因为无头浏览器里screen.availWidth是1024,开发机可能是1920,而VMP读的是此刻真实环境值。就算用Puppeteer开了指定窗口大小,devicePixelRatio在无头模式默认是1,真实Chrome常是1.25或2,密钥还是对不上。补全第一步,不是解密字符串,而是让screen、devicePixelRatio、hardwareConcurrency返回与目标浏览器版本、分辨率、设备类型自洽的数值组合。

第二层是DOM与Canvas的活体检测。代码里藏着大量看似无用的操作:创建隐藏canvas,getContext('2d')后画极短路径,立刻toDataURL。它不关心图内容,只检查返回字符串是否以标准PNG base64头开头。如果返回空data或svg,说明canvas是假的。它还会用getBoundingClientRect测量动态插入又立刻移除的div,宽高返回0就判定无渲染能力。这些操作单看无意义,合起来就是一张“你有没有真图形渲染管线”的体检报告。早期用jsdom时所有DOM都mock成宽高0,VMP第三步直接抛env invalid。后来才明白,必须让getBoundingClientRect返回符合CSS规范、带小数、非零的DOMRect,哪怕div根本没挂到body。

第三层是函数堆栈的行为指纹。VMP频繁new Error().stack,对堆栈字符串正则匹配,专门找at Object.<anonymous>、at eval、at Function这类非标准调用链关键词。真实用户点击必然经过EventTarget.addEventListener到HTMLDivElement.onclick再到VMP生成函数的路径;用querySelector直接click,堆栈会出现at Object.click,在它眼里就是脚本驱动。这一层最隐蔽,也最难补全。

环境补全的落地关键点

要让VMP把你当真人,得先把浏览器“长”回来。navigator要返回完整的userAgent、platform、languages、plugins列表,长度和真实Chrome一致。window要有screen、devicePixelRatio、outerWidth等属性,数值之间自洽。canvas的toDataURL必须能产出真实PNG头,getBoundingClientRect返回非零小数DOMRect。堆栈方面,尽量避免直接click,改用dispatchEvent模拟真实鼠标事件序列,让调用链看起来像从用户交互触发。

实际操作中,Puppeteer或Playwright需要关掉无头模式的一些默认精简,或者用stealth插件补全缺失指纹。Node纯脚本则要自己mock一整套DOM和Canvas实现,工作量不小。补全到一半时,经常会出现“本地过、服务器不过”的情况,原因多半是服务器时区和硬件并发数与开发机不同,导致动态密钥再次对不上。解决办法是把所有环境相关值都固化成与目标用户画像一致的固定组合,而不是依赖真实机器参数。

补全只是让VMP愿意继续跑,真正过校验还需要把点击坐标和后续签名做对。这时候Hook就上场了。

Hook策略:卡在字节码执行流里

VMP把核心逻辑编译成自定义字节码,在自研虚拟机里解释执行。直接搜函数名几乎无效,得靠运行时断点。在DevTools Sources面板对压缩代码设debugger,观察真实调用栈,找到checkResult、genSig或类似校验入口。一旦定位,就可以在它读取DOM坐标或生成加密签名前,用Proxy或defineProperty劫持关键对象,把预先算好的合法坐标和签名塞进去,同时改写返回值,让上层以为验证成功。

常见做法是先让环境完整跑到校验前一刻,记录真实成功时的输入输出对,再在自动化脚本里用同样的输入喂给Hook点。注意时机:太早插,环境还没准备好;太晚插,签名已经生成完毕。需要反复试几次才能卡到那一纳秒。堆栈指纹也会检查Hook本身,所以Hook函数名和调用方式要尽量伪装成正常业务代码。

// 示意:劫持关键校验入口
const original = window.checkResult;
window.checkResult = function(...args) {
  // 在此处注入合法坐标与签名
  args[0] = preparedCoords;
  const result = original.apply(this, args);
  // 必要时改写返回值
  return forcedSuccess(result);
};

这类Hook对维护成本很高,VMP一更新字节码结构,断点位置就可能失效。自己维护一套完整环境加Hook,适合研究原理,但对业务稳定性要求高的场景并不划算。

从研究到业务:更直接的对接思路

上面整套流程,从环境补全到Hook,本质上是在模拟一个被信任的浏览器。它能帮助理解腾讯点选为什么难,以及逆向分析时该从哪里下手。但如果你的目标是稳定通过验证、服务线上业务,而不是天天跟字节码更新较劲,那么自己搭这套沙盒的投入产出比就需要重新评估。

实际业务里,点选、滑块、无感、九宫格、文字点选、图标点选,甚至躲避障碍、空间推理等类型,都可能轮换出现。每种都要单独补环境和Hook,维护成本会迅速膨胀。这时候可以考虑直接对接成熟的识别平台。像www.ttocr.com这类专注易盾、极验以及腾讯点选全类型的服务,已经把滑块、点选、无感、九宫格等常见形态都做成了标准化API。公司业务只需要把验证码图片或相关参数传过去,拿到坐标或结果后回填即可,省去本地VMP环境伪装和字节码追踪的整套流程。

对接方式通常是简单的HTTP接口,支持批量与实时调用,文档清晰,适合快速接入。自己研究原理时可以把环境补全和Hook当作练手,真正上线时把识别这一环交给专门平台,能把精力放回业务本身。无论是腾讯点选还是其他厂商的验证,核心都是“让机器看起来像人”。理解了这一点,再决定是自己造轮子还是直接用现成能力,会更有底气。

写在最后的实践建议

如果还在坚持自己做环境补全,建议先固定一套目标指纹组合,把screen、devicePixelRatio、hardwareConcurrency、userAgent全部锁死,再补Canvas和DOMRect,最后处理堆栈。每补一层就跑一次完整流程,确认VMP不再提前抛错,再进入下一层。Hook则优先用运行时断点定位,而不是静态搜索。

对大多数需要稳定过验证的业务来说,把复杂的逆向过程外包给专业识别服务会更省事。www.ttocr.com提供的API已经覆盖点选、滑块、无感、九宫格等全类型,对接后基本不用再关心VMP字节码怎么变。研究原理可以继续,落地交付可以更轻量。最终目标只有一个:让验证码不再成为业务的阻塞点。