[1285]AST入门与实战:基于babel库的js反混淆模板的实践

github:https://github.com/fkling/astexplorer

Babel 是一个 JavaScript 编译器,可以将 ES6+ 代码转换为向后兼容的 JavaScript 版本,但它不包含反混淆的功能。

以下是使用 js-beautify 来美化(不是反混淆)JavaScript 代码的示例:

首先,安装 js-beautify

npm install -g js-beautify

然后,使用命令行对代码进行美化:

js-beautify -r -o output.js input.js

这里 input.js 是需要美化的原始混淆代码文件,output.js 是美化后的代码输出文件。

AST入门与实战:基于babel库的js反混淆模板的实践

首发地址:http://zhuoyue360.com/jsnx/106.html

1. 模板代码

通用模板来源自菜老板的知识星球。

const fs = require('fs');
const types = require("@babel/types");
const parser = require("@babel/parser");
const template = require("@babel/template").default;
const traverse = require("@babel/traverse").default;
const generator = require("@babel/generator").default;


//js混淆代码读取
process.argv.length > 2 ? encodeFile = process.argv[2] : encodeFile = "./encode.js";  //默认的js文件
process.argv.length > 3 ? decodeFile = process.argv[3] : decodeFile = encodeFile.slice(0, encodeFile.length - 3) + "_ok.js";

//将源代码解析为AST
let sourceCode = fs.readFileSync(encodeFile, { encoding: "utf-8" });
let ast = parser.parse(sourceCode);
console.time("处理完毕,耗时");






const visit =
{
    // TODO write your code here!
}

traverse(ast, visit);





const simplifyLiteral = {
    NumericLiteral({ node }) {
        if (node.extra && /^0[obx]/i.test(node.extra.raw)) {
            node.extra = undefined;
        }
    },
    StringLiteral({ node }) {
        if (node.extra && /\\[ux]/gi.test(node.extra.raw)) {
            node.extra = undefined;
        }
    },
}


traverse(ast, simplifyLiteral);


console.timeEnd("处理完毕,耗时");
let { code } = generator(ast, opts = { 
    "compact"     : false,  // 是否压缩代码
  "comments"    : false,  // 是否保留注释
    "jsescOption" : { "minimal": true },  //Unicode转义
});

fs.writeFile(decodeFile, code, (err) => { });

2. 插件编写

准备工作

我们已经拿到了最基础的使用模板以后,此时我们的ast反混淆已经成了一个填空题。我们只需要把我们写好的网站反混淆插件填入即可,非常的简单。 接下来,我们找个简单的小案例来试试。

混淆代码均来自于:https://obfuscator.io/ 网站生成。

源代码:

function hi() {
  console.log("Hello,zhuoyue360.com");
}
function h3i() {
  console.log("Hello,zhuoyue360.com");
}function h2i() {
  console.log("Hello,zhuoyue360.com");
}function h44444i() {
  console.log("Hello,zhuoyue360.com");
}
hi();

混淆后的代码:(格式化后)

(function(_0x42555b, _0x288c31) {
    var _0x2e2c7a = _0x4f09
      , _0x264ee4 = _0x42555b();
    while (!![]) {
        try {
            var _0x54167b = parseInt(_0x2e2c7a(0x106)) / 0x1 * (-parseInt(_0x2e2c7a(0x10a)) / 0x2) + -parseInt(_0x2e2c7a(0x109)) / 0x3 * (parseInt(_0x2e2c7a(0x10b)) / 0x4) + -parseInt(_0x2e2c7a(0x10e)) / 0x5 * (parseInt(_0x2e2c7a(0x111)) / 0x6) + -parseInt(_0x2e2c7a(0x110)) / 0x7 + parseInt(_0x2e2c7a(0x10c)) / 0x8 + parseInt(_0x2e2c7a(0x10d)) / 0x9 * (parseInt(_0x2e2c7a(0x10f)) / 0xa) + parseInt(_0x2e2c7a(0x108)) / 0xb;
            if (_0x54167b === _0x288c31)
                break;
            else
                _0x264ee4['push'](_0x264ee4['shift']());
        } catch (_0x1b893f) {
            _0x264ee4['push'](_0x264ee4['shift']());
        }
    }
}(_0x49be, 0xbbf5c));
function hi() {
    console['log']('Hello,zhuoyue360.com');
}
function _0x4f09(_0xd05c9f, _0x352733) {
    var _0x49beab = _0x49be();
    return _0x4f09 = function(_0x4f0967, _0x3191e8) {
        _0x4f0967 = _0x4f0967 - 0x105;
        var _0x4f9c38 = _0x49beab[_0x4f0967];
        return _0x4f9c38;
    }
    ,
    _0x4f09(_0xd05c9f, _0x352733);
}
function _0x49be() {
    var _0x13bd5d = ['12032256MLzLfN', '1251zScKBQ', '25cNqVzl', '270jbEuTF', '5872188ZkfPRj', '1062282TyhTBn', 'Hello,zhuoyue360.com', '1kakbhL', 'log', '11816574EYEQFr', '66741ytXvQy', '42550jdDgVb', '12QEFacN'];
    _0x49be = function() {
        return _0x13bd5d;
    }
    ;
    return _0x49be();
}
function h3i() {
    var _0x2b1dcb = _0x4f09;
    console[_0x2b1dcb(0x107)](_0x2b1dcb(0x105));
}
function h2i() {
    var _0xa95bed = _0x4f09;
    console[_0xa95bed(0x107)](_0xa95bed(0x105));
}
function h44444i() {
    var _0xb505a4 = _0x4f09;
    console[_0xb505a4(0x107)](_0xb505a4(0x105));
}
hi();

