JS逆向之AST解混淆----元宇宙

文章介绍了AST(抽象语法树)的概念及其在代码处理中的作用,特别是在代码解混淆中的应用。通过Babel库,可以将JavaScript代码转化为AST,然后修改和还原,以实现解混淆。文中列举了几个使用Babel的示例,包括删除冗余代码、处理控制流平坦化和未使用变量的情况。最后,文章提供了一个实际案例,展示了解混淆混淆JavaScript代码的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

注:本文章仅供学习使用,请勿用做其他商业用途,如有侵权,请联系本人


提示:以下是本篇文章正文内容,下面案例可供参考

一、什么是AST技术 ?

AST(Abstract Syntax Tree),译为抽象语法树,是编译原理中的一个概念,为源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,这种数据结构可以类别为一个大的JSON对象。通过 AST 技术,我们面对的就不再是各种符号混杂空格而成的文本字符串,而是一个严谨规范的树形结构,我们可以通过对 AST树节点的一系列操作,借助机器高效且精准地修改代码。

AST 的用途很广,IDE 的语法高亮、代码检查、格式化、压缩、转译等,都需要先将代码转化成 AST 再进行后续的操作,ES5 和 ES6 语法差异,为了向后兼容,在实际应用中需要进行语法的转换,也会用到 AST。AST 并不是为了逆向而生,但做逆向学会了 AST,在解混淆时可以如鱼得水。

    通过 AST 解析网站:https://astexplorer.net/,左侧为我们要输入的 JavaScript 代码,右侧为 AST 树状结构

1、AST对爬虫工程师有什么意义?

对于爬虫工程师来说,它并不能帮你找到加密参数的具体位置,但是可以使用它把混淆的代码解混淆后替换到浏览器里,方便找加密参数的生成逻辑.

2、在线解混淆工具

3、解混淆--> babel库的使用

安装 npm install @babel/core --save-dev

Babel 是一个 JavaScript 编译器,也可以说是一个解析库,Babel 内置了很多分析 JavaScript 代码的方法,我们可以利用 Babel 将 js 代码转换成 AST 语法树,然后增删改查等操作之后,再转换成 JavaScript 代码。Babel 包含的各种功能包、API、各方法可选参数等,都非常多,在实际使用过程中,应当多查询官方文档 网址:Document

在做逆向解混淆中,主要用到了 Babel 的以下几个功能包

  1. @babel/core:Babel 编译器本身,提供了 babel 的编译 API;
  2. @babel/parser:将 JavaScript 代码解析成 AST 语法树;
  3. @babel/traverse:遍历、修改 AST 语法树的各个节点;
  4. @babel/generator:将 AST 还原成 JavaScript 代码;
  5. @babel/types:判断、验证节点的类型、构建新 AST 节点等。

常见的混淆还原:

const generator = require('@babel/generator').default    //将 AST 还原成 JavaScript 代码
const parser = require("@babel/parser");     // 编译成语法树
const traverse = require("@babel/traverse");  //对语法树进行操作
const types = require("@babel/types");  //判断、验证节点的类型、构建新 AST 节点等
var fs =require("fs");  //读取文件
 
