AST 技巧:使用 AST 分析函数执行路径 "更新" + 对比技巧

大家好,昨天的文章中提到了使用 AST 自动打印函数的执行路径的方法,今天在反混淆另一个 JS 文件的时候我发现了一个问题,所以我对这个功能进行了优化,并且一同分享一下如何对 nodejs 中执行的函数路径和浏览器中执行的函数路径进行快速对比。

函数执行路径

昨天的代码如下:

1
2
3
4
5
6
7
8
9
10
traverse(ast, {
FunctionDeclaration: {
exit(path) {
let {node} = path;
let name=node.id.name;
let t=template('console.log("A");')
node.body.body.unshift(t({'A':name}));
}
}
})

在实际的反混淆中,发现有的 JS 文件混淆后的函数名是一个字符,所以执行的结果可能会是这样的:

image-20250418213940889

就一堆字符,啥也看不出来,b 这个函数名可能在整个 JS 文件的多个地方使用,这样也无法分辨到底是调用了哪个函数。

为了能更好的打印函数的执行路径,需要对上面的 AST 代码进行修改。想了一下,既然每个函数可以在 JS 文件的多个位置使用,那如何定位多个同名的函数呢?可以使用 JS 文件天然自带的属性,行号!使用 AST 的方式将函数名和函数所在的行号一起打印出来。

如何知道当前被调用的函数在哪一行呢?不用担心,babel 库都为我们准备好了,来看一下 AST 网站的解析结果:

image-20250418214845637

在 babel 的解析结果中,是有一个 loc 属性,它里面保存了当前节点的源代码所在的行号,这样就能在 AST 中获取函数所在的行号了。

修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
traverse(ast, {
FunctionDeclaration: {
exit(path) {
let {node} = path;
let name = node.id.name;
let line = node.loc.start.line;
let t = template('console.log("A",' + line + ');')
node.body.body.unshift(t({'A': name}));
}
}
})

image-20250418214600732

虽然还是有些简陋哈哈哈,不过最起码知道了当前被调用的函数在哪一行了,不会再出现重名的函数了,因为同一行不会出现两个函数定义,除非你在使用 AST 前没有进行格式化。

注:这里未处理匿名函数,有需要的小伙伴可以自行处理

行号不准

在使用上面的 AST 代码处理完成以后,查看处理后的源代码,会发现日志中的行号不准确?

image-20250418215113492

计算日志中的行号和实际行号的差值,发现差值还不一样,这是为什么呢?仔细想一想,在使用 AST 解析代码的时候,行号获取的是使用 AST 处理之前的代码的行号,如下图:

image-20250418215334658

在使用 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
traverse(ast, {
FunctionDeclaration: {
exit(path) {
let {node} = path;
let name = node.id.name;
let line = node.loc.start.line;
let t = template('console.log("A",' + line + ');')

node.body.body.unshift(t({'A': name}));
}
}
})

//将上面的代码重新解析成 AST,更新 loc 中的行号值
ast = parser.parse(generator(ast, opts = {
"compact": false,
"comments": false,
"jsescOption": {"minimal": true},
}).code)

traverse(ast, {
FunctionDeclaration: {
exit(path) {
let {node} = path;
let line = node.loc.start.line;//获取行号
node.body.body[0].expression.arguments[1].value = line;//更新行号
}
}
})

运行以上代码,查看修改后的结果:

image-20250418220343334

现在日志中的行号和函数实际所在位置就完全一样了,可以根据日志找到对应的函数,函数被调用了多少次,被谁调用了都一目了然。

核对执行路径

image-20250418220642094

将修改过的代码替换到浏览器中执行,会打印出执行的函数和对应的行号,右键点击复制控制台日志,将日志复制到 pycharm 的临时文件中。

image-20250418220806929

使用替换功能,正则表达式替换,将所有的 JS 文件信息去掉,只保留函数名和行号,方便后面进行比对。

在本地的同一个文件中重复调用相同的函数,注意一定要是同一个文件,不然输出的函数名和行号会对不上!

image-20250418221023481

将调用后输出的结果复制到剪切板,一定要是剪切板哦,然后点击图中的与剪切板比较按钮。

image-20250418221120674

pycharm 会自动对比剪切板的数据和临时文件的数据哪里不一致,就和 Git 的 diff 命令很类似,从图上可以看到,浏览器的执行结果相比于 nodejs 的执行结果多了一些函数调用。

至于为什么多了这么多函数调用,关键的原因就在第 11 行的 n 函数中,查看 n 函数的执行逻辑即可。正常来说 n 函数会被调用两次,但是实际在 nodejs 中只调用了一次,可能是由于某些判断和浏览器不一样导致的,后续就是具体原因具体分析了。

总结

到这里函数路径打印这个功能就变得比较实用了,如果大家遇到类似的需求可以使用上面的代码试一试,可以快速的找到代码中执行逻辑和浏览器不同的地方,方便快速修复环境。当然了,在补环境的时候可以结合代理一起使用,没什么方法是一劳永逸的,多种方法可以共同使用,才能事半功倍。此方法也不止可以在补环境的时候使用,在扣代码的时候一样使用,更多使用方法就靠大家来摸索了。

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

本文作者:LLLibra146

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

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

本文链接
https://blog.d77.xyz/archives/7739844b.html