前言
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 信息
d46K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6H3j5i4y4K6M7r3!0J5N6q4)9J5k6h3u0A6L8r3W2T1K9h3I4A6i4K6u0W2j5$3!0E0i4K6u0r3P5q4)9J5c8Y4m8S2M7%4y4H3L8%4u0@1i4K6u0V1L8r3!0Y4K9h3&6Q4x3V1k6U0j5i4m8@1j5$3S2S2
该接口获取极验的 token、challenge、gt
获取参数
936K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2M7r3W2Q4x3X3g2Y4k6h3g2@1k6i4y4@1i4K6u0W2j5$3!0E0i4K6u0r3k6$3g2@1i4K6u0W2M7r3S2H3i4K6y4r3
该接口获取一些必要参数,后面要用到
获取极验图片
888K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6K6N6r3q4@1K9h3y4Q4x3X3g2Y4k6h3g2@1k6i4y4@1i4K6u0W2j5$3!0E0i4K6u0r3M7%4c8S2N6r3W2U0i4K6u0r3K9Y4y4Q4x3V1k6U0L8r3W2U0K9#2)9J5k6e0y4Q4x3X3f1I4i4K6u0W2x3W2)9J5k6h3A6K6
583K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6K6N6r3q4@1K9h3y4Q4x3X3g2Y4k6h3g2@1k6i4y4@1i4K6u0W2j5$3!0E0i4K6u0r3M7%4c8S2N6r3W2U0i4K6u0r3K9Y4y4Q4x3V1k6Y4j5%4c8Q4x3X3g2T1y4K6q4S2z5e0l9J5y4K6f1H3z5h3u0U0y4X3u0U0k6X3g2X3z5h3k6U0y4X3p5I4z5e0j5@1x3U0c8X3y4g2)9J5k6h3A6K6
这两个 js
实现了极验的加密(下面详细讲)
7f6K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2M7r3W2Q4x3X3g2Y4k6h3g2@1k6i4y4@1i4K6u0W2j5$3!0E0i4K6u0r3j5h3A6S2P5q4)9J5k6i4m8Z5M7l9`.`.
该接口用于提交请求
点击验证提交
逆向流程
构造请求包
首先找到提交 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
字段
结果