AST还原混淆实战
AST还原混淆实战

AST还原混淆实战

1.整体代码结构

本章以一个实际案例,来介绍 AST 在还原 JavaScript (以下简称JS) 混淆上的应用。

当然在还原之前,必要的协议分析以及网站使用了哪些混淆方案,还是要分析清楚的。

AST 还原后的 JS 文件并不是只能静态分析,如果还原做的足够优秀,还可以替换到原网站中进行动态调试。一起来体验一下AST 的强大之处吧。

整体代码结构

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

const jscode = fs.readFileSync("./demo.js", {
    encoding: "utf-8"
});

let ast = parser.parse(jscode);
//此处对AST进行一系列的操作
/*
*
* */
let code = generator(ast).code;
fs.writeFile('./demoNew.js', code, (err)=>{});

首先使用require加载必要的以来,然后读取JS文件中的代码,用parser组件解析成ast,接着对ast进行一系列的操作,最后把ast转为字符串,保存到新文件。

2.字符串解密与去除数组混淆

根据前面几小节的分析可知,原代码中最开始处定义了一个大数组,紧接着是一个用于还原数组顺序的匿名自执行的函数,然后跟着一个字符串解密的函数。而要调用字符串解密函数,又必须先得到大数组和用于还原数组顺序的匿名自执行函数。来看一下原代码的整体结构:

console.log(ast.program.body):
/*
    Node {type:'VariableDeclaration', ... declarations: Array(1)}
    Node {type:'ExpressionStatement', ... expression: Node}
    Node {type:'Variablebeclaration', ... declarations: Array(l)}
    Node {type:'Expressionstatement', ... expression:Node}
    Node {type:'VariableDeclaration', ... declarations: Array(l)}
    Node {type:'VariableDeclaration', ... declarations: Array(l)}
    Node {type:'VariableDeclaration', ... declarations: Array(1)}
    Node {type:'ExpressionStatement', ... expression: Node}
*/

要完成字符串解密,先要得到原代码中的前三部分代码,其中第三部分为字符串解密函数,还需要拿到字符串解密函数的名字。

//拿到解密函数所在节点
let stringDecryptFuncAst = ast.program.body[2];
//拿到解密函数的名字
let DecryptFuncName = stringDecryptFuncAst.declarations[0].id.name;
//新建一个 AST,把原代码中的前三部分,加入到 body 节点中
let newAst = parser.parse('');
newAst.program.body.push(ast.program.body[0]);
newAst.program.body.push(ast.program.body[1]);
newAst.program.body.push(stringDecryptFuncAst);
//把这三部分的代码转为字符串,由于存在格式化检测,需要指定选项,来压缩代码
let stringDecryptFunc = generator(newAst, {compact: true}).code;
//将字符串形式的代码执行,这样就可以在 nodejs 中运行解密函数了
eval(stringDecryptFunc);

这里再次强调,由于源代码中存在格式化检测和内存爆破代码,所以上述代码中,生成字符串代码的时候,需要指定选项,使用压缩后的代码来执行,否则内存会溢出。

现在nodejs中已经有解密函数了,那么接下去就可以直接计算类似_0x2ba9(0x0)这种节点,并用结果替换它,代码如下:

traverse(ast, {
    //遍历所有变量
    VariableDeclarator(path){
        //当变量名与解密函数名相同时,就执行相应操作
        if(path.node.id.name == DecryptFuncName){
            let binding = path.scope.getBinding(DecryptFuncName);
            //如果binding存在,则执行...map遍历
            binding && binding.referencePaths.map(function(v){
                v.parentPath.isCallExpression() &&
                v.parentPath.replaceWith(t.stringLiteral(
                    //节点转代码,得到字符串解密后的结果
                    eval(v.parentPath + '')));
            });
        }
    }
});
//字符串解密以后,原代码的前三部分就没用了,可以去掉
ast.program.body.shift();
ast.program.body.shift();
ast.program.body.shift();
/*_0x2ba9(0xa1) 在AST Explorer 网站中解析后的结构为
    Node{
        type: 'CallExpression',
        ...
        callee: Node { type: 'Identifier', ... name: '_0x2ba9"},
        arguments: [
            Node { type: 'StringLiteral', ... value:  'Oxal' }
        ]
    }
*/

3.剔除花指令

1.花指令剔除思路

示例代码如下:

