吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 8|回复: 0
收起左侧

[Web逆向] 【web逆向】小白之路-极验4滑块

[复制链接]
epiphanyep 发表于 2026-1-29 18:28
大家好,学了web逆向一段时间了,想着用帖子来记录自己的学习过程,逆向过程中的的一些技巧,从而巩固自己的学习的成果(ps:最近AI也太迅猛了,对于我这种小白,AI帮了我的大忙,完成了许多工作)。第一次发帖,如果有哪些不足的地方请多包容,希望能帮助到需要帮助的朋友。
这次是极验4滑块的w值 目标网址aHR0cHM6Ly9ndDQuZ2VldGVzdC5jb20v




点击开始验证 弹出滑块




打开开发者模式,随便拖动滑块一次

这里有两个主要的接口 load 和verify
观察load载荷


load载荷主要就是可以固定的captcha_id 和 callback 是一个’geetest_‘+长度为13位的时间戳和,其他再没有什么特殊的

再看一下load的响应


比较重要的是:
lot_number : 用于验证接口verify 的请求验证
slice : 滑块图片地址
bg : 背景图片地址
pow_detail中的datetime :时间校验
payload:用于验证接口verify 的请求验证
process_token : 用于验证接口verify 的请求验证

接下来是verify接口的载荷:


多次拖动滑块会发现 callback lot_number和process_token w都是变化的。

同样是 callback : 是一个’geetest_‘+长度为13位的时间戳和
captcha_id : 也是和load接口一样可以固定的
lot_number:是load接口响应返回的
payload : 是load接口响应返回的
process_token :  是load接口响应返回的
w:最重要的参数 也是本次分析的重点

验证成功就是响应 status 为success!



现在分析差不多了 开始逆向吧

搜索关键词:”process_token:“


搜出来两个 第一个就是,可以两个都断上 然后划一下滑块调试


发现是严重混淆后的文件,没招了,我不会ast,只能硬分析。。。。
点第一个进去比较快的发现w就在眼前,双击变量变量看提示w的生成位置

其实就是红色框里的函数生成的w

在这里打上断点跟进去函数里面如下图所示

在这个函数最下边有return ,在return位置上打断点跟过去,发现w值就是画红线的两部分相加得来的
那么现在思路就很清晰 只需要解决相加的两部分函数   _ᖚᕹᖚᕺ[_ᖗᖃᖙᖉ(117)]  和变量r  就行


先看r吧

仔细观察 r在上边有定义,打上断点跟上
r 是函数传参的返回值
参数_ᖂᕺᖆᖄ也是在上边能找到


参数_ᖂᕺᖆᖄ也是由函数生成的  跟进去
发现return的是4个l函数相加 我管相加结果叫l4吧 l函数恰巧也在上边
把l函数还原 再调用4次相加就行了


参数解决完 再看函数
函数跟进去就长这样


太乱了 我直接丢给AI了整理了
分析后层级是这是两个嵌套的函数 是实现的RSA加密 内函数是最后三目运算的一部分 三目运算判断奇偶 如果加密后长度是奇数,会在前面补一个 "0"。
其中公钥没找到 ai提示我可以通过this 来通过计算得到公钥
这里的 this 就是当前的 RSA 加密对象实例。公钥的所有信息(模数 N 和指数 E)都存储在 this 里面。

然后就可以实现rsa加密,实现原逻辑了!


再把刚刚_ᖂᕺᖆᖄ变量传进去就可以得到r了!


最后剩下的就是前面的函数了,同样参数传函数
我先把参数解决,参数生成位置在下图断点位置,同样也是参数传递函数生成


这里有两个参数 第一个就是一个json对象变成字符串类型的数据,可以先固定。第二个参就是l4
其中第一个字符串类型的参数里面关键的有:
setLeft : 滑动距离
passtime: 滑动消耗时间,这个时间就随便生成了
userresponse : 直接关键词搜索就能搜到产生位置
lot_number : 需要动态替换
pow_msg : 中间的时间和lot_number 我进行了动态替换
其他的可以固定


跟进函数内部发现 也太乱了
尝试在网页上手动还原,有encrypt字眼,还有一处是iv!

包括第三个参数也有key iv mode

