百度旋转验证码算法逆向分析

查看 73|回复 9
作者:LiSAimer   
声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!若有侵权,请联系作者删除。
逆向目标
目标网站:
aHR0cHM6Ly96aXl1YW4uYmFpZHUuY29tL2xpbmtzdWJtaXQvdXJs
关键要点:ac_c、p、fs、fuid
抓包分析
在demo页面点击提交按钮后弹出验证码


1.png (86.4 KB, 下载次数: 3)
下载附件
2025-9-2 18:33 上传

init 接口请求载荷
_:时间戳
refer:当前网站的url
ak:固定值,只在不同站点会不一样
ver:固定值,验证码版本


2.png (35.91 KB, 下载次数: 3)
下载附件
2025-9-2 18:33 上传

init 接口响应
响应参数 as 和 tk 需要记录下来,后续会用到


3.png (43.42 KB, 下载次数: 3)
下载附件
2025-9-2 18:33 上传

style 接口请求载荷
用到了init接口获取到的 tk


4.png (53.83 KB, 下载次数: 2)
下载附件
2025-9-2 18:33 上传

style 接口响应
响应参数 backstr、path、ext 后续会用到
path是验证码图片的地址


5.png (77.83 KB, 下载次数: 2)
下载附件
2025-9-2 18:33 上传

滑动旋转验证码后会请求 log 接口
log 接口请求载荷
参数 fs 和 fuid 是我们主要分析的目标


6.png (89.92 KB, 下载次数: 3)
下载附件
2025-9-2 18:33 上传

log 接口响应
响应参数 op 值为1的时候说明结果正确
响应中的 ds 和 tk 就可用于后续你要想要请求的接口中使用


7.png (35.8 KB, 下载次数: 3)
下载附件
2025-9-2 18:33 上传

逆向过程
打上log接口的的xhr断点往上跟栈
或者全局搜索 n.fs
可以定位到fs的生成位置在mkd_v2.js这个文件中


8.png (93.47 KB, 下载次数: 2)
下载附件
2025-9-2 18:33 上传

由于这个js文件后缀是带动态时间戳的
mkd_v2.js?cdnversion=1756709834
断点是打不上的
这里我们全局搜索cdnversion
定位到一个url文件的如下位置


9.png (16.06 KB, 下载次数: 3)
下载附件
2025-9-2 18:33 上传

将后缀全部删除后保存文件
其实就是相当于overrides重写文件操作
刷新页面重试后看到已经没有后缀了
可以愉快的上断点了


9_5.png (39.91 KB, 下载次数: 3)
下载附件
2025-9-2 18:37 上传

可以看到fs赋值了两次
先分析第一次的
n.fs = (0, u.Li)(JSON.stringify(this.rzData), this.secondHandle)
将this.rzData字符串序列化
backstr是style接口响应中的
ac_c是根据旋转角度计算的
mv是轨迹
p是根据style接口响应中ext参数计算来的
common.mv这组轨迹不校验可有可无
其它的环境参数可以写死


10.png (33.92 KB, 下载次数: 2)
下载附件
2025-9-2 18:33 上传

往上跟栈先找到ac_c的生成位置


11.png (62.47 KB, 下载次数: 3)
下载附件
2025-9-2 18:33 上传

n = Number((this.distance / (e - 52)).toFixed(2))
this.distance是滑动的距离
e是固定值290,也就是滑动框整体的长度
轨迹和角度识别直接用开源的就行
https://github.com/lumina37/rotate-captcha-crack
再找 p 的生成位置
赋值位置就在fs的上面
从this.powMap获取


12.png (43.26 KB, 下载次数: 3)
下载附件
2025-9-2 18:33 上传

全局搜索powMap定位到如下位置


13.png (35.15 KB, 下载次数: 1)
下载附件
2025-9-2 18:33 上传

分析可知是个worker接口计算的
找到进入woke.js的入口位置


14.png (10.41 KB, 下载次数: 3)
下载附件
2025-9-2 18:33 上传

很明显,将i.pow方法还原出来即可,这样就得出了p值


15.png (14.12 KB, 下载次数: 4)
下载附件
2025-9-2 18:33 上传

到此this.rzData分析完毕
再看this.secondHandle
就个as,由init接口响应中获取


16.png (20.22 KB, 下载次数: 2)
下载附件
2025-9-2 18:33 上传

将这两个参数传入u.Li方法生成第一次的fs
分析u.Li


17.png (13.13 KB, 下载次数: 3)
下载附件
2025-9-2 18:33 上传

先走getNewKey方法传入as获取key


18.png (23.72 KB, 下载次数: 3)
下载附件
2025-9-2 18:33 上传

