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