Javascript的AST处理
通过AST,我们可以对代码进行格式化,也可以用来简化混淆后的JS代码。
1 背景知识
在线混淆网站:
在线AST分析:
资料:
这里使用Babel对代码进行处理,需要用到的工具如下:
@babel/parser
根据代码建立AST@babel/generator
从AST生成代码@babel/traverse
遍历并修改AST@babel/types
AST中的节点类型
详细的使用介绍可以查看手册: Babel Plugin Handbook
各种代码类型的定义以及参数说明见: @babel/types
2 应用
这里拿一个常见的混淆方式练习,这种加密方式目前有一个纯正则的解密方案: NXY666/JsjiamiV6-Decryptor, 现在尝试将其转为AST版本。
-
净化代码
这个步骤是去除注释, 在AST中会自动分离注释。
-
解除全局加密
首先将
![]
和!![]
分别转换为true
和false
,为后续的分割做准备,在AST中不需要。然后将各语句分为:签名信息,预处理函数,解密函数,验证函数,常规语句。
如果混淆后的文件没有被二次修改,那么前3个非空语句分别为:签名信息,预处理函数,解密函数。
- 签名信息类型为
VariableDeclaration
; - 预处理函数的不定义变量或函数,直接对签名信息的变量进行修改;
- 解密函数的类型为
VariableDeclaration
或FunctionDeclaration
。
我们只需要将这3个语句丢到同一个VM环境中,再遍历节点将所有调用解密函数的节点替换为值。
- 签名信息类型为
-
解除代码块加密(递归进行下述操作)
首先删除禁止格式化的代码,一般在内容代码前方。 这里并没有找到样本,先跳过。
然后还原代码块内统一收集的字符串、函数调用(二元函数,比较函数,函数调用)。
- 首先对每个Object进行判断,如果所有的成员都符合上述特征,则说明可能为代码块加密。
- 然后遍历作用域,替换上述元素,并统计这些元素是否都使用到。
-
清理死代码
这里主要有两种形式:
- 由恒定if判断条件引入的无效代码
- 使用switch语句打乱的顺序代码
这边只需要根据特征找到相应的代码还原。
-
解除环境限制
有4个限制:
防止格式化
,禁止控制台调试
,禁止控制台输出
,安全域名
。原作者的方法是通过正则匹配整个函数加以删除。 在这里我们只需要将限制失效,并不一定要清除所有的代码,这样可以减少意外情况。
有些限制代码通过一个“万恶之源”间接调用。 这个函数通过闭包使其只有在第一次调用时才返回传入的函数(并指定this绑定), 后续调用时返回空函数。所以说这个函数也可以不删。
在所有限制条件中,
禁止控制台调试
的特征是最明显的,查找DebuggerStatement
即可。 这个函数由两个函数嵌套而成,我们假设结构为A(x){B(y){}}
。 当函数A传入参数时,直接返回函数B
;否则返回B(0)
。 当函数B传入string类型时,会直接执行空的死循环;否则循环调用debugger。 在删除时,我们只需要回朔至上两层函数,删除最外层的函数定义; 并根据函数名删除其它位置的调用(替换当前作用域函数为空函数)。随后可以删除
防止格式化
函数,在这个代码段中会调用上面的函数A,所以在上一步中一并清理了。 这个函数通过正则匹配检测是否格式化,所以也可以通过这段特征代码查找。然后清理
禁止控制台输出
函数,这边可以遍历空函数,然后为所在函数空间进行特征判断。关于
安全域名
限制,样例太少,就先不添加了。 -
提高代码可读性
这一步和格式化代码类似,不影响主要功能。
-
格式化代码
这一步不是我们的工作内容,可以直接交给其它代码格式化工具。
3 总结
在实际使用中可以发现,原作者使用纯正则实现的工具在修改过程中需要反复将代码模版化, 在代码量较大时,会消耗非常多的时间。 而使用AST时,在修改过程中只需要更新局部位置,相对而言消耗的时间大大减少。 因此,借助AST,我们可以高效地在指定范围内遍历并修改代码。
不只是JavaScript,其它语言都会有类似的工具。 同时,这也是代码编译、解释过程中的一个重要环节。 当然,在这里我们并不需要关心AST与Code是怎么互相转换的,只需要把它当作工具来使用。