JS逆向实战:AST技术让你轻松破解OB混淆保护!

引言

还是原来的爬虫练习平台,本文的重点是 JS 逆向中的 OB 混淆处理。

spa13

spa13 地址:https://spa13.scrape.center/

spa13 说明:

NBA 球星数据网站,数据纯前端渲染,Token 经过加密处理, JavaScript 经过 JavaScript Obfuscator 混淆,适合 JavaScript 逆向分析。

老规矩,先打开 main.js 看看:

image-20250126140545470

初步看还是可以看出来代码结构的,但是似乎所有的字符串都被转成十六进制了,并且有的字符串还被加密了,要调用一个特定的函数才能被解密。

OB 混淆

这个就是 OB 混淆,比较明显的特征就是开头会创建一个很大的数组,里面是被加密后的字符串,然后会对大数组进行反转操作,一般这里面还会掺杂一些格式化检测或者其他的检测,最后就是提供一个函数供解密用。

AST 解混淆

AST 是源代码的抽象语法结构的树状表现形式,它以一种结构化的方式来呈现代码的语法结构 。在这个树状结构里,每个节点都代表着源代码中的一种语法结构,比如变量声明、函数调用、运算符表达式等。

例如,对于简单的 JavaScript 语句 let num = 1 + 2;,AST 会将其解析为一个包含变量声明节点、赋值运算符节点、数字字面量节点和加法运算符节点的树状结构。变量声明节点表示 let num,赋值运算符节点表示 =,两个数字字面量节点分别表示 12,加法运算符节点表示 +,通过这些节点之间的层级关系和连接,精确地描述了代码的语法构成。

AST 可以让我们从结构化的角度来审视混淆后的代码。通过解析混淆代码生成 AST,我们能够清晰地看到代码的真实逻辑结构,不受变量名或代码顺序变化的干扰,这就为我们解码 OB 混淆提供了很大的帮助,我们可以借助 AST 来分析代码结构,并且还可以动态的修改原始代码,去除或还原被混淆的部分代码。

为了方便写 AST 解混淆的代码,我们需要一个网站来帮助我们生成AST 语法树作为参考,我比较常用的是:https://astexplorer.net/

先试试刚才的混淆代码:

image-20250126142824040

上图就是 AST 语法树,左侧是我们的原始代码,右侧是编译后的语法树结构,点击左侧的任意代码,右边就会自动跳转到对应的 AST 语法树节点上。现在图上显示的是一个 ArrayExpression 结构,这是一个数组节点,数组节点中有很多的 StringLiteral 节点,这个节点就是字符串节点,还有很多种节点我这里就不多介绍了,通过代码和语法树的对比我们就可以知道哪一行代码对应哪种结构,并不需要知道所有的节点类型。

为了使用 AST 来解混淆,我们除了使用网页来解混淆,还需要在本地安装 nodejs 运行环境,并且使用 npm 安装对应的 babel 库,它能将我们输入的混淆 JS 编译成语法树并且动态的修改语法树,并且将修改后的语法树重新输出成代码。

还原十六进制字符串

接下来我们使用 AST 来还原代码中的十六进制字符串来感受一下 AST 强大的功能吧。

为了知道如何还原字符串,我们需要先比较一下正常的字符串和十六进制编码的字符串在 AST 语法树中的区别是什么,然后通过 JS 代码将混淆的语法树转换成非混淆的形式即可,几乎所有的 AST 解混淆都是这样的逻辑。

image-20250126152438962

image-20250126152519164

通过对比图片上的两段代码,我们可以看出来,十六进制的字符串它的 extra 属性中的 raw 属性是十六进制的形式,正常的字符串的 extra 属性的 raw 属性是正常的字符串,根据以上区别,我们来写代码尝试还原一下。

新建一个 ast.js,写入以下代码:

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
const fs = require('fs'); //导入需要的库
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generator = require("@babel/generator").default;

encodeFile = "main.js";//定义输入以及输出文件
decodeFile = "main_decode.js";

//将源代码解析成 AST对象
let ast = parser.parse(fs.readFileSync(encodeFile, {encoding: "utf-8"}));

//修改 AST 语法树
let hex_decode = {
//遍历说中的字符串节点,只需要写一遍,框架会自动遍历所有的节点
StringLiteral({node}) {
if (node.value !== node.extra.raw) {
node.extra.raw = "'" + node.value + "'";
}
},
}
//执行实际的修改
traverse(ast, hex_decode);

//将 AST 语法树还原成代码
let {code} = generator(ast, opts = {
"jsescOption": {"minimal": true},
});
//将生成好的代码写入新的文件
fs.writeFile(decodeFile, code, (err) => {
});

image-20250126152957277

运行 ast.js,查看新生成的 decode 文件,可以看到,十六进制的字符串已经被我们还原了,只需要几行代码就可以还原,可见 AST 有多么方便,接下来我们还原被函数加密的字符串。

还原加密字符串

如何还原加密的字符串呢,我们通过分析代码结构可以看出来,JS 代码会在运行时动态的生成一个大的数组,然后调用函数传入一个类似于索引值的字符串,拿到数组中的某个值后返回。那要如何使用 AST 来获取数组的值呢?抱歉,做不到,AST 只能修改语法树,无法执行具体的解密函数。

但是 ast.js 也是一个 JS 文件,我们可以使用扣代码的方式,将解密的函数放到 ast.js 中执行,然后获取所有的加密函数节点,获取函数的参数,直接调用解密函数,将解密函数的返回结果替换之前的加密函数节点,让它变成字符串节点,这样不就能解密字符串了吗,思路有了,那如何找到函数节点呢,来对照看看:

