mouyan4代滑块验证码逆向分析

What matters is what you do, not what you think. ——《疯狂前女友》

行动比空想更重要。

PS: 本文仅供学习参考、仅供学习参考、仅供学习参考,不得用于商业用途。

请求分析

本次目标站点

aHR0cHM6Ly93d3cuZ2VldGVzdC5jb20vYWRhcHRpdmUtY2FwdGNoYS1kZW1v

第一个请求

  需要从源码中提取adaptive-captcha-demo.js链接地址以进行下一步请求

第二个请求

  提取 captchaId 参数

  上面的两个请求一般情况下是可以忽略的,因为构造这两个请求是为了获取 captchaId 的值,但一般情况下这个值是固定的。

第三个请求

  这里面包含了验证码图片、js等文件链接地址,这意味着逆向从这里开始。

分析一下这个请求所需要的参数

captcha_id: 24f56dc13c40dc4a02fd0318567caef5  # 固定值,不出意外以为一个站定就一个
challenge: bcf91a78-71a0-4820-9578-dffff5cebd82  # 动态值,需要寻找生成方式
client_type: web  # 固定值,目标站点类型
risk_type: slide  # 固定值,验证码类型
lang: zh  # 固定值
callback: geetest_1651037128229  # 动态值,geetest_+13位时间戳

  分析 challenge 参数,祭出全局搜索大法,在3个文件中匹配到了7个,经过测试,定位到了 gt4.js 文件第310行,在这打上断点。

  点击进入下一步函数调用,进入到了 uuid 函数

  我们点击堆栈回到上一步,发现 config.challenge 是没有值的,也就是说 challenge 是由uuid 这个函数生成

  这里可以把js代码抠出来,可以改写成 Python 代码,也可以使用 Python 中的 uuid 包生成

使用 Python 中的 uuid 包生成

  综上,看下效果

第四个请求

  点击按钮开始验证,验证码浮出,滑动验证码通过验证,控制台 verify 验证链接出现,接下来的重中之重就是构造这个请求。

分析一下需要哪些参数

captcha_id: 24f56dc13c40dc4a02fd0318567caef5  # 和前面一致
client_type: web  # 和前面一致
lot_number: 4e89854276c2413aa650294441107bd6  # 第三步请求获取
risk_type: slide  #
payload:  # 第三步请求获取
process_token:  # 第三步请求获取
payload_protocol: 1  # 固定值
pt: 1  # 固定值
w: # 动态值需要分析
callback: geetest_1651040592478  # 动态值,geetest_+13位时间戳

  嗯。。。。缺了个 w,点击 verify 链接的启动器(如果你没有修改语言,那么显示的应该是Initiator),发现请求调用堆栈全部来自于 gcaptcha4.js 文件。

  点击进入 gcaptcha4.js 文件,格式化一下。嗯。。。共13485行,混淆的不要不要的。

JS 还原与替换

  为了保住我的头发,我还是不选择硬刚了,万一刚着刚着头发就没了,咋搞!!!还是整点靠谱的方式吧 —— AST还原混淆JS。

babel库安装

1
npm install @babel/core --save-dev

导入模块

1
2
3
4
5
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const t = require("@babel/types");
const generator = require("@babel/generator").default;
const fs = require("fs");

复制源码

  将 gcaptcha4.js 中代码全部复制下来,保存至本地。折叠起来效果如下

AST 还原

主要步骤

  • 删除节点中的extra属性
  • 控制流平坦化
  • 删除无关函数

替换站点 gcaptcha4.js 文件

  可以使用 Chrome 的插件 ReRes (如何安装可自行百度),可以使用 Charles 替换 js 文件,也可以直接在 Chrome 上替换 js 文件。详情请查看 《本地js文件替换源网页js的几种方式

替换效果

替换前

替换后

JS 分析

  全局搜素大法,搜索 w 参数,然后在合适的位置打上断点可以看到 w 是由 var r = (0, d[“default”])(l[“default”][“stringify”](e), a) 生成。

  先来看看 e 是个啥?如下图:

e 的组成

device_id  # 固定值
em  # 固定值
ep # 固定值
geetest # 固定值
lang # 固定值
lot_number # 第三步请求获取
passtime # 通过时间
pow_msg # 需分析
pow_sign # 需分析 
pyxo "1940377373" # 每天更新(key,value都会变),可写死
setLeft # 滑动距离
track  # 计算轨迹得出
userresponse # 需分析