var _0x1f20d3 = { ...
        'ZFmTI': function (_0x4e4bbb, _0x19e1fa) {
            return _0x4e4bbb + _0x19e1fa;
        },
        'aOJeX': "/yzmtest/get.php?t=",  ... 
}
var _0x22b277 = { ...
        'etrqc': function (_0x3fb552, _0x5f9394) {
            return _0x1f20d3["ZFmTI"](_0x3fb552, _0x5f9394);
          },
        'oiFIc': _0x1f20d3["aOJeX"], ...
}
// _0x22b277['oiFIc'] 字符串花指令'
// _0x22b277['etrqc'] 函数花指令

用上述例子来说明花指令剔除的思路。大体上可以分为两种情况。

字符串花指令的别除

对于字符串花指令 _0x22b277['oiFIc'],可以追历所有 MemberExpression节点,取出 object 节点名和 property 节点值。在 ObjectExpression 节点中找到对应的值,如果类型还是为 MemberExpression,就说明还需要继续找,继续取出object 节点名和 property 节点值,继续在 ObjectExpression 节点中找到对应的值,直到找到的值类型为 StringLiteral,就进行替换即可。因此需要用到递归。

函数花指令的去除

对于函数花指令 _0x22b277['etrgc'],也是追历所有MemberExpression 节点取出 object 节点名和 property 节点值。在 ObjectExpression 节点中找到对应的值,如果类型为 FunctionExpression 并且函数体内部有 Membertxpression 节点,就说明还需要继续找,直到找到类型为 FunctionExpression 并且函数体内部没MemberExpression 节点,才是最终需要的节点。

在ObjectExpression节点中找到对应的值有一个比较简单的方式,可以在nodejs中定义一个totalObj对象,然后解析原始代码中所有的ObjectExpression,加入到totalObj对象中,最后把totalObj对象变成如下结构:

{
    _0x1f20d3: {
        'ZFmTI': Node {...},
        'aOJex': Node {...},
        'EDRDI': Node {...},
        ...
    },
    _0x22b277: {
        'etrqc': Node {...},
        'oiFIc': Node {...},
        ...
    },
    ...
}

在nodejs中组合出这样的结构后,如果要获取 _0x22b277['oiFIc']的定义部分的节点,只需要使用totalObj['_0x22b277']['oiFIc']来获取Node节点。生成totalObj对象的代码如下:

var totalObj = {};
function generatorobj(ast) {
    traverse(ast, {
        VariableDeclarator(path) {
            //init 节点为ObjectExpression 的时候,就是需要处理的对象
            if(t.isObjectExpression(path.node.init)){
                //取出对象名
                let objName = path.node.id.name;
                //以对象名作为属性名在 totalObj 中创建对象
                objName && (totalObj[objName] = totalObj[objname] || {});
                //解析对象中的每一个属性,加入到新建的对象中去,注意属性值依然是 Node 类型
                totalObj[objName] && path.node.init.properties.map(
                    function(p){
                        totalObj[objName][p.key.value] = p.value;
                                          });
            }
        }
    });
    return ast
}
ast = generatorObj(ast);

Node {
    type: 'VariableDeclarator', ...
    id: Node { type: 'Identifier', ...name: '_x1f20d3'},
    init: Node {type: 'ObjcetExpression', ...
                    properties: [[Node], [Node], [Node], ...]}
}
// properties中的每一个ObjectProperty结构为
Node {
    type: 'ObjectProperty', ...
    method: false,
    key: Node { type: 'StringLiteral', ...value: 'NmgQU'},
    computed: false,
    shorthand: false,
    value: {type: 'StringLiteral', value: 'submit', ...}
}

2.字符串花指令的剔除

现在可以着手去除花指令了,字符串花指令比较容易去除,遍历 ObjectExpression 节点的properties 属性,每得到一个 ObjectProperty的 value 值,都递归找到真实的字符串后进行替换。代码如下:

traverselast, {
    VariableDeclarator (path) {
        if(t.isObjectExpression (path ,node,init)) {
            path.node.init.properties.map(function (p) {
                let realNode = findRealValue(p.value);
                realNode && (p.value = realNode);
            });
        };
    }
});
/*
    var _0x1f20d3 = { 'aOJex': /yzmtest/get.php?t=", ...}
    var 0x22b277 = { 'oiFIc': _0x1r20d3{'aOJex''}, ... }
    //处理后变为如下形式,不管代码中调用哪个对象中的属性,都是真实字符串,最后统一替换即可
    var _0x1r20d3 = { 'aOJex': '/yzmtest/get,php?t=', ... }
    var _0x22b277 = { 'oiFIc': '/yzmtest/get.php?t=', ... }
*/