我认为AES加密形式就是   AES加密(明文数据, 密钥key, { 初始化向量iv, 加密模式mode, 填充方式padding })
我猜大概率是AES加密

直接丢给AI分析
发现第二个参数确实是key 然后就让AI 还原了算法


最后 前面的函数跟进去
image.png
丢给AI 分析 发现是用于十六进制字符串转换的 用AI直接还原
然后逆向部分大功告成!
滑块识别我最开始用的ddddocr 但是发现识别会出现问题,好多次都会出现识别不到图片,所以换了opencv,效果显著提升。


js部分:

[md]// 新增:微秒级时间格式化函数(生成YYYY-MM-DDTHH:mm:ss.ffffff+08:00,匹配原格式)
function getFormattedTime() {
const now = new Date();
// 补零工具函数:单个数字补0(如5→05,9→09,12→12)
const pad = (num, len = 2) => num.toString().padStart(len, '0');
// 提取时间各部分(月份+1,因为JS月份是0-11)
const year = now.getFullYear();
const month = pad(now.getMonth() + 1);
const day = pad(now.getDate());
const hours = pad(now.getHours());
const minutes = pad(now.getMinutes());
const seconds = pad(now.getSeconds());
// 毫秒转6位微秒(原格式要求6位,补3个0凑数,完全匹配极验格式)
const milliseconds = pad(now.getMilliseconds(), 3);
const microseconds = ${milliseconds}000;
// 拼接最终格式(T分隔日期时间,+08:00固定东八区)
return ${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${microseconds}+08:00;
}

// 你的原有函数:保留所有x/lot_number计算、随机滑动时间、userresponse公式
function get_str(lot_number,x){
let userresponse =  x / 1.0059466666666665 + 2
console.log('userresponse:::',userresponse);
// 生成 130 ≤ 随机整数 ≤ 170 滑动时间
const randomInt_time= Math.floor(Math.random() * (170 - 130 + 1)) + 130;
console.log('滑动时间',randomInt_time);
// 关键修改1:调用动态时间函数
const dynamicTime = getFormattedTime();
let obj = {
"setLeft":x,
"passtime":randomInt_time,
"userresponse":userresponse,
"device_id":"",
"lot_number":lot_number,
// 关键修改2:移除多余双引号 + 替换为动态时间 + 保留lot_number动态注入
"pow_msg":1|8|sha256|${dynamicTime}|54088bb07d2df3c46b79f80300b0abbe|${lot_number}||a4d2b5737d197507,
"pow_sign":"0067ae27ad567c7ffc37abb9bf15224d96c1ba73bf0940826d6eaf171d592495",
"geetest":"captcha",
"lang":"zh",
"ep":"123",
"biht":"1426265548",
"gee_guard":{"roe":{"aup":"3","sep":"3","egp":"3","auh":"3","rew":"3","snh":"3","res":"3","cdc":"3"}},
"fwjn":"DJkH",
"c43a":{"43a1f9":"669a03e1"},
"em":{"ph":0,"cp":0,"ek":"11","wd":1,"nt":0,"si":0,"sc":0}
}
return obj
}

// 你的原有函数:完全未修改,保留所有逻辑
function l(){
return (65536 * (1 + Math["random"]()) | 0)"toString""substring"
}

// 你的原有函数:完全未修改,保留所有逻辑
function get_r_and_str(lot_number,x){
let obj = get_str(lot_number,x)
const _obj_str = JSON.stringify(obj)
let l4 = l() + l() + l() +l()
return  {
"l4": l4,
"_obj_str": _obj_str
}
}[ /md]   #实际写作时 [ /md]没有空格



py部分:

[md]import requests
import time
import uuid
import json
import binascii
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import cv2
import subprocess
from functools import partial
import execjs
import os

subprocess.Popen = partial(subprocess.Popen, encoding="utf-8")

表情符号和美化输出

EMOJIS = {
"rocket": "🚀",
"camera": "📸",
"key": "🔑",
"check": "✅",
"cross": "❌",
"download": "⬇️",
"upload": "⬆️",
"brain": "🧠",
"lock": "🔒",
"magnifier": "🔍",
"ruler": "📏",
"star": "⭐",
"fire": "🔥",
"success": "🎉",
"fail": "💥",
"gear": "⚙️"
}

def print_header(text, emoji="🚀"):
"""打印标题"""
print("\n" + "="60)
print(f"{emoji}  {text}")
print("="
60)

def print_step(step, text):
"""打印步骤"""
print(f"\n【步骤 {step}】 {text}")

def print_info(text, emoji="ℹ️"):
"""打印信息"""
print(f"  {emoji} {text}")

def print_success(text, emoji="✅"):
"""打印成功"""
print(f"  {emoji} {text}")

def print_error(text, emoji="❌"):
"""打印错误"""
print(f"  {emoji} {text}")

def print_data(label, value, emoji="📊"):
"""打印数据"""
print(f"  {emoji} {label:<20}: {value}")

def get_r(lot_number,x):
print_step(3, "生成加密参数")

print_info("读取JS文件并执行...", "📜")
js_obj = execjs.compile(open('./GT4/demo.js', 'r', encoding='utf-8').read())
js_result = js_obj.call('get_r_and_str',lot_number,x)

_ᖈᕹᕺᖆ = js_result.get('l4')
_obj_str = js_result.get('_obj_str')

print_data("随机种子 l4", f"长度: {len(_ᖈᕹᕺᖆ)}", "🔑")
print_data("轨迹字符串", f"长度: {len(_obj_str)}", "📝")

# 显示预览
if len(_ᖈᕹᕺᖆ) > 50:
    print_data("种子预览", _ᖈᕹᕺᖆ[:50] + "...", "👀")

# 1. 配置公钥信息
# 这是你刚刚通过 this.n.toString(16) 拿到的模数
print_info("开始RSA加密...", "🔒")
n_hex = 'c1e3934d1614465b33053e7f48ee4ec87b14b95ef88947713d25eecbff7e74c7977d02dc1d9451f79dd5d1c10c29acb6a9b4d6fb7d0a0279b6719e1772565f09af627715919221aef91899cae08c0d686d748b20a3603be2318ca6bc2b59706592a9219d0bf05c9f65023a21d2330807252ae0066d59ceefa5f2748ea80bab81'.upper()
e_val = 65537 # 指数,通常固定为 65537

# 将 16 进制模数转为整数
n_val = int(n_hex, 16)

# 2. 构造 RSA 公钥对象
pub_key = RSA.construct((n_val, e_val))

# 3. 准备明文
text = _ᖈᕹᕺᖆ
# 确保编码为 bytes
msg = text.encode('utf-8')

# 4. 加密 (PKCS1_v1_5 填充)
cipher = PKCS1_v1_5.new(pub_key)
ciphertext = cipher.encrypt(msg)

# 5. 转为 16 进制字符串 (模拟 JS 的 toString(16))
r = binascii.hexlify(ciphertext).decode()

# 6. 处理长度奇偶性 (完美复刻 JS 的 return 逻辑)
# 0 == (1 & length) ? result : "0" + result
if len(r) % 2 != 0:
    r = "0" + r

print_success(f"RSA加密完成!长度: {len(r)}", "✅")
print_data("RSA结果预览", r[:80] + "..." if len(r) > 80 else r, "🔐")

return r, _obj_str, _ᖈᕹᕺᖆ

def get_aes_data(data_str, l4_seed):
"""
AES 加密
【关键修复】 Key 必须使用 l4_seed 的 bytes 形式
"""
print_info("开始AES加密...", "🔒")

# 1. Key 来源于 l4_seed (字符串转bytes)
# 极验4中,AES Key 就是 l4 这个字符串本身的 utf-8 编码
key = l4_seed.encode('utf-8')

# 2. IV 固定为 0000000000000000
iv = b'0000000000000000'

# 3. 加密
cipher = AES.new(key, AES.MODE_CBC, iv)
padded_data = pad(data_str.encode('utf-8'), 16)
encrypted_bytes = cipher.encrypt(padded_data)

# 4. 返回 Hex
result = encrypted_bytes.hex()

print_success(f"AES加密完成!长度: {len(result)}", "✅")
print_data("AES结果预览", result[:80] + "..." if len(result) > 80 else result, "🔐")

return result

def get_load_api():
print_step(1, "加载验证码")

my_uuid = uuid.uuid4()
headers = {
    "Accept": "*/*",
    "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
    "Connection": "keep-alive",
    "Referer": "https://gt4.geetest.com/",
    "Sec-Fetch-Dest": "script",
    "Sec-Fetch-Mode": "no-cors",
    "Sec-Fetch-Site": "same-site",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36",
    "sec-ch-ua": "\"Google Chrome\";v=\"141\", \"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"141\"",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": "\"Windows\""
}
cookies = {
    "captcha_v4_user": "33f671e6e47f4726a6cef1b6c7829a5d"
}
url = "https://gcaptcha4.geetest.com/load"
params = {
    "callback": f"geetest_{int(time.time() * 1000)}",
    "captcha_id": "54088bb07d2df3c46b79f80300b0abbe",
    "challenge": f"{my_uuid}",
    "client_type": "web",
    "risk_type": "slide",
    "lang": "zh"
}

print_info("发送验证码请求...", "🌐")
response = requests.get(url, headers=headers, cookies=cookies, params=params).text
response = json.loads(response[22: -1])
data = response.get("data")
bg_url = 'https://static.geetest.com/' + data.get("bg")
lot_number = data.get("lot_number")
process_token = data.get("process_token")
slice_url = 'https://static.geetest.com/' + data.get("slice")
payload = data.get("payload")

print_success("验证码加载成功!", "✅")
print_data("背景图URL", bg_url, "📸")
print_data("滑块图URL", slice_url, "📸")
print_data("Process Token", process_token[:30] + "..." if len(process_token) > 30 else process_token, "🔑")
print_data("Lot Number", lot_number, "🏷️")
print_data("Payload长度", f"{len(payload)} 字符", "📏")

return bg_url, process_token, slice_url, payload, lot_number

def get_img(url, filename):
try:
filename_base = os.path.basename(filename)
print_info(f"下载图片: {filename_base}", "⬇️")
res = requests.get(url, timeout=10)
if res.status_code == 200:
with open(filename, 'wb') as f:
f.write(res.content)
file_size = len(res.content) / 1024  # KB
print_success(f"图片下载成功: {filename_base} ({file_size:.1f} KB)", "✅")
else:
print_error(f"下载失败,状态码: {res.status_code}", "❌")
except Exception as e:
print_error(f"下载异常: {e}", "❌")

def identify_gap(bg_path, slice_path):
"""
使用 OpenCV 识别滑块缺口距离
"""
print_info("识别滑块位置...", "🔍")

# 1. 读取图片
bg_img = cv2.imread(bg_path)
slice_img = cv2.imread(slice_path, cv2.IMREAD_UNCHANGED) # 保留透明通道

if bg_img is None or slice_img is None:
    raise Exception("图片读取失败,请检查路径或下载是否成功")

# 显示图片尺寸
print_data("背景图尺寸", f"{bg_img.shape[1]}x{bg_img.shape[0]}", "📏")
print_data("滑块图尺寸", f"{slice_img.shape[1]}x{slice_img.shape[0]}", "📏")

# 2. 处理滑块图片 (slice)
if slice_img.shape[2] == 4:
    slice_gray = cv2.cvtColor(slice_img, cv2.COLOR_BGRA2GRAY)
else:
    slice_gray = cv2.cvtColor(slice_img, cv2.COLOR_BGR2GRAY)

# 3. 处理背景图片 (bg)
bg_gray = cv2.cvtColor(bg_img, cv2.COLOR_BGR2GRAY)

# 4. 边缘检测 (Canny)
slice_edge = cv2.Canny(slice_gray, 50, 150)
bg_edge = cv2.Canny(bg_gray, 50, 150)

# 5. 模板匹配
result = cv2.matchTemplate(bg_edge, slice_edge, cv2.TM_CCOEFF_NORMED)

# 6. 寻找最佳匹配位置
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)

