吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 540|回复: 8
上一主题 下一主题
收起左侧

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

[复制链接]
跳转到指定楼层
楼主
epiphanyep 发表于 2026-3-4 17:40 回帖奖励
本帖最后由 epiphanyep 于 2026-3-4 17:54 编辑

大家好,学了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:“




搜出来四五个 看着像的就是后两个,如果不知道的话可以两个都断上 然后划一下滑块调试,发现第一个就是
或者你可以观察w值就在第一个process_token的下边一点点

看第二个虽然也是process_token 但是没有我们想要的w





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

仔细看其实就是上边红色框里的函数生成的w






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






在这个函数最下边有return ,在return位置上打断点跟过去,发现w值就是画红线的两部分相加得来的






那么现在思路就很清晰 只需要解决相加的两部分 函数 和变量r 就行


先看r吧

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





参数;也是由一个函数生成的  跟进去





发现return的是4个l函数相加 我管相加结果叫l4吧 l函数恰巧也在上边






把l函数还原 再调用4次相加就行了



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


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



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


再把刚刚得到的变量作为参数传进去就可以得到r了!


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 还原了算法


最后 前面的函数跟进去




看见'>>>' 位运算符号 我猜就是位运算和进制转换
丢给AI 分析 发现是用于十六进制字符串转换的 用AI直接还原
然后逆向部分大功告成!
最后还剩滑块部分
滑块识别我最开始用的ddddocr 但是发现识别会出现问题,好多次都会出现识别不到图片,所以换了opencv试一试,效果显著提升!


下面是完整代码
py:

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 编码
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)


js:
// 新增:微秒级时间格式化函数(生成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"](16)["substring"](1)
} 

// 你的原有函数:完全未修改,保留所有逻辑
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
    }
}



最后就是我爱你们

image.png (115.95 KB, 下载次数: 1)

image.png

免费评分

参与人数 8吾爱币 +8 热心值 +8 收起 理由
John_Mac + 1 + 1 我很赞同!
winnerishope + 1 + 1 谢谢@Thanks!
buluo533 + 1 + 1 用心讨论,共获提升!
laozhang4201 + 1 + 1 热心回复!
111mz + 1 + 1 我很赞同!
萌新与小白 + 1 + 1 热心回复!
liuxuming3303 + 1 + 1 我很赞同!
Command + 1 + 1 我很赞同!

查看全部评分

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

沙发
juanyi 发表于 2026-3-4 22:55
虽然作为小小白,看都看不懂,但是感谢分享思路
3#
beiank 发表于 2026-3-5 01:17
我有1个疑问 就是楼主怎么知道那个网站它采用的是 极验滑块啊?   极验滑块 它的指纹特征 楼主是怎么分析出来的啊?
4#
111mz 发表于 2026-3-5 03:56
5#
abslsp 发表于 2026-3-5 08:39
那种看起来都是乱码的,我看了就头大,
6#
mjxiao 发表于 2026-3-5 09:29
小白没看懂
7#
 楼主| epiphanyep 发表于 2026-3-5 10:11 |楼主
beiank 发表于 2026-3-5 01:17
我有1个疑问 就是楼主怎么知道那个网站它采用的是 极验滑块啊?   极验滑块 它的指纹特征 楼主是怎么分析出 ...

首先这个案例就是极验官网所以是极验滑块 如果是其他网站的话 主要看载荷和响应值特征吧?每个大厂加密都有特征加密参数 比如callback 是geetest+时间戳 然后独有的w加密参数  我是这样认为的 不知道准不准
8#
SpiderReverser 发表于 2026-3-8 23:19
大佬,用你的脚本跑了一下,基本都 forbidden 了,是轨迹的问题吧
9#
lyf166 发表于 2026-3-9 08:05
感谢大佬分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2026-3-10 04:13

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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