用Babel操作AST实现JavaScript代码的自动化生成与转换

94 阅读4分钟

学习本文后,你将能够开发自己的代码转换工具!

目录


环境搭建

  1. 安装环境
npm install @babel/parser @babel/traverse @babel/generator @babel/types
  1. ast转换的代码框架
const fs = require('fs');
const vm = require('node:vm');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const t = require('@babel/types');
const generator = require('@babel/generator').default;

// 读取混淆代码文件
const code = fs.readFileSync('obfuscated.js', 'utf-8');

// 解析为AST
const ast = parser.parse(code);

// TODO 修改AST的逻辑将在这里编写

// 生成新代码
const output = generator(ast);
fs.writeFileSync('clean.js', output.code);

代码:修改AST的逻辑

重命名函数名

  1. 案例
function _0x1a2b(s) {
    return atob(s);
}
console.log(_0x1a2b("SGVsbG8=")); // 输出 "Hello"
  1. 结果
function decryptString(s) {
  return atob(s);
}
console.log(_0x1a2b("SGVsbG8=")); // 输出 "Hello"
  1. 代码实现:
//重命名加密函数
    FunctionDeclaration(path) {
        if (path.node.id.name==='_0x1a2b') {
            path.node.id.name='decryptString';
        }
    }

重命名变量并修改变量值

  1. 案例
function _0x12ab() {
  const _0x3cde = ["\\x48\\x65\\x6c\\x6c\\x6f", "\\x77\\x6f\\x72\\x6c\\x64"];
  return _0x3cde[0] + " " + _0x3cde[1];
}
  1. 结果
function _0x12ab() {
  const words = ["Hello", "world"];
  return words[0] + " " + words[1];
}
  1. 代码实现:
//重命名变量并解密字符串
    //变量声明节点
    VariableDeclarator(path) {
        if (path.node.id.name === '_0x3cde') {
            path.node.id.name = 'words';  // 修改变量名
            // 遍历elements数组
            path.node.init.elements = path.node.init.elements.map(element => {
                // 解密十六进制字符串
                const decoded = element.value.replace(/\\x([0-9a-fA-F]{2})/g,
                    (_, hex) => String.fromCharCode(parseInt(hex, 16))
                );
                // 关键
                return { type: 'StringLiteral', value: decoded };
            });
        }
    },
    //对象成员访问节点
    MemberExpression(path) {
        if(path.node.object.name==='_0x3cde'){
            path.node.object.name='words';
        }
    }

函数调用替换

  1. 案例
function _0x1a2b(s) {
    return atob(s);
}
console.log(_0x1a2b("SGVsbG8=")); // 输出 "Hello"
  1. 结果
function _0x1a2b(s) {
  return atob(s);
}
console.log("Hello"); // 输出 "Hello"
  1. 代码实现:
    CallExpression(path) {
        if (path.node.callee.name==='_0x1a2b'&&path.node.arguments[0].type==='StringLiteral') {
            //取出参数
            const encryptedStr=path.node.arguments[0].value;
            // 对参数进行解密
            const decryptedStr=atob(encryptedStr);
            // 把原来的参数调用_0x1a2b("SGVsbG8="),替换为decryptedStr,即对atob(encryptedStr)
            path.replaceWith({
                type:'StringLiteral',
                value:decryptedStr
            });

        }
    }

控制流扁平化还原

  1. 案例
function _0x1234() {
  const _0x5678 = [2, 0, 1];
  while (true) {
    switch (_0x5678.shift()) {
      case 0:
        console.log("world");
        continue;
      case 1:
        console.log("!");
        continue;
      case 2:
        console.log("Hello");
        continue;
    }
    break;
  }
}
  1. 结果