# max_loc 是 (x, y)
target_x = max_loc[0]

match_percent = max_val * 100
if match_percent > 30:
    print_success(f"识别成功!X坐标: {target_x}, 匹配度: {match_percent:.1f}%", "✅")
else:
    print_info(f"识别完成!X坐标: {target_x}, 匹配度: {match_percent:.1f}%", "⚠️")

return target_x

def get_x():
try:
print_step(2, "识别滑块距离")

使用 OpenCV 替代 ddddocr

    x = identify_gap('./GT4/bg.jpg', './GT4/slice.png')
    print_data("识别结果", f"{x} 像素", "🎯")
    return x
except Exception as e:
    print_error(f"识别出错: {e}", "❌")
    return 0

def get_verify_api(process_token, payload, lot_number,w):
print_step(4, "提交验证")

headers = {
    "accept": "*/*",
    "accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
    "referer": "https://gt4.geetest.com/",
    "sec-ch-ua": "\"Google Chrome\";v=\"141\", \"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"141\"",
    "sec-ch-ua-mobile": "?0",
    "sec-ch-ua-platform": "\"Windows\"",
    "sec-fetch-dest": "script",
    "sec-fetch-mode": "no-cors",
    "sec-fetch-site": "same-site",
    "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36"
}
cookies = {
    "captcha_v4_user": "33f671e6e47f4726a6cef1b6c7829a5d"
}
url = "https://gcaptcha4.geetest.com/verify"
params = {
    "callback": f"geetest_{int(time.time() * 1000)}",
    "captcha_id": "54088bb07d2df3c46b79f80300b0abbe",
    "client_type": "web",
    "lot_number": lot_number,
    "risk_type": "slide",
    "payload": payload,
    "process_token": process_token,
    "payload_protocol": "1",
    "pt": "1",
    "w": w
}

