JS逆向实战:加强版的 OB 混淆 AST 处理方法

大家好,今天分享一个加强版的 OB 混淆的处理方法。

加强版 OB 混淆

本次样本来源为猿人学第十题,题目难度为非常困难,本次仅分享 OB 混淆的处理方案,解题方法不在本次分享范围内。

混淆的 JS 一共有两层,外层为大量的反调试,使用 eval 执行加密后的代码,代码一共分为五段,其中四段是 OB 混淆,里层主要是控制流混淆。

image-20250426204740094

image-20250426204819186

为什么说是加强版的 OB 混淆呢,因为正常的 OB 混淆只有一个解密函数,一般抠出来解密函数以后使用 AST 很简单就能解开。但是此次的 OB 混淆,有 N 多个解密函数,每个解密函数最终都会调用原始的解密函数,相当于将解密函数做了多层跳板。

查看上图会发现,_0x478e 是正常的解密函数,但是实际字符串加密后的函数通过类似 _0x1f7593 这样的函数进行了一层封装,对解密函数的参数进行了计算,最终才调用真正的解密函数,本次对这种情况进行处理。

举个例子:

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
const _0x30d0c2 = _0x2a6f46(this, function () {
const _0x337c3b = function (_0x3ab717, _0x323fb8, _0x5c1ff5, _0x4b78ea, _0x3fd75c) {
return _0x478e(_0x3ab717 - -915, _0x5c1ff5);
};
const _0x11a27f = function (_0x128afd, _0x56dee7, _0x1486f3, _0x3f4f9e, _0x359e0c) {
return _0x478e(_0x128afd - -915, _0x1486f3);
};
const _0x26d453 = function (_0x18cf7e, _0x964999, _0x4802f1, _0x36a0b6, _0x16a584) {
return _0x478e(_0x18cf7e - -915, _0x4802f1);
};
const _0x4515b2 = function (_0x55243f, _0x21228c, _0x5a0008, _0x45a1ba, _0xe89000) {
return _0x478e(_0x55243f - -915, _0x5a0008);
};
const _0x40f529 = function (_0x5c822a, _0x5bfea0, _0x281fee, _0x2de99c, _0x48196b) {
return _0x478e(_0x5c822a - -915, _0x281fee);
};
const _0x53ebe5 = {};
_0x53ebe5[_0x337c3b(114, 321, "]cTD", -658, -456)] = function (_0x19407c, _0x311841) {
return _0x19407c(_0x311841);
};
_0x53ebe5[_0x11a27f(219, 143, "Q%6X", -839, -504)] = function (_0x35a294, _0x2afe0e) {
return _0x35a294 !== _0x2afe0e;
};
_0x53ebe5[_0x337c3b(1089, 691, "]v58", 527, 187)] = _0x26d453(172, 1128, "q3V]", -275, 1135);
_0x53ebe5[_0x337c3b(1201, 689, "vh5c", 1537, 2211)] = _0x11a27f(275, 950, "vh5c", 136, 341) + _0x11a27f(1006, 112, "7SjP", 1208, 1291) + _0x40f529(534, 4, "T!Ha", -326, 1103) + _0x40f529(1099, 827, "6#U@", 1767, 1022) + "/";
_0x53ebe5[_0x26d453(-747, -2, "9]P#", -120, -965)] = _0x26d453(573, 1173, "fnIu", 739, -96) + _0x40f529(-315, 747, "Q%6X", -1197, -483) + _0x11a27f(395, 201, "T!Ha", 445, -657) + _0x26d453(1369, 959, "9]P#", 367, 523) + _0x337c3b(366, -49, "Bkl0", -69, -664);
_0x53ebe5[_0x40f529(804, -247, "q3V]", 724, 1216)] = function (_0x1fb1eb) {
return _0x1fb1eb();
};})

部分混淆代码如上,封装后的解密函数有五个参数,但是实际上只有两个是有效的,封装后的解密后数对参数进行计算后才调用真正的解密函数。

AST 处理-合并函数定义

为了简化后续处理,先将变量定义为函数表达式合并为函数定义。

image-20250426205754281

image-20250426205838337

如上图所示,这块相对简单,并且不是文章重点,所以就跳过了。

AST 处理思路

接下来讲一下处理的思路,拿一个解密函数举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function _0x478e(_0x740ada, _0x4798ed) {
//真正的解密函数,这里省略
}

//封装后的解密函数
_0x337c3b(1089, 691, "]v58", 527, 187)
//封装后的解密函数的函数定义
function _0x337c3b(_0x3ab717, _0x323fb8, _0x5c1ff5, _0x4b78ea, _0x3fd75c) {
return _0x478e(_0x3ab717 - -915, _0x5c1ff5);
};

//另一种封装后的解密函数
function _0xdf3351(_0xda88c3, _0x33a0be, _0x3f50a8, _0x50e43f, _0x275a8c) {
return _0x478e(_0x3f50a8 - 891, _0xda88c3);
};
_0xdf3351("N)ZP", 1682, 1824, 1333, 1217)

