js逆向工具-初学AST解混淆

一、AST简单了解
  • 以下文章简单了解下,可以通过案例熟悉再回头再来看这些文章
  • babel手册1babel手册2
  • 13个示例快速入门JS抽象语法树
  • ast的概念和babel这一工具的具体使用
  • 可视化显示AST结构工具
    在这里插入图片描述
  • ast的作用:ast作用主要是将混淆的难读的js还原成可读性高的js,对整个js的逻辑不做任何改变,你可以理解为化繁为简,可见性的瘦身,也许网页上是1w多行的混淆代码,瘦身后只有不到1千行;脱混淆后要注意替换到网页试试看能否能用
  • 处理逻辑:就是一颗树,不停的找分支,确定当前分支的各个属性特性,即定位要改的节点就是找type,然后对该type节点操作即可,你可以简单理解为html里面找到了父标签然后对子标签进行操作,理解为xpath或者css选择器的逻辑也不为过
  • 十分要注意的:每次还原逻辑的时候,都要保证当前代码是可以正常运行的,然后再进行下一步还原逻辑
二、babel环境安装
  • node安装:node环境安装
  • babel相关包安装:在cmd窗口下执行如下命令
    npm install -g @babel/core
    npm install -g @babel/parser
    npm install -g @babel/template
    npm install -g @babel/traverse
    npm install -g @babel/types
    npm install -g @babel/generator
    
三、快速入门例子
1、入门例子讲解:修改变量值
  • 分为三部分:引入原js文件,ast操作节点还原逻辑,还原后的js文件存储
  • 首先打开可视化显示AST结构工具,查看原js文件结构,我们这里只需要关注type节点是什么,以及你想操作的结果在哪个节点下
    在这里插入图片描述
  • 比如我这里想把原js文件var a = 1通过ast操作变成var a = "shirmay",那对应的type节点VariableDeclarator,要修改的是该节点的init键
  • 对应代码脚本,其中t.stringLiteral(“shirmay”)即要赋的值
    var traverses_1 = {
        VariableDeclarator(path) {
            var cur_node = path.node;
            // console.log(cur_node.id.name);
            // console.log(cur_node.init.value);
            cur_node.init = t.stringLiteral("shirmay");  // 修改变量值
        }
    }
    
  • 最终完整的ast操作js修改变量值的脚本,完整代码在下面案例1,各个代码注释已经在图片中标注,后续我们只需要修改traverses还原逻辑即可,其它基本就是个固定框架
    在这里插入图片描述
2、入门案例代码:修改变量值
  • 把原js文件var a = 1通过ast操作变成var a = "shirmay"
  • 原js文件:demo_1.js
    var a = 1;
    
  • ast还原文件:ast_demo_1.js
    // 将JS源码转换成语法树
    var parser = require("@babel/parser");
    // 为parser提供模板引擎
    var template = require("@babel/template").default;
    // 遍历AST树节点
    var traverse = require("@babel/traverse").default;
    // 操作节点,比如判断节点类型,生成新的节点等
    var t = require("@babel/types");
    // 将语法树转换为源代码
    var generator = require("@babel/generator").default;
    
    
    // 1、读取原始文件
    var fs = require("fs");
    var jscode = fs.readFileSync("demo_1.js", {encoding: "utf-8"}); 
    console.log("原js文件内容:", jscode);
    // 2、对原始文件进行操作,使用traverse遍历语法树,因为变量定义为VariableDeclarator类型,所以我们只对type为VariableDeclarator的节点进行处理
    var ast = parser.parse(jscode); 
    var traverses_1 = {
        VariableDeclarator(path) {
            var cur_node = path.node;
            // console.log(cur_node.id.name);
            // console.log(cur_node.init.value);
            cur_node.init = t.stringLiteral("shirmay");  // 修改变量值
        }
    }
    traverse(ast, traverses_1) 
    // 3、生成还原后的js文件
    var ast_js_code = generator(ast);
    console.log("ast操作后的js文件内容:", ast_js_code.code)
    
