共计 18185 个字符,预计需要花费 46 分钟才能阅读完成。
什么是 AST
AST(Abstract Syntax Tree),中文形象语法树,简称语法树(Syntax Tree),是源代码的形象语法结构的树状表现形式,树上的每个节点都示意源代码中的一种构造。语法树不是某一种编程语言独有的,JavaScript、Python、Java、Golang 等简直所有编程语言都有语法树。
小时候咱们失去一个玩具,总喜爱把玩具拆解成一个一个小整机,而后依照咱们本人的想法,把整机从新组装起来,一个新玩具就诞生了。而 JavaScript 就像一台精妙运作的机器,通过 AST 解析,咱们也能够像童年时拆解玩具一样,深刻理解 JavaScript 这台机器的各个零部件,而后从新依照咱们本人的志愿来组装。
AST 的用处很广,IDE 的语法高亮、代码查看、格式化、压缩、转译等,都须要先将代码转化成 AST 再进行后续的操作,ES5 和 ES6 语法差别,为了向后兼容,在理论利用中须要进行语法的转换,也会用到 AST。AST 并不是为了逆向而生,但做逆向学会了 AST,在解混同时能够蛟龙得水。
AST 有一个在线解析网站:https://astexplorer.net/,顶部能够抉择语言、编译器、是否开启转化等,如下图所示,区域①是源代码,区域②是对应的 AST 语法树,区域③是转换代码,能够对语法树进行各种操作,区域④是转换后生成的新代码。图中原来的 Unicode 字符通过操作之后就变成了失常字符。
语法树没有繁多的格局,抉择不同的语言、不同的编译器,失去的后果也是不一样的,在 JavaScript 中,编译器有 Acorn、Espree、Esprima、Recast、Uglify-JS 等,应用最多的是 Babel,后续的学习也是以 Babel 为例。
AST 在编译中的地位
在编译原理中,编译器转换代码通常要通过三个步骤:词法剖析(Lexical Analysis)、语法分析(Syntax Analysis)、代码生成(Code Generation),下图活泼展现了这一过程:
词法剖析
词法分析阶段是编译过程的第一个阶段,这个阶段的工作是从左到右一个字符一个字符地读入源程序,而后依据构词规定辨认单词,生成 token 符号流,比方 isPanda('🐼')
,会被拆分成 isPanda
,(
,'🐼'
,)
四局部,每局部都有不同的含意,能够将词法剖析过程设想为不同类型标记的列表或数组。
语法分析
语法分析是编译过程的一个逻辑阶段,语法分析的工作是在词法剖析的根底上将单词序列组合成各类语法短语,比方“程序”,“语句”,“表达式”等,后面的例子中,isPanda('🐼')
就会被剖析为一条表白语句 ExpressionStatement
,isPanda()
就会被剖析成一个函数表达式 CallExpression
,🐼
就会被剖析成一个变量 Literal
等,泛滥语法之间的依赖、嵌套关系,就形成了一个树状构造,即 AST 语法树。
代码生成
代码生成是最初一步,将 AST 语法树转换成可执行代码即可,在转换之前,咱们能够间接操作语法树,进行增删改查等操作,例如,咱们能够确定变量的申明地位、更改变量的值、删除某些节点等,咱们将语句 isPanda('🐼')
批改为一个布尔类型的 Literal
:true
,语法树就有如下变动:
Babel 简介
Babel 是一个 JavaScript 编译器,也能够说是一个解析库,Babel 中文网:https://www.babeljs.cn/,Babel 英文官网:https://babeljs.io/,Babel 内置了很多剖析 JavaScript 代码的办法,咱们能够利用 Babel 将 JavaScript 代码转换成 AST 语法树,而后增删改查等操作之后,再转换成 JavaScript 代码。
Babel 蕴含的各种性能包、API、各办法可选参数等,都十分多,本文不一一列举,在理论应用过程中,该当多查问官网文档,或者参考文末给出的一些学习材料。Babel 的装置和其余 Node 包一样,须要哪个装置哪个即可,比方 npm install @babel/core @babel/parser @babel/traverse @babel/generator
在做逆向解混同中,次要用到了 Babel 的以下几个性能包,本文也仅介绍以下几个性能包:
@babel/core
:Babel 编译器自身,提供了 babel 的编译 API;@babel/parser
:将 JavaScript 代码解析成 AST 语法树;@babel/traverse
:遍历、批改 AST 语法树的各个节点;@babel/generator
:将 AST 还原成 JavaScript 代码;@babel/types
:判断、验证节点的类型、构建新 AST 节点等。
@babel/core
Babel 编译器自身,被拆分成了三个模块:@babel/parser
、@babel/traverse
、@babel/generator
,比方以下办法的导入成果都是一样的:
const parse = require("@babel/parser").parse;
const parse = require("@babel/core").parse;
const traverse = require("@babel/traverse").default
const traverse = require("@babel/core").traverse
@babel/parser
@babel/parser
能够将 JavaScript 代码解析成 AST 语法树,其中次要提供了两个办法:
parser.parse(code, [{options}])
:解析一段 JavaScript 代码;parser.parseExpression(code, [{options}])
:思考到了性能问题,解析单个 JavaScript 表达式。
局部可选参数 options
:
参数 | 形容 |
---|---|
allowImportExportEverywhere |
默认 import 和 export 申明语句只能呈现在程序的最顶层,设置为 true 则在任何中央都能够申明 |
allowReturnOutsideFunction |
默认如果在顶层中应用 return 语句会引起谬误,设置为 true 就不会报错 |
sourceType |
默认为 script ,当代码中含有 import 、export 等关键字时会报错,须要指定为 module |
errorRecovery |
默认如果 babel 发现一些不失常的代码就会抛出谬误,设置为 true 则会在保留解析谬误的同时持续解析代码,谬误的记录将被保留在最终生成的 AST 的 errors 属性中,当然如果遇到重大的谬误,仍然会终止解析 |
举个例子看得比较清楚:
const parser = require("@babel/parser");
const code = "const a = 1;";
const ast = parser.parse(code, {sourceType: "module"})
console.log(ast)
{sourceType: "module"}
演示了如何增加可选参数,输入的就是 AST 语法树,这和在线网站 https://astexplorer.net/ 解析进去的语法树是一样的:
@babel/generator
@babel/generator
能够将 AST 还原成 JavaScript 代码,提供了一个 generate
办法:generate(ast, [{options}], code)
。
局部可选参数 options
:
参数 | 形容 |
---|---|
auxiliaryCommentBefore |
在输入文件内容的头部增加正文块文字 |
auxiliaryCommentAfter |
在输入文件内容的开端增加正文块文字 |
comments |
输入内容是否蕴含正文 |
compact |
输入内容是否不增加空格,防止格式化 |
concise |
输入内容是否缩小空格使其更紧凑一些 |
minified |
是否压缩输入代码 |
retainLines |
尝试在输入代码中应用与源代码中雷同的行号 |
接着后面的例子,原代码是 const a = 1;
,当初咱们把 a
变量批改为 b
,值 1
批改为 2
,而后将 AST 还原生成新的 JS 代码:
const parser = require("@babel/parser");
const generate = require("@babel/generator").default
const code = "const a = 1;";
const ast = parser.parse(code, {sourceType: "module"})
ast.program.body[0].declarations[0].id.name = "b"
ast.program.body[0].declarations[0].init.value = 2
const result = generate(ast, {minified: true})
console.log(result.code)
最终输入的是 const b=2;
,变量名和值都胜利更改了,因为加了压缩解决,等号左右两边的空格也没了。
代码里 {minified: true}
演示了如何增加可选参数,这里示意压缩输入代码,generate
失去的 result
失去的是一个对象,其中的 code
属性才是最终的 JS 代码。
代码里 ast.program.body[0].declarations[0].id.name
是 a 在 AST 中的地位,ast.program.body[0].declarations[0].init.value
是 1 在 AST 中的地位,如下图所示:
@babel/traverse
当代码多了,咱们不可能像后面那样挨个定位并批改,对于雷同类型的节点,咱们能够间接遍历所有节点来进行批改,这里就用到了 @babel/traverse
,它通常和 visitor
一起应用,visitor
是一个对象,这个名字是能够随便取的,visitor
里能够定义一些办法来过滤节点,这里还是用一个例子来演示:
const parser = require("@babel/parser");
const generate = require("@babel/generator").default
const traverse = require("@babel/traverse").default
const code = `
const a = 1500;
const b = 60;
const c = "hi";
const d = 787;
const e = "1244";
`
const ast = parser.parse(code)
const visitor = {NumericLiteral(path){path.node.value = (path.node.value + 100) * 2
},
StringLiteral(path){path.node.value = "I Love JavaScript!"}
}
traverse(ast, visitor)
const result = generate(ast)
console.log(result.code)
这里的原始代码定义了 abcde 五个变量,其值有数字也有字符串,咱们在 AST 中能够看到对应的类型为 NumericLiteral
和 StringLiteral
:
而后咱们申明了一个 visitor
对象,而后定义对应类型的解决办法,traverse
接管两个参数,第一个是 AST 对象,第二个是 visitor
,当 traverse
遍历所有节点,遇到节点类型为 NumericLiteral
和 StringLiteral
时,就会调用 visitor
中对应的解决办法,visitor
中的办法会接管一个以后节点的 path
对象,该对象的类型是 NodePath
,该对象有十分多的属性,以下介绍几种最罕用的:
属性 | 形容 |
---|---|
toString() |
以后门路的源码 |
node |
以后门路的节点 |
parent |
以后门路的父级节点 |
parentPath |
以后门路的父级门路 |
type |
以后门路的类型 |
PS:path
对象除了有很多属性以外,还有很多办法,比方替换节点、删除节点、插入节点、寻找父级节点、获取同级节点、增加正文、判断节点类型等,可在须要时查问相干文档或查看源码,后续介绍 @babel/types
局部将会举局部例子来演示,当前的实战文章中也会有相干实例,篇幅无限本文不再细说。
因而在下面的代码中,path.node.value
就拿到了变量的值,而后咱们就能够进一步对其进行批改了。以上代码运行后,所有数字都会加上 100 后再乘以 2,所有字符串都会被替换成 I Love JavaScript!
,后果如下:
const a = 3200;
const b = 320;
const c = "I Love JavaScript!";
const d = 1774;
const e = "I Love JavaScript!";
如果多个类型的节点,解决的形式都一样,那么还能够应用 |
将所有节点连接成字符串,将同一个办法利用到所有节点:
const visitor = {"NumericLiteral|StringLiteral"(path) {path.node.value = "I Love JavaScript!"}
}
visitor
对象有多种写法,以下几种写法的成果都是一样的:
const visitor = {NumericLiteral(path){path.node.value = (path.node.value + 100) * 2
},
StringLiteral(path){path.node.value = "I Love JavaScript!"}
}
const visitor = {NumericLiteral: function (path){path.node.value = (path.node.value + 100) * 2
},
StringLiteral: function (path){path.node.value = "I Love JavaScript!"}
}
const visitor = {
NumericLiteral: {enter(path) {path.node.value = (path.node.value + 100) * 2
}
},
StringLiteral: {enter(path) {path.node.value = "I Love JavaScript!"}
}
}
const visitor = {enter(path) {if (path.node.type === "NumericLiteral") {path.node.value = (path.node.value + 100) * 2
}
if (path.node.type === "StringLiteral") {path.node.value = "I Love JavaScript!"}
}
}
以上几种写法中有用到了 enter
办法,在节点的遍历过程中,进入节点(enter)与退出(exit)节点都会拜访一次节点,traverse
默认在进入节点时进行节点的解决,如果要在退出节点时解决,那么在 visitor
中就必须申明 exit
办法。
@babel/types
@babel/types
次要用于构建新的 AST 节点,后面的示例代码为 const a = 1;
,如果想要减少内容,比方变成 const a = 1; const b = a * 5 + 1;
,就能够通过 @babel/types
来实现。
首先察看一下 AST 语法树,原语句只有一个 VariableDeclaration
节点,当初减少了一个:
那么咱们的思路就是在遍历节点时,遍历到 VariableDeclaration
节点,就在其前面减少一个 VariableDeclaration
节点,生成 VariableDeclaration
节点,能够应用 types.variableDeclaration()
办法,在 types 中各种办法名称和咱们在 AST 中看到的是一样的,只不过首字母是小写的,所以咱们不须要晓得所有办法的状况下,也能大抵推断其办法名,只晓得这个办法还不行,还得晓得传入的参数是什么,能够查文档,不过 K 哥这里举荐间接看源码,十分清晰明了,以 Pycharm 为例,按住 Ctrl 键,再点击办法名,就进到源码里了:
function variableDeclaration(kind: "var" | "let" | "const", declarations: Array<BabelNodeVariableDeclarator>)
能够看到须要 kind
和 declarations
两个参数,其中 declarations
是 VariableDeclarator
类型的节点组成的列表,所以咱们能够先写出以下 visitor
局部的代码,其中 path.insertAfter()
是在该节点之后插入新节点的意思:
const visitor = {VariableDeclaration(path) {let declaration = types.variableDeclaration("const", [declarator])
path.insertAfter(declaration)
}
}
接下来咱们还须要进一步定义 declarator
,也就是 VariableDeclarator
类型的节点,查问其源码如下:
function variableDeclarator(id: BabelNodeLVal, init?: BabelNodeExpression)
察看 AST,id 为 Identifier
对象,init 为 BinaryExpression
对象,如下图所示:
先来解决 id,能够应用 types.identifier()
办法来生成,其源码为 function identifier(name: string)
,name 在这里就是 b 了,此时 visitor
代码就能够这么写:
const visitor = {VariableDeclaration(path) {let declarator = types.variableDeclarator(types.identifier("b"), init)
let declaration = types.variableDeclaration("const", [declarator])
path.insertAfter(declaration)
}
}
而后再来看 init 该如何定义,首先依然是看 AST 构造:
init 为 BinaryExpression
对象,left 右边是 BinaryExpression
,right 左边是 NumericLiteral
,能够用 types.binaryExpression()
办法来生成 init,其源码如下:
function binaryExpression(
operator: "+" | "-" | "/" | "%" | "*" | "**" | "&" | "|" | ">>" | ">>>" | "<<" | "^" | "==" | "===" | "!=" | "!==" | "in" | "instanceof" | ">" | "<" | ">=" | "<=",
left: BabelNodeExpression | BabelNodePrivateName,
right: BabelNodeExpression
)
此时 visitor
代码就能够这么写:
const visitor = {VariableDeclaration(path) {let init = types.binaryExpression("+", left, right)
let declarator = types.variableDeclarator(types.identifier("b"), init)
let declaration = types.variableDeclaration("const", [declarator])
path.insertAfter(declaration)
}
}
而后持续结构 left 和 right,和后面的办法一样,察看 AST 语法树,查问对应办法应该传入的参数,层层嵌套,直到把所有的节点都结构结束,最终的 visitor
代码应该是这样的:
const visitor = {VariableDeclaration(path) {let left = types.binaryExpression("*", types.identifier("a"), types.numericLiteral(5))
let right = types.numericLiteral(1)
let init = types.binaryExpression("+", left, right)
let declarator = types.variableDeclarator(types.identifier("b"), init)
let declaration = types.variableDeclaration("const", [declarator])
path.insertAfter(declaration)
path.stop()}
}
留神:path.insertAfter()
插入节点语句前面加了一句 path.stop()
,示意插入实现后立刻进行遍历以后节点和后续的子节点,增加的新节点也是 VariableDeclaration
,如果不加进行语句的话,就会有限循环插入上来。
插入新节点后,再转换成 JavaScript 代码,就能够看到多了一行新代码,如下图所示:
常见混同还原
理解了 AST 和 babel 后,就能够对 JavaScript 混同代码进行还原了,以下是局部样例,带你进一步相熟 babel 的各种操作。
字符串还原
文章结尾的图中举了个例子,失常字符被换成了 Unicode 编码:
console['\u006c\u006f\u0067']('\u0048\u0065\u006c\u006c\u006f\u0020\u0077\u006f\u0072\u006c\u0064\u0021')
察看 AST 构造:
咱们发现 Unicode 编码对应的是 raw
,而 rawValue
和 value
都是失常的,所以咱们能够将 raw
替换成 rawValue
或 value
即可,须要留神的是引号的问题,原本是 console["log"]
,你还原后变成了 console[log]
,天然会报错的,除了替换值以外,这里间接删除 extra 节点,或者删除 raw 值也是能够的,所以以下几种写法都能够还原代码:
const parser = require("@babel/parser");
const generate = require("@babel/generator").default
const traverse = require("@babel/traverse").default
const code = `console['\u006c\u006f\u0067']('\u0048\u0065\u006c\u006c\u006f\u0020\u0077\u006f\u0072\u006c\u0064\u0021')`
const ast = parser.parse(code)
const visitor = {StringLiteral(path) {
// 以下办法均可
// path.node.extra.raw = path.node.rawValue
// path.node.extra.raw = '"'+ path.node.value +'"'
// delete path.node.extra
delete path.node.extra.raw
}
}
traverse(ast, visitor)
const result = generate(ast)
console.log(result.code)
还原后果:
console["log"]("Hello world!");
表达式还原
之前 K 哥写过 JSFuck 混同的还原,其中有介绍 ![]
可示意 false,!![]
或者 !+[]
可示意 true,在一些混同代码中,常常有这些操作,把简略的表达式复杂化,往往须要执行一下语句,能力失去真正的后果,示例代码如下:
const a = !![]+!![]+!![];
const b = Math.floor(12.34 * 2.12)
const c = 10 >> 3 << 1
const d = String(21.3 + 14 * 1.32)
const e = parseInt("1.893" + "45.9088")
const f = parseFloat("23.2334" + "21.89112")
const g = 20 < 18 ? '未成年' : '成年'
想要执行语句,咱们须要理解 path.evaluate()
办法,该办法会对 path 对象进行执行操作,主动计算出后果,返回一个对象,其中的 confident
属性示意置信度,value
示意计算结果,应用 types.valueToNode()
办法创立节点,应用 path.replaceInline()
办法将节点替换成计算结果生成的新节点,替换办法有一下几种:
replaceWith
:用一个节点替换另一个节点;replaceWithMultiple
:用多个节点替换另一个节点;replaceWithSourceString
:将传入的源码字符串解析成对应 Node 后再替换,性能较差,不倡议应用;replaceInline
:用一个或多个节点替换另一个节点,相当于同时有了前两个函数的性能。
对应的 AST 解决代码如下:
const parser = require("@babel/parser");
const generate = require("@babel/generator").default
const traverse = require("@babel/traverse").default
const types = require("@babel/types")
const code = `
const a = !![]+!![]+!![];
const b = Math.floor(12.34 * 2.12)
const c = 10 >> 3 << 1
const d = String(21.3 + 14 * 1.32)
const e = parseInt("1.893" + "45.9088")
const f = parseFloat("23.2334" + "21.89112")
const g = 20 < 18 ? '未成年' : '成年'
`
const ast = parser.parse(code)
const visitor = {"BinaryExpression|CallExpression|ConditionalExpression"(path) {const {confident, value} = path.evaluate()
if (confident){path.replaceInline(types.valueToNode(value))
}
}
}
traverse(ast, visitor)
const result = generate(ast)
console.log(result.code)
最终后果:
const a = 3;
const b = 26;
const c = 2;
const d = "39.78";
const e = parseInt("1.89345.9088");
const f = parseFloat("23.233421.89112");
const g = "\u6210\u5E74";
删除未应用变量
有时候代码里会有一些并没有应用到的多余变量,删除这些多余变量有助于更加高效的剖析代码,示例代码如下:
const a = 1;
const b = a * 2;
const c = 2;
const d = b + 1;
const e = 3;
console.log(d)
删除多余变量,首先要理解 NodePath
中的 scope
,scope
的作用次要是查找标识符的作用域、获取并批改标识符的所有援用等,删除未应用变量次要用到了 scope.getBinding()
办法,传入的值是以后节点可能援用到的标识符名称,返回的要害属性有以下几个:
identifier
:标识符的 Node 对象;path
:标识符的 NodePath 对象;constant
:标识符是否为常量;referenced
:标识符是否被援用;references
:标识符被援用的次数;constantViolations
:如果标识符被批改,则会寄存所有批改该标识符节点的 Path 对象;referencePaths
:如果标识符被援用,则会寄存所有援用该标识符节点的 Path 对象。
所以咱们能够通过 constantViolations
、referenced
、references
、referencePaths
多个参数来判断变量是否能够被删除,AST 解决代码如下:
const parser = require("@babel/parser");
const generate = require("@babel/generator").default
const traverse = require("@babel/traverse").default
const code = `
const a = 1;
const b = a * 2;
const c = 2;
const d = b + 1;
const e = 3;
console.log(d)
`
const ast = parser.parse(code)
const visitor = {VariableDeclarator(path){const binding = path.scope.getBinding(path.node.id.name);
// 如标识符被批改过,则不能进行删除动作。if (!binding || binding.constantViolations.length > 0) {return;}
// 未被援用
if (!binding.referenced) {path.remove();
}
// 被援用次数为 0
// if (binding.references === 0) {// path.remove();
// }
// 长度为 0,变量没有被援用过
// if (binding.referencePaths.length === 0) {// path.remove();
// }
}
}
traverse(ast, visitor)
const result = generate(ast)
console.log(result.code)
解决后的代码(未应用的 b、c、e 变量已被删除):
const a = 1;
const b = a * 2;
const d = b + 1;
console.log(d);
删除冗余逻辑代码
有时候为了减少逆向难度,会有很多嵌套的 if-else 语句,大量判断为假的冗余逻辑代码,同样能够利用 AST 将其删除掉,只留下判断为真的,示例代码如下:
const example = function () {
let a;
if (false) {a = 1;} else {if (1) {a = 2;}
else {a = 3;}
}
return a;
};
察看 AST,判断条件对应的是 test
节点,if 对应的是 consequent
节点,else 对应的是 alternate
节点,如下图所示:
AST 解决思路以及代码:
- 筛选出
BooleanLiteral
和NumericLiteral
节点,取其对应的值,即path.node.test.value
; - 判断
value
值为真,则将节点替换成consequent
节点下的内容,即path.node.consequent.body
; - 判断
value
值为假,则替换成alternate
节点下的内容,即path.node.alternate.body
; - 有的 if 语句可能没有写 else,也就没有
alternate
,所以这种状况下判断value
值为假,则间接移除该节点,即path.remove()
const parser = require("@babel/parser");
const generate = require("@babel/generator").default
const traverse = require("@babel/traverse").default
const types = require('@babel/types');
const code = `
const example = function () {
let a;
if (false) {a = 1;} else {if (1) {a = 2;}
else {a = 3;}
}
return a;
};
`
const ast = parser.parse(code)
const visitor = {enter(path) {if (types.isBooleanLiteral(path.node.test) || types.isNumericLiteral(path.node.test)) {if (path.node.test.value) {path.replaceInline(path.node.consequent.body);
} else {if (path.node.alternate) {path.replaceInline(path.node.alternate.body);
} else {path.remove()
}
}
}
}
}
traverse(ast, visitor)
const result = generate(ast)
console.log(result.code)
处理结果:
const example = function () {
let a;
a = 2;
return a;
};
switch-case 反控制流平坦化
控制流平坦化是混同当中最常见的,通过 if-else
或者 while-switch-case
语句合成步骤,示例代码:
const _0x34e16a = '3,4,0,5,1,2'['split'](',');
let _0x2eff02 = 0x0;
while (!![]) {switch (_0x34e16a[_0x2eff02++]) {
case'0':
let _0x38cb15 = _0x4588f1 + _0x470e97;
continue;
case'1':
let _0x1e0e5e = _0x37b9f3[_0x50cee0(0x2e0, 0x2e8, 0x2e1, 0x2e4)];
continue;
case'2':
let _0x35d732 = [_0x388d4b(-0x134, -0x134, -0x139, -0x138)](_0x38cb15 >> _0x4588f1);
continue;
case'3':
let _0x4588f1 = 0x1;
continue;
case'4':
let _0x470e97 = 0x2;
continue;
case'5':
let _0x37b9f3 = 0x5 || _0x38cb15;
continue;
}
break;
}
AST 还原思路:
- 获取控制流原始数组,将
'3,4,0,5,1,2'['split'](',')
之类的语句转化成['3','4','0','5','1','2']
之类的数组,失去该数组之后,也能够抉择把 split 语句对应的节点删除掉,因为最终代码里这条语句就没用了; - 遍历第一步失去的控制流数组,顺次取出每个值所对应的 case 节点;
- 定义一个数组,贮存每个 case 节点
consequent
数组外面的内容,并删除continue
语句对应的节点; - 遍历实现后,将第三步的数组替换掉整个 while 节点,也就是
WhileStatement
。
不同思路,写法多样,对于如何获取控制流数组,能够有以下思路:
- 获取到
While
语句节点,而后应用path.getAllPrevSiblings()
办法获取其后面的所有兄弟节点,遍历每个兄弟节点,找到与switch()
外面数组的变量名雷同的节点,而后再取节点的值进行后续解决; - 间接取
switch()
外面数组的变量名,而后应用scope.getBinding()
办法获取到它绑定的节点,而后再取这个节点的值进行后续解决。
所以 AST 解决代码就有两种写法,办法一:(code.js 即为后面的示例代码,为了不便操作,这里应用 fs 从文件中读取代码)
const parser = require("@babel/parser");
const generate = require("@babel/generator").default
const traverse = require("@babel/traverse").default
const types = require("@babel/types")
const fs = require("fs");
const code = fs.readFileSync("code.js", {encoding: "utf-8"});
const ast = parser.parse(code)
const visitor = {WhileStatement(path) {
// switch 节点
let switchNode = path.node.body.body[0];
// switch 语句内的控制流数组名,本例中是 _0x34e16a
let arrayName = switchNode.discriminant.object.name;
// 取得所有 while 后面的兄弟节点,本例中获取到的是申明两个变量的节点,即 const _0x34e16a 和 let _0x2eff02
let prevSiblings = path.getAllPrevSiblings();
// 定义缓存控制流数组
let array = []
// forEach 办法遍历所有节点
prevSiblings.forEach(pervNode => {let {id, init} = pervNode.node.declarations[0];
// 如果节点 id.name 与 switch 语句内的控制流数组名雷同
if (arrayName === id.name) {
// 获取节点整个表达式的参数、宰割办法、分隔符
let object = init.callee.object.value;
let property = init.callee.property.value;
let argument = init.arguments[0].value;
// 模仿执行 '3,4,0,5,1,2'['split'](',') 语句
array = object[property](argument)
// 也能够间接取参数进行宰割,办法不通用,比方分隔符换成 | 就不行了
// array = init.callee.object.value.split(',');
}
// 后面的兄弟节点就能够删除了
pervNode.remove();});
// 贮存正确程序的控制流语句
let replace = [];
// 遍历控制流数组,按正确程序取 case 内容
array.forEach(index => {let consequent = switchNode.cases[index].consequent;
// 如果最初一个节点是 continue 语句,则删除 ContinueStatement 节点
if (types.isContinueStatement(consequent[consequent.length - 1])) {consequent.pop();
}
// concat 办法拼接多个数组,即正确程序的 case 内容
replace = replace.concat(consequent);
}
);
// 替换整个 while 节点,两种办法都能够
path.replaceWithMultiple(replace);
// path.replaceInline(replace);
}
}
traverse(ast, visitor)
const result = generate(ast)
console.log(result.code)
办法二:
const parser = require("@babel/parser");
const generate = require("@babel/generator").default
const traverse = require("@babel/traverse").default
const types = require("@babel/types")
const fs = require("fs");
const code = fs.readFileSync("code.js", {encoding: "utf-8"});
const ast = parser.parse(code)
const visitor = {WhileStatement(path) {
// switch 节点
let switchNode = path.node.body.body[0];
// switch 语句内的控制流数组名,本例中是 _0x34e16a
let arrayName = switchNode.discriminant.object.name;
// 获取控制流数组绑定的节点
let bindingArray = path.scope.getBinding(arrayName);
// 获取节点整个表达式的参数、宰割办法、分隔符
let init = bindingArray.path.node.init;
let object = init.callee.object.value;
let property = init.callee.property.value;
let argument = init.arguments[0].value;
// 模仿执行 '3,4,0,5,1,2'['split'](',') 语句
let array = object[property](argument)
// 也能够间接取参数进行宰割,办法不通用,比方分隔符换成 | 就不行了
// let array = init.callee.object.value.split(',');
// switch 语句内的控制流自增变量名,本例中是 _0x2eff02
let autoIncrementName = switchNode.discriminant.property.argument.name;
// 获取控制流自增变量名绑定的节点
let bindingAutoIncrement = path.scope.getBinding(autoIncrementName);
// 可抉择的操作:删除控制流数组绑定的节点、自增变量名绑定的节点
bindingArray.path.remove();
bindingAutoIncrement.path.remove();
// 贮存正确程序的控制流语句
let replace = [];
// 遍历控制流数组,按正确程序取 case 内容
array.forEach(index => {let consequent = switchNode.cases[index].consequent;
// 如果最初一个节点是 continue 语句,则删除 ContinueStatement 节点
if (types.isContinueStatement(consequent[consequent.length - 1])) {consequent.pop();
}
// concat 办法拼接多个数组,即正确程序的 case 内容
replace = replace.concat(consequent);
}
);
// 替换整个 while 节点,两种办法都能够
path.replaceWithMultiple(replace);
// path.replaceInline(replace);
}
}
traverse(ast, visitor)
const result = generate(ast)
console.log(result.code)
以上代码运行后,原来的 switch-case
控制流就被还原了,变成了按程序一行一行的代码,更加简洁明了:
let _0x4588f1 = 0x1;
let _0x470e97 = 0x2;
let _0x38cb15 = _0x4588f1 + _0x470e97;
let _0x37b9f3 = 0x5 || _0x38cb15;
let _0x1e0e5e = _0x37b9f3[_0x50cee0(0x2e0, 0x2e8, 0x2e1, 0x2e4)];
let _0x35d732 = [_0x388d4b(-0x134, -0x134, -0x139, -0x138)](_0x38cb15 >> _0x4588f1);
参考资料
本文有参考以下材料,也是比拟举荐的在线学习材料:
- Youtube 视频,Babel 入门:https://www.youtube.com/watch…(作者 Nicolò Ribaudo,视频中的 PPT 材料可在 K 哥爬虫公众号后盾回复 Babel 收费获取!)
- 官网手册 Babel Handbook:https://github.com/jamiebuild…
- 非官方 Babel API 中文文档:https://evilrecluse.top/Babel…
END
Babel 编译器国内的材料其实不是很多,多看源码、同时在线对照可视化的 AST 语法树,急躁一点儿一层一层剖析即可,本文中的案例也只是最基本操作,理论遇到一些混同还得视状况进行批改,比方须要加一些类型判断来限度等,后续 K 哥会用实战来率领大家进一步相熟解混同当中的其余操作。