那么 findRealValue 函数如何实现呢? 根据前面对原代码的分析可知,ObjectProperty的value 值有三种类型,分别是MemberExpression,FunctionExpression 和 StringLiteral。先不处理 FunctionExpression 节点,而 StringLiteral 节点就是真实的字符串也不用处理所以只需处理 MemberExpression 节点。实现的代码如下:

function findRealValue(node) {
    if (t.isMemberExpression(node)) {
        let objName = node.object.name;
        let propName = node.property.value;
        if (totalObj[objName][propName]) {
            //递归
            return findRealValue(totalObj[objName][propName]);
        } else {
            return false;
        }
    } else {
        return node;
    }
}
//剔除字符串花指令以后,更新totalObj对象
ast = generatorObj(ast);

/*
    var _0x1f20d3 = { 'a0JeX': '/yzmtest/get.php?t=', ...}
    var _0x22b277 = { 'a0JeX': '/yzmtest/get.php?t=', ...}
*/

以上述代码中的注释为例,假如传给 findReaValue 的参数为 _0x1f20d3['aoJex']节点,这是一个 MemberExpression 节点,因此会取出 '_0x1f20d3',赋值给 objName,取出'aoJeX'赋值给propName,然后获取totalObj['0x1t20d3']['aoJex']中存放的Node节点,继续传入到findRealValue 中进行递归。这时候传入的是'/yzmtest/get.php?t=',并非成员表达式,因此直接返回原节点。一路返回,最后执行 realNode && (p.value = realNode)进行节点替换。
上述过程只是把 ObjectExpression 节点的属性值都处理了一下,最后还要对代码中引用的地方都进行替换(替换之前记得更新 totalObj 对象)。实现的代码如下:

traverse(ast, {
    VariableDeclarator(path) {
        if (t.isObjectExpression(path.node.init)) {
            path.node.init.properties.map(function (p) {
                let realNode = findRealValue(p.value);
                realNode && (p.value = realNode);
            });
        };
    }
});

遍历所有MemberExpression节点,去除objName和prop Name,只要对应节点在totalObj对象中存在,并且类型为StringLiteral,就进行了替换。

以原始代码中经过字符串解密以后的某段代码为例,还原前后的代码分别为:

//还原前
var _0x167f85 = _0x120d3["oseqj"]["split"]('|'), _0x57e351 = 0x0;
//还原后
var 0x167E85 ="2|9|3|10|12|0|15|7|6|4|1|8|16|5|11|17|13|14|18"["split"]('|')

此时完整代码:

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

const jscode = fs.readFileSync("./demo.js", {
    encoding: "utf-8"
});

let ast = parser.parse(jscode);

//拿到解密函数所在节点
let stringDecryptFuncAst = ast.program.body[2];
//拿到解密函数的名字
let DecryptFuncName = stringDecryptFuncAst.declarations[0].id.name;
//新建一个 AST,把原代码中的前三部分,加入到 body 节点中
let newAst = parser.parse('');
newAst.program.body.push(ast.program.body[0]);
newAst.program.body.push(ast.program.body[1]);
newAst.program.body.push(stringDecryptFuncAst);
//把这三部分的代码转为字符串,由于存在格式化检测,需要指定选项,来压缩代码
let stringDecryptFunc = generator(newAst, {compact: true}).code;
//将字符串形式的代码执行,这样就可以在 nodejs 中运行解密函数了
eval(stringDecryptFunc);

traverse(ast, {
    //遍历所有变量
    VariableDeclarator(path){
        //当变量名与解密函数名相同时,就执行相应操作
        if(path.node.id.name == DecryptFuncName){
            let binding = path.scope.getBinding(DecryptFuncName);
            binding && binding.referencePaths.map(function(v){
                v.parentPath.isCallExpression() &&
                v.parentPath.replaceWith(t.stringLiteral(eval(v.parentPath + '')));
            });
        } 
    }
});

ast.program.body.shift();
ast.program.body.shift();
ast.program.body.shift();

var totalObj = {};
function generatorObj(ast){
    traverse(ast, {
        VariableDeclarator(path){
            //init 节点为 ObjectExpression 的时候,就是需要处理的对象
            if(t.isObjectExpression(path.node.init)){
                //取出对象名
                let objName = path.node.id.name;
                //以对象名作为属性名在 totalObj 中创建对象
                objName && (totalObj[objName] = totalObj[objName] || {});
                //解析对象中的每一个属性,加入到新建的对象中去,注意属性值依然是 Node 类型
                totalObj[objName] && path.node.init.properties.map(function(p){
                    totalObj[objName][p.key.value] = p.value;
                });
            };
        }
    });
    return ast;
}
ast = generatorObj(ast);

