jscrambler 混淆器,主要是可见性的问题,一些 unicode 字符的去除,还有 decodeURI
里面的内容不能直接解码,得用源代码保留
混淆器手法
逆向该混淆器的时候有一些通用的模板
首先是类似于这样的形式的代码,我们通过查看能够知道 f3.$_Ct
是一个函数,通过输入对应的索引就能得出对应的字符串,类似于字符串池,而以下的代码有用的只有 v507
和 v509
其余的代码都是死代码( v510
几乎不会被用到)
var v507 = f3.$_Ct; var v508 = ["$_HCDg"].concat(v507); var v509 = v508[1]; v508.shift(); var v510 = v508[0];
没什么用的 switch
,单步即可
function f151(p529) { var v1994 = f3.$_DA()[10][14]; while (v1994 !== f3.$_DA()[8][13]) { switch (v1994) { case f3.$_DA()[8][14]: var v1995; var vV1965 = v1965(42); var vV19652 = v1965(42); for (v1995 = 0; v1995 <= 3; v1995++) { vV1965 += (vV19652 = v1963(37) + (p529 >>> v1995 * 8 & 255)[v1965(339)](16))[v1965(784)](vV19652[v1963(138)] - 2, 2); } return vV1965; break; } } }
270
索引的字符串为 prototype
,而 f52
是一个函数,为的是给函数添加新的函数,类似于继承
f52[v465(270)][v467(271)] = function f55(p191) { var v539 = f3.$_Ct; var v540 = ["$_HDIJ"].concat(v539); var v541 = v540[1]; v540.shift(); var v542 = v540[0]; if (p191[v541(236)] < 0 || p191[v541(220)](this[v539(244)]) >= 0) { return p191[v541(242)](this[v539(244)]); } else { return p191; } }; f52[v465(270)][v465(294)] = function f56(p192) { var v543 = f3.$_Ct; var v544 = ["$_HEDH"].concat(v543); var v545 = v544[1]; v544.shift(); var v546 = v544[0]; return p192; }; f52[v465(270)][v465(241)] = function f57(p193) { var v547 = f3.$_Ct; var v548 = ["$_HEIt"].concat(v547); var v549 = v548[1]; v548.shift(); var v550 = v548[0]; p193[v549(204)](this[v547(244)], null, p193); }; f52[v467(270)][v467(284)] = function f58(p194, p195, p196) { var v551 = f3.$_Ct; var v552 = ["$_HFDa"].concat(v551); var v553 = v552[1]; v552.shift(); var v554 = v552[0]; p194[v553(213)](p195, p196); this[v551(241)](p196); };
API 信息
-
https://passport.bilibili.com/x/passport-login/captcha
该接口获取极验的 token、challenge、gt
获取参数
-
https://api.geetest.com/get.php?
该接口获取一些必要参数,后面要用到
获取极验图片
-
https://static.geetest.com/static/js/click.3.1.2.js
https://static.geetest.com/static/js/gct.b71a9027509bc6bcfef9fc6a196424f5.js
这两个
js
实现了极验的加密(下面详细讲) -
https://api.geetest.com/ajax.php
该接口用于提交请求
点击验证提交
逆向流程
构造请求包
首先找到提交 post
请求的函数
post 提交
调用者上面就是请求的参数,可以很清晰的看到
通过查看,我们可以发现 gt
challenge
lang
在上述 get.php
的接口就可以找到,剩下的参数有 pt
client_type
w
生成的地方
pt 参数
通过搜索 643 这个索引即可找到赋值的地方,它由 v452
决定,可能为 0
或 3
var v452 = /Mobi/i[v11(107)](v450[v11(141)]);
我们通过索引 107
和 141
我们可以知道源代码长这样
var v452 = /Mobi/i.test(v450.userAgent);
而其中 v450 为 Navigator,是浏览器提供的结构,至此我们可以知道 pt
的值代表着 UA
字符串中是否 Mobi
字样,如果有,则为 3
,如果没有则为 0
client_type 参数
在 pt
参数附近,见上图,也就是
v452 ? v11(683) : v11(660);
翻译一下就是
V452 ? 'web_mobile' : 'web'
同理,非 Mobi
字样 UA 选择 'web'
w 参数
w
是一个组合的参数
w: v2021 + v2019
var v2019 = vThis25[v1925(751)](); var v2020 = vF7[v1927(374)](vF10[v1927(109)](vO16), vThis25[v1925(750)]()); var v2021 = vO8[v1927(774)](v2020);
分析该三个数据即可
v2019
$_CABQ: function (p538) { var v2081 = f3.$_Ct; var v2082 = ["$_CBGDt"].concat(v2081); var v2083 = v2082[1]; v2082.shift(); var v2084 = v2082[0]; var v2085 = new vF6()[v2081(374)](this[v2081(750)](p538)); while (!v2085 || v2085[v2083(138)] !== 256) { v2085 = new vF6()[v2081(374)](this[v2081(750)](true)); } return v2085; }
其中主要的是 new vF6()[v2081(374)](this[v2081(750)](p538));
内部的函数调用 $_CACd
,而通过调试我们可以知道它会返回 v1811
,它从 vF9()
初始化得到
vF9
可知它返回一个 16 字符长度的字符串,也就是 AES key,然后该生成的随机 AES key 会通过 RSA 加密,其中构造 vF6
的时候会自动加载公钥,然后调用 encrypt
函数
通过断点 setPublic
我们可以知道它会获取公钥
00C1E3934D1614465B33053E7F48EE4EC87B14B95EF88947713D25EECBFF7E74C7977D02DC1D9451F79DD5D1C10C29ACB6A9B4D6FB7D0A0279B6719E1772565F09AF627715919221AEF91899CAE08C0D686D748B20A3603BE2318CA6BC2B59706592A9219D0BF05C9F65023A21D2330807252AE0066D59CEEFA5F2748EA80BAB81 10001
这里它虽然写的是这个,但是后面的解析其实是不需要首部的 00
字节的,所以我们去掉 00
然后转换为 PEM
格式公钥
-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDB45NNFhRGWzMFPn9I7k7IexS5 XviJR3E9Je7L/350x5d9AtwdlFH3ndXRwQwprLaptNb7fQoCebZxnhdyVl8Jr2J3 FZGSIa75GJnK4IwNaG10iyCjYDviMYymvCtZcGWSqSGdC/Bcn2UCOiHSMwgHJSrg Bm1Zzu+l8nSOqAurgQIDAQAB -----END PUBLIC KEY-----
通过跟踪 encrypt
函数,我们可以从中发现填充的代码,通过动态跟踪我们可以知道这是 RSAES-PKCS1-v1_5
填充
var v_ = function f91(p240, p241) { var v740 = f3.$_Ct; var v741 = ["$_JBIY"].concat(v740); var v742 = v741[1]; v741.shift(); var v743 = v741[0]; if (p241 < p240[v742(138)] + 11) { if (console && console[v742(65)]) { console[v740(65)](v740(365)); } return null; } var vA5 = []; var v744 = p240[v742(138)] - 1; while (v744 >= 0 && p241 > 0) { var v745 = p240[v742(166)](v744--); if (v745 < 128) { vA5[--p241] = v745; } else if (v745 > 127 && v745 < 2048) { vA5[--p241] = v745 & 63 | 128; vA5[--p241] = v745 >> 6 | 192; } else { vA5[--p241] = v745 & 63 | 128; vA5[--p241] = v745 >> 6 & 63 | 128; vA5[--p241] = v745 >> 12 | 224; } } vA5[--p241] = 0; var v746 = new f42(); var vA6 = []; while (p241 > 2) { vA6[0] = 0; while (vA6[0] == 0) { v746[v740(217)](vA6); } vA5[--p241] = vA6[0]; } vA5[--p241] = 2; vA5[--p241] = 0; return new f44(vA5); }(p239, this[v736(219)][v738(383)]() + 7 >> 3);
至此我们分析完了 v2019 的流程,也就是随机生成 AES key,然后再拿 RSA
公钥加密 AES key,返回 hex
格式字符串
v2020
将 vO16
对象序列化,然后调用 AES
加密,密钥是刚刚生成的 AES key
跟踪到里面就可以获取 IV
是 0000000000000000
字符串,然后返回字节数组
v2020 生成的内容
v2021
自定义编码函数,类似于 base64
但是拓展到了 74 位
()*,-./0123456789:?@ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz~
这里可以直接把代码扣下来
let $_DBe = f3.$_Ct(174); let $_DCz = f3.$_Ct(71); let $_DDr = 7274496; let $_DEA = 9483264; let $_DFp = 19220; let $_DGb = 235; let $_DHo = 24; function $_EAe(p143) { var v387 = f3.$_Ct; var v391 = $_DBe; if (p143 < 0 || p143 >= v391.length) { return v387(71); } else { return v391.charAt(p143); } } function $_ECv(p145, p146) { return p145 >> p146 & 1; } function $_EDt(p147) { var v402 = f3.$_Ct; var vF4 = function (p149, p150) { var vLN09 = 0; for (var v408 = $_DHo - 1; v408 >= 0; v408 -= 1) { if ($_ECv(p150, v408) === 1) { vLN09 = (vLN09 << 1) + $_ECv(p149, v408); } } return vLN09; }; var vV402 = v402(42); var vV4022 = v402(42); for (var v409 = p147.length, vLN010 = 0; vLN010 < v409; vLN010 += 3) { var v410; if (vLN010 + 2 < v409) { v410 = (p147[vLN010] << 16) + (p147[vLN010 + 1] << 8) + p147[vLN010 + 2]; vV402 += $_EAe(vF4(v410, $_DDr)) + $_EAe(vF4(v410, $_DEA)) + $_EAe(vF4(v410, $_DFp)) + $_EAe(vF4(v410, $_DGb)); } else { var v411 = v409 % 3; if (v411 == 2) { v410 = (p147[vLN010] << 16) + (p147[vLN010 + 1] << 8); vV402 += $_EAe(vF4(v410, $_DDr)) + $_EAe(vF4(v410, $_DEA)) + $_EAe(vF4(v410, $_DFp)); vV4022 = $_DCz; } else if (v411 == 1) { v410 = p147[vLN010] << 16; vV402 += $_EAe(vF4(v410, $_DDr)) + $_EAe(vF4(v410, $_DEA)); vV4022 = $_DCz + $_DCz; } } } return { res: vV402, end: vV4022 }; } function $_EFu_EncodeAESBytes(p152) { var v421 = $_EDt(p152); return v421.res + v421.end; }
构造数据包
至此,我们能够构造出请求包 vO18
了,但是别忘了,AES 加密的内容还不知道是什么,也就是 vO16
,vO16
在函数头部进行定义
其中 lang
pic
已知,在 get.php
中可以找到
a
passtime
是传入的点击位置与消耗时间
其余需要构造
tt 参数
tt: function (p476, p477, p478) { var v1932 = f3.$_Ct; var v1933 = ["$_CADIZ"].concat(v1932); var v1934 = v1933[1]; v1933.shift(); var v1935 = v1933[0]; if (!p477 || !p478) { return p476; } var v1936; var vLN052 = 0; var vP476 = p476; var v1937 = p477[0]; var v1938 = p477[2]; var v1939 = p477[4]; while (v1936 = p478[v1932(784)](vLN052, 2)) { vLN052 += 2; var vParseInt = parseInt(v1936, 16); var v1940 = String[v1932(181)](vParseInt); var v1941 = (v1937 * vParseInt * vParseInt + v1938 * vParseInt + v1939) % p476[v1932(138)]; vP476 = vP476[v1934(784)](0, v1941) + v1940 + vP476[v1932(784)](v1941); } return vP476; }(v1931, v1929[v1927(729)], v1929[v1927(236)])
其中三个参数分别为:鼠标移动点位数据,get.php
提供的 c
,get.php
提供的 s
,然后通过该函数能够得出编码的轨迹数据
ep 参数
调用以下函数,其中
this[v2086(617)][v2088(630)]
为点位数据中的点击数据,见下
v
是极验的版本 3.1.2
$_FG
为 touchEvent
的值
me
为 mouseEvent
的值
tm
为浏览器中 window.performance.timing
的数据
$_CAAe: function () { var v2086 = f3.$_Ct; var v2087 = ["$_CBGIo"].concat(v2086); var v2088 = v2087[1]; v2087.shift(); var v2089 = v2087[0]; console.log(`let dots = JSON.parse('${JSON.stringify(this[v2086(617)][v2088(630)])}')`); return { ca: this[v2088(617)] && this[v2086(617)][v2088(630)] || v2086(42), v: v2086(759), $_FG: vV1612[v2086(614)], me: vV1612[v2086(648)], tm: new f129()[v2086(731)]() }; }
点击数据(注意该数据是所有点击的位置,而传入参数中的点击只有验证图像中的点击位置)
点击数据的构造由该函数添加,监听 click
事件,p577
第一个参数是元素,第二个参数是点击事件,其中这几个值的对应的元素是这样的
vThis44[v2487(600)][v2489(587)](v2487(453), function (p577) { var v2494 = f3.$_Ct; var v2495 = ["$_CFDIW"].concat(v2494); var v2496 = v2495[1]; v2495.shift(); var v2497 = v2495[0]; var v2498 = p577[v2496(413)][v2496(942)] || p577[v2496(413)][v2496(998)]; var v2499 = v2492(v2496(719))[v2494(112)]; var v2500 = v2492(v2496(737))[v2496(112)]; var v2501 = v2492(v2496(762))[v2494(112)]; var v2502 = v2492(v2496(748))[v2494(112)]; var v2503 = vThis44[v2496(736)](v2494(832))[v2494(112)]; var vO21 = { x: p577[v2496(509)](), y: p577[v2496(567)]() }; if (f156(v2503, v2498)) { vO21[v2496(278)] = 1; } else if (f156(v2502, v2498)) { vO21[v2496(278)] = 3; } else if (f156(v2500, v2498)) { vO21[v2494(278)] = 4; } else if (f156(v2499, v2498)) { vO21[v2496(278)] = 5; } else if (f156(v2501, v2498)) { vO21[v2494(278)] = 6; } else { vO21[v2494(278)] = 0; } if (v2496(865) !== v2491[v2496(744)]) { var v2504 = v2498[v2496(436)] || v2494(42); if (v2504[v2496(191)](vV11 + v2496(990)) >= 0) { var v2505 = v2504[v2496(191)](vV11 + v2496(813)) >= 0; vO21[v2496(278)] = v2505 ? 1 : 2; } if (v2504[v2496(191)](vV11 + v2494(969)) >= 0) { vO21[v2494(278)] = 2; } } vThis44[v2496(901)](vO21); vThis44[v2494(630)][v2494(108)](vO21); vThis44[v2494(954)](); });
v2498
是当前点击的元素
vThis44[v2496(901)](vO21);
添加时间差
所以点击的结构体是这样的
t
是点击的元素,1
为点击验证码图像,3
为点击提交按钮(最后一个一定是 3
)
dt
为距离上一个 pointerdown
的时间差(毫秒)
[{"x":943,"y":365,"t":1,"dt":3881}, {...}]
附加额外字段
除此之外还有一个 vO17
对象需要构造,但是直接使用 gct.js
即可,然后 vO16
会附加上 vO17
的部分数据(也可以直接给定值)
"h9s9"
为 "1816378497"
gct.js 中含有反调试,建议是直接给固定结果 1803797734,该值会永远计算出 1816378497
最后 vO16
还会使用 gt
+ challenge
+ passtime
字符串进行 md5
计算得出 rp
字段