AST自动化混淆JS
The night is dark and full of terrors
0x01 Prerequisites
Object Attribute Access
console.log("test");
console["log"]("test");对比两种对象属性访问方式下的AST
点访问

括号访问

MemberExpression下的object均name为console的Identifier
括号访问下computed为true,property为StringLiteral
点访问下computed为false,property为Identifier
通过AST将点访问改为括号访问(比用python正则改写太香了)
JS Builtin Object
Date是JS中的标准内置对象,一般内置对象都是window下的属性,可以转化为window["Date"]这种形式
t.memberExpression的ts定义
0x02 Constant and Identifier
Number Constant Encrypt
遍历
NumericLiteral节点,获取其value属性随机生成一个数值记为key
cipher = key ^ value
value = cipher ^ key
替换节点为
BinaryExpressionleft: cipheroperator: ^right: key
String Constant Encrypt
遍历
StringLiteral,获取其value属性base64.encode(value)
替换节点为base64.decode(value), 为一个
CallExpression
callee: 函数名,传入一个Identifier即可_arguments: 参数数组
Array Index Confusion
遍历
StringLiteral,获取其value属性查看arr中是否存在该value(
arr.indexOf(value))若arr存在该value,返回索引,否则将该value加入arr
替换节点为
CallExpression函数名(
callee)Identifier为atob参数数组(
_arguments)为MemberExpressionarr[index]
上面的步骤将字符串都改成通过数组索引获取了,我们还需在代码最上面加上该数组,需要生成一个ArrayExpression数组表达式
最后通过unshift插入到program.body的前面
Array Shuffle Confusion
将数组乱序,新生成的代码中需要先对数组进行还原
shuffle.js用于还原数组,parser.parse()得到AST后,提取program.body[0]将其加入原AST的program.body前面
Hex Confusion
上面的数组顺序还原代码中,像push、shift这些方法可以转化为字符串,由于这段代码用于还原数组顺序,不能将其提取到大数组中,所以就简单将其编码为十六进制
Babel在处理节点时,会自动转义反斜杠,转成代码后,将其替换为单个斜杠即可。
unicode字符串实现方式一致,把hex编码的函数换成unicode编码的函数即可
Identifier Confusion
一般情况下,标识符都是有语义的,根据标识符可以大致猜测出代码的意图,因此对标识符进行混淆是十分有必要的。上一节已经介绍了简单的标识符混淆,实际可以让各个函数内的标识符名相同,函数内部的局部标识符名还能与没被引用到的全局标识符名相同。
Program节点下使用getOwnBinding可以获取到全局标识符名
FunctionExpression和FunctionDeclaration使用getOwnBinding可以获取到函数自身定义的局部标识符名
遍历当前节点中所有的
Identifier,getOwnBinding判断其name是否为当前节点自己的绑定若
binding为undefined,将其name放入globalBindingObj若
binding存在,将该标识符作为属性名,binding作为属性值,放入OwnBindingObj
Random Indentifier
上面重命名标识符都是_0x2ba6ea加上一个自增数字来作为新的标识符名,现在使用大写字母O、小写字母o和数字0三个字符来组成标识符名
将十进制转三进制,把0、1、2分别用大写字母O、小写字母o、数字0来替换
retval存储是倒序的余数,通过map把余数0、1、2映射到对于的大写字母O、小写字母o、数字0
对于长度小于6的标识符,用大写字母O或小写字母o补全位数
对于长度大于等于6且第一个字符串是0的标识符,往前补一个大写字母O或小写字母o(标识符不能以数字开头)
将var newName = '_0x2ba6ea' + i++;换成var newName = generateIdentifier(i++);即可
0x03 Blocks
BinExpr 2 Junk Code
花指令用来尽可能隐藏源代码的真实意图
下面用AST实现二项式转函数和函数调用表达式转函数
实现思路:
遍历
BinaryExpression节点,取出operator、left和right生成一个函数,函数名不能和当前节点的标识符冲突。参数固定为a和b,返回原来的
BinaryExpression找到最近的
BlockStatement节点,将生成的函数加入到body数组的最前面原生的
BinaryExpression替换为CallExpression,callee为函数名,_arguments为原二项式的left和right
新生成的函数的标识符可以随机设置,因为最后还要进行标识符混淆。
为了更好地增大代码量,遇到相同的二项式(比如都是加法二项式)不进行operator的判断,直接生成新的函数。
Eval Encrypt
先把代码转字符串,再把字符串加密后传入解密函数,解密得出的明文传入eval执行
遍历
FunctionExpression节点,path.node.body即BlockStatement节点。BlockStatement.body是一个数组,每个元素对应函数的一行语句。generator(v).node将函数中每一行语句转化为字符串,接着对字符串进行加密,这里简单地进行Base64编码。对于return语句直接返回,不进行加密
所有语句处理完后新建一个
BlockStatement替换原来的节点
emmm,混淆得到的代码特征太明显了,好几个eval,不建议大规模使用,但可以用来混淆部分代码。源码中可以在需混淆的地方加上注释

