JS逆向-OB混淆

​ 讲OB混淆前,先要讲讲什么是混淆,混淆就是在不影响原本的代码的逻辑情况下对代码的形式和结构进行变动,使代码变得难以阅读和理解,增加代码逆向的难度。

​ 在JavaScript中,OB混淆一般是指的网站 https://obfuscator.io 提供的代码混淆,这里面提供了一个JavaScript代码混淆工具,可进行多种类型的代码混淆。

OB混淆识别

​ OB混淆基本上由四个部分组成:

1
2
3
4
- 一个大数组或者有一个大数组的函数 //用于存储加密的字符串和关键数据
- 一个自执行函数 //数组混淆器,用于打乱数组的顺序
- 解密函数 //数组访问器
- 加密后的函数 //原始逻辑

​ 这是OB混淆代码的基本构成,OB混淆有如下识别特征:

1
2
3
4
5
6
7
8
9
10
11
//变量名和函数名混淆 ,形如 _0xab1cd2。由"_0x"开头、无规律字母数字开头,后面跟上1-6位数字字母组合。
var _0xabcd = (function() {
var _0x1 = function(_0x2, _0x3) {
return _0x1234[_0x2 - 0x0];
};
_0x1['XyzAbc'] = function(_0x4, _0x5) {
return atob(_0x4);
};
return _0x1;
}());
//自执行函数中有push shift 字样

​ 纸上得来终觉浅,动手试一试才能知道OB混淆前后的改动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//混淆前的代码
!function(e){
console.log("Hello ", e);
}("Maododo")

//混淆后的代码
function _0x6cd1(_0x2fe9d3, _0x578f74) {
_0x2fe9d3 = _0x2fe9d3 - 0xd0;
var _0x4838f2 = _0x4838();
var _0x6cd151 = _0x4838f2[_0x2fe9d3];
return _0x6cd151;
}

var _0x158908 = _0x6cd1;

function _0x4838() {
var _0xce77dd = ['3856821yyeMCS', '5493156EGSXls', 'Hello\x20', 'Maododo', '346717LaHyhs', '39063eTpdiF', '7841840ZMbEsb', '4810420OBpuHU', '52ZUnfxK', '128SJfwCf', '17811900VqAsEG'];
_0x4838 = function () {
return _0xce77dd;
};
return _0x4838();
}

(function (_0x122f90, _0x42566e) {
var _0x5937d2 = _0x6cd1, _0x219585 = _0x122f90();
while (!![]) {
try {
var _0x28d2fe = -parseInt(_0x5937d2(0xd5)) / 0x1 * (parseInt(_0x5937d2(0xd8)) / 0x2) + parseInt(_0x5937d2(0xd0)) / 0x3 + parseInt(_0x5937d2(0xd7)) / 0x4 + parseInt(_0x5937d2(0xd6)) / 0x5 + -parseInt(_0x5937d2(0xd1)) / 0x6 + -parseInt(_0x5937d2(0xd4)) / 0x7 * (-parseInt(_0x5937d2(0xd9)) / 0x8) + -parseInt(_0x5937d2(0xda)) / 0x9;
if (_0x28d2fe === _0x42566e) break; else _0x219585['push'](_0x219585['shift']());
} catch (_0x342f06) {
_0x219585['push'](_0x219585['shift']());
}
}
}(_0x4838, 0xe533c),
!function (_0x44a1d9) {
var _0x355098 = _0x6cd1;
console['log'](_0x355098(0xd2), _0x44a1d9);
}(_0x158908(0xd3)));

​ 上面的代码就是一个标准的四个模块,让我来给拆解一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//第一块是解密函数,可以看到里面直接调用了大数组函数
function _0x6cd1(_0x2fe9d3, _0x578f74) {
_0x2fe9d3 = _0x2fe9d3 - 0xd0;
var _0x4838f2 = _0x4838();
var _0x6cd151 = _0x4838f2[_0x2fe9d3];
return _0x6cd151;
}