function _0x1234() {
  console.log("Hello");
  console.log("world");
  console.log("!");
}
  1. AST转换逻辑 · 识别switch-case结构:找到SwitchStatement节点 · 提取case顺序:通过_0x5678数组的初始值确定执行顺序(本例顺序为2→0→1) · 重建代码顺序:按顺序合并case块中的语句,删除switch和while结构

  2. 代码实现:

    FunctionDeclaration(path) {
        // 1. 定位控制流数组声明
        const controlFlowDecl=path.node.body.body.find(n=>
           (t.isVariableDeclaration(n)&&
           n.declarations[0].id.name==='_0x5678')
        );
        if (!controlFlowDecl) return;
        // 2. 提取控制流顺序 [2, 0, 1]
        const controlFlowArray = controlFlowDecl.declarations[0].init.elements
            .map(e=>e.value);
        // 3. 删除控制流数组声明
        path.node.body.body=path.node.body.body.filter(n=>
            n!==controlFlowDecl);

        // 4. 提取switch语句
        const switchStmt = path.node.body.body
            .find(n => t.isWhileStatement(n)).body.body
            .find(n => t.isSwitchStatement(n));;

        // 5. 删除while语句
        path.node.body.body=path.node.body.body.filter(n=>!t.isWhileStatement(n));

        // 6. 建立case值到语句的映射
        const caseMap=new Map();
        switchStmt.cases.forEach(n=>{
           caseMap.set(n.test.value,n.consequent);
        });

        // 7. 按控制流顺序重组语句
        const orderedStatements=[];
        for(const caseVal of controlFlowArray){
            const stmts=caseMap.get(caseVal)
                .filter(n=>!t.isContinueStatement(n));// 移除continue
            orderedStatements.push(...stmts);
        }

        // 8. 插入到函数体头部(保留其他语句)
        path.node.body.body.unshift(...orderedStatements);
    }

删除未使用的变量

  1. 案例
// 原始代码
const unusedVar = "test"; // 无任何地方使用
const activeVar = "data";
console.log(activeVar);
  1. 结果
const activeVar = "data";
console.log(activeVar);
  1. 代码实现:
    VariableDeclarator(path){
        const binding = path.scope.getBinding(path.node.id.name);
        // 检测变量是否未被引用
        if (!binding || binding.references === 0) {
            // 删除整个 VariableDeclaration(需判断是否最后一个声明)
            const parent = path.parent;
            if (parent.declarations.length === 1) {
                // 情况1:整个 VariableDeclaration 只有一个声明 eg: const a = 1;
                path.parentPath.remove();// 删除父节点(即整个声明语句)
            } else {
                // 情况2:声明语句中有多个变量 eg: let a = 1, b = 2;
                path.remove();// 只删除当前 VariableDeclarator
            }
        }
    }

对象属性简化

  1. 案例
const _0xabc = {
  "xYz": function(s) { return s.toUpperCase(); }
};
console.log(_0xabc["xYz"]("hello")); // 输出 "HELLO"
  1. 结果
const _0xabc = {
  upper: function (s) {
    return s.toUpperCase();
  }
};
console.log(utils.upper("hello")); // 输出 "HELLO"
  1. AST转换逻辑
    1. 识别对象属性:找到ObjectProperty节点中的动态键(如"xYz")
    2. 重命名属性和调用方式:将_0xabc["xYz"]改为utils.upper
  2. 代码实现:
    ObjectProperty(path) {
        // 重命名键名
        if (path.node.key.value === "xYz") {
            path.node.key = t.identifier("upper"); // 改为标识符形式
        }
    },
    MemberExpression(path) {
        // 转换动态属性访问为静态
        if (
            t.isIdentifier(path.node.object, { name: "_0xabc" }) &&
            t.isStringLiteral(path.node.property, { value: "xYz" })
        ) {
            path.node.object.name = "utils";
            path.node.property = t.identifier("upper");
            path.node.computed = false; // 改为.访问方式
        }
    }

条件表达式优化

  1. 案例
const isVIP = !![]; // 混淆写法:!![] → true
console.log(isVIP ? "VIP User" : "Guest");
  1. 结果
  1. AST转换逻辑 · 计算常量表达式:在AST遍历阶段预计算!![]的值 · 删除无效分支:根据计算结果删除false分支

  2. 代码实现:

    没写完。。。。