traverse(ast, {
    VariableDeclarator(path) {
        if (t.isObjectExpression(path.node.init)) {
            path.node.init.properties.map(function (p) {
                let realNode = findRealValue(p.value);
                realNode && (p.value = realNode);
            });
        };
    }
});

function findRealValue(node) {
    if (t.isMemberExpression(node)) {
        let objName = node.object.name;
        let propName = node.property.value;
        if (totalObj[objName][propName]) {
            return findRealValue(totalObj[objName][propName]);
        } else {
            return false;
        }
    } else {
        return node;
    }
}

ast = generatorObj(ast);

traverse(ast, {
    MemberExpression(path) {
        let objName = path.node.object.name;
        let propName = path.node.property.value;
        totalObj[objName] && t.isStringLiteral(totalObj[objName][propName]) &&
        path.replaceWith(totalObj[objName][propName]);
    }
});

let code = generator(ast).code;
fs.writeFile('./demoNew.js', code, (err)=>{});

3.函数花指令的剔除

函数花指令的剔除原理与字符串花指令的剔除相似,只不过需要更改递归的函数,实现的代码如下:

traverse(ast, {
    VariableDeclarator(path) {
        if (t.isObjectExpression(path.node.init)) {
            path.node.init.properties.map(function (p) {
                let realNode = findRealFunc(p.value);
                realNode && (p.value = realNode);
            });
        };
    }
});
function findRealFunc(node) {
    if (t.isFunctionExpression(node) && node.body.body.length == 1) {
        let expr = node.body.body[0].argument.callee;
        if (t.isMemberExpression(expr)) {
            let objName = expr.object.name;
            let propName = expr.property.value;
            if (totalObj[objName]) {
                return findRealFunc(totalObj[objName][propName]);
            } else {
                return false;
            }
        }
        return node;
    } else {
        return node;
    }
}
//去除函数花指令以后,更新一下 totalObj 对象
ast = generatorObj(ast);

以上述代码中的注释为例,将它们的特点归纳为以下 4 点:

  1. 它们都是 FunctionExpression 节点。
  2. 函数体只有一个 ReturnStatement 节点
  3. ReturnStatement 节点的 argument 属性类型如果不是 CallExpression,那么该节点就是最终需要的节点,例如上述注释中的 ZFmTI函数。
  4. ReturnStatement 节点的 argument 属性类型如果是 CallExpression,但是其 calle属性类型不是 MemberExpression,那么该节点也是最终需要的节点,例如上述注释中的EDRDI 函数。

综上所述,findRealFunc 函数最终需要递归的节点的特点为: ReturnStatement 节点的argument 属性类型为 CallExpression,并且 CallExpression 节点的 callee 属性类型为MemberExpression.

findRealFunc的实现方式为:

  1. 筛选出类型为 FunctionExpression,且函数体只有一行代码的节点,其余节点直接返回原节点。
  2. 取出 ReturnStatement 节点的 argument 属性中的 callee。
  3. 如果第 2步得到的节点类型为 MemberExpression,就取出对象名和属性名,从totalObi 中获取对应的节点,传人 findRealFunc 函数进行递归。
  4. 如果第2步得到的是 undefined,就说明不是 CallExpression 节点,该节点已经是最终需要的节点,直接返回原节点即可。
  5. 如果第 2 步得到的不是 undefined,也不是 MemberExpression 节点,该节点已经是最终需要的节点,直接返回原节点即可。
  6. 一直返回,执行 realNode && (p.value = realNode)替换节点

上述过程只处理 ObjectExpression 节点的属性值,处理后的效果如下:

var _0x1f20d3 ={
    'ZFmTI': function (_0x4e4bbb, _0x19elfa){
        return  _0x4e4bbb + _0x19elfa;
    }
    'EDRDI': function( _0x247161,_0x4c4leb){
        return _0x247161(_0x4c4leb);
    },
    ...
};
var _0x22b277 = {
    'etrqc': function (_0x3fb552,_0x5f9394) {
        return _0x1f20d3['ZFmTI'](_0x3fb552, _0x5f9394);
    };
    ...
};
/*上面的代码被修改为
var _0xlf20d3 = {
    'ZFmTI': function (_0x4e4bbb, _0x19elfa) {
        return _0x4e4bbb + _0x19elfa;
    },
    'EDRDI': function( _0x247161, _0x4c4leb) {
        return _0x247161(_0x4c4leb);
    },
    ...
};

var _0x22b277 = {
    'etrqc': function (_0x4e4bbb, _0x19elfa) {
        return _0x4e4bbb, _0x19elfa;
    },
    ...
};
*/

