JavaScript反混淆插件七:如何编写一个专用的插件?

本文缘由

随着反爬的升级,前端的JavaScript代码越来越难以阅读,一个简单的字符串声明竟然可以拆分成多行代码,虽然并不会给动态调试带来困难,但是在静态分析时着实让人难受。明明一行代码可以搞定的事情,偏偏写成了十行甚至百行,代码冗余非常严重。

我们以下面的代码为例,来讲讲如何还原。

for (var e = "\u0270\u026D\u0274\u0274\u0277\u0234\u0249\u025B\u025C\u0229", a = "", s = 0; s < e.length; s++) {
  var r = e.charCodeAt(s) - 520;
  a += String.fromCharCode(r);
}

代码很常见,一个for循环 + 一个String.fromCharCode 完成了代码的还原。在浏览器上运行可以得到结果:

其实运行下来,也就是将a的值设置为 "hello,AST!",如果结合实际的代码,其实可以发现它和下面的代码是等价的:

var a = "hello,AST!";

因为其他的变量根本就没有用到过!

也许你有疑问,为什么不转换成赋值语句,而变成声明语句,如果你处理过作用域就知道了。

从源代码:

for (var e = "\u0270\u026D\u0274\u0274\u0277\u0234\u0249\u025B\u025C\u0229", a = "", s = 0; s < e.length; s++) {
  var r = e.charCodeAt(s) - 520;
  a += String.fromCharCode(r);
}

变成目标代码:

var a = "hello,AST!";

现在只差一个AST的插件了。

分析

  1. 这是一个for循环,因此我们需要遍历 ForStatement

  2. 第二行源代码是一个声明语句,并且包含charCodeAt函数

  3. 第三行代码是一个表达式语句,并包含String.fromCharCode

根据这些特征,我们就可以写一个专用的插件:

const types     = require("@babel/types");
const forToString = {
    //遍历ForStatement
    ForStatement(path) {
        let body = path.get("body.body");
        if (!body || body.length !== 2)
        //根据在线解析网站可以看出body的长度为 2
            return;
        if (!body[0].isVariableDeclaration() || !body[1].isExpressionStatement()) {
        //循环体的第一个语句为声明语句,第二个语句为表达式语句
            return;
        }


        let body0_code = body[0].toString();
        let body1_code = body[1].toString();


        if (body0_code.indexOf("charCodeAt") != -1 && body1_code.indexOf("String.fromCharCode") != -1)
        {//根据上面的分析而来
        //dosomething
        }
    },
}

核心代码的处理

上面下来判断的代码,下面来写核心代码。

  1. 运行for循环,得到 "hello,AST!" 这个值。

  2. 构造一个 VariableDeclaration 节点,并进行替换

如何得到这个for循环的结果呢?我这里借助 new 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();

根据value构造VariableDeclaration节点,代码如下:

let new_node = types.VariableDeclaration("var",[types.VariableDeclarator(types.Identifier(name),types.valueToNode(value))]);

然后再进行替换:

path.replaceWith(new_node);

代码合并起来是这样的:

const for_to_string = {
    ForStatement(path) {
        let body = path.get("body.body");


        if (!body || body.length !== 2)
            return;
        if (!body[0].isVariableDeclaration() || !body[1].isExpressionStatement()) {
            return;
        }


        let body0_code = body[0].toString();
        let body1_code = body[1].toString();


        if (body0_code.indexOf("charCodeAt") != -1 && body1_code.indexOf("String.fromCharCode") != -1) {
            try {
                let expression = body[1].node.expression;
                let name = expression.left.name;


                let code = path.toString() + "\nreturn " + name;


                let func = new Function("",code);
                let value = func();


                let new_node = types.VariableDeclaration("var", [types.VariableDeclarator(types.Identifier(name), types.valueToNode(value))]);
                path.replaceWith(new_node);


            } catch (e) {};
        }
    }
}

这里我用了try...catch语句是因为在真正遍历的时候,运行func可能会报错,有可能需要某些依赖,因此用 try...catch语句来捕捉异常。

结语

随着多次的还原练习,只要是结构固定的代码,皆可以编写一键还原的工具,再使用reres映射进行动态调试即可,实在是保头发的不二选择。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值