目标captcha_id: 045e2c229998a88721e32a763bc0f7b8
文章写的不太好, 完全是靠运气逆向出来的, 还请多多包涵
环境使用极验给出的demo代码
碎碎念
不知道还有没有人记得我, 春节解题领红包那会我还是初三, 现在我已经是一名高一新生咯! (还有高中三年, 可怕)
协议分析
总览
刷新网页, 打开开发者工具, 并完成一次验证操作,
不难发现两个主要请求: 显然load是加载验证码, 在verify进行验证, 中间的图片即为九宫格与小图
先看load (第一个请求)
先看看他请求有什么参数:
| 主要参数 |
说明 |
| captcha_id |
验证码appid |
| callback |
JSONP格式回调 |
| challenge |
随机UUID, verify中原样传递 |
再看看返回了什么:
返回数据为 JSONP 格式,
对比后续的两个图片请求可以发现: ques中返回的是验证码问题处的小图标, imgs对应的是九宫格大图 (省略了http://static.geetest.com/前缀), 两张图片没有被乱序;
继续查看可以发现: pow_detail是和腾讯滑块差不多的POW计算信息; process_token, lot_number, payload在verify请求中会被原样传递
verify分析 (验证请求)
这里需要逆向的只有w和其中的pow_msg, pow_sign, 其余参数均可直接从 load 请求获取或为固定值, 原样传入即可
w值逆向(连蒙带猜)
拿到js, 直接傻眼, 这混淆挺离谱啊, 按照以下步骤进行解混淆并替换文件:
- 丢入deobfuscate.io进行初步解混淆
- 再对上一步的结果使用UglifyJS Online, 设置
beautify: true和toplevel: true (用Uglifyjs的目的是为了换掉这个离谱的变量名)
- 用DevTools的替换功能替换掉
gcaptcha4.js原始的内容
然后要怎么办呢? 肯定是不能用常规方法了, 尝试搜索encrypt试试吧! (万一蒙中了呢)
极验似乎用了CryptoJS, Property的名字并没有被更改, encrypt匹配到了好几个, 全部在函数开头写上console.log(arguments), 重新完成一次验证操作
有两个函数输出了, 在第一个输出那里下断点, 去上一个调用堆栈
能够发现, 该函数返回的值恰好就是w值, 也就是说w的数据是通过t = n[i][o(980)][o(990)](t, D), (0, B[C(142)])(t) + _得到, t是要加密的数据, D是加密的密钥, 而_是由D进行加密得到, 接下来我们将其分为前半段与后半段进行分析
w前半段(AES)
在所调用的另一个encrypt函数中下断点, 发现其被上一个encrypt所调用:
偷一下懒, 问问GPT这附近的代码:
知道了加密方法是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();
}
接下来, 只差最后一点了: 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)
刷新页面, 控制台输出:
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, 不再赘述
结语
看一下最终效果
其实吧, 按理说这篇文章还有一个训练模型识别九宫格验证码的内容, 但是这文章是我大半夜写的, 太困了... 睡觉!
(马上要开学了, 我不想开学啊)