最近分析了一条登录页里的滑块验证链路。页面上看起来只是拖一下滑块,Network 里却能看到好几段流程:先拿业务配置,再初始化 SDK,然后加载挑战,拖动结束后生成状态参数,最后把验证结果带到登录接口里。
我这次的思路比较朴素:先不急着扣混淆代码,先把请求顺序和字段流向理清楚。验证码这类链路如果一上来就看压缩后的 JS,很容易陷在变量名和分支里;先从请求入手,后面下断点会顺很多。

先看整体流程
打开开发者工具后,我先清空 Network,然后刷新登录页。只看 XHR 和脚本请求,大概能看到这几段:

第一段是业务配置。页面先向业务后端要验证码配置,拿到验证类型、初始化参数之类的信息。
第二段是 SDK 初始化。前端把配置交给滑块 SDK,SDK 接管后续挑战加载和用户交互。
第三段是挑战加载。这个阶段会返回本次滑块所需的图片资源、批次标识和一些后续会进入验证参数的字段。
第四段是拖动验证。用户拖动结束后,前端生成一份状态对象,再提交给验证服务。
第五段是业务登录。验证服务返回结果以后,登录接口会带着这份结果继续走账号密码校验。
到这一步,我已经知道后面要盯两个位置:一个是 load 阶段返回了什么,另一个是 verify 阶段提交了什么。
抓包:先把请求角色分清楚
我把请求先简化成下面这个形状:
text
/config -> 获取验证码配置
/load -> 获取本轮挑战
/verify -> 提交拖动状态
/login -> 提交业务登录
这里不要急着解释每一个参数。抓包阶段最重要的是给请求定角色。
config 解决的是业务配置问题;load 解决的是本轮题目和上下文问题;verify 才是拖动后的核心提交点;login 则是业务侧最终校验。
把这四段分清以后,后面看参数会很直观:字段如果来自 load,说明它属于挑战上下文;字段如果拖动结束后才出现,大概率和交互状态有关;字段如果只在登录接口里出现,那就是业务侧消费验证结果。
Hook:把 SDK 回调看清楚
只看 Network 还不够。Network 能告诉我请求发出去了,但看不到字段是什么时候被组装进去的。
这类滑块 SDK 一般会把一部分流程藏在回调里,所以我接着做了一层 Hook,主要看三个点:

