less than 1 minute read

通过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版本。

  1. 净化代码

    这个步骤是去除注释, 在AST中会自动分离注释。

  2. 解除全局加密

    首先将![]!![]分别转换为truefalse,为后续的分割做准备,在AST中不需要。

    然后将各语句分为:签名信息,预处理函数,解密函数,验证函数,常规语句。

    如果混淆后的文件没有被二次修改,那么前3个非空语句分别为:签名信息,预处理函数,解密函数。

    • 签名信息类型为VariableDeclaration
    • 预处理函数的不定义变量或函数,直接对签名信息的变量进行修改;
    • 解密函数的类型为VariableDeclarationFunctionDeclaration

    我们只需要将这3个语句丢到同一个VM环境中,再遍历节点将所有调用解密函数的节点替换为值。

  3. 解除代码块加密(递归进行下述操作)

    首先删除禁止格式化的代码,一般在内容代码前方。 这里并没有找到样本,先跳过。

    然后还原代码块内统一收集的字符串、函数调用(二元函数,比较函数,函数调用)。

    • 首先对每个Object进行判断,如果所有的成员都符合上述特征,则说明可能为代码块加密。
    • 然后遍历作用域,替换上述元素,并统计这些元素是否都使用到。
  4. 清理死代码

    这里主要有两种形式:

    • 由恒定if判断条件引入的无效代码
    • 使用switch语句打乱的顺序代码

    这边只需要根据特征找到相应的代码还原。

  5. 解除环境限制

    有4个限制:防止格式化禁止控制台调试禁止控制台输出安全域名

    原作者的方法是通过正则匹配整个函数加以删除。 在这里我们只需要将限制失效,并不一定要清除所有的代码,这样可以减少意外情况。

    有些限制代码通过一个“万恶之源”间接调用。 这个函数通过闭包使其只有在第一次调用时才返回传入的函数(并指定this绑定), 后续调用时返回空函数。所以说这个函数也可以不删。

    在所有限制条件中,禁止控制台调试的特征是最明显的,查找DebuggerStatement即可。 这个函数由两个函数嵌套而成,我们假设结构为A(x){B(y){}}。 当函数A传入参数时,直接返回函数B;否则返回B(0)。 当函数B传入string类型时,会直接执行空的死循环;否则循环调用debugger。 在删除时,我们只需要回朔至上两层函数,删除最外层的函数定义; 并根据函数名删除其它位置的调用(替换当前作用域函数为空函数)。

    随后可以删除防止格式化函数,在这个代码段中会调用上面的函数A,所以在上一步中一并清理了。 这个函数通过正则匹配检测是否格式化,所以也可以通过这段特征代码查找。

    然后清理禁止控制台输出函数,这边可以遍历空函数,然后为所在函数空间进行特征判断。

    关于安全域名限制,样例太少,就先不添加了。

  6. 提高代码可读性

    这一步和格式化代码类似,不影响主要功能。

  7. 格式化代码

    这一步不是我们的工作内容,可以直接交给其它代码格式化工具。

3 总结

在实际使用中可以发现,原作者使用纯正则实现的工具在修改过程中需要反复将代码模版化, 在代码量较大时,会消耗非常多的时间。 而使用AST时,在修改过程中只需要更新局部位置,相对而言消耗的时间大大减少。 因此,借助AST,我们可以高效地在指定范围内遍历并修改代码。

不只是JavaScript,其它语言都会有类似的工具。 同时,这也是代码编译、解释过程中的一个重要环节。 当然,在这里我们并不需要关心AST与Code是怎么互相转换的,只需要把它当作工具来使用。

Tags:

Categories:

Updated: