基于 Babel 对 JS 代码进行混淆与还原操作的网站 JS 代码混淆与还原 (kuizuo.cn)
还原前言
AST 能做为逆向分析的利器,可以将还原出来的代码替换原来的代码,以便更好的动态分析找出相关点。在还原时,并不是所有的代码都能还原成一眼就识破代码执行逻辑的,ast 也并非万能,如果你拥有强大的 js 逆向能力,有时候动态调试甚至比 AST 静态分析来的事半功倍。
还原不出最原始的代码
标识符是可以随便定义的,只要变量不冲突,我可以随意定义,那么就已经决定我们还原不出源代码的变量名,所以能还原的只有一些花指令,使其代码变好看,方便调试。
还原也不是万能的
混淆的方式有很多,与之对应还原的方式也有很多,上面那套混淆的还原可能只针对那一套混淆的代码,如果拿另一份混淆过的代码,然后执行这个还原程序的话,那程序多半有可能报错。所以绝对没有万能的还原代码,所有的还原程序,都需要针对不同的混淆手段来进行处理的。
我只是将我所遇到的混淆手段整合到一套代码上,而非所有的混淆手段都能进行还原处理的。
同时也别过于追求还原,因为还原很容易破坏原有代码,导致一些未知 bug。
例子
下文将会针对主流的一些混淆手段(至少是在我遇到的混淆中相对比较好还原的),并会附上对应代码供参考(不放置代码出处)。
接下来我将要演示一个混淆代码是如何还原的,这个例子是我第一次接触混淆的例子,也可以说是我玩的最溜的一次还原了,反正折腾了也有 4,5 来次。
贴上代码 git 地址 js-de-obfuscator/example/deobfuscator/cx
注:该 js 文件是通过工具JavaScript Obfuscator Tool进行混淆处理的。
分析 AST
首先一定一定要将混淆的代码解析成 AST 树结构,任何混淆的还原都是如此。首先简单随便看看代码,不难发现这些代码中都有'\x6a\x4b\x71\x4b'
这样的十六进 制编码字符,可以使用现成的工具,格式化便会限制编码前的结果,不过这边使用 ast 来进行操作
通过 AST 查看 node 节点,可以发现value
正是我们想要的数据,但这里确显示的是extra.raw
,实际上只需要遍历到相应的节点,然后 extra 属性给删除即可,同样的 Unicode 编码也是按上述方式显示。
具体遍历的代码如下
// 将所有十六进制编码与Unicode编码转为正常字符
hexUnicodeToString() {
traverse(this.ast, {
StringLiteral(path) {
var curNode = path.node;
delete curNode.extra;
},
NumericLiteral(path) {
var curNode = path.node;
delete curNode.extra;
}
})
}
然后将遍历后处理过的代码与 demo.js 替换一下,方便接下来的还原处理。不过处理完还是有大部分未知的字符串需要解密,当然也有一些没处理过的代码。
找解密函数
如果你尝试过静态分析该代码,会发现一些参数都通过_0x3028 来调用,像这样
_0x3028['nfkbEK']
_0x3028('0x0', 'jKqK')
_0x3028('0x1', ')bls')
不过认真查看会发现像成员表达式MemberExpression
语句_0x3028["nfkbEK"]
,但在第三条语句却定义函数_0x3028
。其实是 js 的特性,比方说下面的代码就可以给函数添加一个自定义属性
let add = function (a, b) {
add['abc'] = 123
return a + b
}
console.log(add(1, 2))
console.log(add['abc'])
// 3
// 123
不过不影响,这里只是提一嘴,并不是代码的问题。而其中**_0x3028
就是解密函数**,且遍历_0x3028
调用表达式,且参数为两个的 CallExpression。
那么接下来就要着重查看前三个语句,因为这三条语句便是这套混淆的关键所在。
var _0x34ba = ["JcOFw4ITY8KX", "EHrDoHNfwrDCosO6Rkw=",...]
(function(_0x2684bf, _0x5d23f1) {
// 这里只是定义了一个数组乱序的函数,但是调用是在后面
var _0x20d0a1 = function(_0x17cf70) {
while (--_0x17cf70) {
_0x2684bf['push'](_0x2684bf['shift']());
}
};
var _0x1b4e1d = function() {
var _0x3dfe79 = {
'data': {
'key': 'cookie',
'value': 'timeout'
},
"setCookie": function (_0x41fad3, _0x155a1e, _0x2003ae, _0x48bb02) {
...
},
"removeCookie": function () {
return "dev";
},
"getCookie": function (_0x23cc41, _0x5ea286) {
_0x23cc41 = _0x23cc41 || function (_0x20a5ee) {
return _0x20a5ee;
};
// 在这里定义了一个花指令函数调用来调用
var _0x267892 = function (_0x51e60d, _0x57f223) {
_0x51e60d(++_0x57f223);
};
// 实际调用的地方
_0x267892(_0x20d0a1, _0x5d23f1);
return _0x1c1cc3 ? decodeURIComponent(_0x1c1cc3[1]) : undefined;
}
}
};
};
_0x1b4e1d();
}(_0x34ba, 296));
var _0x3028 = function (_0x2308a4, _0x573528) {
_0x2308a4 = _0x2308a4 - 0;
var _0x29a1e7 = _0x34ba[_0x2308a4];
// 省略百行代码...
return _0x29a1e7;
};
其中省略的代码没必要细读,因为后续都只将这三条语句写入到 node 内存中(eval),然后来调用。接下来分析每一个语句都是干嘛的。
大数组
基本 99%的混淆第一条语句都是一个大数组,存放这所有加密过的字符串,而我们要做的就是找到所有加密过的字符串,将其还原。