AST学习笔记 至少入个大门

我的理解:AST相当于把js代码所有语法解析为抽象的树.用处大概就是逆向的时候把混淆的代码还原逻辑,方便看逻辑.,以下所有笔记都是抄自悦来客栈的老板的星球 jsvmp相关请看 JSVMP js加密

一. 直观地看AST

AST在线解析
用这个网站输入JS源码就可以看到AST解析出来的语法树了

  • type节点类型
  • 结构path->node->type
  • start,end节点起始.结尾位置
  • program源代码
  • comments注释
    具体还是输入代码到网站上看对应的类型,这样比较快速学习
    偷图地址在水印
    AST学习笔记 至少入个大门_第1张图片

二.代码框架

来源:悦来客栈的老板的星球


//babel库及文件模块导入
const fs = require('fs');

//babel库相关,解析,转换,构建,生产
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const types = require("@babel/types");
const generator = require("@babel/generator").default;

//读取文件
let encode_file = "./encode.js",decode_file = "./decode_result.js";
if (process.argv.length > 2)
{
  encode_file = process.argv[2];
}
if (process.argv.length > 3)
{
  decode_file = process.argv[3];
}

let jscode = fs.readFileSync(encode_file, {encoding: "utf-8"});
//转换为ast树
let ast    = parser.parse(jscode);

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


//some function code

//调用插件,处理源代码
traverse(ast,visitor);

//生成新的js code,并保存到文件中输出
let {code} = generator(ast);
fs.writeFile('decode.js', code, (err)=>{});

三. bable库解析

这个抄的好啊https://blog.csdn.net/weixin_33826609/article/details/93164633
复制的是官方文档,但是上面的搜索比较方便
这个介绍的挺好https://www.cnblogs.com/YikaJ/p/10073540.html

我的笔记:

  • Bable三大步:解析(parse),转换(transform),生成(generate)
  1. 解析
    解析步骤接收代码并输出 AST。 这个步骤分为两个阶段: 词法分析 -> 语法分析。
    然而我们看到的一般都是语法分析后的

  2. 转换(核心)
    简单来说就是对节点的增删改查

  3. 生成
    把AST还原成源码,深度优先遍历AST,然后构建转换后的代码字符串

  • 其他看文章吧…没得精简的了.还需走一下文章内的demo流程

四. 直接实战

建议每个实践都对比一下AST树,大概就可以理解了

1.处理十六进制、中英文Unicode字符串或数值

从\x6c\x6f\x67 -> log

混淆代码


var _0x1201 = ['\x6c\x6f\x67', '\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72\x6c\x64\x21'];
(function(_0x2b91f5, _0x120157) {
    var _0x2e36e7 = function(_0x40e9dc) {
        while (--_0x40e9dc) {
            _0x2b91f5['\x70\x75\x73\x68'](_0x2b91f5['\x73\x68\x69\x66\x74']());
        }
    };
    _0x2e36e7(++_0x120157);
}(_0x1201, 0xa3));
var _0x2e36 = function(_0x2b91f5, _0x120157) {
    _0x2b91f5 = _0x2b91f5 - 0x0;
    var _0x2e36e7 = _0x1201[_0x2b91f5];
    return _0x2e36e7;
};
function hi() {
    var _0x379bb0 = _0x2e36;
    console[_0x379bb0('\x30\x78\x31')](_0x379bb0('\x30\x78\x30'));
}
hi();

解混淆代码
输入到第二点的框架中运行

const transform_literal = {
  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;
    }
  },
}

解析:
适配是乱码的部分,删除掉extra节点(在AST中不是必须的),然而其value节点是可阅读的字符串,所以会显示解析完得字符串,具体去AST解析那儿看看就知道.

2. MemberExpression和ObjectProperty key值Literal化

从 a.length -> a[‘length’]

源代码:

var a = b.length;

解混淆代码

const member_property_literals = {
  MemberExpression:
  {
    exit({node})
    {
      const prop = node.property;
      if (!node.computed && types.isIdentifier(prop))
      {
        node.property = types.StringLiteral(prop.name);
        node.computed = true;
      }
    }
  },  
  ObjectProperty: 
  {
    exit({node})
    {
      const key = node.key;
      if (!node.computed && types.isIdentifier(key))
      {
        node.key = types.StringLiteral(key.name);
      }
    }
  },  
}