js = fs.readFileSync('mfw.js',{encoding:'utf-8'})
let ast = parser.parse(js);
function writeFile(code) {
    console.log("Write start\\n");
    fs.writeFile(file_out, code, function (err) {
        if (err) {
            return console.error(err);
        }
    });
    console.log("Write finish\\n");
}
 
 
visitor1={
    "Program"(path){
        var body =path.get('body.0');
        var node =body.node;
        var args=node.expression.argument;
        if(args==undefined)return;
        var params=args.callee.params;
        var paramsvalue=args.arguments;
        var name,valuelist;
        for(var i=0;i<params.length;i++){
            name=params[i].name;
            valuelist=paramsvalue[i].elements;
            body.traverse({
                MemberExpression(_path){
                    var _node=_path.node;
                    var _name=_node.object.name;
                    if(!types.isNumericLiteral(_node.property))return;
                    var _value=_node.property.value;
                    if(name==_name){
                        if(valuelist[_value]==undefined)return;
                        if(valuelist[_value].value==undefined)return;
                        rvalue=valuelist[_value].value;
                        switch(typeof rvalue){
                            case "string":
                                _path.replaceWith(types.StringLiteral(rvalue));
                                break;
                            case "number":
                                _path.replaceWith(types.NumericLiteral(rvalue));
                                break;
                        }
                    }
                }
            });
        
 
        }
        
    }
}
const visitor2={
    VariableDeclarator(path)
        {
        const {id,init}=path.node;
        if(!types.isLiteral(init))return;
        const binding=path.scope.getBinding(id.name);
        if(binding.constantViolations.length===0)
            {
                for(const refer_path of binding.referencePaths)
                    {
                    refer_path.replaceInline(init);
                    }
                //path.remove();
            }
        }
}
replaceliteral=function(path,value){
    switch(typeof value){
        case 'boolean':
            path.replaceWith(types.booleanLiteral(value));
            break;
        case 'number':
            path.replaceWith(types.NumericLiteral(value));
            break;
        case 'string':
            path.replaceWith(types.stringLiteral(value));
            break;
        default:
            break;
    }
}
const visitor3={
    "UnaryExpression|BinaryExpression|CallExpression|ConditionalExpression":{
        enter:function(path){
            const{value}=path.evaluate();
            replaceliteral(path,value);
        }
 
    }
}
const visitor4={
    "FunctionDeclaration"(path){
        let {id}=path.node;
        let code=path.toString();
        if(code.indexOf("try")!=-1 ||code.indexOf("random")!=-1||code.indexOf("Date")!=-1){
            return;
        }
        eval(code);
        let scope =path.scope;
        const binding = path.scope.parent.getBinding(id.name);
        let isdel=false;
        if(!binding || binding.constantViolations.length>0){
            return;
        }
        for(const refer_path of binding.referencePaths)
        {
            
            let call_express=refer_path.findParent(p=>p.isCallExpression());
            let arguments=call_express.get('arguments');
            let args=[];
            arguments.forEach(arg=>{args.push(arg.isLiteral())});
            if(args.length ===0 || args.indexOf("false")!=-1){
                continue;
            }
            try{
                let value= eval(call_express.toString());
                if(value==undefined)return;
                switch(typeof value){
                    case "string":
                        call_express.replaceWith(types.StringLiteral(value));
                        isdel=true;
                        break;
                    case "number":
                        call_express.replaceWith(types.NumericLiteral(value));
                        isdel=true;
                        break;
                }
                
            }catch(e){
 
            }
        }
        if(isdel){
            //path.remove();
        }
    
    }
}
const visitor5={
    "StringLiteral|NumericLiteral"(path){
        delete path.node.extra;
    }
}
const visitor6={
    "CallExpression"(path){
        var node =path.node;
        var code=path.toString();
        var value;
        if(!node.arguments.length>0)return;
        if(!types.isLiteral(node.arguments[0]))return;
        if(code.indexOf("Time")!=-1)return;
        try{
            value=eval("value="+code);
            
        }catch(e){
 
        }
        if(value==undefined)return;
        switch(typeof value){
            case "string":
                path.replaceWith(types.StringLiteral(value));
                break;
            case "number":
                path.replaceWith(types.NumericLiteral(value));
                break;
            case "boolean":
                path.replaceWith(types.BooleanLiteral(value));
                break;
        }
        
    }
}
 
 
traverse.default(ast,visitor1);
traverse.default(ast,visitor2);
traverse.default(ast,visitor3);
traverse.default(ast,visitor4);
traverse.default(ast,visitor5);
traverse.default(ast,visitor6);
 
 
const {code} = generator(ast,opts = {"comments":false},js);
 
fs.writeFile('mfw_decode.js', code, (err)=>{});
 
 
 

 switch-case 反控制流平坦化混淆还原:

// const _0x34e16a = '3,4,0,5,1,2'['split'](',');
// let _0x2eff02 = 0x0;
// while (!![]) {
//     switch (_0x34e16a[_0x2eff02++]) {
//         case'0':
//             let _0x38cb15 = _0x4588f1 + _0x470e97;
//             continue;
//         case'1':
//             let _0x1e0e5e = _0x37b9f3[_0x50cee0(0x2e0, 0x2e8, 0x2e1, 0x2e4)];
//             continue;
//         case'2':
//             let _0x35d732 = [_0x388d4b(-0x134, -0x134, -0x139, -0x138)](_0x38cb15 >> _0x4588f1);
//             continue;
//         case'3':
//             let _0x4588f1 = 0x1;
//             continue;
//         case'4':
//             let _0x470e97 = 0x2;
//             continue;
//         case'5':
//             let _0x37b9f3 = 0x5 || _0x38cb15;
//             continue;
//     }
//     break;
// }
const parser = require("@babel/parser");
const generate = require("@babel/generator").default
const traverse = require("@babel/traverse").default
const types = require("@babel/types")
const fs = require("fs");