生成的AST多了一个TrailingComments节点,表示行尾注释,是一个CommentLine数组,eval加密中可以判断有注释且注释为evalEncrypt才进行加密。很奇怪下一个节点还会多出一个LeadingComments,这个也要删掉
除了上面用base64加密原代码,还可以用charCodeAt将字符串转到ASCII,再用String.fromCharCode还原,最后用eval来执行
字符串在JS中是只读数组,但不能直接调用数组的map,所以使用[].map.call来间接调用
使用eval加密,要在标识符混淆之后
整合上面这些混淆方案,代码已上传至仓库👉点我
混淆效果如下:
0x04 Execution Flow
Control Flow Flattening
打乱语句顺序,将其放入switch语句中,再通过循环遍历分发器来决定执行顺序
首先看一下switch语句对应的AST结构

SwitchStatementswitch语句discriminant: 判别式,为Identifiercases: case分支,是一个数组

SwitchCasecase语句testcase后面跟着的匹配值consequent存在case语句块中的具体语句, 是一个数组
遍历
FunctionExpression, 其body为BlockStatement, 获取BlockStatement的body数组, 提取索引和值, 这个索引就是代码的执行顺序接着打乱数组, 构造
SwitchCase语句, 每条语句为打乱后的元素跟上continue语句构造分发器, 决定
SwitchCase语句的执行顺序构造while死循环和
SwitchStatement声明语句while循环由
switchStatement和break语句构成
控制流平坦化需要和其他混淆方案一起使用。如case后面跟的值可以用数值常量加密,分发器中的字符串可以用字符串加密
Comma Expression
逗号运算符把多个表达式或语句连成一个复合语句
考虑几种语句之间的连接: 声明语句与声明语句、普通语句与return语句
声明语句之间的连接
要连接两个声明语句,需要提取出VariableDeclaration的declarations数组,该数组是声明语句中定义的变量,再将其处理为一条声明语句
普通语句与return语句之间的连接
普通语句和return语句连接时,需要提取出return语句的argument
需要把变量声明都放到函数的形参,JS允许参数实参和形参数量不一致,没有传入实参为undefined
遍历
FunctionExpression节点,取出BlockStatement节点的body,是一个数组存放中函数里面每一条语句。若语句少于两条则不做处理遍历当前函数下的所有
VariableDeclaration节点,获取其declarations,为VariableDeclarator数组将
VariableDeclarator的id即Identifier放到函数参数数组中。若原变量声明语句有初始化(init不为null),构造赋值语句AssignmentExpression加入statements数组,最后替换原变量声明语句
有时候函数体中的语句外面包裹着一层ExpressionStatement节点,会影响语句类型的判断,需要将外层ExpressionStatement去掉
逗号表达式可以使用toSequenceExpression完成
若
secondState是返回语句,构造returnStatement直接
return firstStatement, secondStatement.argumentargument为跟在return后面的内容
若
secondState是赋值语句若
secondState.right是函数调用表达式,提取其callee,构造(firstState, callee.object).(callee.property)(arguments)若
secondState.right不是函数调用表达式,让secondState.right为逗号表达式即
secondState.left = firstState, secondState.right由于逗号表达式返回最后一条语句,因此转化前后结果一样
其他情况直接添加到逗号表达式后面
逗号表达式混淆的实现比较复杂,实际还得考虑更多情况
Last updated