var _0x158908 = _0x6cd1;
// 很明显的大数组,用于存放变量,可以看到我们的常量数据‘Maododo’和‘hello’也在里面
function _0x4838() {
var _0xce77dd = ['3856821yyeMCS', '5493156EGSXls', 'Hello\x20', 'Maododo', '346717LaHyhs', '39063eTpdiF', '7841840ZMbEsb', '4810420OBpuHU', '52ZUnfxK', '128SJfwCf', '17811900VqAsEG'];
_0x4838 = function () {
return _0xce77dd;
};
return _0x4838();
}
//自执行函数,这就是就是数组混淆器,里面也有push和shift字样
(function (_0x122f90, _0x42566e) {
var _0x5937d2 = _0x6cd1, _0x219585 = _0x122f90();
while (!![]) {
try {
var _0x28d2fe = -parseInt(_0x5937d2(0xd5)) / 0x1 * (parseInt(_0x5937d2(0xd8)) / 0x2) + parseInt(_0x5937d2(0xd0)) / 0x3 + parseInt(_0x5937d2(0xd7)) / 0x4 + parseInt(_0x5937d2(0xd6)) / 0x5 + -parseInt(_0x5937d2(0xd1)) / 0x6 + -parseInt(_0x5937d2(0xd4)) / 0x7 * (-parseInt(_0x5937d2(0xd9)) / 0x8) + -parseInt(_0x5937d2(0xda)) / 0x9;
if (_0x28d2fe === _0x42566e) break; else _0x219585['push'](_0x219585['shift']());
} catch (_0x342f06) {
_0x219585['push'](_0x219585['shift']());
}
}
}(_0x4838, 0xe533c), //逗号表达式,下面的是第四块,也就是混淆后的执行逻辑,也是很清楚能看到这里,将变量和常量给替换成了'_0xab1cd2'格式的变量了
!function (_0x44a1d9) {
var _0x355098 = _0x6cd1;
console['log'](_0x355098(0xd2), _0x44a1d9);
}(_0x158908(0xd3)));

如何处理OB混淆?

解OB混淆工具、网站

​ 既然有工具能够直接生成OB混淆,那也有工具能直接解OB混淆

https://thanhle.io.vn/de4js/

https://github.com/Tsaiboss/decodeObfuscator

​ 这个工具整一下午没整好,有点失望,我后续将补充这部分内容,暂且搁置。

手动补OB混淆环境

​ 这里就跟OB混淆关系不是很大了,只需要将OB混淆后的部分,全部copy下来,简单替换几个函数就可以了,主要是耐心,

例子

​ OB混淆只不过是混淆之万一,用的多了,将规律记录下来,下次遇到便会得心应手。

某公众号功能

某公众号功能

​ 这里用的是真机adb调试Chrome inspect调试,针对不能开启f12的公众号、内嵌webview的APP还是挺好用的,公众号使用的话,还需要在微信输入并点击:

1
http://debugxweb.qq.com/?inspector=true

打开浏览器调试能力

​ 然后就可以进去直接调试,还需要再绕过一下无限debugger,这个网上文章有很多,这里用的是这个修改原型链:

1
2
3
4
5
6
7
8
var _constructor = constructor;
Function.prototype.constructor = function(s) {
if (s == "debugger"){
console.log(s);
return null;
}
return _constructor(s);
}

请求荷载

​ 可以发现这里,请求荷载被加密了,打断点跟一下,看看在哪里处理加密解密的

加mac函数