举个例子,假如代码中的引用为_0x22b277['etrqe'](1000,2000),这时候应当遍历CallExpression 节点,判断 callee 的节点类型为MemberExpression,就去 totalObj对象中查找对应代码节点。取出该代码节点里面的 ReturnStatement 中的argument 属性,然后判断节点类型为 BinaryExpression,就取出其中的 operator 属性,与传入的实参1000和2000构建新的 BinaryExpression 节点,替换整个_0x22b277['etrqe'](1000, 2000)

再举个例子,假如代码中的引用为_0x1f20d3['EDRDI'](0x247162, 2000),这时候应当遍历CallExpression 节点,判断 callee 的节点类型为 MemberExpression,就去 totalObj对象中查找对应代码节点。取出该代码节点里面的 ReturnStatement 中的argument 属性,然后判断节点类型和CallExpression,就用传入的实参构建一个新的CallExpression 节点,_0x247162 作为callee,2000作为argument,来替换掉原先的整个节点。

因此,最后对代码中引用的地方进行替换(替换之前记得更新 totalObj 对象)的代码如下:

traverse(ast, {
    CallExpression(path){
    //callee 不为MemberExpression 的节点,不做处理
    if(!t.isMemberExpression(path.node.callee)) return;
    //取出对象名和属性名
    let objName = path.node.callee.object.name;
    let propertyName = path.node.callee.property.value;
    //如果在 totalObj 中有相应节点,就是需要进行替换的
    if(totalObj[objName] && totalObj[objName][propertyName]){
        //totalObj 中存放的是函数节点
        let myFunc = totalObj[objName][propertyName];
        //在原代码中,函数体其实就一行 return 语句,取出其中的argument节点
        let returnExpr = myFunc.body.body[0].argument;
        //判断argument 节点类型,并且用相应的实参来构建二项式或者调用表达式
        //然后替换当前遍历到的整个 CallExpression 节点即可
        if(t.isBinaryExpression(returnExpr)){
            let binExpr = t.binaryExpression(returnExpr.operator,
                      path.node.arguments[0], path.node.arguments[1]);
            path.replaceWith(binExpr);
        }else if(t.isCallExpression(returnExpr)){
            //把arguments 数组中的下标为1和以后的成员,放入 newArray 中
            let newArray = pathnode.arguments.slice(1);
            let callExpr = t.callExpression(path.node.arguments[0],
                                                        newArray);
            path .replaceWith(callExpr);
        }
    }
    }
});
//花指令都别除后,原代码中的 bjectExpression 就可以删除了。当然最好是判断下,没有引用,再删
traverse(ast, {
    VariableDeclarator(path){
        if(t.isObjectExpression(path.node.init)){
            path.remove();
        };
    }
});

以还原代码中,经字符串解密以后的某段代码为例,还原前后的代码分别为:

//还原前
this["$strlen"] = _0x22b277["hdoPm"](Math["floor"]( 0x22b277["cdTwA"](Math["random"](),0x5)), 0x5);
//还原后
this["$strlen"] = Math["floor"](Math["random"]() * 0x5) + 0x5;

4.还原流程平坦化

1.获取分发器

注意,还原流程平坦化之前,应当先进行字符串解密以及剔除花指令。在还原流程平坦化的过程中,需要先获取分发器,因为分发器中记录着代码原先的真实顺序。以 1.5小节中的代码为例,来看一下分发器在 AST 中的结构:

Node {
    type:'MemberExpression',
    ...
    object: Node{ type: 'stringLiteral', ... value:'1|2|4|7|5|3|8|0|6'},
    property: Node { type: 'StringLiteral', ... value: 'split'} 
    computed: true
}

从上述结构中可以看出,只要遍历 MemberExpression 节点,如果其中的 object 节点为StringLiteral类型,property 节点为StringLiteral类型并且 value 为'split'就是分发器所在的 MemberExpression 节点。其中 path.node.object.value 就是记录着代码原先真实顺序的字符串。因此,获取分发器的代码如下:

traverse(ast, {
    MemberExpression (path) {
        if (t.isStringLiteral(path.node.object) && 
                t.isStringLiteral(path.node.property, {value: 'split'})){
            console.log(path.node.object.value);
        }
    }
});
// '1|2|4|7|5|3|8|0|6'