四、实际案例1-ob混淆之ast还原
  • ob混淆分为以下几个步骤:
    • ①【数组 + 移位自执行函数 + 解密字符串函数】还原
    • ②【定义的对象Object,有规律的key和value】还原
    • ③【while + switch 组合控制流平坦化】还原
  • 分为三部分:引入原js文件,ast操作节点还原逻辑,还原后的js文件存储,通用的代码结构如下,我们每次需要该段就是还原逻辑的处理traverses_1、traverses_2、traverses_3等…
    // 通用的引用模块
    var parser = require("@babel/parser");  // 将JS源码转换成语法树AST
    var template = require("@babel/template").default;  // 为parser提供模板引擎
    var traverse = require("@babel/traverse").default;  // 遍历AST树节点操作
    var types = require("@babel/types");  // 操作节点,比如判断节点类型,生成新的节点等
    var generator = require("@babel/generator").default;  // 将语法树AST转换为js代码
    var fsrw = require("fs");
    var jscode = fsrw.readFileSync("ob_1_before.js", {encoding: "utf-8"});  // 读取原始混淆文件
    var ast_parse = parser.parse(jscode);  //将JS源码转换成语法树AST
    
    
    // 定义每个还原逻辑
    var traverses_1 = {........................}
    var traverses_2 = {........................}
    var traverses_3 = {........................}
    // 还原逻辑运行
    traverse(ast_parse, traverses_1) 
    traverse(ast_parse, traverses_2) 
    traverse(ast_parse, traverses_3) 
    
    
    // 生成还原后的js文件
    var ast_js_code = generator(ast_parse);
    // console.log("ast操作后的js文件内容:", ast_js_code.code)
    fsrw.writeFileSync('./ob_1_after.js', ast_js_code.code)
    
1、数组 + 移位自执行函数 + 解密字符串函数 还原
  • 其目的是将js文件的如此类的$_0x4f70('\x30\x78\x31\x34', '\x64\x5a\x59\x67') + '\x6f\x64' 还原成"nDAod",其中还涉及了将十六进制字符串还原成正常易读的字符串,最终效果图如下
    在这里插入图片描述

  • 首先打开可视化显示AST结构工具,查看原js文件结构,并确定解密函数字符串为"$_0x4f70",分出前三部分并加载到内存后删除
    在这里插入图片描述

    // 获取解密函数前三部分(大数组+自执行函数+解密字符串函数),并写入内存后删除
    var decryptFunNameStr = "$_0x4f70"
    let descrypt_strfun_js = '';
    for(let i=0;i<=2;i++){ 
        descrypt_strfun_js += generator(ast_parse.program.body[i], {compact:true}).code  // compact:true, 设置js压缩
        delete ast_parse.program.body[i]
    }
    eval(descrypt_strfun_js);
    
  • 其次观察$_0x4f70(’\x30\x78\x63\x62’, ‘\x69\x59\x23\x63’)树结构,需要解混淆的节点type为CallExpression,所以我们对CallExpression节点逐个判断,满足当前节点的callee.name是解密字符串函数时,对当前节点进行统一解混淆替换
    在这里插入图片描述

    // 调用解密函数的逻辑还原
    var decryptFunNameStr = "$_0x4f70"
    var traverses_1 = {
        CallExpression(path) {
            var cur_node = path.node;       
            if(cur_node.callee.name === decryptFunNameStr && cur_node.arguments.length === 2){
                path.replaceInline(types.valueToNode(eval(path.toString())));
            }
        }
    }
    
  • 最后关于一些十六进制,unicode的字符串还原,借鉴前人经验,直接删除extra这个节点即可
    在这里插入图片描述

    // 处理十六进制等字符串还原
    var traverses_2 = {
        // A.处理十六进制字符串,针对"\x68\x65\x6c\x6c"  》》 'hell'
        NumericLiteral(path) {
            cur_node = path.node;
            if (cur_node.extra && /^0[obx]/i.test(cur_node.extra.raw)) {
                cur_node.extra = undefined;
            }
        },
        // B. 处理Unicode字符串,针对"\u0068\u0065\u006c\u006c" 》》'hell'
        StringLiteral(path) {
            cur_node = path.node;
            if (cur_node.extra && /\\[ux]/gi.test(cur_node.extra.raw)) {
                cur_node.extra = undefined;
            }
        },
        // C. 字符串合并,exit的方式可以往深层判断是否都是满足这个, 针对 "whi" + "le " + "(tr" + "ue)" + " {}" 》》 "while (true) {}"
        BinaryExpression: {
            exit: function(path){  
                cur_node = path.node;
                if (cur_node.left.type==="StringLiteral"&&cur_node.right.type==="StringLiteral"&&cur_node.operator==='+'){
                    path.replaceWith(types.valueToNode(cur_node.left.value + cur_node.right.value))
                }
            }
        }
    }
    