print_info("发送验证请求...", "🌐")
response = requests.get(url, headers=headers, cookies=cookies, params=params)
return response.text

if name == "main":
print_header("极验验证码识别系统", "🚀")
print_info("开始执行验证码识别任务...", "🤖")

# 确保目录存在
os.makedirs('./GT4', exist_ok=True)

try:
    # 1. 加载验证码
    bg_url, process_token, slice_url, payload, lot_number = get_load_api()

    # 2. 下载图片
    get_img(bg_url, './GT4/bg.jpg')
    get_img(slice_url, './GT4/slice.png')

    # 3. 识别距离
    x = get_x()

    if x == 0:
        print_error("滑块识别失败,任务终止!", "💥")
        exit()

    # 4. 生成加密参数
    r, _ᕾᖃᖆᕺ_str, _ᖈᕹᕺᖆ = get_r(lot_number, x)

    # 5. AES加密
    aes_data = get_aes_data(_ᕾᖃᖆᕺ_str, _ᖈᕹᕺᖆ)

    # 6. 合成最终参数
    w = aes_data + r
    print_info("合成最终参数...", "⚙️")
    print_data("RSA长度", f"{len(r)} 字符", "📏")
    print_data("AES长度", f"{len(aes_data)} 字符", "📏")
    print_data("总长度", f"{len(w)} 字符", "📏")
    print_data("参数预览", w[:100] + "..." if len(w) > 100 else w, "👀")

    # 7. 提交验证
    res = get_verify_api(process_token, payload, lot_number, w)

    # 解析响应
    json_start = res.find('(') + 1
    json_end = res.rfind(')')
    pure_json_str = res[json_start:json_end]
    result = json.loads(pure_json_str)
    status = result.get('status')

    print("\n" + "="*60)
    if status == 'success':
        print_success(f"验证成功✅✅✅!状态: {status}", "🎉")
        print_data("返回Token", result.get('result', 'N/A'), "🔑")
    elif status == 'fail':
        print_error(f"验证失败!状态: {status}", "💥")
        print_data("失败原因", result.get('reason', '未知'), "⚠️")
    else:
        print_info(f"验证结果:✅✅✅ {status}", "ℹ️")

    # 显示完整响应
    print_info("完整响应:", "📋")
    for key, value in result.items():
        print_data(key, str(value), "➡️")

except Exception as e:
    print_error(f"程序运行出错: {e}", "💥")
    import traceback
    traceback.print_exc()

print("\n" + "="*60)
print_info("程序执行完毕", "🏁")
print("="*60)[ /md]


本人第一次发帖,希望大家能喜欢

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

您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2026-1-30 17:07

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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