2.解析整个switch

把AST 中的 switch 结构,解析到 nodejs 的数组中。这样在复原代码顺序的时候,就可以快速准确地取到对应的代码节点。想要解析整个 switch,需要先来学习下 WileStatement节点和 SwitchStatement 节点的结构,以 1.5小节中的代码为例:

//WhileStatement 的结构
Node {
    type: 'Whilestatement', ...
    test: Node {
        type: 'UnaryExpression', ...
        operator: '!',
        prefix: true,
        argument: Node {
            type: 'UnaryExpression', ...
            operator: '!',
            prefix: true,
            argument : [Node]
        }
    },
        body:Node { ... }
}

WhileStatement 节点的 test 属性就是 while 循环的条件,body 属性就是 while 循环体中的代码,接着来观察下 body 体中的结构:

Node {
    type: 'BlockStatement' , ...
    body: [Node {
        type: 'SwitchStatement', ...
        discriminant: Node { type: 'MemberExpression', ...
            object: Node { type: 'identifier', ... name:  '_0x27fd60'},
            property: Node {
                type: 'UpdateExpression', ...
                operator: '++',
                prerix: false,
                argument : [Node]
            },
            computed: true
        },
        cases: [
            Node {
                type: 'witchCase', ...
                consequent: [Array],
                test: Node { type: 'stringLiteral', ... value: '0' }
            },
            ...
        ]
    },
      Node { type 'BeakStatenent', ...label: null}
  ],
  directives: []
}

SwitchStatement 节点中的 discriminant 属性,就是 switch 语句中用来控制跳转的表达式,cases 属性中存放着 switch 中所有的 case代码块。SwitchCase 节点的 test 属性就是 case 后面跟的值,consequent 属性就是具体的代码块。

熟悉了这些结构以后,来看一下解析整个 switch 的代码:

traverse(ast, {
    MemberExpression(path) {
        if (t.isStringLiteral(path.node.object) && 
            t.isStringLiteral(path.node.property, {value: 'split'})) {
            //找到类型为 VariableDeclaration 的父节点
            let varPath = path.findparent (function (p) {
                return t.isVariableDeclaration(p);
            });
            //获取下一个同级节点
            let whilePath = varPath.getsibling(varPath.key + 1);
            //解析整个 switch
            let myArr = [];
            whilePath.node.body.body[0].cases.map(function (p) {
                myArr[p.test.value] = p.consequent[0];
            });
        }
    }
});

来介绍一下上述代码的实现过程,首先是沿用4.1 小节中的代码,定位到分发器所在的MemberExpression 节点。接着要定位到 switch 节点,才能解析整个 switch 节点。先找到WhileStatement节点,来看一下源代码在AST中的整体结构:

Node {
  type: 'variableDeclaration',
  ...
  declarations: [
    Node {
      type: 'VariableDeclarator',
      ...
      id: Node { type: 'Identifier', ... name:'_0x27fd60',
      init: Node { type: 'CallExpression', ...
        callee: Node {
          type:'MemberExpression',
          ...
          object: Node { type:'stringLiteral', ...value:'1|2|4|7|5|3|8|0|6'},
          property: Node { type: 'stringLiteral', ... value: 'split' },
          computed: true
        },
        arguments: [ [Node] ]
        }
    },
    Node { type: 'VariableDeclarator' ...}
    ],
    kind: 'var'
}
Node { type: 'WhileStatement', ...}           

从上述结构中可以看出,WileStatement 节点是 VariableDeclaration 节点的下一个同级节点,而当前定位到的是分发器所在的 MemberExpression 节点。因此在本小节最开始的代码中,通过 path.findParent 找到分发器所在节点的类型为 VariableDeclaration 的父节点,赋值给 varPath。然后通过 varPath.getSibling 获取到下一个同级节点即可。

接着通过 WhileStatement 节点,找到其下属的 switch 节点中的 cases 数组,也就是存放着所有 case 代码块的节点。最后遍历 cases 数组,用 case 后面的值作为数组索引,case中具体的代码节点作为对应的数组的成员,把整个 switch 解析到预先定义的 myArr 数组中。

3.复原语句顺序

一切准备工作都已就绪,接下去就可以复原代码顺序了。实现的代码也很简单:

let parentPath = whilePath.parent;
varPath.remove ();
whilePath.remove();
// path.node.object.value 取到的是1|2|4|7|5|3|8|0|6
let shufferArr = path.node.object.value.split ("|");
shufferArr.map (function (v) {
    parentPath.body.push(myArr[v]);
});

在上述代码中,先找到 WhileStatement 的父节点,也就是 BlockStatement 节点。然后把分发器所在的节点和WhileStatement 节点整个移除,其实就相当于是把 BlockStatement的 body 节点清空。然后把存有代码真实顺序的字符串,分割成数组 shufferArr。遍历该数组,从之前解析好的 myArr 中,取出对应索引的代码节点,塞回到 BlockStatement 的 body节点中。

单个 switch 流程平坦化还原完成了,现在要应用到整个JS 文件中去。由于原代码中存在嵌套的 switch 流程平坦化,为了防止顺序错乱,笔者在这里采用每遍历一轮 ast,只处理一个 switch 流程平坦化的方案,完整的还原代码如下:

//粗暴的多循环几次,而不用去判断源代码中到底有多少个switch流程平坦化
for(let i = 0; i < 20; i++){
    traverse(ast, {
        MemberExpression(path) {
            if (t.isStringLiteral(path.node.object) &&
                    t.isStringLiteral(path.node.property, {
                        value: 'split'
                    })) {
                //找到类型为 VariableDeclaration 的父节点
                let varPath = path.findParent(function (p) {
                        return t.isVariableDeclaration(p);
                    });
                //获取下一个同级节点
                let whilePath = varPath.getSibling(varPath.key + 1);
                //解析整个 switch
                let myArr = [];
                whilePath.node.body.body[0].cases.map(function (p) {
                    myArr[p.test.value] = p.consequent[0];
                });

                let parentPath = whilePath.parent;
                varPath.remove();
                whilePath.remove();
                // path.node.object.value 取到的是 '1|2|4|7|5|3|8|0|6'
                let shufferArr = path.node.object.value.split("|");
                shufferArr.map(function (v) {
                    parentPath.body.push(myArr[v]);
                });
                //每遍历一轮 ast,只处理一个 switch 流程平坦化就停止遍历
                path.stop();
            }
        }
    });
}

switch 流程平坦化,还原前后的效果,读者们可以参考 11.1.5 小节中的代码。其中还有一些十六进制字符串没有还原。还原的方法在第 10 章中有详细介绍,这里不再赘述。还原 switch 流程平坦化的代码其实没有多少行,相对比别除花指令还要容易一点。当然 switch 流程平坦化混淆并不是只有这一种。因此读者们应当掌握原理,在实际应用中具体情况具体分析。

5.最终代码

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

const jscode = fs.readFileSync("./demo.js", {
    encoding: "utf-8"
});

let ast = parser.parse(jscode);

//拿到解密函数所在节点
let stringDecryptFuncAst = ast.program.body[2];
//拿到解密函数的名字
let DecryptFuncName = stringDecryptFuncAst.declarations[0].id.name;
//新建一个 AST,把原代码中的前三部分,加入到 body 节点中
let newAst = parser.parse('');
newAst.program.body.push(ast.program.body[0]);
newAst.program.body.push(ast.program.body[1]);
newAst.program.body.push(stringDecryptFuncAst);
//把这三部分的代码转为字符串,由于存在格式化检测,需要指定选项,来压缩代码
let stringDecryptFunc = generator(newAst, {compact: true}).code;
//将字符串形式的代码执行,这样就可以在 nodejs 中运行解密函数了
eval(stringDecryptFunc);

traverse(ast, {
    //遍历所有变量
    VariableDeclarator(path){
        //当变量名与解密函数名相同时,就执行相应操作
        if(path.node.id.name == DecryptFuncName){
            let binding = path.scope.getBinding(DecryptFuncName);
            binding && binding.referencePaths.map(function(v){
                v.parentPath.isCallExpression() &&
                v.parentPath.replaceWith(t.stringLiteral(eval(v.parentPath + '')));
            });
        } 
    }
});

ast.program.body.shift();
ast.program.body.shift();
ast.program.body.shift();

var totalObj = {};
function generatorObj(ast){
    traverse(ast, {
        VariableDeclarator(path){
            //init 节点为 ObjectExpression 的时候,就是需要处理的对象
            if(t.isObjectExpression(path.node.init)){
                //取出对象名
                let objName = path.node.id.name;
                //以对象名作为属性名在 totalObj 中创建对象
                objName && (totalObj[objName] = totalObj[objName] || {});
                //解析对象中的每一个属性,加入到新建的对象中去,注意属性值依然是 Node 类型
                totalObj[objName] && path.node.init.properties.map(function(p){
                    totalObj[objName][p.key.value] = p.value;
                });
            };
        }
    });
    return ast;
}
ast = generatorObj(ast);

traverse(ast, {
    VariableDeclarator(path) {
        if (t.isObjectExpression(path.node.init)) {
            path.node.init.properties.map(function (p) {
                let realNode = findRealValue(p.value);
                realNode && (p.value = realNode);
            });
        };
    }
});

function findRealValue(node) {
    if (t.isMemberExpression(node)) {
        let objName = node.object.name;
        let propName = node.property.value;
        if (totalObj[objName][propName]) {
            return findRealValue(totalObj[objName][propName]);
        } else {
            return false;
        }
    } else {
        return node;
    }
}

ast = generatorObj(ast);

traverse(ast, {
    MemberExpression(path) {
        let objName = path.node.object.name;
        let propName = path.node.property.value;
        totalObj[objName] && t.isStringLiteral(totalObj[objName][propName]) &&
        path.replaceWith(totalObj[objName][propName]);
    }
});

traverse(ast, {
    VariableDeclarator(path) {
        if (t.isObjectExpression(path.node.init)) {
            path.node.init.properties.map(function (p) {
                let realNode = findRealFunc(p.value);
                realNode && (p.value = realNode);
            });
        };
    }
});
function findRealFunc(node) {
    if (t.isFunctionExpression(node) && node.body.body.length == 1) {
        let expr = node.body.body[0].argument.callee;
        if (t.isMemberExpression(expr)) {
            let objName = expr.object.name;
            let propName = expr.property.value;
            if (totalObj[objName]) {
                return findRealFunc(totalObj[objName][propName]);
            } else {
                return false;
            }
        }
        return node;
    } else {
        return node;
    }
}
//去除函数花指令以后,更新一下 totalObj 对象
ast = generatorObj(ast);

traverse(ast, {
    CallExpression(path) {
        //callee 不为 MemberExpression 的节点,不做处理
        if (!t.isMemberExpression(path.node.callee))
            return;
        //取出对象名和属性名
        let objName = path.node.callee.object.name;
        let propertyName = path.node.callee.property.value;
        //如果在 totalObj 中有相应节点,就是需要进行替换的
        if (totalObj[objName] && totalObj[objName][propertyName]) {
            //totalObj 中存放的是函数节点
            let myFunc = totalObj[objName][propertyName];
            //在原代码中,函数体其实就一行 return 语句,取出其中的 argument 节点
            let returnExpr = myFunc.body.body[0].argument;
            //判断 argument 节点类型,并且用相应的实参来构建二项式或者调用表达式
            //然后替换当前遍历到的整个 CallExpression 节点即可
            if (t.isBinaryExpression(returnExpr)) {
                let binExpr = t.binaryExpression(returnExpr.operator,
                        path.node.arguments[0], path.node.arguments[1]);
                path.replaceWith(binExpr);
            } else if (t.isCallExpression(returnExpr)) {
                //把 arguments 数组中的下标为 1 和以后的成员,放入 newArray 中
                let newArray = path.node.arguments.slice(1);
                let callExpr = t.callExpression(path.node.arguments[0],
                        newArray);
                path.replaceWith(callExpr);
            }
        }
    }
})

traverse(ast, {
    VariableDeclarator(path) {
        if (t.isObjectExpression(path.node.init)) {
            path.remove();
        };
    }
});

for(let i = 0; i < 20; i++){
    traverse(ast, {
        MemberExpression(path) {
            if (t.isStringLiteral(path.node.object) &&
                    t.isStringLiteral(path.node.property, {
                        value: 'split'
                    })) {
                //找到类型为 VariableDeclaration 的父节点
                let varPath = path.findParent(function (p) {
                        return t.isVariableDeclaration(p);
                    });
                //获取下一个同级节点
                let whilePath = varPath.getSibling(varPath.key + 1);
                //解析整个 switch
                let myArr = [];
                whilePath.node.body.body[0].cases.map(function (p) {
                    myArr[p.test.value] = p.consequent[0];
                });

                let parentPath = whilePath.parent;
                varPath.remove();
                whilePath.remove();
                // path.node.object.value 取到的是 '1|2|4|7|5|3|8|0|6'
                let shufferArr = path.node.object.value.split("|");
                shufferArr.map(function (v) {
                    parentPath.body.push(myArr[v]);
                });
                path.stop();
            }
        }
    });
}

let code = generator(ast).code;
fs.writeFile('./demoNew.js', code, (err)=>{});

发表回复

您的电子邮箱地址不会被公开。