pow_msg 与 pow_sign

pow_msg

  全局搜索大法,打上断点


  可以看到 pow_msg 是 u 和 p组成的

1
"pow_msg": u + p

u 的组成

1
u = n + "|" + a + "|" + s + "|" + o + "|" + t + "|" + e + "|" + r + "|";
n:"1" # 第三个请求获取的 version 固定就行
a:0 # 第三个请求获取的 bits 固定就行
s:"md5" # 第三个请求获取的 hashfunc 固定就行
o:"2022-04-28T09:38:40.768352+08:00" # 第三个请求获取的 datetime
t:"24f56dc13c40dc4a02fd0318567caef5" # captchaId
e:"37cda35e6a0b4c8d86a2fd8cfaa75af5" # 第三个请求获取的 lot_number
r:"" 固定值

p 的组成

1
var p = (0, b["guid"])()

  分析下 b[“guid”]

pow_sign

可以明显看出 pow_sign 是 g 进行 md5 生成,而 g = u + p 即 g = pow_msg,那我们直接将 pow_msg md5 即可。

setLeft、track、passtime、userresponse

setLeft

  setLeft 就是 滑动距离取整

track 与 passtime

  track 是由 _ = p[“default”][“prototype”][“$_BABR”](i); 生成,i 就是滑块轨迹,这句代码上面的一行是做了个判断,轨迹的长度范围在 0-160 之间。

  我们进入 p[“default”][“prototype”][“$_BABR”]

  passtime 是由当前时间戳减去点击滑块开始的时间。

  不过这玩意好像没那么简单,我们在使用的时候也不好控制。哎!怎么感觉这个 a 的值有点熟悉啊?这个不就是 i 最后一个列表的最后一个值吗?

  多试几次,发现还真的是啊!而且是除去第一个列表后是一个不断增大值。

  继续分析 i ,发现 i 的第一个列表中第一个与第二个值的范围大致是 -50-0,第三个值总是0;第二个列表中的三个值都是 0 。如下面的一些图:


  经过分析得出大致结论:

  • i 的第一个列表中第一个与第二个值的范围大致是 -50-0,第三个值总是0;
  • 第二个列表中的三个值都是 0;
  • 从第三个列表开始,列表中的第一个值是递增的变化不大,列表中的第二个值也是递增的,但是变化不大,所以可以直接把第二个值定死,第三个值也是不断累加,但是变化较大。细心的朋友或许还发现了另一个规律,那就是:最后一个列表的第一个值如果可以被10整除那它就等于 setLeft ,否则加上1等于setLeft;
  • i 的长度在 0-160 之间,当然在生成的时候肯定不能太小;
  • 当上述条件成立轨迹生成完毕
userresponse

  userresponse 直接由 setLeft 除去一个定值生成

l[“default”][“stringify”]

  可以看到 l[“default”][“stringify”] 就是将 e 转换成字符串

d[“default”]

  接下来着重看 d[“default”] 做了些什么,通过断点跳转至如下图方法中。

  可以看到该方法中两个 if 仅做判断,没有参与计算,所以可以直接忽略掉,将其余代码美化一下方便阅读。

1
2
3
4
5
6
var n = (0,c["guid"])(), a = new _["default"]()["encrypt"](n);
while (!a || 256 !== a["length"])
n = (0, c["guid"])(), a = new _["default"]()["encrypt"](n);

var o = i["default"]["encrypt"](e, n);
return (0, c["arrayToHex"])(o) + a;

  接下来一点点分析

c[“guid”]

_[“default”]

  这个方法就是 C 方法,但不止下图的这么点,全部扣下来大概要500来行代码,因为太长这里就不做展示了。

i[“default”]

  下图展示的是 i[“default”][“encrypt”],而我们要想正常运行需要完整的将 i[“default”] 扣下来,整体大概200来行代码。

c[“arrayToHex”]

c[“arrayToHex”] 方法就下图这么多,全部扣下来即可。

  至此 w 参数的分析就完活了。。。验证码分析也结束了。。。剩下的就是用代码实现以上逻辑即可。

写在最后

  本文的难点主要集中在还原上,代码不还原就很难进行下一步;还原后,捋清楚整体流程,慢慢摸索就能够搞定了。本文并没有提到具体滑块距离的实现,算是个小坑,感兴趣的朋友可以自行探索,实现的方法可不止一种哦!以上,搞下来整体通过率在 97% 左右,最后附上效果图。
  第一次

  第二次

  第三次