const code = fs.readFileSync("code.js", {encoding: "utf-8"});
const ast = parser.parse(code)

const visitor = {
    WhileStatement(path) {
        // switch 节点
        let switchNode = path.node.body.body[0];
        // switch 语句内的控制流数组名,本例中是 _0x34e16a
        let arrayName = switchNode.discriminant.object.name;
        // 获得所有 while 前面的兄弟节点,本例中获取到的是声明两个变量的节点,即 const _0x34e16a 和 let _0x2eff02
        let prevSiblings = path.getAllPrevSiblings();
        // 定义缓存控制流数组
        let array = []
        // forEach 方法遍历所有节点
        prevSiblings.forEach(pervNode => {
            let {id, init} = pervNode.node.declarations[0];
            // 如果节点 id.name 与 switch 语句内的控制流数组名相同
            if (arrayName === id.name) {
                // 获取节点整个表达式的参数、分割方法、分隔符
                let object = init.callee.object.value;
                let property = init.callee.property.value;
                let argument = init.arguments[0].value;
                // 模拟执行 '3,4,0,5,1,2'['split'](',') 语句
                array = object[property](argument)
                // 也可以直接取参数进行分割,方法不通用,比如分隔符换成 | 就不行了
                // array = init.callee.object.value.split(',');
            }
            // 前面的兄弟节点就可以删除了
            pervNode.remove();
        });

        // 储存正确顺序的控制流语句
        let replace = [];
        // 遍历控制流数组,按正确顺序取 case 内容
        array.forEach(index => {
                let consequent = switchNode.cases[index].consequent;
                // 如果最后一个节点是 continue 语句,则删除 ContinueStatement 节点
                if (types.isContinueStatement(consequent[consequent.length - 1])) {
                    consequent.pop();
                }
                // concat 方法拼接多个数组,即正确顺序的 case 内容
                replace = replace.concat(consequent);
            }
        );
        // 替换整个 while 节点,两种方法都可以
        path.replaceWithMultiple(replace);
        // path.replaceInline(replace);
    }
}

traverse(ast, visitor)
const result = generate(ast)
console.log(result.code)

删除冗余代码混淆还原:

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

const code = `
const example = function () {
    let a;
    if (false) {
        a = 1;
    } else {
        if (1) {
            a = 2;
        }
        else {
            a = 3;
        }
    }
    return a;
};
`
const ast = parser.parse(code)
 
const visitor = {
    enter(path) {
        if (types.isBooleanLiteral(path.node.test) || types.isNumericLiteral(path.node.test)) {
            if (path.node.test.value) {
                path.replaceInline(path.node.consequent.body);
            } else {
                if (path.node.alternate) {
                    path.replaceInline(path.node.alternate.body);
                } else {
                    path.remove()
                }
            }
        }
    }
}

traverse(ast, visitor)
const result = generate(ast)
console.log(result.code)

删除未使用变量混淆还原: 

const parser = require("@babel/parser");
const generate = require("@babel/generator").default
const traverse = require("@babel/traverse").default

const code = `
const a = 1;
const b = a * 2;
const c = 2;
const d = b + 1;
const e = 3;
console.log(d)
`
const ast = parser.parse(code)

const visitor = {
    VariableDeclarator(path){
        const binding = path.scope.getBinding(path.node.id.name);

        // 如标识符被修改过,则不能进行删除动作。
        if (!binding || binding.constantViolations.length > 0) {
            return;
        }

        // 未被引用
        if (!binding.referenced) {
            path.remove();
        }

        // 被引用次数为0
        // if (binding.references === 0) {
        //     path.remove();
        // }

        // 长度为0,变量没有被引用过
        // if (binding.referencePaths.length === 0) {
        //     path.remove();
        // }
    }
}

traverse(ast, visitor)
const result = generate(ast)
console.log(result.code)

 

 

 

二、案例

网站地址 :Imh0dHBzOi8vd3d3Lm5mdGNuLmNvbS9oNS8jLyI=

接口分析

