AST 是源代码的抽象语法结构的树状表示。利用它可以还原混淆后的js代码。
@babel/parser 是js语法编译器 Babel 的 nodejs 包,内置很多分析 js 的方法,可以实现js到AST的转换。
JS 转为 AST:https://astexplorer.net/
准备工作:
需安装nodejs环境以及babel,babel 安装:
npm install @babel/node @babel/core @babel/cli @babel/preset-env
新建目录 AST,其下新建文件 .babelrc,内容如下:
{
"presets": [
"@babel/preset-env"
]
}
这样就完成了初始化。
节点类型
打开 https://astexplorer.net/
Parser Settings选择@babel/parser, 然后在左侧任意输入一段js,右侧会展示对应的AST,其是由一层层的数据结构嵌套构成,每一个含有type属性的内容都可以视为该类型的一个节点,常见的节点类型如下:
Literal 字面量,简单的文字表示,如3,abc,null,true 等。它进一步分为 RegExpLiteral、NullLiteral、StringLiteral、BooleanLiteral、NumericLiteral、BigIntLiteral 等类型;
Declarations 声明,如 FunctionDeclaration、VariableDeclaration 分别表示声明一个方法和变量;
Expressions 表达式,它本身会返回一个计算结果,通常有两个作用,一个是放在赋值语句的右边赋值,另一个是作为方法的参数,如 LogicalExpression、ConditionalExpression、ArrayExpression 分别表示逻辑运算表达式、三元运算表达式、数组表达式;此外,还有一些特殊的表达式,如YieldExpression、AwaitExpression、ThisExpression;
Statements 语句,如 IfStatements、SwitchStatements、BreakStatement 等控制语句,和一些特殊语句 DebuggerStatement、BlockStatements等;
Identifier 标识符,指代一些变量的名称,如 name
Classes 类,代表一个类的定义,包括 Class、ClassBody、ClassMethod、ClassProperty等
Functions 方法声明,一般代表 FunctionDeclaration、FunctionExpression 等
Modules 模块,可以理解为一个 nodejs 模块,包括 ModuleDeclaration、ModuleSpecifier 等
Program 程序,整个代码可以成为 Program
@babel/parser 的使用
它是 Babel 的js解释器,也是一个nodejs包,提供一些重要的方法,parse 解析js代码,parseExpression 尝试解析单个js表达式并考虑性能。一般使用parse就足够了。
parse 输入:一段js代码;输出:该js代码对应的抽象语法树AST
js代码包含多种类型的表达,归类如下:
https://github.com/babel/babel/blob/master/packages/babel-parser/ast/spec.md
接下来,使用一下parse,首先在目录AST下新建一个文件夹code,新建文件 code1.js ,内容如下:
const a = 3;
let string = 'hello';
for (let i = 0; i < a; i++) {
string += 'world';
}
console.log('string', string)
简单写了一段js代码,然后同目录下新建文件 basic1.js,内容如下:
import { parse } from "@babel/parser";
import fs from 'fs';
const code = fs.readFileSync('./code1.js', 'utf-8');
let ast = parse(code);
console.log(ast)
使用parse将代码转为ast抽象语法树,命令行输入 babel-node basic1.js 运行,输出如下:
Node {
type: 'File',
start: 0,
end: 128,
loc: SourceLocation {
start: Position { line: 1, column: 0, index: 0 },
end: Position { line: 8, column: 0, index: 128 },
filename: undefined,
identifierName: undefined
},
errors: [],
program: Node {
type: 'Program',
start: 0,
end: 128,
loc: SourceLocation {
start: [Position],
end: [Position],
filename: undefined,
identifierName: undefined
},
sourceType: 'script',
interpreter: null,
body: [ [Node], [Node], [Node], [Node] ],
directives: []
},
comments: []
}
可以看到,整个AST的根节点就是一个Node,type是File,代表其是一个 File 类型的节点;其下有很多属性 start、end 等等,其中的 progarm 也是一个 Node,type为Program,代表其是一个程序。同样,program 也包含一些属性,其中 body 是比较重要的属性,这里是一个列表类型,其中每个元素也都是一个Node,只不过输出结果没有详细展示了。
可以通过 console.log(ast.program.body) 详细打印Node的内容。
js转为ast后,如何转换回来呢,可以使用generate方法。
@babel/generate 的使用
它也是一个nodejs包,提供了 generate 方法将 AST 还原为 js 代码
import { parse } from "@babel/parser";
import generate from "@babel/generator"
import fs from 'fs';
const code = fs.readFileSync('./code1.js', 'utf-8');
let ast = parse(code);
const { code: output} = generate(ast)
console.log(output)
同样命令行键入 babel-node basic1.js 运行,输出如下:
const a = 3;
let string = 'hello';
for (let i = 0; i < a; i++) {
string += 'world';
}
console.log('string', string);
generate 方法还可以传入第二个参数,接收一些配置选项,第三个参数接收原代码作为输出的参考,用法:
const output = generate(ast, { /* options */ }, code);
options 可选部分配置:
auxiliaryCommentBefore string类型,在输出文件开头添加注释可选字符串;
auxiliaryCommentAfter string类型,在输出文件末尾添加注释可选字符串;
retainLines boolean类型,默认false,尝试在输出代码中使用与源代码相同的行号;
retainFunctionParens boolean类型,默认false,保留表达式周围的括号;
comments boolean类型,默认true,输出中是否应包含注释;
compact boolean或auto类型,默认opts.minfied,设置为true以避免添加空格进行格式化;
minified boolean类型,默认false,是否压缩后输出;
@babel/traverse 的使用
知道了如何在 js 和 AST 间转换,还是不能实现 js 代码的反混淆,还需要了解另一个强大的功能,AST的遍历和修改。
遍历的使用的是 @babel/traverse,它接收一个 AST,利用 traverse 方法就可以遍历其中的所有节点。在遍历方法中,就可以对所有节点操作了。
先感受下遍历的基本实现,新建 basic2.js:
import { parse } from "@babel/parser";
import fs from 'fs';
import { traverse } from "@babel/core";
const code = fs.readFileSync('./code1.js', 'utf-8');
let ast = parse(code);
traverse(ast, {
enter(path) {
console.log(path)
},
})
命令行输入 babel-node basic2.js 运行,结果很长,会输出每一个path对象,取其中一部分如下:
NodePath {
contexts: [ [TraversalContext] ],
state: undefined,
opts: { enter: [Array], _exploded: true, _verified: true },
_traverseFlags: 0,
skipKeys: null,
parentPath: NodePath {
contexts: [Array],
state: undefined,
opts: [Object],
_traverseFlags: 0,
skipKeys: null,
parentPath: [NodePath],
container: [Array],
listKey: 'body',
key: 3,
node: [Node],
type: 'ExpressionStatement',
parent: [Node],
hub: undefined,
data: null,
context: [TraversalContext],
scope: [Scope]
},
container: Node {
type: 'ExpressionStatement',
start: 95,
end: 124,
loc: [SourceLocation],
expression: [Node]
},
listKey: undefined,
key: 'expression',
node: Node {
type: 'CallExpression',
start: 95,
end: 124,
loc: [SourceLocation],
callee: [Node],
arguments: [Array]
},
type: 'CallExpression',
parent: Node {
type: 'ExpressionStatement',
start: 95,
end: 124,
loc: [SourceLocation],
expression: [Node]
},
hub: undefined,
data: null,
context: TraversalContext {
queue: [Array],
priorityQueue: [],
parentPath: [NodePath],
scope: [Scope],
state: undefined,
opts: [Object]
},
scope: Scope {
uid: 0,
path: [NodePath],
block: [Node],
labels: Map(0) {},
inited: true,
bindings: [Object: null prototype],
references: [Object: null prototype],
globals: [Object: null prototype],
uids: [Object: null prototype] {},
data: [Object: null prototype] {},
crawling: false
}
}
可以看到,这是一个 NodePath 类型的节点,里面还有 node、parent 等多个属性,我们可以利用 path.node 拿到当前对应的Node对象,也可以利用 path.parent 拿到当前Node对象的父节点。
这样,就可以使用它来对Node进行一些处理,如把最初的代码修改为 a=5, string = "hi",可以这样:
import { parse } from "@babel/parser";
import generate from "@babel/generator"
import fs from 'fs';
import traverse from "@babel/traverse";
const code = fs.readFileSync('./code1.js', 'utf-8');
let ast = parse(code);
traverse(ast, {
enter(path) {
let node = path.node;
if (node.type === "NumericLiteral" && node.value === 3) {
node.value = 5;
}
if (node.type === "StringLiteral" && node.value === "hello") {
node.value = "hi";
}
},
});
const { code: output } = generate(ast, {
retainLines: true,
});
console.log(output)
// 输出如下
const a = 5;
let string = "hi";
for (let i = 0; i < a; i++) {
string += 'world';
}
console.log('string', string);
除了 enter 外,还可以直接定义对应类型的解析方法,这样遇到此类型的节点就会被自动调用:
traverse(ast, {
NumericLiteral(path) {
if (path.node.value === 3) {
path.node.value = 5;
}
},
StringLiteral(path) {
if (path.node.value === 'hello') {
path.node.value = 'hi';
}
}
})
traverse部分改成如上内容,输出是一样的。
还可以通过 remove 方法删除某个节点,如:
traverse(ast, {
CallExpression(path) {
let node = path.node;
if (node.callee.object.name === 'console' && node.callee.property.name === 'log') {
path.remove();
}
},
});
这样就删除了所有console.log语句。
如果想插入节点,就要用到 types 了。
@babel/types 的使用
使用它可以方便的声明新的节点,比如 const a = 1; 如果想增加一行 const b = a + 1; 可以这么写:
import { parse } from "@babel/parser";
import generate from "@babel/generator";
import * as types from "@babel/types";
import traverse from "@babel/traverse";
const code = 'const a = 1;';
let ast = parse(code);
traverse(ast, {
VariableDeclaration(path) {
let init = types.binaryExpression(
'+',
types.identifier('a'),
types.numericLiteral(1)
);
let declarator = types.variableDeclarator(types.identifier('b'), init);
let declaration = types.variableDeclaration("const", [declarator]);
path.insertAfter(declaration);
path.stop();
},
})
const { code: output } = generate(ast, {
retainLines: true,
});
console.log(output)
# 输出为 const a = 1;const b = a + 1;
至于为什么这么写,可以结合js转为 AST 的内容,配合官方文档 (https://astexplorer.net/ 右上角点击 Parser: @babel/parser-.**.),来构造节点。
本例中首先把 const b = a + 1; 转为 AST 查看对应内容:
可以看到,这个语句转为 AST 后是一个 type 为 VariableDeclaration 的节点,查看官方文档对应内如如下,
想构造一个 type 为 VariableDeclaration 的节点,需使用 types 的 variableDeclaration 方法,传入的第一个参数为声明的关键词,第二个为 Array<VariableDeclarator> 类型的节点,对比 AST 可以看到
第一个传入的是const,第二个传入的是一个 type 为 VariableDeclarator 的节点构成的列表,当然本例中这个列表只有一个元素。
所以,构造一个 type 为 VariableDeclaration 的节点,代码大致是这样的:let declaration = types.variableDeclaration("const", [VariableDeclarator]);
VariableDeclarator 类型的节点又该如何构造呢?继续查询官方文档:
可以看到,需要传入一个id和一个init,对比 AST 可以知道需要传入的具体内容,id 是一个Identifier类型的节点,其name是 b,init 是一个BinaryExpression类型的节点,这两个节点如何构造,继续查阅官方文档,这里不再赘述,最终实现的结果就如上方代码。
了解了这些知识,来看几个简单的反混淆 js 的例子。
1. 表达式还原
原始代码:
const a = !![];
const b = "abc" === "bcd"
const c = (1 << 3) | 2
const d = parseInt('5' + '0')
还原代码:
import { parse } from "@babel/parser";
import generate from "@babel/generator"
import fs from 'fs';
import traverse from "@babel/traverse";
import * as types from "@babel/types";
const code = fs.readFileSync('./code1.js', 'utf-8');
let ast = parse(code);
traverse(ast, {
// 键名分别对应于处理 一元表达式、布尔表达式、条件表达式、调用表达式
"UnaryExpression|BinaryExpression|ConditionalExpression|CallExpression"(path) {
// evaluate 方法会对path对象进行计算(执行),得到可信度和结果,confident = true
let { confident, value } = path.evaluate();
// 如果是标识符或NaN,就跳过
if (value === Infinity || value === -Infinity || isNaN(value)) return;
// 如果可信,即 confident 为 true,就替换 evaluate(计算)得到的值
confident && path.replaceWith(types.valueToNode(value));
},
});
const { code: output } = generate(ast);
console.log(output)
// 输出如下
const a = true;
const b = false;
const c = 10;
const d = 50;
2. 字符串还原
有一些字符会被混淆为 Unicode 或 UTF-8 编码,如
const strings = ["\x68\x65\x6c\x6c\x6f", "\x77\x6f\x72\x6c\x64"]
把此行代码放入 AST Explore 查看对应的 AST,部分如下:
可以看到,extra下显示了编码字符及对应的原始值,只需要把 raw 的内容修改为 rawValue 的内容即可,代码如下:
import { parse } from "@babel/parser";
import generate from "@babel/generator"
import fs from 'fs';
import traverse from "@babel/traverse";
import * as types from "@babel/types";
const code = fs.readFileSync('./code1.js', 'utf-8');
let ast = parse(code);
traverse(ast, {
StringLiteral({ node }) {
if (node.extra && /\\[ux]/gi.test(node.extra.raw)) {
node.extra.raw = node.extra.rawValue;
}
}
})
const { code: output } = generate(ast);
console.log(output)
// 输出如下:
const strings = [hello, world];
3. 无用代码剔除
const _0x16c18d = function () {
if (!![[]]) {
console.log('hello world');
} else {
console.log('this');
console.log('is');
console.log('dead');
console.log('code');
}
};
const _0x1f7292 = function () {
if ("xmv2nOdfy2N".charAt(4) !== String.fromCharCode(110)) {
console.log('this');
console.log('is');
console.log('dead');
console.log('code');
} else {
console.log('nice to meet you')
}
};
_0x16c18d();
_0x1f7292();
这段代码只是打印了两行内容,多了很多无效代码,将其转为 AST,部分如下:
这是第一个if语句转换为的 AST,test 为 if 语句的判断条件,consequent 是 if 语句块内的代码;alternate 是 else 语句块内的代码;
据此,删除无用代码的代码如下:
import { parse } from "@babel/parser";
import generate from "@babel/generator"
import fs from 'fs';
import traverse from "@babel/traverse";
import * as types from "@babel/types";
const code = fs.readFileSync('./code1.js', 'utf-8');
let ast = parse(code);
traverse(ast, {
IfStatement(path) {
let { consequent, alternate } = path.node;
let testPath = path.get('test');
// evaluateTruthy 方法返回path对应的真值,比如第一个if条件是 !![[]],它为 true,该方法就返回true
const evaluateTest = testPath.evaluateTruthy();
if (evaluateTest === true) {
// 如果if的判断条件是true,就把 if 语句块的内容节点替换原本的IfStatement节点
if (types.isBlockStatement(consequent)) {
consequent = consequent.body;
}
path.replaceWithMultiple(consequent);
} else if (evaluateTest === false) {
// 如果if的判断条件是false,就把 else 语句块的内容节点替换原本的IfStatement节点
if (evaluateTest != null) {
if (types.isBlockStatement(alternate)) {
alternate = alternate.body;
}
path.replaceWithMultiple(alternate);
} else {
path.remove();
}
}
}
})
const { code: output } = generate(ast);
console.log(output)
// 输出如下
const _0x16c18d = function () {
console.log('hello world');
};
const _0x1f7292 = function () {
console.log('nice to meet you');
};
_0x16c18d();
_0x1f7292();
4. 反控制流平坦化
一个简单的代码如下:
const c = 0;
const a = 1;
const b = 3;
经过简单的控制流平坦化后代码如下:
const s = '3|1|2'.split('|');
let x = 0;
while (true) {
switch (s[x++]) {
case '1':
const a = 1;
continue;
case '2':
const b = 3;
continue;
case '3':
const c = 0;
continue;
}
break;
}
还原思路:
首先找到switch语句相关节点,拿到对应的节点对象,如各个case语句对应的代码区块;
分析 switch 语句的判定条件 s 变量对应的列表结果,比如将 "3|1|2".split("|") 转化为 ["3", "1", "2"];
遍历 s 变量对应的列表,将其和各个 case 匹配,顺序得到对应的结果并保存;
用上一步得到的代码替换原来的代码。
还是把上面代码转为 AST 查看,switch 部分如下:
可以看到,它是一个 SwitchStatement 节点,discriminant 就是判断条件,这个例子中对应 s[x++],cases 就是case语句的集合,对应多个 SwitchCase 节点。
可以先把可能用到的节点取到,如 discriminant、case、discriminant的 object 和 property
traverse(ast, {
WhileStatement(path) {
const { node, scope } = path;
const { test, body } = node;
let switchNode = body.body[0];
let { discriminant, cases } = switchNode;
let { object, property } = discriminant
}
})
接下来追踪下判定条件 s[x++] ,展开 object,可以看到其 name 是 s,可以通过 scope 的 getBinding 方法获取到绑定它的节点;绑定的就是 "3|1|2".split("|") ,查看绑定的代码,可以看到是一个 CallExpression 节点,根据AST逐层拿到对应的值,然后动态调用:
let arrName = object.name;
let binding = scope.getBinding(arrName);
let { init } = binding.path.node;
object = init.callee.object;
property = init.callee.property;
let argument = init.arguments[0].value;
let arrayFlow = object.value[property.name](argument);
拿到 arrayFlow (["3", "1", "2"])后遍历它,找到对应的 case 语句对应的代码即可
let resultBody = [];
arrayFlow.forEach((index) => {
let switchCase = cases.filter((c) => c.test.value === index)[0];
let caseBody = switchCase.consequent;
if (types.isContinueStatement(caseBody[caseBody.length - 1])) {
caseBody.pop();
}
resultBody = resultBody.concat(caseBody);
});
最后替换即可:path.replaceWithMultiple(resultBody)
完整代码(全部代码均对照AST实现):
import { parse } from "@babel/parser";
import generate from "@babel/generator"
import fs from 'fs';
import traverse from "@babel/traverse";
import * as types from "@babel/types";
const code = fs.readFileSync('./code1.js', 'utf-8');
let ast = parse(code);
traverse(ast, {
WhileStatement(path) {
const { node, scope } = path;
const { test, body } = node;
let switchNode = body.body[0];
let { discriminant, cases } = switchNode;
let { object, property } = discriminant;
let arrName = object.name;
let binding = scope.getBinding(arrName);
let { init } = binding.path.node;
object = init.callee.object;
property = init.callee.property;
let argument = init.arguments[0].value;
let arrayFlow = object.value[property.name](argument);
let resultBody = [];
arrayFlow.forEach((index) => {
let switchCase = cases.filter((c) => c.test.value === index)[0];
let caseBody = switchCase.consequent;
if (types.isContinueStatement(caseBody[caseBody.length - 1])) {
caseBody.pop();
}
resultBody = resultBody.concat(caseBody);
});
path.replaceWithMultiple(resultBody)
}
})
const { code: output } = generate(ast);
console.log(output)
// 输出如下:
const s = '3|1|2'.split('|');
let x = 0;
const c = 0;
const a = 1;
const b = 3;
标签:node,const,AST,babel,简述,let,import,path
From: https://www.cnblogs.com/achangblog/p/18194272