算法很简单直接还原
def get_aes_key(self, _as):
    mode_dict = {
        "DZ": ["0", "1", "2", "3", "4"],
        "FB": ["A", "B", "C", "D", "E", "F", "G", "a", "b", "c", "d", "e", "f", "g"],
        "JQ": ["O", "P", "Q", "R", "S", "T", "o", "p", "q", "r", "s", "t"],
        "NZ": ["5", "6", "7", "8", "9"],
        "eR": ["H", "I", "J", "K", "L", "M", "N", "h", "i", "j", "k", "l", "m", "n"],
        "o": ["U", "V", "W", "X", "Y", "Z", "u", "v", "w", "x", "y", "z"],
    }
    r = _as[-1]
    data = f'{_as}appsapi2'
    if r in mode_dict['FB']:
        n = hashlib.md5(data.encode('utf-8')).hexdigest()
    elif r in mode_dict['eR']:
        n = hashlib.sha1(data.encode('utf-8')).hexdigest()
    elif r in mode_dict['JQ']:
        n = hashlib.sha256(data.encode('utf-8')).hexdigest()
    elif r in mode_dict['o']:
        n = hashlib.sha512(data.encode('utf-8')).hexdigest()
    elif r in mode_dict['DZ']:
        n = hashlib.sha3_256(data.encode()).hexdigest()
    elif r in mode_dict['NZ']:
        n = hashlib.sha3_512(data.encode()).hexdigest()
    else:
        return
    return n[0:16]
再走分支aes-ecb的encrypt方法


19.png (14.1 KB, 下载次数: 2)
下载附件
2025-9-2 18:33 上传

标准的aes加密没啥好说的,这里是ecb模式零填充
def zero_pad(self, data, block_size):
    padding_length = block_size - (len(data) % block_size)
    padding = b'\0' * padding_length
    return data + padding
def aes_zero_encrypt(self, data, key):
    plaintext_bytes = data.encode('utf-8')
    cipher = AES.new(key.encode('utf-8'), AES.MODE_ECB)
    padded_plaintext = self.zero_pad(plaintext_bytes, AES.block_size)
    ciphertext = cipher.encrypt(padded_plaintext)
    encoded_ciphertext = base64.b64encode(ciphertext)
    return encoded_ciphertext.decode()
到此第一次fs的加密ok了
然后第二次
参数全部已知,将第一次生成的fs加入进来再加密一遍就是最终的fs了
n.fs = (0, u.Li)(
  JSON.stringify(
    {
      common_en: n.fs,
      backstr: this.cfg.backstr
    }
  ),
  {
    key: this.newKey,
    as: this.cfg.as,
    method: "aes-ecb"
  }
)
最后再来分析 fuid
全局搜索fuid定位到如下位置


20.png (8.75 KB, 下载次数: 3)
下载附件
2025-9-2 18:33 上传

fuid由window.passFingerPrint()方法生成
断点后进入fingerprint.js文件
拉倒最后


21.png (15.12 KB, 下载次数: 3)
下载附件
2025-9-2 18:33 上传

一些环境检测和canvas指纹
最后再将字典字符串序列化后走U方法


22.png (32.76 KB, 下载次数: 3)
下载附件
2025-9-2 18:33 上传

进入U方法


23.png (18.32 KB, 下载次数: 2)
下载附件
2025-9-2 18:33 上传

标准的aes加密,但和fs的加密不同,这里填充方式是Pkcs7
def aes_encrypt(self, data, key):
    plaintext_bytes = data.encode('utf-8')
    cipher = AES.new(key.encode('utf-8'), AES.MODE_ECB)
    padded_plaintext = pad(plaintext_bytes, AES.block_size)
    ciphertext = cipher.encrypt(padded_plaintext)
    encoded_ciphertext = base64.b64encode(ciphertext)
    return encoded_ciphertext.decode()
结果验证


24.png (60.1 KB, 下载次数: 2)
下载附件
2025-9-2 18:33 上传

下载次数, 下载附件

不忘形影   

感谢大佬分享,刚好学习百度的
777444   

能用python解开吧
whatcha_say_   

大佬 有没有完整的代码或者py文件,非常感谢大佬分享
mfpss95134   

厉害呀大佬,好有研究精神呀
ke6204   

这个也有人写软件厉害了
zhufuziji   

我要好好研究
辰城   

感谢大佬分享
gao52pojie   

厉害呀大佬,好有研究精神呀
BY丶显示   

刚学会,感谢分享,细节比我好,值得参考。
您需要登录后才可以回帖 登录 | 立即注册

返回顶部