AST自动化混淆JS

The night is dark and full of terrors

0x01 Prerequisites

Object Attribute Access

console.log("test");
console["log"]("test");

对比两种对象属性访问方式下的AST

  1. 点访问

image-20230712150821357
  1. 括号访问

image-20230712151044437

MemberExpression下的objectnameconsoleIdentifier

括号访问下computedtruepropertyStringLiteral

点访问下computedfalsepropertyIdentifier

通过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

  • 替换节点为BinaryExpression

    • left: cipher

    • operator: ^

    • 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)Identifieratob

    • 参数数组(_arguments)为MemberExpression

      • arr[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可以获取到全局标识符名

FunctionExpressionFunctionDeclaration使用getOwnBinding可以获取到函数自身定义的局部标识符名

  • 遍历当前节点中所有的IdentifiergetOwnBinding判断其name是否为当前节点自己的绑定

  • bindingundefined,将其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节点,取出operatorleftright

  • 生成一个函数,函数名不能和当前节点的标识符冲突。参数固定为a和b,返回原来的BinaryExpression

  • 找到最近的BlockStatement节点,将生成的函数加入到body数组的最前面

  • 原生的BinaryExpression替换为CallExpressioncallee为函数名,_arguments为原二项式的leftright

新生成的函数的标识符可以随机设置,因为最后还要进行标识符混淆。

为了更好地增大代码量,遇到相同的二项式(比如都是加法二项式)不进行operator的判断,直接生成新的函数。

Eval Encrypt

先把代码转字符串,再把字符串加密后传入解密函数,解密得出的明文传入eval执行

  • 遍历FunctionExpression节点,path.node.bodyBlockStatement节点。BlockStatement.body是一个数组,每个元素对应函数的一行语句。

  • generator(v).node将函数中每一行语句转化为字符串,接着对字符串进行加密,这里简单地进行Base64编码。

  • 对于return语句直接返回,不进行加密

  • 所有语句处理完后新建一个BlockStatement替换原来的节点

emmm,混淆得到的代码特征太明显了,好几个eval,不建议大规模使用,但可以用来混淆部分代码。源码中可以在需混淆的地方加上注释

image-20230713211701842

生成的AST多了一个TrailingComments节点,表示行尾注释,是一个CommentLine数组,eval加密中可以判断有注释且注释为evalEncrypt才进行加密。很奇怪下一个节点还会多出一个LeadingComments,这个也要删掉

除了上面用base64加密原代码,还可以用charCodeAt将字符串转到ASCII,再用String.fromCharCode还原,最后用eval来执行

字符串在JS中是只读数组,但不能直接调用数组的map,所以使用[].map.call来间接调用

使用eval加密,要在标识符混淆之后

整合上面这些混淆方案,代码已上传至仓库👉点我arrow-up-right

混淆效果如下:

0x04 Execution Flow

Control Flow Flattening

打乱语句顺序,将其放入switch语句中,再通过循环遍历分发器来决定执行顺序

首先看一下switch语句对应的AST结构

image-20230714161219123
  • SwitchStatement switch语句

  • discriminant: 判别式,为Identifier

  • cases: case分支,是一个数组

image-20230714161807540
  • SwitchCase case语句

  • test case后面跟着的匹配值

  • consequent 存在case语句块中的具体语句, 是一个数组

  • 遍历FunctionExpression, 其bodyBlockStatement, 获取BlockStatementbody数组, 提取索引和值, 这个索引就是代码的执行顺序

  • 接着打乱数组, 构造SwitchCase语句, 每条语句为打乱后的元素跟上continue语句

  • 构造分发器, 决定SwitchCase语句的执行顺序

  • 构造while死循环和SwitchStatement声明语句

  • while循环由switchStatement和break语句构成

控制流平坦化需要和其他混淆方案一起使用。如case后面跟的值可以用数值常量加密,分发器中的字符串可以用字符串加密

Comma Expression

逗号运算符把多个表达式或语句连成一个复合语句

考虑几种语句之间的连接: 声明语句与声明语句、普通语句与return语句

  • 声明语句之间的连接

要连接两个声明语句,需要提取出VariableDeclarationdeclarations数组,该数组是声明语句中定义的变量,再将其处理为一条声明语句

  • 普通语句与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.argument

    argument为跟在return后面的内容

  • secondState是赋值语句

    • secondState.right是函数调用表达式,提取其callee,构造

      (firstState, callee.object).(callee.property)(arguments)

    • secondState.right不是函数调用表达式,让secondState.right为逗号表达式

      secondState.left = firstState, secondState.right

      由于逗号表达式返回最后一条语句,因此转化前后结果一样

  • 其他情况直接添加到逗号表达式后面

逗号表达式混淆的实现比较复杂,实际还得考虑更多情况

Last updated