吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 6616|回复: 60
上一主题 下一主题
收起左侧

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

  [复制链接]
跳转到指定楼层
楼主
LiSAimer 发表于 2025-9-2 18:38 回帖奖励
本帖最后由 LiSAimer 于 2025-9-2 18:49 编辑

声明

本文章中所有内容仅供学习交流使用,不用于其他任何目的,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!若有侵权,请联系作者删除。

逆向目标

目标网站:
aHR0cHM6Ly96aXl1YW4uYmFpZHUuY29tL2xpbmtzdWJtaXQvdXJs
关键要点:ac_c、p、fs、fuid

抓包分析

在demo页面点击提交按钮后弹出验证码

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

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

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

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

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

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

逆向过程

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

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

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

可以看到fs赋值了两次
先分析第一次的

n.fs = (0, u.Li)(JSON.stringify(this.rzData), this.secondHandle)

将this.rzData字符串序列化
backstr是style接口响应中的
ac_c是根据旋转角度计算的
mv是轨迹
p是根据style接口响应中ext参数计算来的
common.mv这组轨迹不校验可有可无
其它的环境参数可以写死

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

n = Number((this.distance / (e - 52)).toFixed(2))

this.distance是滑动的距离

e是固定值290,也就是滑动框整体的长度
轨迹和角度识别直接用开源的就行

https://github.com/lumina37/rotate-captcha-crack

再找 p 的生成位置
赋值位置就在fs的上面
从this.powMap获取

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

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

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

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

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

先走getNewKey方法传入as获取key

算法很简单直接还原

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方法

标准的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定位到如下位置

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

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

进入U方法

标准的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()

结果验证

免费评分

参与人数 20威望 +2 吾爱币 +120 热心值 +19 收起 理由
msmvc + 1 + 1 谢谢@Thanks!
ShenShuiLn + 1 + 1 我很赞同!
ziliuxing2008 + 1 + 1 我很赞同!
小栗子 + 1 + 1 谢谢@Thanks!
hot355 + 1 我很赞同!
大毛孩 + 1 + 1 谢谢@Thanks!
hitachimako + 1 + 1 谢谢@Thanks!
Victor365 + 1 + 1 热心回复!
gaosld + 1 + 1 热心回复!
opddsvj + 1 + 1 我很赞同!
DevisMe + 1 + 1 用心讨论,共获提升!
fengbolee + 2 + 1 用心讨论,共获提升!
涛之雨 + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
purehai + 1 + 1 谢谢@Thanks!
monicx + 1 + 1 谢谢@Thanks!
15617713149 + 1 + 1 我很赞同!
liuxuming3303 + 1 + 1 谢谢@Thanks!
helian147 + 1 + 1 热心回复!
theWoon + 1 + 1 谢谢@Thanks!
xuanle + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
 楼主| LiSAimer 发表于 2025-11-26 17:43 |楼主
kimtps 发表于 2025-11-26 13:58
有个小小的问题 我python还原算法的结果和node的结果不一样。不知道是怎么回事 我就只能用execjs运行
问了 ...

因为js的crypto-js库和python的hashilib库实现的sha3是有差异的。
你可以试试用这个库,这样就和js结果一致了。
pip install pycryptodome

from Crypto.Hash import keccak
elif r in group5:
    # SHA3-256 (标准)
    k = keccak.new(digest_bits=256)
    k.update(e.encode())
    n = k.hexdigest()
elif r in group6:
    # SHA3-512 (标准)
    k = keccak.new(digest_bits=512)
    k.update(e.encode())
    n = k.hexdigest()
沙发
不忘形影 发表于 2025-9-3 10:25
3#
777444 发表于 2025-9-3 10:29
4#
whatcha_say_ 发表于 2025-9-3 11:44
大佬 有没有完整的代码或者py文件,非常感谢大佬分享
5#
mfpss95134 发表于 2025-9-3 13:15
厉害呀大佬,好有研究精神呀
6#
ke6204 发表于 2025-9-3 13:24
这个也有人写软件厉害了
7#
zhufuziji 发表于 2025-9-3 14:53
我要好好研究
8#
辰城 发表于 2025-9-3 15:11

感谢大佬分享
9#
gao52pojie 发表于 2025-9-3 17:02

厉害呀大佬,好有研究精神呀
10#
BY丶显示 发表于 2025-9-3 22:53
刚学会,感谢分享,细节比我好,值得参考。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-4-15 08:03

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表