吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 5464|回复: 44
收起左侧

[Web逆向] 极验4代 九宫格协议分析

  [复制链接]
Command 发表于 2025-8-30 23:55
本帖最后由 Command 于 2025-8-31 00:02 编辑

目标captcha_id: 045e2c229998a88721e32a763bc0f7b8

文章写的不太好, 完全是靠运气逆向出来的, 还请多多包涵

环境使用极验给出的demo代码

碎碎念

不知道还有没有人记得我, 春节解题领红包那会我还是初三, 现在我已经是一名高一新生咯! (还有高中三年, 可怕)

协议分析

总览

刷新网页, 打开开发者工具, 并完成一次验证操作,

不难发现两个主要请求: 显然load是加载验证码, 在verify进行验证, 中间的图片即为九宫格与小图

GEE_2.png

先看load (第一个请求)

先看看他请求有什么参数:
主要参数 说明
captcha_id 验证码appid
callback JSONP格式回调
challenge 随机UUID, verify中原样传递

GEE_3.png

再看看返回了什么:

GEE_4.png

返回数据为 JSONP 格式,

对比后续的两个图片请求可以发现: ques中返回的是验证码问题处的小图标, imgs对应的是九宫格大图 (省略了http://static.geetest.com/前缀), 两张图片没有被乱序;

继续查看可以发现: pow_detail是和腾讯滑块差不多的POW计算信息; process_token, lot_number, payload在verify请求中会被原样传递

verify分析 (验证请求)

GEE_5.png

这里需要逆向的只有w和其中的pow_msg, pow_sign, 其余参数均可直接从 load 请求获取或为固定值, 原样传入即可

w值逆向(连蒙带猜)

GEE_6.png

拿到js, 直接傻眼, 这混淆挺离谱啊, 按照以下步骤进行解混淆并替换文件:

  1. 丢入deobfuscate.io进行初步解混淆
  2. 再对上一步的结果使用UglifyJS Online, 设置beautify: truetoplevel: true (用Uglifyjs的目的是为了换掉这个离谱的变量名)
  3. 用DevTools的替换功能替换掉gcaptcha4.js原始的内容

然后要怎么办呢? 肯定是不能用常规方法了, 尝试搜索encrypt试试吧! (万一蒙中了呢)

极验似乎用了CryptoJS, Property的名字并没有被更改, encrypt匹配到了好几个, 全部在函数开头写上console.log(arguments), 重新完成一次验证操作

GEE_7.png

有两个函数输出了, 在第一个输出那里下断点, 去上一个调用堆栈

GEE_8.png

GEE_9.png

能够发现, 该函数返回的值恰好就是w值, 也就是说w的数据是通过t = n[i][o(980)][o(990)](t, D), (0, B[C(142)])(t) + _得到, t是要加密的数据, D是加密的密钥, 而_是由D进行加密得到, 接下来我们将其分为前半段与后半段进行分析

w前半段(AES)

在所调用的另一个encrypt函数中下断点, 发现其被上一个encrypt所调用:

GEE_11.png

偷一下懒, 问问GPT这附近的代码:

GEE_12.png

知道了加密方法是AES, 接下来是从参数中提取IV, Padding信息

{
    ciphertext: n,
    key: C,
    iv: D[o(1037)],
    /* 
    {
      "words": [
        808464432,
        808464432,
        808464432,
        808464432
      ],
      "sigBytes": 16
    }
    */
    algorithm: t,
    mode: D[r(1023)], // 查看 D[r(1023)].Encryptor.processBlock, 询问GPT得是CFB模式 (CBC也可以)
    padding: D[r(1032)],
    blockSize: t[o(1e3)],
    formatter: i[r(1046)]
}

从中提取到IV为固定值0000000000000000,查看padding中函数发现为PKCS7填充

function(t, n) {
        for (var C = j.$_Ct, i = [ "$_DCBJO" ].concat(C), o = i[1], D = (i.shift(), i[0], (n = 4 * n) - t[C(1007)] % n), r = D << 24 | D << 16 | D << 8 | D, c = [], _ = 0; _ < D; _ += 4) c[C(172)](r);
        n = f[o(106)](c, D), t[o(105)](n);
}

IV, Padding都已解决, 那么Key从哪来的呢? 需要我们从调用栈中找到密钥被传入的位置(还是第一个encrypt), 可以看到密钥由B[C(192)]()生成


// 生成4位随机十六进制字符串
function o() {
    for (var t = j.$_DL()[4][10]; t !== j.$_DL()[0][9]; ) if (t === j.$_DL()[0][10]) return (65536 * (1 + Math[s(103)]()) | 0)[s(101)](16)[s(85)](1);
}
// B[C(192)]()   生成16位字符串
function() {
    var t = j.$_Ct, t = [ "$_EBJt" ].concat(t);
        t[1], t.shift(), t[0];
        return o() + o() + o() + o();
}

GEE_13.png

接下来, 只差最后一点了: CryptoJS加密结果并不是HEX格式, 他是如何转换到HEX的呢?  查看B[C(142)]函数得:

// B[C(142)]
function (t) {
        for (var n = j.$_Ct, C = ['$_CJEI'].concat(n), i = C[1], o = (C.shift(), C[0], []), D = 0, r = 0; r < 2 * t['length']; r += 2) o[r >>> 3] |= parseInt(t[D], 10) << 24 - r % 8 * 4, D++;
    for (var c = [], _ = 0; _ < t['length']; _++) {
        var a = o[_ >>> 2] >>> 24 - _ % 4 * 8 & 255;
        c['push']((a >>> 4)['toString'](16)), c['push']((15 & a)['toString'](16));
    }
    return c['join']('');
}

用Python重写为:

def ArrayToHex(self, e: list) -> str:
    t = [0] * ((2 * len(e) + 7) // 8)
    s = 0
    for n in range(0, 2 * len(e), 2):
        t[n >> 3] |= int(e[s]) << (24 - (n % 8) * 4)
        s += 1
    i = []
    for r in range(len(e)):
        o = (t[r >> 2] >> (24 - (r % 4) * 8)) & 255
        i.append(format(o >> 4, 'x'))
        i.append(format(o & 15, 'x'))
    return ''.join(i)

那么到此, 我们就已经解决了参数w的前半段, 总结一下:

AES CBC模式 PKCS7填充, IV为0000000000000000,  密钥为16位随机十六进制字符串 加密后通过ArrayToHex函数将字节数组转为HEX格式

w后半段(RSA)

依旧是在调用encrypt的函数中, 有如下代码

if (r[C(129)](i[o(667)])) {
                    for (var c = C(861) === i[C(667)], i = i[C(667)], _ = n[i][o(905)][C(990)](D) /* 后半段在此生成 */; c && (!_ || 256 !== _[C(107)]); ) D = (0, 
                    B[o(192)])(), _ = new f[o(183)]()[C(990)](D);
                    return t = n[i][o(980)][o(990)](t, D), (0, B[C(142)])(t) + _; // 加号前面是w的前半段, _是w的后半段, 由 n[i][o(905)][C(990)](D)计算得到
                }

进入函数n[i][o(905)][C(990)]查看发现有以下字符串(console输出处): Message too long for RSA, 故可知使用了RSA加密算法, 继续分析可知使用了PKCS1v15填充

接下来就需要找公钥了, 找到setPublic函数, 就在上方(箭头处), 修改该函数, 使其输出参数内容  (第一个参数为N, 第二个参数为E)

GEE_14.png

刷新页面, 控制台输出:

N=0080......BAB81
E=1....1

将其转换为PEM即得到公钥

那么现在, w后半段也清晰了: RSA PKCS1v15填充 使用上述公钥加密前半段AES的密钥即可得到后半段的内容

pow_sign&pow_msg

function(t, n, C, i, o, D, r) {
                for (var c = j.$_DL()[0][10]; c !== j.$_DL()[0][9]; ) if (c === j.$_DL()[4][10]) {
                    for (var _, a = o % 4, s = parseInt(o / 4, 10), f = (_ = A(138), 
                    new Array(s + 1)[A(156)](_)), E = i + A(134) + o + A(134) + C + A(134) + D + F(134) + n + F(134) + t + F(134) + r + A(134); ;) {
                        var h = (0, U[F(192)])(), B = E + h, $ = void 0;
                        switch (C) {
                          case A(679):
                            $ = new v[A(183)][F(747)]()[A(649)](B);
                            break;

                          case A(789):
                            $ = new v[F(183)][F(714)]()[A(649)](B);
                            break;

                          case A(799):
                            $ = new v[A(183)][F(776)]()[F(649)](B);
                        }
                        if (0 == a) {
                            if (0 === $[F(69)](f)) return {
                                pow_msg: E + h,
                                pow_sign: $
                            };
                        } else if (0 === $[A(69)](f)) {
                            var e = void 0, u = $[s];
                            switch (a) {
                              case 1:
                                e = 7;
                                break;

                              case 2:
                                e = 3;
                                break;

                              case 3:
                                e = 1;
                            }
                            if (u <= e) return {
                                pow_msg: E + h,
                                pow_sign: $
                            };
                        }
                    }
                    c = j.$_DL()[4][9];
                }
            };
        }

比较简单, 按照version|bits|hashfunc|datetime|captcha_id|lot_number||随机16位16进制字符串顺序拼接一下, 就得到pow_msg, md5一下就得到pow_sign, 不再赘述

结语

看一下最终效果

GEE_16.png

其实吧, 按理说这篇文章还有一个训练模型识别九宫格验证码的内容, 但是这文章是我大半夜写的, 太困了... 睡觉!

(马上要开学了, 我不想开学啊)

免费评分

参与人数 17吾爱币 +17 热心值 +17 收起 理由
yycca + 1 我很赞同!
HongChenBaBa + 2 + 1 热心回复!
yanghaohuid + 1 + 1 我很赞同!
shenxing + 1 热心回复!
dandelion2sunny + 1 + 1 我很赞同!
JonesDean + 1 + 1 用心讨论,共获提升!
ravi + 1 + 1 用心讨论,共获提升!
jiniu233 + 1 + 1 厉害 向你学习啊
mjhwzwg6 + 1 + 1 用心讨论,共获提升!
nekous + 1 + 1 我很赞同!
GoingUp + 1 + 1 热心回复!
gjyqj + 1 + 1 谢谢@Thanks!
mscsky + 1 + 1 我很赞同!
daxz + 1 + 1 谢谢@Thanks!
ytfh1131 + 1 + 1 谢谢@Thanks!
fengbolee + 2 + 1 用心讨论,共获提升!
yiqibufenli + 1 + 1 我很赞同!

查看全部评分

本帖被以下淘专辑推荐:

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

涛之雨 发表于 2025-8-31 20:36
居然才是高中啊,真的是跟你当前的论坛等级一样啊(前途无量)
逻辑清晰,精华鼓励一下,

此外对于“完全是靠运气逆向出来的”不敢苟同,个人感觉这就类似于以前上学时候老师讲过的“语感”,有一些是固定搭配,有一些就是纯经验,
问(什么)餮盛宴,那肯定是(饕),
你搜encrypt,加密标准库大概率都会是CryptoJS,这是固定搭配
你说pow_detail是和腾讯滑块差不多的POW计算信息,这是经验

逆向本来就是要考运气的(包括不会因为逆向进去)

此外,这个精华是预支的,记得学习闲暇之余把“训练模型识别九宫格验证码的内容”补一下,(盲猜一手yolo,或者直接直接魔改一份ddddocr)
lwj1367 发表于 2025-8-31 00:37
hufu8888 发表于 2025-8-31 08:30
buluo533 发表于 2025-8-31 08:47
太卷了,建议再卷一下ast,极验3和4的ast逻辑是一样的太可怕了,我上大学才接触到的
QQGGKKXX 发表于 2025-8-31 16:23
太厉害了,技术控
yxf515321 发表于 2025-8-31 16:32
太卷了,太卷了
chenflyin 发表于 2025-8-31 20:34
太厉害了
yanghaohuid 发表于 2025-8-31 20:39
太厉害了!我都看不懂了!但可以发现知识的无穷魅力!科学文化是第一生产力!
imxz 发表于 2025-8-31 20:48
牛逼,赶紧收藏学习
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2026-2-1 14:08

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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