​ 来看看这个函数,使用了经典的平坦流控制进行关键函数的混淆,也就是while true 和 switch 进行代码执行流程的混淆,只需要打断点,然后把执行的代码,一行一行拼一起,不过我们只在乎请求报文加密,这里可以忽视。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function encryptParamNEW(_$H, _$l, _$n) {
var Hg = a053b76H5;
var _$W = {
'Hbpen': Hg(0x1f1),
'ZFdMc': function(_$U, _$h) {
return _$U === _$h;
},
'XFqac': function(_$U, _$h) {
return _$U(_$h);
},
'LFjsF': function(_$U, _$h) {
return _$U + _$h;
},
'fgiYN': function(_$U, _$h) {
return _$U * _$h;
},
'EeZUw': function(_$U, _$h, _$L) {
return _$U(_$h, _$L);
}
};
var _$c = _$W[Hg(0x239)][Hg(0x296)]('|');
var _$w = 0x0;
while (!![]) {
switch (_$c[_$w++]) {
case '0':
if (_$W[Hg(0x307)](_$n, '1')) {
_$W[Hg(0x226)](encryptJSONSM, _$H);
} else {
var _$D = _$W['XFqac'](parseInt, _$W[Hg(0x245)](_$W[Hg(0x25f)](0x55d4a7f, Math[Hg(0x2fa)]()), 0x989680)) + '';
_$H = _$W[Hg(0x226)](wrapParam, _$H);
_$W['EeZUw'](encryptJSON, _$H, _$D);
}
continue;
case '1':
_$l['macKey'] = _$T;
continue;
case '2':
var _$T = '';
continue;
case '3':
var _$P = encryptParam(_$H);
continue;
case '4':
return _$H;
case '5':
_$T = JSON['parse'](_$P[Hg(0x305)]).mac;
continue;
}
break;
}
}

​ 简单看一下,这个函数是用来加mac的,进行报文防篡改的,可以pass了。

报文加密函数

报文加密函数加密后

​ 很清楚的看到,报文被加密了,这个ob混淆很简单呢,加密函数一下就能找到,也不用处理大数组等东西,算是一个失败的例子。

然后再进行代码本地化。

补环境01

​ 补环境过程中,遇到这种本地不能被解析的,可以直接去调试,看看这里是什么,直接替换成明文。因为这个getPublicKey函数比较短,环境不变的情况下,一般只有一个返回值,这里就可以自行判断,只留下return的值,也是个不错的补环境方法。

​ 其中有一个反调试函数,可以尝试将它删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
function _$o(_$H) {
var l1 = a053b76H5;
var _$l = {
'vtNxu': function(_$W, _$c) {
return _$W + _$c;
},
'wBNvU': function(_$W, _$c) {
return _$W > _$c;
},
'vWIAu': l1(0x22b),
'wetVp': l1(0x22a),
'SIxJk': function(_$W, _$c) {
return _$W !== _$c;
},
'BFLGu': function(_$W, _$c) {
return _$W / _$c;
},
'Kjmsr': l1(0x2cc),
'PgkEJ': function(_$W, _$c) {
return _$W + _$c;
},
'cjBVC': l1(0x2f1),
'GBnkh': l1(0x273),
'IuCnL': l1(0x259),
'YwJsb': function(_$W, _$c) {
return _$W > _$c;
}
};
function _$n(_$W) {

}
try {
if (_$H) {
return _$n;
} else {
_$n(0x0);
}
} catch (_$W) {}
}


//还有调用这个函数的自执行函数
(function() {
var g = a053b76n;
var _$H = {
'iwjOC': function(_$W, _$c) {
return _$W(_$c);
},
'tSvXd': function(_$W, _$c) {
return _$W + _$c;
}
};
var _$l = function() {
var K = a053b76n;
var _$W;
try {
_$W = _$H[K(0x228)](Function, _$H[K(0x22e)](K(0x31b) + '{}.constructor(\x22return\x20this\x22)(\x20)', ');'))();
} catch (_$c) {
_$W = typeof window !== K(0x2ac) ? window : this;
}
return _$W;
};
var _$n = _$l();
_$n[g(0x204)](_$o, 0xfa0);
}());

​ 将这两个部分的代码注释掉就可以,接下来就是很基础的补环境,还有ob混淆的补环境。大体就是这个流程。

报文加密