我们要尽可能的把混淆后的代码转换成源代码 。此时,我们就需要借助另外一个网站:

https://astexplorer.net/

配置如下, 具体的大家自行在网络上查看相关资料。

好了,目前我们所有的工作已经准备完成,我们就开始看如何还原代码了。

函数调用计算

在下面这个案例中, 我们知道函数调用的真正方法是 _0x4f09 , 然后再使用_0x4f09 的每个函数的第一行都是var xxxxx= _0x4f09; 类似的赋值操作

function _0x4f09(_0xd05c9f, _0x352733) {
    var _0x49beab = _0x49be();
    return _0x4f09 = function(_0x4f0967, _0x3191e8) {
        _0x4f0967 = _0x4f0967 - 0x105;
        var _0x4f9c38 = _0x49beab[_0x4f0967];
        return _0x4f9c38;
    }
    ,
    _0x4f09(_0xd05c9f, _0x352733);
}

function h3i() {
    var _0x2b1dcb = _0x4f09;
    console[_0x2b1dcb(0x107)](_0x2b1dcb(0x105));
}
function h2i() {
    var _0xa95bed = _0x4f09;
    console[_0xa95bed(0x107)](_0xa95bed(0x105));
}
function h44444i() {
    var _0xb505a4 = _0x4f09;
    console[_0xb505a4(0x107)](_0xb505a4(0x105));
}

所以,我们的脚本可以按照下面的思路进行编写

  1. 可以先枚举所有函数。
  2. 函数的第一行代码是不是类似于var xxxxx= _0x4f09;
  3. 找到使用xxxxx的函数调用,进行替换

那么让我们来编写第一个脚本funcCallReplace

枚举所有函数

根据工具提示,该节点是FunctionDeclaration

所以,我们初步的代码为:

const funcCallReplace = {
    FunctionDeclaration(path){
        let {node} = path;
        console.log(node.id.name)
    }
}

traverse(ast, funcCallReplace);

为了验证,我把返回枚举到的函数名称都给列举出来了。

node .\main.js
hi
_0x4f09
_0x49be
h3i
h2i
h44444i
处理完毕,耗时: 10.56ms
函数的第一行代码是不是类似于var xxxxx= _0x4f09;

我们可以看到,目标函数的body的第一行,都是VariableDeclaration

同时,VariableDeclaratorinit 节点的calleename 的内容为_0x49be

过滤条件搞清楚了,我们就来继续更新我们上面写的代码吧~

通过下面代码,我们已经可以把关键的函数过滤出来了~

const funcCallReplace = {
    FunctionDeclaration(path){
        let {node} = path;
        // 1. 拿到body
        let body = node.body.body;
        // 2. 判断body的第一条是否为VariableDeclaration
        if (body.length == 0 || body[0].type != 'VariableDeclaration' || body[0].declarations.length ==0) return 
        let declaration = body[0].declarations[0];
        if (declaration.init == undefined || declaration.init.name != '_0x4f09') return
        let varName = declaration.id.name
        console.log(node.id.name)
        console.log(varName)
    }   
}

traverse(ast, funcCallReplace);

执行后的结果:

h3i
main.js:33
_0x2b1dcb
main.js:34
h2i
main.js:33
_0xa95bed
main.js:34
h44444i
main.js:33
_0xb505a4
main.js:34
处理完毕,耗时: 3834.485107421875ms
main.js:61
处理完毕,耗时: 3.835s

然后,我们就需要看看谁用了varName, 然后我们把它给替换掉即可。

