前言
普通的ob混淆,可以 根据这些网址来进行还原:
https://webcrack.netlify.app/
https://obf-io.deobfuscate.io/
当请求返回的JS代码无法通过这些网址进行反混淆只能解一部分混淆时,就要通过AST,解析代码,然后进行还原。
AST基础可以参考:
视频学习AST基础:
https://www.bilibili.com/video/BV1FQ7pzZEmc?vd_source=61bd07e56393e4c482b0f4e60b0a66c6
这篇博客,总结的很全面:
https://blog.51cto.com/u_11866025/6047324
这篇文章,简单的动态js还原:
https://www.52pojie.cn/thread-2085320-1-1.html
一. OB混淆的基本特征
1.一般由一个大数组或者包含大数组的函数,一个自执行函数,一个解密函数构成;
2.大数组中的列表元素一般为字符串,可能为明文,也可能是经过base64等编码,还可能是经过RC4算法加密的密文;
3.自执行函数,自执行函数的参数,通常为包括 大数组/包含大数组的函数[如果大数组或者大数组函数不在参数上,那就可能在函数体内],偏移量或者计算偏移量的相关参数[数值,十六进制显示];自执行函数会根据偏移量的要求对大数组列表的元素进行位移,因此函数体应该会包含 push,shift关键字;
4.解密函数,如果解密函数中包含类似 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=' 的变量(可能字符顺序不一样),大数组的列表的元素很可能是经过了 base64编码,需要解码;
5.函数名和变量名通常以_0x或者0x开头,后接1~6位数字或字母组合;
简单样例,数组位移:
// 列表元素
var _0x30bc = ['log', 'Hello\x20World!'];
// 自执行函数,加密函数,实现列表元素位移
(function (_0x36d89d, _0x30bcb2) {
var _0xae0a32 = function (_0x2e4e9d) {
while (--_0x2e4e9d) {
_0x36d89d['push'](_0x36d89d['shift']());
}
};
_0xae0a32(++_0x30bcb2);
}(_0x30bc, 0x133));
// 解密函数
var _0xae0a = function (_0x38d89d) {
_0x38d89d = _0x38d89d - 0x0;
var _0xae0a32 = _0x30bc[_0x38d89d];
return _0xae0a32;
};
// ob混淆的js代码
function hi() {
console[_0xae0a('0x1')](_0xae0a('0x0'));
}
hi();
加上 base64 编码的样例:
// 列表元素
var _0x30bc = [ 'Bg9N','AgvSBg8GD29YBgq'];
// 自执行函数,加密函数,实现列表元素位移
(function (_0x36d89d, _0x30bcb2) {
var _0xae0a32 = function (_0x2e4e9d) {
while (--_0x2e4e9d) {
_0x36d89d['push'](_0x36d89d['shift']());
}
};
_0xae0a32(++_0x30bcb2);
}(_0x30bc, 0x101));
// 解密函数
var _0xae0a = function (_0x38d89d) {
function base64_decode(t) {
// base64解码
for (var e, n, r = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=", o = "", a = "", c = 0, i = 0;
n = t.charAt(i++);
~n && (e = c % 4 ? e * 64 + n : n, c++ % 4) ? o += String.fromCharCode(e >> (c * -2 & 6) & 255) : 0) {
n = r.indexOf(n);
}
for (var u = 0, s = o.length; u < s; u++) {
a += "%" + ("00" + o.charCodeAt(u).toString(16)).slice(-2);
}
return decodeURIComponent(a);
}
_0x38d89d = _0x38d89d - 0x0;
var _0xae0a32 = _0x30bc[_0x38d89d];
return base64_decode(_0xae0a32);
};
// ob混淆的js代码
function hi() {
console[_0xae0a('0x1')](_0xae0a('0x0'));
}
hi();
还可以解密函数上
1. 添加 RC4算法解密;
2. 添加 格式化检查语句;
动态识别OB混淆
动态识别OB数组混淆都是根据其基本特征来进行识别还原, 如
- 识别 大数组或者大数组函数,可以通过 找到 数组 或者 包含数组的函数,并且数组里面的元素都必须是 字符串
- 识别 解密函数,可以根据函数是否包含 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/='这样子的字符串来识别
二. OB混淆变体情况一
想要让动态识别OB混淆失效,就可以打破OB混淆基本特征的约束,比如:
- 数组列表的元素要求必须是字符串,那就给某个列表元素的数据类型改为变量,或者三元表达式之类的
- 数组/数组函数,自执行函数,解密函数 是ob数组混淆的三要素,可以去掉自执行函数,在数组和解密函数中下功夫,数组的列表元素经过 base64编码或者RC4算法加密
- 函数名和变量名通常以_0x或者0x开头,那就把 _0x或者0x替换成其他字符
- 解密函数中包含 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=' 的字符串,那就把 base64码表的顺序打乱
数组列表元素包含非字符串的样例:
var _0x30bb = 'log';
// 列表元素中包含变量
var _0x30bc = [_0x30bb, 'Hello\x20World!'];
// 解密函数,自执行函数,js混淆的代码 和 第一个简单样例一样
不包含自执行函数
// 列表元素
var _0x30bc = [ 'Bg9N','AgvSBg8GD29YBgq'];
// 去掉自执行函数,不做位移
// 解密函数 和 加上 base64 编码的样例 中的解密函数一样
// ob混淆的js代码,解密函数参数顺序和数组类别顺序一致
function hi() {
console[_0xae0a('0x0')](_0xae0a('0x1'));
}
hi();
解密函数中base64码表的顺序打乱的样例:
// 列表元素,base64码表顺序打乱得到的列表元素
var _0x30bc = [ 'Ov9W','NvyQOv8VU29BOve'];
// 自执行函数,加密函数,实现列表元素位移
(function (_0x36d89d, _0x30bcb2) {
var _0xae0a32 = function (_0x2e4e9d) {
while (--_0x2e4e9d) {
_0x36d89d['push'](_0x36d89d['shift']());
}
};
_0xae0a32(++_0x30bcb2);
}(_0x30bc, 0x101));
// 解密函数,base64码表顺序打乱
var _0xae0a = function (_0x38d89d) {
function base64_decode(t) {
// base64解码
for (var e, n, r = "nozufdvtxsrgawjkelqcpyihbmNOZUFDVTXSRGAWJKELQCPYIHBM0123456789+/=", o = "", a = "", c = 0, i = 0;
n = t.charAt(i++);
~n && (e = c % 4 ? e * 64 + n : n, c++ % 4) ? o += String.fromCharCode(e >> (c * -2 & 6) & 255) : 0) {
n = r.indexOf(n);
}
for (var u = 0, s = o.length; u < s; u++) {
a += "%" + ("00" + o.charCodeAt(u).toString(16)).slice(-2);
}
return decodeURIComponent(a);
}
_0x38d89d = _0x38d89d - 0x0;
var _0xae0a32 = _0x30bc[_0x38d89d];
return base64_decode(_0xae0a32);
};
// ob混淆的js代码
function hi() {
console[_0xae0a('0x1')](_0xae0a('0x0'));
}
hi();
解决思路
以上以打破OB混淆基本特征的约束使动态识别失效的样例,解决办法就是通过手动将 数组/数组函数,自执行函数,解密函数,变量等相关的代码都加载到内存中,
如果只是 数组列表元素包含 一两个非字符串的元素,直接根据其变量或者三元表达式返回的字符串替换掉原来的元素,也可以通过反混淆网址实现反混淆;
如果存在 格式化检查代码,就把代码压缩后加载到内存中;
然后通过 AST识别 解密函数的调用的path,将其替换为 解密函数调用的结果 即可
三.OB混淆变体情况二
既然能够通过手动加载OB混淆的相关代码到内存中来解决上面的解混淆问题,那么也可以从其他方面去使其反混淆失效,比如:
- 解密函数分身,把解密函数赋值给另外一个变量,由这个变量实现函数调用
- 还可以进一步实现分身,把解密函数赋值给一个变量a,这个变量a又赋值给另外一个变量b,由这个变量b实现函数调用,还有可能实现嵌套赋值的情况
解密函数赋值给一个变量的样例:
// 数组,加密函数,解密函数 和 第一个简单样例一样
// 解密函数赋值给另外一个变量
var dd = _0xae0a;
// ob混淆的js代码
function hi() {
console[dd('0x1')](dd('0x0'));
}
hi();
解密函数多重赋值的样例:
// 数组,加密函数,解密函数 和 第一个简单样例一样
// 解密函数赋值给另外一个变量
var dd = _0xae0a;
// ob混淆的js代码
function hi() {
var uu = dd;
console[uu('0x1')](uu('0x0'));
}
hi();
解决思路
解密函数被多重赋值的情况,可以通过 AST定位到解密函数的path,通过 path.scope.getbinding(name) 获取变量赋值的path,获取其节点赋值的变量名,将其变量名调用的path中的函数名称改为解密函数的名称;
多重赋值,要用到递归;将相关的变量赋值的函数调用名称都改成解密函数名称后,在将解密函数调用的path 改为 解密函数调用的结果
可以参考这个视频来学习如何解决这种解密函数被多重赋值的问题:
https://www.bilibili.com/video/BV1SfTjzaEBG?p=5&vd_source=61bd07e56393e4c482b0f4e60b0a66c6
四.OB混淆变体情况三
上面的情况,都是围绕OB混淆的特征和解密函数多重赋值来解混淆,还可以引入字典,和OB混淆相互作用,使其只能解决部分混淆的情况,
字典和数组有点相似,字典是通过调用key来返回value,数组是通过调用index来返回value,隐藏真实值
字典的key值,可以是数值,也可以是字符串
字典的value值,可以是数值,是字符串,还可以是函数等等,尤其是函数,这和 数组混淆 最大不同的,函数要根据实参替换掉形参,执行函数后,然后返回结果
字典结合数组混淆的样例1,字典的value是字面量:
// 数组,加密函数,解密函数 和 第一个简单样例一样
// 加入字典
var dict ={
'aaa':'0x1',
'bbb':'0x0',
}
// ob混淆的js代码
function hi() {
console[_0xae0a(dict['aaa'])](_0xae0a(dict['bbb']));
}
hi();
字典结合数组混淆的样例2,字典的value可以是函数,函数的参数类型还可能是函数:
// 列表元素
var _0x30bc = ['sXyej', 'Hello\x20World!'];
// 加密函数,解密函数 和 第一个简单样例 一样
// 加入字典,字典的value的类型可以是字面量,也可以是函数
var ix = {
'MLMYr': function (Pi, PD) {
return Pi == PD; // 返回值为 二进制表达式如:算术运算,关系运算,位运算 等等
},
'KWXXu': function (Pi, PD) {
return Pi(PD); //Pi 是函数
},
'sXyej': "log",
}
// ob混淆的js代码
function hi(name){
if(ix['MLMYr']('lili','lili')){
console[ix[_0xae0a('0x1')]](name);
}
}
ix['KWXXu'](hi, _0xae0a('0x0'))
解决思路
字典结合数组混淆,
- 要判断是先执行字典的混淆还是先执行数组的混淆,是字典的value作为数组解密函数的参数,还是字典的value函数参数是数组解密函数,会存在多种情况,字典的个数不止一个;
- 字典的value是函数,实参 替换 形参时,函数中 params 和 body 中的 arguments 要配对替换
- 如果字典的value是函数,实参有可能是字典另外一个key值的调用,字典嵌套问题,所以还要考虑执行顺序问题等等问题,可以考虑利用堆栈来解决[参数中存在函数调用就先入栈]
- 字典和解密函数一样,也存在多重嵌套赋值问题,解决思路一样
五.OB混淆变体情况四
除了引入字典,还可以引入函数,函数 a 调用 数组解密函数,函数b 调用 函数a,就是在解密函数外面套多层外衣,目的就是把人绕晕。
引入函数的样例:
// 数组,加密函数,解密函数 和 第一个简单样例一样
// 引入两个函数,给解密函数套外衣
function uu(a,b){
return _0xae0a(a+b)
}
function nn(a,b,c){
return uu(a+b,c)
}
// ob混淆的js代码
function hi() {
console[nn(0,'x',1)](nn(0,'x',0));
}
hi();
解决思路
- 定位解密函数最外层的外衣,函数的特点是body只有一个return语句,并且函数参数的实参是常量(数值或者字符串或者一元表达式[操作符加常量]);
- 也可以定位解密函数,一层层往上查找,直到函数调用的参数为常量;
- 复杂两种方式应该联合使用
- 还有,要先考虑函数名称和函数参数名称相同的问题,如 function a(a,b,c),函数名称和参数名称都为a,AST识别的时候会识别错,要先把 函数中 params 和 body 中的 arguments 和函数名相同的参数名称改为其他名称
- 函数嵌套的层级和作用域错综复杂,可以考虑引入沙箱进行隔离
六.碎碎念
一开始,我以为不懂OB混淆,不懂AST也没关系,有反混淆的网站;
数组混淆的特征也不清楚,只拿了数组和解密函数,发现无法解混淆。。。只能通过调试的时候把解混淆的结果拿下来;
后来看到一个函数,有好多if-else分支,全是混淆的代码,想要把调试的结果拿下来是异想天开;
后来才了解OB混淆的特征,但 AST 了解的很模糊,基础没学会,跟着视频学习,t.isIdentifier()是什么东西,t是怎么来的都不知道;
到了后面看了 jsvmp正向 ,对AST了解也还是模模糊糊的,直到学到了 jsvmp 逆向的视频,才算是入了门,终于知道 types.isIdentifier(),types.identifier() 怎么用;
然后慢慢了解到,path.scope 的重要性,在每个visitor 最后加上 path.crawl() 解决节点被替换后AST结构发生变化导致影响下一个visitor无法查找到节点问题;