目录
编辑
1. 什么是Ast-了解
2. 什么是反混淆-了解
3. 了解Ast结构
4. 思考
5. 前置准备
6. 什么是babel
7. 安装babel
8. ast反混淆代码基本结构
9. babel中的组件
parser与generator 组件
traverse 组件 与visitor
enter与exit
visitor 其他写法
traverse 指定节点向下遍历
types组件
10. path对象
path和node
path常用属性和方法
常用属性:
常用方法:
node节点
scope常用属性和方法
binding常用属性和方法
11. AST 节点类型对照表
AST抽象语法树在线转换工具:AST explorer
- 这个网站作用:将代码转换成抽象语法树
1. 什么是Ast-了解
- AST是抽象语法树的缩写。
- AST是一种用于表示程序代码结构的树状数据结构。
- 在编译器和解释器中,AST被用于解析和表示源代码的语法结构
- AST可以看作是源代码的一种抽象表示形式,它去除了源代码中的具体细节,只保留了语法结构和逻辑关系。
- AST中每个节点(Node) 表示源代码的一个语法元素,例如:变量声明,函数定义,循环语句,等,而节点之间的关系表示了语法结构的层次和关联关系
- 作用:
- 通过构建AST,编译器和解析器可以对源代码进行分析和处理
- 编译器可以在AST上进行语法检查、类型检查和优化等操作
- 解析器可以根据AST生成中间代码或目标代码
- AST还能用于代码重构,代码生成等领域
以下是将正常代码通过在线AST在线网站进行转换为AST语法树
2. 什么是反混淆-了解
什么是混淆?
- 混淆可以理解为是一种代码加密技术,主要用于隐藏代码的真实功能,以防止代码被逆向工程师分析和修改。通过混淆,让代码变得复杂和难以理解,使得逆向工程师在调试工程中消耗大量的时间或者放弃,从而达到一种保护
- 混淆总的来说就是一种代码保护方案,将原始代码转换为可读性较差或者没有可读性的代码🤢
什么是反混淆?
- 反混淆就是将混淆后的代码,还原成具有可读性代码,方便逆向工程师进行调试。
混淆是一种艺术
3. 了解Ast结构
AST在线转换为抽象语法树的网站:AST explorer
- 这个网站在反混淆中经常用到,因为我们要将被混淆的代码放到上面,然后转换为抽象语法树,然后对抽象语法树的结构进行分析。从而编写还原混淆的代码
在线解析工具使用:
- 打开网站后,将parser settings(解析器设置),设置成 @babel-parser,因为我们编写的反混淆代码使用的是 @babel 库,所以选择@babel/parser。在代码中@babel/parser的用处也就是将javascript代码转换为ast语法树结构
- 左侧的空白区域我们可以输入一些javascript代码(没混淆的或者混淆后的代码都可以),然后在右侧会展示转换好的ast抽象语法树
- 在左侧输入: console.log("0基础菜鸟学习")
- 在右侧展示抽象语法树结构的时候,默认的是 树 结构,我们也可以转换为 JSON 结构
节点解释:
- type: 表示当前节点的类型
- start:表示当前节点的起始位置,图片中的start为0,起始位置是从 c 前面开始起算
- end:表示当前节点的末尾
- loc:表示当前节点所在的行列位置
- loc中的start,表示节点所在起始的行列位置
- loc中的end,表示节点所在末尾的行列位置
节点解释:
- ExpressionStatement表示为当前的代码是一个console.log()
- ExpressionStatement中会有一个expression的子节点,expression节点的类型是 CallExpression,
- expression节点中又包含了callee和arguments两个节点,callee可以理解为函数名,而arguments这是它的参数
- callee节点包含了object,property和computed节点
- object表示对象,当前对象为 console
- property表示属性,当前属性为 log
- computed表示其方式
在后续的ast反混淆编写中,就是将原有的节点进行增删改查。然后得到一个新的代码
4. 思考
场景1:遇到以下情况的时候可以复制粘贴到控制台输出拿到正确值,但是一般不会有这样的场景,因为混淆不可能只混淆一点,而是整个文件代码都混淆,所以想多了
场景2:但是如果遇到这样的场景,如果手动一个一个复制到控制台解开,会很浪费时间,所以学习Ast的用处就是将混淆代码进行批量还原
5. 前置准备
- 具备一些javascript基础和nodejs基础
- 已安装好nodejs运行环境
6. 什么是babel
- 在ast中,babel是一个广泛使用的JavaScript编译器工具。
- 它可以将高级的JavaScript代码转换为向后兼容的版本,以便在更旧的浏览器或环境中运行
- Babel使用AST(抽象语法树)来分析和修改代码。
- 通过在各个阶段插入插件,Babel可以执行各种转换操作,如转换ES6代码为ES5代码、添加Polyfills和补丁、优化代码等。
- Babel的主要目标是提供一个灵活且可扩展的工具,以适应不同的项目需求,并帮助开发者更好地利用新的JavaScript语言特性。
7. 安装babel
- 安装命令:
- npm i @babel/core --save-dev
- npm i @babel/types 判断节点类型,构建新的AST节点等
- npm i @babel/parser 将Javascript代码解析成AST语法树
- npm i @babel/traverse 遍历,修改AST语法树的各个节点
- npm i @babel/generator 将AST还原成Javascript代码
8. ast反混淆代码基本结构
以下是使用babel库来反混淆代码的模板
// fs模块 用于操作文件的读写
const fs = require("fs");
// @babel/parser 用于将JavaScript代码转换为ast树
const parser = require("@babel/parser");
// @babel/traverse 用于遍历各个节点的函数
const traverse = require("@babel/traverse").default;
// @babel/types 节点的类型判断及构造等操作
const types = require("@babel/types");
// @babel/generator 将处理完毕的AST转换成JavaScript源代码
const generator = require("@babel/generator").default;
// 混淆的js代码文件
const encode_file = "./encode.js"
// 反混淆的js代码文件
const decode_file = "./decode.js"
// 读取混淆的js文件
let jsCode = fs.readFileSync(encode_file, {encoding: "utf-8"});
// 将javascript代码转换为ast树(json结构)
let ast = parser.parse(jsCode)
// todo 编写ast插件
const visitor = {
}
// 调用插件,处理混淆的代码
traverse(ast,visitor)
// 将处理后的ast转换为js代码(反混淆后的代码)
let {code} = generator(ast);
// 保存代码
fs.writeFile('decode.js', code, (err)=>{});
9. babel中的组件
parser与generator 组件
- parser和generator这两个组件的作用是相反的。
- parser用于将js代码转换成ast,generator用于将ast转换成js代码
- parser将代码转换为ast:
- let ast = parser.parse(参数一,参数二)
- 参数一:混淆的js代码
- 参数二:配置参数
- sourceType: 默认是script,当解析的js代码中,含有 import,export 等关键字的时候需要指定sourceType为module,不然会报错
- generator将ast转换为代码:
- let code = generator(参数一,参数二)
- 参数一:ast语法树
- 参数二:配置参数
- retainLines:表示是否使用与源代码相同的行号,默认为false,也就是输出的是格式化后的代码
- comments:表示是否保留注释,默认为true
- compact:表示是否压缩代码,与其相同作用的选项还有minified,concise只不过压缩的程度不一样,minified压缩的最多,concise压缩的最少。
- parser和generator集合使用的示例代码:
const fs = require("fs"); const parser = require("@babel/parser"); const traverse = require("@babel/traverse").default; const types = require("@babel/types"); const generator = require("@babel/generator").default; const encode_file = "./encode.js" const decode_file = "./decode.js" let jsCode = fs.readFileSync(encode_file, {encoding: "utf-8"}); // 将js代码转换为ast let ast = parser.parse(jsCode,{ sourceType:"script" }) // todo 编写ast插件 const visitor = { } traverse(ast,visitor) let {code} = generator(ast,{ retainLines:false, comments:true, compact:"concise" }); fs.writeFile('decode.js', code, (err)=>{});
traverse 组件 与visitor
- traverse 用于遍历和转换抽象语法树(AST)的工具,转换语法树需要配置visitor使用
- visitor 是一个对象,里面可以定义一些方法,用来过滤节点
- visitor 示例代码:
- 1. 在代码中首先声明了visitor对象,对象的名字可以任意取。
- 2. 在visitor对象中定义一个名为ExpressionStatement的方法,这个方法的名字是需要遍历的是节点类型。
- 3. traverse会遍历所有节点,当节点类型为 ExpressionStatement时,调用visitor中对应的方法。
- 4. 如果想要处理其他节点类型,那么可以继续在visitor中继续定义对应的方法
- 5. visitor 对象中的方法接收一个参数,traverse在遍历的时候会把当前节点的Path对象传给它,传进去的Path对象不是节点node
- 6. 最后把visitor作为第二个参数传入traverse中,传给traverse的第一个参数是整个ast。
- traverse(ast,visitor) 的意思是,从头开始遍历ast中的所有节点,过滤出ExpressionStatement节点,执行相应的方法。在ast中如果有多个ExpressStatement就会输出对应的次数
- visitor 示例代码:
enter与exit
- 在遍历节点的过程中,实际上有两次机会来访问节点,enter表示进入节点时,exit表示退出节点时。可以在代码中编写遍历时进入要做的操作和退出时要做的操作。
visitor 其他写法
一个函数同时处理两个节点:
- 可以把方法名用 | 连接成FunctionExpression|BinaryExperssion形式的字符串,把同一个函数应用到多个节点,也就是说函数中的功能可以同时处理FunctionExpression|BinaryExpression节点
多个函数处理一个节点:
- 把多个函数应用于同一个节点。把函数赋值给enter或exit,将enter改为接收一个函数数组就行
traverse 指定节点向下遍历
- traverse 可以指定在任意节点向下遍历。
- 例如,想要把代码中所有函数的第一个参数改为 x
types组件
- types组件,主要用于判断节点类型,生成新的节点
判断节点:
- 方式1:不推荐的判断节点类型写法
- 方式2:
- 使用types组件提供的节点判断方式,存在返回true,不存在返回false
- 语法格式:types.is节点名称
生成新的节点:
- 生成新的节点,然后使用generator组件转换为代码
- 也可以按照AST的结构来构造一段json数据。然后生成代码,不过还是推荐使用types组件来生成
10. path对象
path和node
path和node区别在于,path对象中包含node。node只是path对象中的一个属性。
path常用属性和方法
常用属性:
- path.node 获取当前路径对应的节点
- path.parent 获取当前路径对应节点的父节点
- path.parentPath 获取当前路径对应节点的父路径
- path.scope 表示当前path下的作用域,写插件经常用到
- path.container 获取当前path下的所有兄弟节点(包括自身)
- path.type 获取当前path的节点类型
- path.key 获取当前path的key值,key通常用于path.get函数
常用方法:
- path.get(key) 获取当前路径下指定属性名(key),对应的子路径。
- 例如:path.get("body") 获取当前路径下名为 body 的子路径
- path.getSibling(index) 获取当前路径对应节点的兄弟节点的路径。通过指定索引(index)可以获取相应的兄弟路径。
- path.getFunctionParent() 获取当前路径对应节点的最近的函数父节点的路径。
- path.getPrevSibling() 获取当前path的前一个兄弟节点,返回的是path类型。
- path.getAllPrevSiblings() 获取当前path的所有前兄弟节点,返回的是Array类型,其元素都是path类型。
- path.getNextSibling() 获取当前path的后一个兄弟节点,返回的是path类型。
- path.getAllNextSiblings() 获取当前path的所有后兄弟节点,返回的是Array类型,其元素都是path类型。
- path.evaluate() 用于计算表达式的值,大家可以参考 constantFold 插件的写法。
- path.findParent() 向上查找满足回调函数特征的path,即判断上级路径是否包含有XXX类型的节点。
- path.find() 功能与 path.findParent 方法一样,只不过从当前path开始进行遍历。
- path.getFunctionParent() 获取函数类型父节点,如果不存在,返回 null。
- path.getStatementParent() 获取Statement类型父节点,这个基本上都会有返回值,如果当前遍历的是 Program 或者 File 节点,则会报错
- path.getAncestry() 获取所有的祖先节点,没有实参,返回的是一个Array对象。
- path.isAncestor(maybeDescendant) 判断当前遍历的节点是否为实参的祖先节点.
- path.isDescendant(maybeAncestor) 判断当前遍历的节点是否为实参的子孙节点.
- path.traverse(visitor) 遍历当前路径下的所有子节点,并应用指定的 visitor。
- path.replaceWith(node) 用指定的节点替换当前路径对应的节点。
- path.remove() 从 AST 中移除当前路径对应的节点。
- path.insertBefore(nodes) 在当前路径对应节点之前插入一个或多个节点。
- path.insertAfter(nodes) 在当前路径对应节点之后插入一个或多个节点。
- path.toString() 用于将 AST 节点转换回对应的源代码字符串。
node节点
- node节点是path对象中的一个属性
- node节点中常用的属性:
- path.node.type 获取当前节点类型
- path.node.declarations 对于 VariableDeclaration 节点, 获取变量声明列表。
- path.node.init.value 获取某个节点的值。
- delete path.node.init; 删除节点,使用系统的 delete 方法。
scope常用属性和方法
- scope.block 表示当前作用域下的所有node
- scope.dump() 输出当前每个变量的作用域信息。调用后直接打印,不需要加打印函数
- scope.crawl() 重构scope,在某种情况下会报错,不过还是建议在每一个插件的最后一行加上。
- scope.rename(oldName, newName, block) 修改当前作用域下的的指定的变量名,oldname、newname表示替换前后的变量名,为字符串。注意,oldName需要有binding,否则无法重命名。
- scope.traverse(node, opts, state) 遍历当前作用域下的某些(个)插件。和全局的traverse用法一样。
- scope.getBinding(name) 获取某个变量的binding,可以理解为其生命周期。包含引用,修改之类的信息
binding常用属性和方法
- binding.path 用于定位初始拥有binding的path;
- binding.constant 用于判断当前变量是否被更改,true表示未改变,false表示有更改变量值。
- binding.referenced 用于判断当前变量是否被引用,true表示代码下面有引用该变量的地方,false表示没有地方引用该变量。注意,引用和改变是分开的。
- binding.referencePaths 它是一个Array类型,包含所有引用的path,多用于替换。