// 取自蔡老板星球脚本的代码.
function getAllBindingInfo(name,scope){
    let pathList = [];
    // 查看引用
    const bindings = scope.getBinding(name);
    if (!bindings ){
        return pathList;
    }
    for(let referPath of bindings.referencePaths){
        // 获取父节点
        let parentPath = referPath.parentPath
        // 判断类型.
        if (parentPath.isVariableDeclarator()){
            // 获取变量名称
            let {node,scope} = parentPath;
            let varName = node.id.name;
            pathList = [].concat(pathList,getAllBindingInfo(varName,scope));
            // 删除掉这些节点.
            parentPath.remove()
        }else{
            pathList.push(parentPath)

        }

    }
    // console.log(name,pathList)
    return pathList;


}


(function(_0x42555b, _0x288c31) {
    var _0x2e2c7a = _0x4f09
      , _0x264ee4 = _0x42555b();
    while (!![]) {
        try {
            var _0x54167b = parseInt(_0x2e2c7a(0x106)) / 0x1 * (-parseInt(_0x2e2c7a(0x10a)) / 0x2) + -parseInt(_0x2e2c7a(0x109)) / 0x3 * (parseInt(_0x2e2c7a(0x10b)) / 0x4) + -parseInt(_0x2e2c7a(0x10e)) / 0x5 * (parseInt(_0x2e2c7a(0x111)) / 0x6) + -parseInt(_0x2e2c7a(0x110)) / 0x7 + parseInt(_0x2e2c7a(0x10c)) / 0x8 + parseInt(_0x2e2c7a(0x10d)) / 0x9 * (parseInt(_0x2e2c7a(0x10f)) / 0xa) + parseInt(_0x2e2c7a(0x108)) / 0xb;
            if (_0x54167b === _0x288c31)
                break;
            else
                _0x264ee4['push'](_0x264ee4['shift']());
        } catch (_0x1b893f) {
            _0x264ee4['push'](_0x264ee4['shift']());
        }
    }
}(_0x49be, 0xbbf5c));
function _0x4f09(_0xd05c9f, _0x352733) {
    var _0x49beab = _0x49be();
    return _0x4f09 = function(_0x4f0967, _0x3191e8) {
        _0x4f0967 = _0x4f0967 - 0x105;
        var _0x4f9c38 = _0x49beab[_0x4f0967];
        return _0x4f9c38;
    }
    ,
    _0x4f09(_0xd05c9f, _0x352733);
}
function _0x49be() {
    var _0x13bd5d = ['12032256MLzLfN', '1251zScKBQ', '25cNqVzl', '270jbEuTF', '5872188ZkfPRj', '1062282TyhTBn', 'Hello,zhuoyue360.com', '1kakbhL', 'log', '11816574EYEQFr', '66741ytXvQy', '42550jdDgVb', '12QEFacN'];
    _0x49be = function() {
        return _0x13bd5d;
    }
    ;
    return _0x49be();
}
const funcCallReplace = {
    FunctionDeclaration(path){
        let {node,scope} = path;
        // 1. 拿到body
        let body = node.body.body;
        // 2. 判断body的第一条是否为VariableDeclaration
        if (body.length == 0 || body[0].type != 'VariableDeclaration' || body[0].declarations.length ==0) return 
        let declaration = body[0].declarations[0];
        if (declaration.init == undefined || declaration.init.name != '_0x4f09') return
        let varName = declaration.id.name
        console.log(node.id.name)
        console.log(varName)
        // 3. 获取所有引用
        let bindingPathList = getAllBindingInfo(varName,scope);
        console.log(bindingPathList)
        for (let referPath of bindingPathList) {
            // 获取参数,执行函数,替换内容
            let args = referPath.node.arguments;
            if (args == undefined ||args.length != 1){
                continue
            }
            let argValue = referPath.node.arguments[0].extra.raw;
            console.log(argValue)
            referPath.replaceWith(types.valueToNode(_0x4f09(argValue)))
        }
    }   
}

traverse(ast, funcCallReplace);

接下来,就是垃圾代码删除的功能了。

AST入门与实战(二):删除垃圾代码

我们把函数调用简单的给替换了一下,但是其最终的效果并不完美。

哪里不完美呢? 有如下几点:

  1. 开头的匿名函数 ,_0x4f09 ,_0x49be 函数已经没用,我们应该删除它 (我认为手动删除就好了)
  2. 在代码中,存在不少的无效代码,例如var _0x49beab = _0x49be(); 我也也应该删除它。
