一次滑块验证链路的授权逆向复盘:从抓包、Hook 到参数结构分析

最近分析了一条登录页里的滑块验证链路。页面上看起来只是拖一下滑块,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 请求附近停住,然后往上看调用栈。这个过程比较像倒着走:

  1. 先看最终提交点
  2. 找到提交前的状态对象
  3. 看状态对象在哪里被组装
  4. 再看各个字段来自哪里

跟栈时最有用的不是变量名,而是字段形状。比如拖动距离、耗时、挑战批次、环境摘要、工作量证明这几类字段,即使变量名被压缩了,结构上也能看出分组。

我当时看到的核心判断是: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 补出来的?

把这些问题搞清楚以后,最后那层编码反而没那么神秘了。

相关推荐
西洼工作室14 天前
B站登录流程全解析:RSA+极验验证
前端·python·极验
西洼工作室18 天前
unipp+vue3+python h5+app极验验证码集成全流程解析
前端·uni-app·全栈·极验
newxtc1 年前
【随行付-注册安全分析报告-无验证方式导致隐患】
人工智能·安全·网易易盾·极验
newxtc1 年前
【北交互联-注册/登录安全分析报告】
人工智能·安全·网易易盾·政务·极验
newxtc1 年前
【中检在线-注册安全分析报告】
人工智能·安全·网易易盾·极验
newxtc2 年前
【新华妙笔-注册/登录安全分析报告-无验证方式导致安全隐患】
人工智能·安全·ai写作·极验·行为验证
newxtc2 年前
【搜狐简单AI-注册/登录安全分析报告-无验证方式导致安全隐患】
人工智能·安全·ai写作·极验·行为验证
newxtc2 年前
【澜舟科技-注册/登录安全分析报告】
人工智能·科技·安全·网易易盾·极验
newxtc2 年前
【天壤智能-注册安全分析报告-无验证纯IP限制存在误拦截隐患】
人工智能·tcp/ip·安全·网易易盾·ai写作·极验