AST反混淆实战:提升JavaScript代码的可读性与调试便利性

博客标题:AST反混淆:提升JavaScript代码的可读性与调试便利性

引言

JavaScript代码混淆是一种常见的保护源码的方法,但这也给代码的维护和调试带来了不小的挑战。抽象语法树(AST)提供了一种结构化的方式来分析和转换代码。本文将通过一个具体的案例,展示如何使用AST进行代码反混淆的具体流程。

AST基础知识

AST是源代码的树状结构表示,每个节点代表了代码中的一个构成元素,如表达式、语句等。使用AST,我们可以方便地对代码进行遍历和变换。

准备工作

首先,我们需要安装@babel/parser@babel/traverse@babel/generator@babel/types这几个npm包,它们将帮助我们解析代码、遍历AST、生成新的代码以及操作AST节点。

bash 复制代码
npm install @babel/parser @babel/traverse @babel/generator @babel/types

AST反混淆的目标

AST反混淆的目的是为了使代码更加易于阅读和调试。具体目标包括:

  • 代码可读性:将混淆后的代码转换为清晰、易于理解的形式。
  • 调试便利性:简化调试过程,使开发者能够快速定位问题js。

AST反混淆的代码分析步骤

1 判断混淆的类型,找到混淆的位置

是字符串编码的混淆、赋值操作混淆还是表达式混淆等

任何技术都是有限的,没有统一的解混淆的办法,只有具体问题具体分析。查看js代码的语法树,分析混淆的类型就和写Xpath解析找父节点还是兄弟节点一样。

2 明确解混淆后的js代码

AST解析网址
https://astexplorer.net/

AST反混淆的代码实现步骤

1. 读取JS文件并转化为AST

首先,我们需要读取混淆后的JavaScript文件,并使用@babel/parser将其转换为AST语法树。这一步是整个反混淆过程的基础。

javascript 复制代码
let jsCode = fs.readFileSync('path/to/obfuscated.js', 'utf-8');
let ast = parser.parse(jsCode);

2. 遍历AST并进行增删改查

接下来,我们使用@babel/traverse遍历AST语法树。在遍历过程中,我们可以对节点进行增加、删除、修改等操作,以实现代码的反混淆,这里每种混淆都不一样,具体问题具体分析,每个人思路也不完全一样。

  • 增加:为代码添加注释或辅助信息,提高可读性。
  • 删除:移除无用的代码或混淆的部分。
  • 修改:将混淆的变量名、属性名等替换为更易读的形式。
  • 查询:分析代码结构,为后续的修改提供依据。

3. 将AST重新转化为JS代码

最后,我们使用@babel/generator将修改后的AST语法树重新转换为JavaScript代码。这样,我们就得到了可读性更强、更方便调试的代码。

javascript 复制代码
const fs = require('fs');
let result = generator(ast).code;
fs.writeFileSync('path/to/demangled.js', result, 'utf-8');

案例1 表达式混淆

我们有一段混淆后的JavaScript代码,其目的是将几个字符串拼接赋值给变量f,然后通过f访问全局对象G的一个属性。我们的任务是使用AST来反混淆这段代码,使其更加易于阅读和调试。

具体解混淆流程

步骤1:读取并解析代码为AST

首先,我们读取混淆后的JavaScript代码,并使用@babel/parser将其转换为AST语法树。

javascript 复制代码
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generator = require('@babel/generator').default;
const types = require('@babel/types');
const fs = require('fs');

let jsCode = `f = "cdc_adoQpoasnfa76pfcZLmcf";
f += "cd";
f += "qwe";
f += "qwe";
f += "qad";
f += "\u0065\u006e\u0075\u006d\u0065\u0072\u0061\u0062\u006c\u0065";
C = G[f += "_Array"];`; // 混淆后的JavaScript代码
let ast = parser.parse(jsCode);

步骤2:定义变换逻辑

接着,我们定义变换逻辑,使用@babel/traverse遍历AST。我们将关注两个节点类型:MemberExpressionAssignmentExpression

  • MemberExpression :处理成员表达式,例如G[f]
  • AssignmentExpression :处理赋值表达式,例如f += "cd"

步骤3:处理MemberExpression(成员函数对象)

我们遍历AST,寻找MemberExpression节点。如果属性名是_Array,我们将这个属性名追加到前一个表达式的值上,并移除当前的MemberExpression

javascript 复制代码
traverse(ast, {
    'MemberExpression'(path){
        let {object, property} = path.node;
        let {left, operator, right} = property;

        let prevExp = path.parentPath.parentPath.getPrevSibling().node;
        prevExp.expression.right.value += right.value;
        path.node.property = left;
    }
})

步骤4:处理AssignmentExpression(赋值操作对象)