image-20250126153753574

通过上图可以看到,函数节点叫 CallExpression,它的函数保存在 arguments 中,我们只需要遍历所有的 CallExpression 节点,并且获取到 arguments 中的参数值调用解密函数即可。

在此之前,别忘了将解密函数搬过来。

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
const fs = require('fs'); //导入需要的库
const parser = require("@babel/parser");
const types = require("@babel/types");
const traverse = require("@babel/traverse").default;
const generator = require("@babel/generator").default;

encodeFile = "main.js";//定义输入以及输出文件
decodeFile = "main_decode.js";

//将源代码解析成 AST对象
let ast = parser.parse(fs.readFileSync(encodeFile, {encoding: "utf-8"}));

//修改 AST 语法树
let hex_decode = {
//遍历说中的字符串节点,只需要写一遍,框架会自动遍历所有的节点
StringLiteral({node}) {
if (node.value !== node.extra.raw) {
node.extra.raw = "'" + node.value + "'";
}
},
}
//执行实际的修改
traverse(ast, hex_decode);

// 解密函数开始//
const _0x4afa = ['1993-03-11', '79.4KG', '1984-05-29', 'stringify', '128.8KG', '1991-06-29', '198cm', 'davis.png', '208cm', '卡尔-安东尼-唐斯', '188cm', '196cm', 'antetokounmpo.png', '83.9KG', '112.5KG', 'toString', 'embiid.png', '88.5KG', '114.8KG', '203cm', '206cm', '斯蒂芬-库里', '1988-03-14', 'JD8wgBMgVjdQbBUVbMarpZMAadLD7yvfzVV', 'Base64', '考瓦伊-莱昂纳德', '扬尼斯-安特托昆博', 'leonard.png', '安东尼-戴维斯', '达米安-利拉德', '109.8KG', 'harden.png', '99.8KG', 'durant.png', '102.1KG', 'paul.png', '1989-08-26', '1985-05-06', 'key', 'parse', '201cm', '113.4KG', '108.9KG', '1988-11-12', 'Utf8', '90.7KG', '尼科拉-约基奇', '213cm', 'pad', 'enc', '卡梅罗-安东尼', 'westbrook.png', 'encrypt', '127.0KG', 'thompson.png', '1994-12-06', 'irving.png', '185cm', 'lillard.png', '拉塞尔-威斯布鲁克', '1990-02-08', 'anthony.png', '191cm'];
(function (_0x35db0b, _0x4afab2) {
const _0x343162 = function (_0x6f5802) {
while (--_0x6f5802) {
_0x35db0b['push'](_0x35db0b['shift']());
}
};
_0x343162(++_0x4afab2);
})(_0x4afa, 0xed);
const _0x3431 = function (_0x35db0b, _0x4afab2) {
_0x35db0b = _0x35db0b - 0x0;
let _0x343162 = _0x4afa[_0x35db0b];
return _0x343162;
};
//解密函数结束//


traverse(ast, {
CallExpression(path) {
let {node} = path;
//别忘了判断一下是不是我们要的函数,如果不是我们需要,则直接返回不做任何处理,不然会破坏其他正常的函数
if (node.arguments.length !== 1 || node.callee.name !== '_0x5e920f') {
return;
}
let args = node.arguments[0].value; //获取函数参数,例如:0x30
let result = _0x3431(args)//调用实际的解密函数
console.log(result)
path.replaceWith(types.stringLiteral(result));//构造一个字符串节点,替换原来的节点
}
});

//将 AST 语法树还原成代码
let {code} = generator(ast, opts = {
"jsescOption": {"minimal": true},
});
//将生成好的代码写入新的文件
fs.writeFile(decodeFile, code, (err) => {
});

image-20250126154627824

可以看到,数据都被还原了,为了方便的拿到数据,我们还可以使用 AST 来删除后续的 new Vue 代码,防止在 nodejs 中执行报错。

删除 new

image-20250126155551487

来看一下 new Vue 的代码语法树长什么样子,原来是 NewExpression 节点,因为我们的代码中只有一个 NewExpression 节点,那么我们就删除所有的 NewExpression 节点就好了。在原来的 ast.js 文件中添加以下代码:

1
2
3
4
5
6
//删除 NewExpression节点
traverse(ast, {
NewExpression(path) {
path.remove();
}
})

看效果:

image-20250126155741596

代码只剩下 100 多行了,现在可以直接使用 nodejs 来执行上面的代码,然后打印 players 变量的值了。至此,我们使用 AST 技术还原了 OB 混淆。

完整代码见:https://github.com/libra146/learnscrapy/tree/main/js/spa13

总结

使用 AST 语法树的方式来还原类似的混淆是非常方便的,再也不用使用全局替换或者正则的方式来还原代码了,也不用顶着十六进制的恶心代码在浏览器中调试了,使用 AST 的方式就类似于一个手术刀对符合要求的代码进行精准分析和切割,不用怕误伤其他的代码,非常的方便。

另外,其实不止 JS 有 AST 语法树,只有是编程语言,基本上都有语法树,语法树是编译原理中很重要的一环,只有有了语法树,编译器才能对代码进行一些优化,优化的原理其实和我们解混淆其实差不多,都是对特定的节点进行特定的操作。

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

本文作者:LLLibra146

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

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

本文链接
https://blog.d77.xyz/archives/10bf322b.html