一. Ast - 反混淆(基础篇)

目录

​编辑

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

  • 这个网站在反混淆中经常用到,因为我们要将被混淆的代码放到上面,然后转换为抽象语法树,然后对抽象语法树的结构进行分析。从而编写还原混淆的代码

在线解析工具使用:

  1. 打开网站后,将parser settings(解析器设置),设置成 @babel-parser,因为我们编写的反混淆代码使用的是 @babel 库,所以选择@babel/parser。在代码中@babel/parser的用处也就是将javascript代码转换为ast语法树结构
  2. 左侧的空白区域我们可以输入一些javascript代码(没混淆的或者混淆后的代码都可以),然后在右侧会展示转换好的ast抽象语法树
  3. 在左侧输入: console.log("0基础菜鸟学习")
  4. 在右侧展示抽象语法树结构的时候,默认的是 树 结构,我们也可以转换为 JSON 结构

节点解释:

  1. type: 表示当前节点的类型
  2. start:表示当前节点的起始位置,图片中的start为0,起始位置是从 c 前面开始起算
  3. end:表示当前节点的末尾
  4. loc:表示当前节点所在的行列位置
    1. loc中的start,表示节点所在起始的行列位置
    2. 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. 前置准备

  1. 具备一些javascript基础和nodejs基础
  2. 已安装好nodejs运行环境

6. 什么是babel

  1. 在ast中,babel是一个广泛使用的JavaScript编译器工具。
  2. 它可以将高级的JavaScript代码转换为向后兼容的版本,以便在更旧的浏览器或环境中运行
  3. Babel使用AST(抽象语法树)来分析和修改代码。
  4. 通过在各个阶段插入插件,Babel可以执行各种转换操作,如转换ES6代码为ES5代码、添加Polyfills和补丁、优化代码等。
  5. 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就会输出对应的次数

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.path    用于定位初始拥有binding的path;
  • binding.constant    用于判断当前变量是否被更改,true表示未改变,false表示有更改变量值。
  • binding.referenced    用于判断当前变量是否被引用,true表示代码下面有引用该变量的地方,false表示没有地方引用该变量。注意,引用和改变是分开的。
  • binding.referencePaths    它是一个Array类型,包含所有引用的path,多用于替换。

11. AST 节点类型对照表


序号    类型原名称    中文名称    描述
1    Program    程序主体    整段代码的主体
2    VariableDeclaration    变量声明    声明一个变量,例如 var let const
3    FunctionDeclaration    函数声明    声明一个函数,例如 function
4    ExpressionStatement    表达式语句    通常是调用一个函数,例如 console.log()
5    BlockStatement    块语句    包裹在 {} 块内的代码,例如 if (condition){var a = 1;}
6    BreakStatement    中断语句    通常指 break
7    ContinueStatement    持续语句    通常指 continue
8    ReturnStatement    返回语句    通常指 return
9    SwitchStatement    Switch 语句    通常指 Switch Case 语句中的 Switch
10    IfStatement    If 控制流语句    控制流语句,通常指 if(condition){}else{}
11    Identifier    标识符    标识,例如声明变量时 var identi = 5 中的 identi
12    CallExpression    调用表达式    通常指调用一个函数,例如 console.log()
13    BinaryExpression    二进制表达式    通常指运算,例如 1+2
14    MemberExpression    成员表达式    通常指调用对象的成员,例如 console 对象的 log 成员
15    ArrayExpression    数组表达式    通常指一个数组,例如 [1, 3, 5]
16    NewExpression    New 表达式    通常指使用 New 关键词
17    AssignmentExpression    赋值表达式    通常指将函数的返回值赋值给变量
18    UpdateExpression    更新表达式    通常指更新成员值,例如 i++
19    Literal    字面量    字面量
20    BooleanLiteral    布尔型字面量    布尔值,例如 true false
21    NumericLiteral    数字型字面量    数字,例如 100
22    StringLiteral    字符型字面量    字符串,例如 vansenb
23    SwitchCase    Case 语句    通常指 Switch 语句中的 Case

  • 36
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值