根据接口不难发现,头部【nftcnApiNonce、Nftcnapiappsecret、Nftcnapinonce、Nftcnapisignature】就是我们需要破解的参数;

全局搜索"Nftcnapisignature",很快就发现参数加密的位置,jS文件被混淆了。

解混淆代码:

 

 

 

nftcnapitimestamp:时间戳

nftcnapinonce:随机数

  function _() {

            for (var t = '0123456789', n = "", i = 0; i < 21; i++) {
                n += t['charAt'](Math['floor'](Math.random() * t.length));
            }
            return n;
        }

nftcnApiAppSecret:(nftcnApiAppId+nftcnApiNonce)转大写

//nftcnApiAppSecret  
_0xd4f37a = (0x0, _0xa7ae8['default'])(_0x3a9b25 + _0xaa94dc)[_0x474b47(0x58e)](),   
var _0x51900b = function(_0x29df97) {
    return function(_0x2e1e5d) {
                    var _0x1a5093 = a0_0x2631;
        return new _0x308cec(!0x0)[_0x1a5093(0x1e14)](_0x2e1e5d)[_0x29df97]();
    }
    ;
}

nftcnApiSignature: 加密算法SHA256withRSA

RSA加密参数:

'marketTabId=37&nftcnApiAppId=nftcn-web-h5&nftcnApiAppSecret=7840F2FF0A5DC96227DDA8D662706A63&nftcnApiNonce=442366595145494719079&nftcnApiTimestamp=1689738060972&pageNum=1&pageSize=10'

至于算法,你可以尝试还原混淆,把代码抠出来,篇幅比较长,这里我就演示啦~

  _0x1d3ac4 = _0x52e66c(_0x57eec2),
      function _0x52e66c(_0x468382) {
            var _0x467755 = _0x37795e
              , _0x2aa469 = new _0x4c8849[(_0x467755(0x343))]['RSAKey']()
              , _0x546ab1 = _0x467755(0x480);
            _0x2aa469 = _0x4c8849['default'][_0x467755(0x9c4)][_0x467755(0x79f)](_0x546ab1);
            var _0x2dbdb3 = new _0x4c8849[(_0x467755(0x343))][(_0x467755(0x1e3))][(_0x467755(0x5f8))]['Signature']({
                'alg': _0x467755(0x79c)
            });
            _0x2dbdb3[_0x467755(0xb36)](_0x2aa469),
            _0x2dbdb3[_0x467755(0x800)](_0x468382);
            var _0x18b063 = _0x4c8849[_0x467755(0x343)][_0x467755(0x373)](_0x2dbdb3[_0x467755(0x67b)]())
              , _0x1b934b = _0x18b063;
            return _0x1b934b;
        }

 运行结果:

 

 

 


总结

案例网站涉及加密算法比较多,JS有又被混淆,是一个可以练练手的网站,小伙伴们一起来探讨一下吧

安装AST混淆的具体步骤如下: 1. 首先,下载并安装Java开发工具包(JDK)。AST混淆工具是基于Java的,所以您需要先安装JDK并将其配置到系统环境变量中。 2. 下载并安装AST混淆工具。您可以在AST混淆工具的官方网站或代码托管平台上找到安装包,并按照提供的指示进行安装。 3. 打开AST混淆工具的安装目录,并找到主程序的可执行文件。 4. 配置AST混淆工具。在主程序的安装目录中,找到配置文件并打开它。根据您的需求,配置文件中的选项包括输入文件路径、输出文件路径、混淆算法选择等。根据您的实际需求,修改这些选项。 5. 使用AST混淆工具进行混淆。在命令行或终端中导航到AST混淆工具的安装目录,并执行主程序的可执行文件。根据您在配置文件中设置的选项,输入需要混淆的文件路径,并选择适当的混淆算法。然后,执行混淆命令。 6. 等待混淆过程完成。根据您需要混淆的文件大小和复杂性,混淆过程可能需要一些时间。请耐心等待,直到混淆过程完成。 7. 检查混淆结果。混淆工具通常会生成一个混淆后的文件,并将其保存到您在配置文件中指定的输出路径中。您可以打开该文件以查看混淆结果,并验证其有效性。 总之,安装AST混淆工具的过程大致分为下载安装、配置选项、执行混淆命令和检查结果等步骤。请按照以上步骤操作,以完成安装和使用AST混淆工具。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值