然后,我们继续遍历AST,寻找AssignmentExpression节点。如果右侧是字符串字面量,并且操作符是+=,我们将这个字符串追加到前一个赋值表达式的值上,并移除当前的赋值表达式。

javascript 复制代码
traverse(ast, {
    'AssignmentExpression'(path){
        let {left, operator, right} = path.node;
        if (!types.isStringLiteral(right) || operator != '+='){
            return
        }
        let value = right.value;
        let prevExp = path.parentPath.getPrevSibling().node;
        prevExp.expression.right.value += right.value;
        path.remove()
    }
})//移除当前的赋值表达式。

步骤5:生成新的代码

最后,我们使用@babel/generator将修改后的AST语法树重新转换为JavaScript代码。

javascript 复制代码
let result = generator(ast).code;
console.log(result);

示例二:属性名解码

代码分析

这段代码中的对象属性名被编码,我们的目标是将其解码为可读的形式。

javascript 复制代码
// ast反混淆目的为了代码可读性更强,更方便调试。
//
// 1. 读取js文件,使用parser.parse转化为ast语法树
//
// 2. 遍历ast,增删改查ast语法树 path.node
//
// 3. 重新把ast转化为js代码。
const types = require('@babel/types');
const parser = require('@babel/parser');
const generator = require('@babel/generator').default;
const traverse = require('@babel/traverse').default;

let jsCode = `
a = {
    "\u0065\u006e\u0075\u006d\u0065\u0072\u0061\u0062\u006c\u0065": !0,
    "\u0067\u0065\u0064": n
}
`;

let ast = parser.parse(jsCode);

// 遍历AST,解码属性名
traverse(ast, {
    'StringLiteral': function(path) {
        let decoded = decodeURIComponent(path.node.value);
        path.node.value = decoded;
    }
});

// 打印反混淆后的代码
console.log(generator(ast).code);

为什么是StringLiteral,因为这里混淆的类型type是Literal

知识点总结

在JavaScript的抽象语法树(AST)中,不同的节点类型代表了代码中的各种语法结构。

  1. MemberExpression

    • 表示对对象成员的访问,包括属性和方法。例如,在表达式 obj.propobj.method() 中,obj.propobj.method() 都是 MemberExpression 节点。
  2. AssignmentExpression

    • 表示赋值操作。这是基本的赋值(如 x = y),复合赋值(如 x += y),或扩展赋值(如 x ??= y)的语法结构。在 AssignmentExpression 节点中,赋值运算符连接了左侧的变量(或解构模式)和右侧的值或表达式。
  3. StringLiteral

    • 表示字符串字面量。这是用双引号 " 或单引号 ' 包围的文本,例如 "hello"'world'。在AST中,StringLiteral 节点包含了字符串的值。

除了这些,还有一些常用的AST节点类型:

  • Identifier :表示一个变量名或标识符,如 xy
  • BinaryExpression :表示二元表达式,它包含两个操作数和一个运算符,如 x + yx > y
  • CallExpression :表示函数或方法的调用,如 func()obj.method()
  • FunctionDeclaration / FunctionExpression :两者都表示函数,区别在于 FunctionDeclaration 是通过 function 关键字声明的,而 FunctionExpression 是通过赋值给变量的函数表达式。
  • ReturnStatement :表示 return 语句,用于从函数中返回一个值。
  • IfStatement :表示 if 条件语句。
  • ForStatement / WhileStatement :表示循环结构,如 forwhile 循环。

理解这些AST节点类型对于进行代码分析、转换或生成等操作非常重要,特别是在使用编译器工具如Babel时。

结语

通过上述步骤,我们成功地将一段混淆的JavaScript代码反混淆,使其更加易于阅读和调试。掌握这项技能对于前端和逆向开发者来说非常有价值。AST为我们提供了一种强大的方式来分析和转换代码,这不仅有助于提高代码质量,还能在开发过程中节省大量的时间和精力。掌握AST的使用,对于前端开发者来说是一项宝贵的技能。

相关推荐
blammmp26 分钟前
Java:数据结构-枚举
java·开发语言·数据结构
何曾参静谧38 分钟前
「C/C++」C/C++ 指针篇 之 指针运算
c语言·开发语言·c++
暗黑起源喵44 分钟前
设计模式-工厂设计模式
java·开发语言·设计模式
WaaTong1 小时前
Java反射
java·开发语言·反射
Troc_wangpeng1 小时前
R language 关于二维平面直角坐标系的制作
开发语言·机器学习
王哲晓1 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
努力的家伙是不讨厌的1 小时前
解析json导出csv或者直接入库
开发语言·python·json
理想不理想v1 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
Envyᥫᩣ1 小时前
C#语言:从入门到精通
开发语言·c#
酷酷的阿云1 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js