前言
补环境的难度,还得看国外的验证码盾。hCaptcha 的环境检测点少说也有千八百个,想完整补出来真得沉下心。我这次光补环境代码就写了三千多行。
另外它还设计了大量异步调用,很容易把人绕晕。
本文主要记录逆向分析的完整过程,以及补环境过程中遇到的关键检测点。
什么是 hCaptcha
hCaptcha 是一种现代、隐私优先的人机验证(CAPTCHA)服务,用来帮网站区分真实用户和自动化机器人,比如爬虫、垃圾发送器或恶意脚本,从而阻止滥用、欺诈和数据抓取。自 2018 年推出以来,hCaptcha 已经成了很多企业、政府机构以及开发者的验证方案选择,尤其是它比较重视用户隐私。
官网: https://www.hcaptcha.com
开发者文档: https://docs.hcaptcha.com
目标网站
这次分析的 demo 网站是 Steam 账号注册页面:
https://store.steampowered.com/join
如果访问不了,需要先处理网络环境。
抓包分析
打开注册页面后,正常情况下会看到验证框。如果本地浏览器环境干净,勾选后可以直接无感通过。

如果环境异常,则会弹出图片验证码。验证码类型很多,拖拽、点击都有。

下面这张截图是未通过验证时弹出来的图片选择任务。

接下来打开控制台抓包分析。
第一个关键请求是 hcaptcha.html。地址里 v1 后面跟着的是版本号,大概一周更新一次。

第二个是 checksiteconfig。注意 sitekey 每个网站唯一。

响应内容里有一段 JWT 格式的数据。

把它解码之后,可以看到 hsw 类型的字段,后面跟着一串加密字符串。

接着加载的是 hsw.js,这是核心加密文件。

再往后就是最后的验证接口 getcaptcha。

可以看到请求体是一堆乱码,实际上就是二进制 buffer。

响应体同样是乱码,也是 buffer 格式。

其实可以手动改它的代码,让接口改用标准 JSON 格式。在 hcaptcha.html 里找到下图中这个位置,把 accept 和 content-type 相关代码处理一下。

还有一处也要改。在 html 头部位置,这是内容安全策略(CSP),用来防止篡改。这里只保留 worker 相关属性就行。

改完刷新页面重新验证,显示的内容就正常了。

表单字段整理如下:
v:hCaptcha 版本号
sitekey:网站唯一 key
host:网站域名
hl:语言
pdc、pem:包含时间相关参数
c:checksiteconfig 接口中的参数
motionData:包含时间戳、轨迹等环境信息
n:hsw.js 文件核心加密结果
验证接口的响应里,generated_pass_UUID 的值如果以 P1 开头,就表示验证通过。

逆向分析
这一节主要分析怎么找到 n 值的加密入口位置。
先打上全局 XHR 断点,然后勾选验证码进行验证。断点会停在 hcaptcha.html 文件里,这时要发送的参数就是 ArrayBuffer 格式。
我这里没有提前改文件,如果改完就会显示明文。

往上翻代码,发现当前正处在一个 Promise 链里。那我们在这个 Promise 之前再打个断点看看。

这时 t.data 已经有值了,继续往上跟调用栈。

跟了几个栈之后,找到了 data 的新位置 l。l 是前面 Promise 链式调用传过来的,继续跟 Promise。

l 是由 s 赋值过来的,s 就是明文的参数对象。

这里可以看到所有参数都添加进了 s 里面。我们主要目标还是找 n 的来源。

从代码关系看,n = r.solved,而 r = e.proof。再继续往上跟栈找 e。

可以看到这又是 Promise 异步传过来的。proof 在代码里有多处,不确定的话就全部打上断点,看实际进哪个。
不过实际入口还在更上层,还要继续找 t。

下面这张图继续展示异步链路里的关键位置。

这里可以看到调用了 Promise.all 方法,迭代里有一个 xr 方法,它也是异步的。

可以直接在控制台调用 xr 看结果,它执行完后会输出我们想要的数据。

接着进入 xr 内部继续分析。xr 里面只有下图位置中的 solved 有值,打上断点跟过去。

到这里 n 值已经有了,是上面异步 return 出来的。

执行 n(i.req, o) 方法后,返回了我们需要的最终结果。传入了两个参数,分别是 JWT 解码后的请求数据和配置对象。
继续进入 n(i.req, o) 方法,这回终于进了 hsw.js 文件内部。

首先它会将传进来的 JWT 数据进行解码。

最后又 return 了一个异步。aG 方法实际是加载了一个 WebAssembly 模块。

手动调用后也能正常返回结果。

到此,加密入口的流程就分析完了。接下来就可以把 hsw.js 文件拿下来,开始补环境。
插桩分析点
因为用到了 WebAssembly,JS 会一直和 wasm 交互存取参数。所以我们需要找到交互过程中到底检测了哪些环境。
回到加密入口位置。

往下走,进入 kc.Ob 方法里。

fm 里面都是 wasm 的方法。

先进入 Mv 方法里。

这里面操作了一个大数组。补环境的时候也要在这里对照一下,看自己补的和浏览器上是否一致。可以在相关位置打上日志。

日志跑起来之后,能看到很多环境调用的索引和参数。

末尾就是日志的出值,可以拿自己的补环境结果跟这里一一对照。

在补环境过程中,经常能见到一种方法调用:第一个参数是一串数字。进入方法最后的位置。

可以看到,所有检测出来的环境信息都被存进了一个大数组。所以这也是一个很重要的校验点,补出来的内容最好和浏览器这边完全对齐。

补环境
下面说一些我遇到的环境检测点。
1. RTCPeerConnection
WebRTC 的核心接口,用于在浏览器之间建立点对点连接,实现实时音视频或数据传输。
这个在之前分析腾讯滑块时也见过,所以还算熟悉。

2. RTCRtpSender、RTCRtpReceiver
这两个接口用来处理音视频流的发送和接收。

3. OfflineAudioContext
需要模拟音频数据的处理过程。

4. WebGL2RenderingContext
这块参数非常多,建议直接从浏览器里复制下来。

5. 一堆描述符检测
这里要注意,创建函数时要用箭头函数,因为箭头函数没有 prototype 属性。
检测代码如下:
"prototype" in AudioBuffer.prototype.getChannelData
在浏览器中执行的结果是 false。

在本地 Node.js 里测试,如果写法不同,结果就会不一样。

6. Math 精度差异
不同 JS 引擎的底层数学库有差异,会导致计算结果不完全一致。
浏览器中的结果:

Node.js 中的结果:

7. 字体指纹
写个方法直接从浏览器里导出即可,主要涉及 measureText 的返回值模拟。

8. Canvas 指纹
同样可以通过导出的方式补齐,重点在于 toDataURL 的返回处理。

9. Worker、SharedWorker
这两个也需要模拟,尤其是 postMessage 和消息事件的处理。

结果验证
环境补好之后运行脚本,可以看到成功拿到了验证 key,返回的 pass 也是 True。

如果环境校验不过,服务端会返回验证码图片链接,要求继续做图片验证任务。

到这里,hCaptcha 的逆向和补环境流程就跑通了。整篇文章涉及大量 JavaScript 调试和 WebAssembly 交互,真正动手的时候建议多对照浏览器日志,耐心对齐每一个检测点。如果你也在做类似验证码逆向,欢迎到云栈社区一起交流。