2、定义的对象Object有规律的key和value 还原
  • 本案例的对象Object是_0x3f5d3a,后又赋值给了对象 _0x1104e6 ,再后面一直调用的也是 _0x1104e6 对象
  • 所以先将_0x3f5d3a临时存储,然后再分析 _0x1104e6 对象引用时如何替换,其中我们发现_0x3f5d3a对象的值就两种类型,要么是function,要么是字符串,所以在后面分析 _0x1104e6 引用时,就只分析这两种情况
    在这里插入图片描述
    在这里插入图片描述
  • 对象Object是_0x3f5d3a临时存储代码
    // 将有规律的对象临时存储,并删除相应的节点
    var temp_obj = {};
    var regularObjNameStr = '_0x3f5d3a'
    var traverses_3 = {
        AssignmentExpression(path){
            var cur_node = path.node;       
            if((cur_node.right.type === 'FunctionExpression' || cur_node.right.type === 'StringLiteral')){
                    if(cur_node.left.object.name === regularObjNameStr){
                        temp_obj[cur_node.left.property.value] = cur_node.right;
                        path.remove();
                }
            }
        },
    }
    
  • _0x1104e6 对象的值是字符串时被引用
    在这里插入图片描述
  • _0x1104e6 对象的值是函数时被引用
    在这里插入图片描述
  • ast代码,注意本案例当中cur_node.callee.object.name === '_0x1104e6'
    // 有规律的对象引用替换
    var refererObjNameStr = '_0x1104e6'
    var traverses_4 = {
        MemberExpression(path){
            cur_node = path.node;
            if(cur_node.object.name === refererObjNameStr && (path.inList || path.parent.type === 'AssignmentExpression' || path.parent.type === 'BinaryExpression'|| path.parent.type === 'MemberExpression')){
                path.replaceInline(temp_obj[cur_node.property.value])
            }
        },
        CallExpression(path){
            cur_node = path.node
            if(cur_node.callee.object && cur_node.callee.object.name === refererObjNameStr){
                const y_node = temp_obj[cur_node.callee.property.value];
                if (y_node && y_node.body.body[0].argument.type === 'BinaryExpression'){
                    const operator = y_node.body.body[0].argument.operator;
                    path.replaceInline(types.binaryExpression(operator, cur_node.arguments[0], cur_node.arguments[1]))
                }
                else if(y_node && y_node.body.body[0].argument.type === 'CallExpression'){
                    const arg = cur_node.arguments.slice(1);
                    path.replaceInline(types.callExpression(cur_node.arguments[0], arg))
                }
            }
    
        }
    }
    
3、while + switch控制流平坦化
  • 控制流平坦化还原思路参考Nanda的这篇文章

  • 控制流基本语法包括两种:一种是if / else 条件语句判断,另一种是while / switch / case 条件语句判断,本案例是第二种,还原效果如图
    在这里插入图片描述

  • 还原逻辑,可以将控制流数组先取出来》然后遍历控制流数组,依次取出依次取出对应的SwitchCase节点》再将每个SwitchCase节点的consequent代码块添加到临时数组中,如果有continue则删除该代码节点》遍历完后用用临时数组替换掉SwitchStatement节点
    在这里插入图片描述

    // 控制流平坦化while + switch
    var traverses_5 = {
        WhileStatement(path){
            cur_node = path.node;
            if(cur_node.body.body[0].type === "SwitchStatement"){
                var swithStm = cur_node.body.body[0];     
                // 找到path节点的前兄弟节点,即 _0x1eb1f3所在的节点,然后获取 _0x1eb1f3数组
                var arrNodePath = path.getAllPrevSiblings()[1]
                var arrValue = arrNodePath.node.declarations[0].init.callee.object.value.split('|');
                // SwitchCase节点遍历并临时存储到一个数组     
                var caseList = swithStm.cases;     
                var temp_arr_switch_case = [];
                arrValue.map(targetIdx => { 
                    var targetBody = caseList[targetIdx].consequent;    
                    // 如果最后一个节点是,则删除ContinueStatement块(continue语句)     
                    if (types.isContinueStatement(targetBody[targetBody.length - 1])){
                        targetBody.pop();    
                    }       
                    temp_arr_switch_case = temp_arr_switch_case.concat(targetBody)     
                    });
                // 多个节点替换一个节点的话使用replaceWithMultiple方法     
                path.replaceWithMultiple(temp_arr_switch_case);           
            }
        },
    }
    
js逆向AST混淆是一种通过析和修改JavaScript的抽象语法树(AST)来还原混淆代码的过程。首先,我们需要获取到混淆代码的AST表示形式。然后,根据特定的混淆算法,对AST进行遍历和修改,以还原原始代码的结构和逻辑。在这个过程中,我们可以使用不同的技术和工具来帮助我们完成混淆任务。 在提供的引用中,涉及了一些对AST进行遍历和修改的代码片段。例如,在引用中,使用了traverse函数来遍历AST,然后通过修改AST节点来进行替换和替换。在引用中,通过迭代和遍历AST,找到变量名和取值方法名,然后将它们替换或删除。在引用中,使用了traverse函数和eval函数来移除赋值表达式和成员表达式。 以上是一些常见的技术和方法,用于js逆向AST混淆。具体的混淆过程可能因代码结构和混淆方式而有所不同。为了成功混淆代码,可能需要更多的详细信息和专业知识。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [【JavaScript 逆向AST 混淆](https://blog.csdn.net/pyzzd/article/details/130613135)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值