解析:
通过把Identifier构建为StringLiteral实现

3.优化无实参的自执行函数

!(function()
{
a = b;
})();
->
a = b;

反混淆代码

const simplify_auto_exec = {
    UnaryExpression(path) {
        let { operator, argument } = path.node;
        if (operator != "!" || !types.isCallExpression(argument)) return;//找到"!"和Call类型的
        let { arguments, callee } = argument;
        if (arguments.length != 0 || !types.isFunctionExpression(callee)) return;
        let { id, params, body } = callee;
        if (id != null || params.length != 0 || !types.isBlockStatement(body)) return;
        path.replaceWithMultiple(body.body);
    },
}

解析:
其实就是将函数体里面的内容替换整个表达式

1.因为是替换表达式,因此这里遍历UnaryExpression节点,这个可以在对照网站看的很清楚。
2.特征判断,UnaryExpression表达式的operator节点为符号"!";而argument节点是CallExpression类型
3.再次对argument节点进行特征判断,具体见代码。
4.满足条件后进行替换。

4.JavaScript全局函数计算值替换

var a = parseInt(“12345”,16),b = Number(“123”),c = String(true),d = unescape(“hello%2CAST%21”);
eval(“a = 1”);
->
var a = 74565,b = 123,c = “true”,d = “hello,AST!”;
eval(“a = 1”);

反混淆代码

const evaluate_global_func = 
{
  "CallExpression"(path)
  {
    let {callee,arguments} = path.node;
    if (!types.isIdentifier(callee) || callee.name == "eval") return; //跳过eval,限定替换Identifier
    if (!arguments.every(arg=>types.isLiteral(arg))) return;//需要不是Literal
    
    let func = global[callee.name];

    if (typeof func !== "function") return;//获取是否自带的对象,不是的话跳过
    
    let args = [];
    arguments.forEach((ele,index) =>{args[index] = ele.value;});//有些值可能是数组
    
    let value = func.apply(null,args);//主动调用.例如->Number('123')

    if (typeof value == "function") return;//调用完如果是函数就过滤
    path.replaceInline(types.valueToNode(value));
  },
}

解析:
开始变得基操了…但是需要对节点的理解才能不误伤不想修改的节点…

5.其它

Unicode转中文,代码压缩,删除注释,删除空行
删除空行和空语句(比较简单,放在一起)
删除辣鸡代码
删除屎代码

直接看文章吧,这部分属于善后工作

6.还原简单的CallExpression 类型 实战

这个时候需要插入蔡老板的node,path入门教学
自己写的代码,应该可以适配更多类型

源码

let a = 111;
var all = function () {
  var Xor = function (p, q) {
    return q + p;
  };
    let b = 222;
  let a = Xor (b,a);
};
var all2 = function () {
  let b = 444;
};

反混淆源码


const call2Express = {
    CallExpression(path) {
        let { arguments, callee } = path.node;
        let arg = [];
        arguments.every(element => {//获取这个Call的所有变量
            if (types.isLiteral(element)) {//常规类型
                arg.push(element.value);
            } else if (types.isIdentifier(element)) {//变量的话找一下有没有
                let elementName = element.name;
                path.findParent((pathTemp) => {
                    if (pathTemp.isVariableDeclaration()) {
                        // console.log(pathTemp.container);
                        let argTemp = '';
                        pathTemp.container.every(node1 => {//有的话存到数组中
                            node1.declarations.every(node2 => {
                                if (node2.id.name === elementName && types.isLiteral(node2.init)) {
                                    argTemp = node2.init.value;
                                    console.log(argTemp);
                                    return false;
                                }
                                return true;
                            })
                            return true;
                        });
                        if (argTemp !== '') {
                            arg.push(argTemp);
                            return true;
                        }
                    }
                });
            } else {
                return false;
            };
            return true;
        })
        if (arg.length === arguments.length) {
            let functionName = callee.name;
            path.findParent((pathTemp) => {//找到对应的方法
                if (pathTemp.isVariableDeclaration()) {
                    pathTemp.container.forEach(node => {
                        if (node.declarations.length != 1) return;
                        if (node.declarations[0].id.name === functionName) {
                            let { code } = generator(node);
                            let argStr = '';
                            for (let index = 0; index < arg.length; index++) {
                                const element = arg[index];
                                argStr += (',' + element);
                            }
                            argStr = argStr.substring(1);
                            let res = eval(`${code} ${functionName}(${argStr});`);//运行结果
                            path.replaceInline(types.valueToNode(res));//替换路径
                            // console.log(res);
                            return;
                        }
                    });
                    return;
                }
            });
        }
    },
}