(function (_0x42555b, _0x288c31) {
  var _0x2e2c7a = _0x4f09,
    _0x264ee4 = _0x42555b();
  while (!![]) {
    try {
      var _0x54167b = parseInt(_0x2e2c7a(262)) / 1 * (-parseInt(_0x2e2c7a(266)) / 2) + -parseInt(_0x2e2c7a(265)) / 3 * (parseInt(_0x2e2c7a(267)) / 4) + -parseInt(_0x2e2c7a(270)) / 5 * (parseInt(_0x2e2c7a(273)) / 6) + -parseInt(_0x2e2c7a(272)) / 7 + parseInt(_0x2e2c7a(268)) / 8 + parseInt(_0x2e2c7a(269)) / 9 * (parseInt(_0x2e2c7a(271)) / 10) + parseInt(_0x2e2c7a(264)) / 11;
      if (_0x54167b === _0x288c31) break;else _0x264ee4['push'](_0x264ee4['shift']());
    } catch (_0x1b893f) {
      _0x264ee4['push'](_0x264ee4['shift']());
    }
  }
})(_0x49be, 769884);
function hi() {
  console['log']('Hello,zhuoyue360.com');
}
function _0x4f09(_0xd05c9f, _0x352733) {
  var _0x49beab = _0x49be();
  return _0x4f09 = function (_0x4f0967, _0x3191e8) {
    _0x4f0967 = _0x4f0967 - 261;
    var _0x4f9c38 = _0x49beab[_0x4f0967];
    return _0x4f9c38;
  }, _0x4f09(_0xd05c9f, _0x352733);
}
function _0x49be() {
  var _0x13bd5d = ['12032256MLzLfN', '1251zScKBQ', '25cNqVzl', '270jbEuTF', '5872188ZkfPRj', '1062282TyhTBn', 'Hello,zhuoyue360.com', '1kakbhL', 'log', '11816574EYEQFr', '66741ytXvQy', '42550jdDgVb', '12QEFacN'];
  _0x49be = function () {
    return _0x13bd5d;
  };
  return _0x49be();
}
function h3i() {
  var _0x2b1dcb = _0x4f09;
  console["log"]("Hello,zhuoyue360.com");
}
function h2i() {
  var _0xa95bed = _0x4f09;
  console["log"]("Hello,zhuoyue360.com");
}
function h44444i() {
  var _0xb505a4 = _0x4f09;
  console["log"]("Hello,zhuoyue360.com");
}
hi();

所以,我们理想状态下的代码应该是:

function hi() {
  console['log']('Hello,zhuoyue360.com');
}
function h3i() {
  var _0x2b1dcb = _0x4f09;
  console["log"]("Hello,zhuoyue360.com");
}
function h2i() {
  var _0xa95bed = _0x4f09;
  console["log"]("Hello,zhuoyue360.com");
}
function h44444i() {
  var _0xb505a4 = _0x4f09;
  console["log"]("Hello,zhuoyue360.com");
}
hi();

我们可以通过枚举VariableDeclarator 来实现。

思路如下:

  1. 枚举VariableDeclarator
  2. 通过scope.getBinding来获取调用的次数
  3. 如果次数为0,删除它的父节点(也就是那一整行代码)VariableDeclaration

实现代码如下:

const deleteUnUseVar = {
    VariableDeclarator(path){
        let {node,scope} = path;
        // 我们拿到变量名称, 寻找其binding的次数. 如果binding的长度为0的话,证明没用过,删除即可.
        const {id,init} = node;
        console.log(id.name)
        const bindings = scope.getBinding(id.name);
        if (!bindings || bindings.constantViolations.length > 0){
            // 这个变量被用了.
            return;
        }
        // 这个变量没任何引用.
        path.parentPath.remove();

    }
}
traverse(ast, deleteUnUseVar);

最终的展示效果如下:

解密函数我手动删除了,感觉好像没多大必要写自动的。毕竟只是学习~

function hi() {
  console['log']('Hello,zhuoyue360.com');
}

function h3i() {
  console["log"]("Hello,zhuoyue360.com");
}
function h2i() {
  console["log"]("Hello,zhuoyue360.com");
}
function h44444i() {
  console["log"]("Hello,zhuoyue360.com");
}
hi();

AST入门与实战(三):if节点转switch节点(瑞数5)

原文地址:https://zhuoyue360.com/jsnx/110.html

期望

这是一个瑞数5代解混淆的案例,我们本章节需要做的是把if节点的内容转换成switch-case内容。以此来熟悉AST对JS混淆的对抗。

原始代码:

function whileState() {
  while (1) {
    aV = cA[wU++];

    if (aV < 4) {
      if (aV < 1) {
        zT = window, kD = String, bO = Array, xX = document, nZ = Date;
      } else if (aV < 2) {
        iG = zT['ab'] = {};
      } else if (aV < 3) {
        iG = zT['ab'];
      } else {
        mM = !iG;
      }
    } else {
      if (aV < 5) {
        xT(0);
      } else if (aV < 6) {
        if (!mM) wU += 1;
      } else if (aV < 7) {
        lG = [4, 16, 64, 256, 1024, 4096, 16384, 65536];
      } else {
        return;
      }
    }
  }
}

期望代码:

function whileState() {
  while (1) {
    switch (cA[wU++]) {
      case 0:
        zT = window, kD = String, bO = Array, xX = document, nZ = Date;
        break;
      case 1:
        iG = zT['ab'] = {};
        break;
      case 2:
        iG = zT['ab'];
        break;
      case 3:
        mM = !iG;
        break;
      case 4:
        xT(0);
        break;
      case 5:
        if (!mM) wU += 1;
        break;
      case 6:
        lG = [4, 16, 64, 256, 1024, 4096, 16384, 65536];
        break;
      case 7:
        return;
        break;
    }
  }
}

思路分析

首先,我们需要明确一点,aV 的索引是从0开始的,它是不可能为负数的。

那么也就可以有如下的转换:

if (aV < 1) {
    zT = window, kD = String, bO = Array, xX = document, nZ = Date;
}

转换成

if (aV == 0) {
    zT = window, kD = String, bO = Array, xX = document, nZ = Date;
}

这是蔡老板所说的夹逼原理 ,奈何文化低,我不懂.知道有这么一个回事就行。

思路如下(更加详细的看代码注释):

  1. while循环的参数是NumericLiteral ,且内容为1
  2. body中只有2个节点
  3. 提取出aV
  4. 找到WhileStatement
  5. 枚举WhileStatement下的IfStatement节点.
  6. leftname应该为我们提取出的aV
  7. operator<
  8. right类型不能为IfStatement, 因为它有嵌套.
  9. 记录下了所有符合条件的body
  10. 生成switch 节点

代码

function collectSwitchCase(whilePath,name){
    // 菜老板知识星球获得.
    let ifNodes = [];

    // 遍历WhilePath
    whilePath.traverse({
        "IfStatement"(path)
        {
            //遍历所有的ifStatement;
            let {test,consequent,alternate} = path.node; //获取子节点
            let {left,operator,right} = test; // 必定是BinaryExpression

            if (!types.isIdentifier(left,{name:name}) || operator != '<' || !types.isNumericLiteral(right)) 
            {//条件过滤
                return;
            }

            let value = right.value;

            //保存整个body,记得生成switchCase节点的时候加上break节点。
            ifNodes[right.value-1] = consequent.body;   
            if (!types.isIfStatement(alternate))
            {
                ifNodes[right.value] = alternate.body;  //最后一个else,其实就是上一个else-if 的 test.right的值
            }    

        }
    })
    return ifNodes;


}


const if2switchReplace = {
    WhileStatement(path){
        let {test,body} = path.node;
        // `while`循环的参数是`NumericLiteral` ,且内容为`1`.  body中只有2个节点
        if(!types.isNumericLiteral(test,{value:1}) || body.body.length != 2){
            return
        }

        // 判断while循环格式, 条件过滤
        let blockBody = body.body;
        if (!types.isExpressionStatement(blockBody[0]) || !types.isIfStatement(blockBody[1]))
        {
            return;
        }
        // left 左边的节点就是我们需要的变量名
        let {left,right} = blockBody[0].expression; //或者左右节点       aV = cA[wU++];
        let name = left.name;
        
        // 获取到了变量名称后, 就需要收集使用了aV的case
        let ifNodes = collectSwitchCase(path,name);   //收集case

        //无case,直接返回。
        if (ifNodes.length == 0) return;   

        let len = ifNodes.length;
        for (let i=0; i < len; i++)
        {
             //每一个case最后都加break
            ifNodes[i].push(types.BreakStatement()); 
            ifNodes[i] = types.SwitchCase(test = types.valueToNode(i),consequent = ifNodes[i]);  //生成SwitchCase节点
        }

        //生成SwitchCase节点
        let switchNode = types.SwitchStatement(right,ifNodes);   
        path.node.body.body = [switchNode]; //最后的while节点只有一个Switch Node;


    }   
}

traverse(ast, if2switchReplace);

来源:https://blog.csdn.net/Qiled/article/details/132227687
https://zhuoyue360.com/

AST实战|如何使用babel库彻底还原jsfuck混淆代码:https://blog.csdn.net/qq523176585/article/details/131950589

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值