| 观察点 | 我想确认的内容 |
|---|---|
fetch / XHR |
登录请求体的大概结构 |
| 动态脚本 | JSONP 请求和 callback 名称 |
| SDK 回调 | 初始化参数、验证后的结果对象 |
比如我先包一层 fetch,只看登录请求什么时候发,以及 body 里大概有哪些块:
js
const rawFetch = window.fetch;
window.fetch = async function hookedFetch(input, init) {
const url = typeof input === "string" ? input : input?.url;
if (String(url).includes("/login")) {
console.debug("[flow]", {
stage: "login",
bodyShape: ["channel", "account", "password", "captcha"]
});
}
return rawFetch.apply(this, arguments);
};
这段的作用不是抓完整数据,而是确认登录接口消费了哪些字段。看到 captcha 这一块以后,后面就可以反推它来自哪个 SDK 回调。
JSONP:普通 fetch 看不到的那层
继续看请求时,我发现验证服务有一部分请求是动态插入 <script> 发起的,也就是 JSONP 风格。
这种情况下,只 Hook fetch 会漏东西。我又包了一层 appendChild:
js
const rawAppendChild = Node.prototype.appendChild;
Node.prototype.appendChild = function hookedAppendChild(node) {
if (node?.tagName === "SCRIPT") {
console.debug("[script]", {
type: "dynamic-script",
hasCallback: true
});
}
return rawAppendChild.apply(this, arguments);
};
这一步能看到动态脚本什么时候插入,也能顺着 callback 名称继续观察返回结构。
这里有个细节:JSONP 的返回不是普通 Response,而是执行一个全局 callback。所以要看返回数据,就得在 callback 被设置的时候包一层,而不是只盯 Network。
断点:顺着 verify 往上找
Hook 能让我看到阶段,但要知道字段来源,还是得下断点。
我先在 verify 请求附近停住,然后往上看调用栈。这个过程比较像倒着走:
- 先看最终提交点
- 找到提交前的状态对象
- 看状态对象在哪里被组装
- 再看各个字段来自哪里
跟栈时最有用的不是变量名,而是字段形状。比如拖动距离、耗时、挑战批次、环境摘要、工作量证明这几类字段,即使变量名被压缩了,结构上也能看出分组。
我当时看到的核心判断是:verify 提交的并不是单独一个距离,而是一整个状态对象。距离只是其中一部分。
这点很重要。很多人看到滑块验证,会自然以为关键就是缺口距离;但实际链路里,距离只是交互结果,真正提交的是"交互结果 + 挑战上下文 + SDK 补充字段"的组合。
状态对象:先分层,再看字段
分析到这里,我把状态对象拆成四类来看:
| 类别 | 来源 | 作用 |
|---|---|---|
| 交互结果 | 拖动过程 | 记录位移、耗时、响应值 |
| 挑战上下文 | load 返回 |
标识本轮挑战 |
| 环境摘要 | SDK 采集 | 描述浏览器环境特征 |
| 业务凭证 | verify 返回 |
给登录接口继续校验 |
把它写成结构,大概是这样:
json
{
"interaction": {
"distance": 210,
"duration": 860
},
"challenge": {
"lot": "sample_lot",
"payload": "sample_payload"
},
"environment": {
"summary": {
"browser": "sample",
"feature": "sample"
}
},
"proof": {
"message": "sample_pow_message",
"signature": "sample_pow_signature"
}
}
这一步我没有先去看最后的编码函数,而是先确认 state 里到底有什么。因为最后怎么编码是一回事,编码前的内容是什么才是这条链路的核心。
如果 state 分层理解错了,后面就算找到编码入口,也很难判断哪里出了问题。
距离不是唯一重点
滑块验证最直观的字段是距离。图片上有缺口,滑块拖到缺口位置,前端自然会记录一个横向位移。
但从请求结构看,距离不是唯一重点。
我看到状态里还会带耗时、挑战上下文、证明字段和环境相关字段。换句话说,验证服务并不只是问"你拖到了哪里",还会看"你在什么上下文里拖的""拖动用了多久""这次挑战对应哪一轮"。
这也是为什么我不建议一开始就只研究图像识别。图像距离只是入口,状态对象才是提交点。
怎么判断方向没跑偏
逆向分析里,我比较喜欢看错误阶段变化。
比如登录接口可能有两类结果:
json
{
"stage": "captcha_check",
"result": "failed"
}
和:
json
{
"stage": "account_check",
"result": "failed"
}
如果请求从验证码阶段走到了账号校验阶段,就说明前面的验证码结果已经被业务后端接受。这个信号很有用,因为它能证明分析方向没有偏。
这次链路里,我就是用这种方式确认的:验证码相关字段被业务侧消费后,后续失败点已经进入账号校验。到这一步,load -> verify -> login 的字段流向基本就能闭上了。
回头看防护
把流程跑清楚以后,再回头看防护侧,就会发现几个比较关键的点。
第一,前端编码只能提高分析成本。编码函数在浏览器里运行,就一定有被观察和还原的可能。真正的校验还是要放在服务端。
第二,challenge 最好和业务会话绑定。load 返回的上下文、登录表单、服务端 nonce、浏览器会话之间如果没有关系,后面就会出现更多可组合空间。
第三,服务端不能只看最终位移。位移和耗时是基础字段,但更有价值的是完整交互过程,比如轨迹点、速度变化、回拉、停顿和事件间隔。
第四,环境摘要要参与动态判断。如果环境字段长期稳定,或者和本轮挑战没有关系,它的判断价值就会下降。
第五,图片扰动只是其中一层。缺口识别可以做得更复杂,但如果服务端行为校验不够,单纯增加图片干扰并不能解决根本问题。
这些点看起来都是防护建议,但其实也是逆向分析的反向总结:我能从前端观察到哪些东西,就说明哪些东西不应该单独承担安全判断。
几个调试小结
这次最省时间的地方,是没有一开始就钻混淆代码。
先看请求链路,再看 Hook,再顺调用栈找 state,这个顺序很舒服。每一步都有明确目标:Network 定阶段,Hook 看回调,断点找来源,状态对象做归类。
还有一个小经验:看到"加密参数"不要马上兴奋。最后的参数只是结果,真正应该先问的是:它编码前是什么?从哪里来?哪些字段来自用户交互,哪些字段来自服务端 challenge,哪些字段是 SDK 补出来的?
把这些问题搞清楚以后,最后那层编码反而没那么神秘了。