解析在注释,目前可以一直,实际还可以深入,例如把一些不是调用的常量相加."a=b+c,b=1,c=2"这种也加起来

6.

五.小知识点

全在蔡老板的星球和他公众号学的

  • 在AST获取代码,构建Function并执行获取结果
//获取赋值语句左边的 a
let expression = body[1].node.expression;
let name = expression.left.name;
//根据Function函数进行构造
let code = path.toString() + "\nreturn " + name;
//构造并运行,即可得到for循环的结果
let func = new Function("",code);
let value = func();
  • 待补充

笔记 https://mp.weixin.qq.com/s/yFcSXmXChGNaT7wPlaj0uw

构建节点的时候查询 -> https://babeljs.io/docs/en/babel-types

binding (只有变量定义和函数定义有)

变量定义:var a = 123; 这里的 a 就拥有 binding。
函数定义 function test(a,b,c) {}; a,b,c 有binding

  • let binding = scope.getBinding(name) 获取binding
  • binding.referencePaths可以获取到所有调用的地方,然后进行替换
  • binding.constant 表示是否可被修改, true才能改
  • binding.path 用于定位初始拥有binding的path;
  • binding.referenced 用于判断当前变量是否被引用,true表示代码下面有引用该变量的地方,false表示没有地方引用该变量。注意,引用和改变是分开的。
  • binding.constantViolations 它是一个Array类型,包含所有改变的path,多用于判断
path https://mp.weixin.qq.com/s/gd7anzKk4dFYuv_bGEpNOg
  • path.replaceWith 替换单个node
  • path.replaceWithMultiple 替换nodes
  • path.replaceInline 这个好像比较通用
  • path.insertBefore 在当前节点前面插入新的节点
  • path.insertAfter 在当前节点后面插入新的节点
  • path.traverse 在当前节点下遍历其他的节点
  • path.get 获取当前路径下的子孙节点
  • path.getSibling 根据key值来获取节点
  • path.getPrevSibling 获取当前path的前一个兄弟节点
  • path.getAllPrevSiblings 获取当前path的所有前兄弟节点
  • path.getNextSibling 获取当前path的后一个兄弟节点
  • path.getAllNextSiblings 获取当前path的所有后兄弟节点
  • path.evaluate 用于计算表达式的值
  • path.findParent 向上查找满足回调函数特征的path,即判断上级路径是否包含有XXX类型的节点
  • path.find 功能与 path.findParent 方法一样,只不过从当前path开始进行遍历
  • path.getFunctionParent 获取函数类型父节点,如果不存在,返回 null。
  • path.getStatementParent 获取Statement类型父节点,这个基本上都会有返回值,如果当前遍历的是 Program 或者 File 节点,则会报错。
  • path.getAncestry 获取所有的祖先节点,没有实参,返回的是一个Array对象。
  • path.isAncestor 判断当前遍历的节点是否为实参的祖先节点
  • path.isDescendant 判断当前遍历的节点是否为实参的子孙节点
scope
  • scope.block 表示当前作用域下的所有node,参考上面的 this.block = node;
  • scope.dump() 输出当前每个变量的作用域信息。调用后直接打印,不需要加打印函数
  • scope.crawl() 重构scope,在某种情况下会报错,不过还是建议在每一个插件的最后一行加上。
  • scope.rename(oldName, newName, block) 修改当前作用域下的的指定的变量名,oldname、newname表示替换前后的变量名,为字符串。注意,oldName需要有binding,否则无法重命名。
  • scope.traverse(node, opts, state) 遍历当前作用域下的某些(个)插件。和全局的traverse用法一样。
  • scope.getBinding(name) 获取某个变量的binding,可以理解为其生命周期。包含引用,修改之类的信息

你可能感兴趣的:(js,逆向分析,学习,javascript,前端)