以上面的函数为例,来讲一下思路:

  1. 首先要遍历所有的被封装后的解密函数的实际调用,例如这里的:_0x337c3b(1089, 691, "]v58", 527, 187)
  2. 取出它所有的参数值,并且保存起来,其中有四个整形和一个字符串,可以在这里来判断是否是需要处理的函数,特征就是整形参数和字符串参数的数量,像上面的函数,参数值是:1089, 691, "]v58", 527, 187
  3. 获取这个函数的函数定义,因为函数定义中有针对真正解密函数的封装逻辑,主要是对参数的计算和具体哪个参数才是有效的。如下所示,获取到函数的定义,可以看到第一个参数参与了计算,并且实际有效的参数是第三个,剩余的参数都是没有用到的。
1
2
3
function _0x337c3b(_0x3ab717, _0x323fb8, _0x5c1ff5, _0x4b78ea, _0x3fd75c) {
return _0x478e(_0x3ab717 - -915, _0x5c1ff5);
};
  1. 根据函数定义获取到真正的解密函数,拿到解密函数使用到的参数,并且获取计算逻辑中用到的数字,使用的参数是 _0x3ab717,计算逻辑是 +915,有效的参数是: _0x3ab717_0x5c1ff5
  2. 注意上面的 +915 是经过计算后得到的,实际上是减去负的 915,这里稍微有些复杂,因为要判断是正值还是负值,并且要兼容不同情况,减去正值还是减去负值。
  3. 使用 _0x3ab717_0x5c1ff5 在被封装的函数参数中获取有效参数的索引,索引就是 03,根据索引获取实际参数值,实际参数值为:1089, 691, "]v58", 527, 187,索引 03 就是 1089"]v58"
  4. 根据上面的逻辑,如果替换成真正的解密函数调用的话,应该是 _0x478e(1089- -915, "]v58"),使用以上内容构造一个新的函数调用,参数值为刚刚获取到的两个参数,到此处理逻辑结束。

以上逻辑仅为我个人的思路,可能写的不太好,大家可以手动操作替换一下应该就能理解了。

实现代码

具体代码请关注文末的公众号获取。

总结

以上就是针对 OB 混淆的加强版的处理方案了,建议大家具体实操一下,可以加深对 AST 的理解,如果能处理这种程度的混淆,那么其他的 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
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
let mergeFunction = {
CallExpression: {
exit(path) {
let {node, scope} = path;
//判断整形参数的数量
let num = node.arguments.filter(x => {
return types.isNumericLiteral(x) || types.isUnaryExpression(x)
})
//判断字符型参数的数量
let str = node.arguments.filter(x => {
return types.isStringLiteral(x)
})
//如果参数两不是五个或者字符型和整形参数的数量不对,则返回
if (node.arguments.length !== 5 || num.length !== 4 || str.length !== 1) {
return
}
//获取封装后的解密函数的所有参数,拿到实际的 value 值
let args = node.arguments.map(x => {
return types.isNumericLiteral(x) || types.isStringLiteral(x) ? x.value : -x.argument.value
});
//获取封装后的解密函数的函数定义,找到函数的定义位置
let binding = scope.getBinding(node.callee.name);
if (!binding) {
return;
}
//binding 的 path 属性就是函数定义了
let func = binding.path.node;
let value;
let a = [];
//判断是不是调用了真正的解密函数
if (types.isReturnStatement(func.body.body[0]) && types.isCallExpression(func.body.body[0].argument)) {
//获取封装后的函数所有的参数名,方便后续获取真正解密函数所使用的参数索引位置
let params = func.params.map(x => {
return x.name
});
//遍历在调用真正的解密函数时,用到了哪些函数,对参数进行了哪些计算,获取计算所需要的值
for (const argument of func.body.body[0].argument.arguments) {
//进行类型判断,有的时候是减去一个负数,所以有多次判断
if (types.isBinaryExpression(argument)) {
if (types.isUnaryExpression(argument.right)) {
//如果是减去负数,则 value 值为负
value = argument.right.argument.value;
if (argument.right.operator === '-') {
value = -value;
}
} else {
//如果是减去正数,则直接获取值
value = argument.right.value;
}
//获取被计算的参数在封装后的函数参数中的索引,说白了就是看看哪个参数被减去了某个值,因为其他的参数都是无用的
let argIndex = params.indexOf(argument.left.name)
if (argument.operator === '-') {
//将参数的值保存起来,并且判断正负号
a.push(args[argIndex] - value);
} else {
a.push(args[argIndex] + value);
}
}
//如果不计算直接原样传递
if (types.isIdentifier(argument)) {
let argIndex = params.indexOf(argument.name)
a.push(args[argIndex]);
}
}
//根据参数类型构造 callExpression 的参数
a = a.map(x => {
if (typeof x === 'string') {
return types.stringLiteral(x)
}
if (typeof x === 'number') {
return types.numericLiteral(x)
}
return x;
})
//替换原有的函数调用
path.replaceWith(types.callExpression(types.identifier(func.body.body[0].argument.callee.name), a))
}
}
}
}
traverse(ast, mergeFunction)

本文章首发于个人博客 LLLibra146’s blog

本文作者:LLLibra146

更多文章请关注公众号 (LLLibra146):LLLibra146

版权声明:本博客所有文章除特别声明外,均采用 © BY-NC-ND 许可协议。非商用转载请注明出处!严禁商业转载!

本文链接
https://blog.d77.xyz/archives/4d338523.html