特殊混淆案例的还原
除了基于 javascript-obfuscator 的混淆,还有其他混淆方式,这里介绍几种有代表性的混淆方案(比如 AAEncode、JJEncode、JSFuck)的还原方法。
AAEncode 的还原
AAEncode 是一种 JavaScript 代码混淆算法,利用它,我们可以将 JavaScript 代码转换成颜文字表示的 JavaScript 代码。
这里有一个示例网站 https://utf-8.jp/public/aaencode.html ,打开之后我们便可以看到如图 11-88 所示的样例。
图 11-88 颜文字表示的 JavaScript 代码
可以看到,一个最简单的 Hello World 就被转变成了很长的颜文字,代码被混淆得面目全非。但实际上,混淆后的代码其实还是遵循了 JavaScript 语法的,只不过其中的一些变量被替换成了表情符的样子。
这里我们再看一个示例网站 https://spa11.scrape.center/ ,这是一个 NBA 球星网站,展示了球星的一些数据,但与此同时,每个球星的信息面板上都对应了一串字符,我们把鼠标移动到面板上就可以看到,如图 11-89 所示:
图 11-89 每个球星的信息面板上都对应了一串字符
实际上,这个字符包含了一定规律,其结果其实和这些球星的数据有关系。
接下来,我们来探究一下究竟是怎么回事。查看页面源码,如图 11-90 所示。
图 11-90 页面源码
可以看到,index 页面引人了一些标准库 Vue、ElementUI、Crypto 等,正常情况下应该不会出现在这里面。最后,我们发现页面还引人了一个 JavaScript 文件 main.js,下面观察该 main.js 里面都有什么。
可以看到,这里面就是一整行颜文字,如图 11-91 所示。
图 11-91 main.js 文件
这其实就是用了 AAEncode 混淆。我们尝试点击左下角的格式化按钮,发现格式化也是无效的。那么这种混淆方式有的解吗?看也看不懂,格式化也无效。
当然是有的,我们可以试着先观察一下代码的规律。从代码的前后两端入手,可以观察到开头基本上都是 ')=/"m"))~士/,结尾基本上都是(A)["o"])("@"))("_");。因为这段 JavaScript 代码是可以运行的,那么它一定是符合 JavaScript 语法的。但最后是以一个括号结尾的,按照 JavaScript 的语法,可以判定前面的整体是一个方法声明。就比如类似这样的代码:
(function(a){ console.log('hello', a)})('world');
前面是一个方法的声明,然后整个通过大括号括起来,最后再传入一个参数来调用,运行结果如下:
hello world
其实 AAEncode 的原理也是将前面的内容转化成一个方法声明,最后传入一个参数来调用执行只不过最后传的参数是一个下划线而已。
对于上面的例子,假如我们不知道 (function(a) { console.log('hello', a)}) 这个方法声明究竞是怎么写的,可以将其输出到控制台上。下面看下运行效果,如图 11-92 所示。
图 11-92 运行效果
可以看到,这个方法的声明就被打印出来了。
对于 AAEncode 来说,我们也可以试着将最后的参数 ('_') 去掉,将前面的代码输出到控制台,看下运行效果,如图 11-93 所示。
图 11-93 运行效果
可以看到,这个方法被解析出了 “真正面目”,当然这里还不太好观察。我们可以进一步将方法转化为字符串,在后面加一个 toString 方法的调用,如图 11-94 所示。
图 11-94 添加 toString 方法的调用
这时我们发现这个方法的声明被转化为字符串了,内容一自了然。
将代码整理后格式化一下,就能得到如下结果:
function anonymous() {
const players = [
{
name: "凯文-杜兰特",
image: "durant.png",
birthday: "1988-09-29",
height: "208cm",
weight: "108.9KG",
},
...
];
new Vue({
el: "#app",
data: function() {
return { players, key: "nCQ7ywzJVEqGTTxncPFJzXv8juDWwPMrZAr");
};
},
methods: {
getToken(player) {
let key = CryptoJS.enc.Utf8.parse(this.key);
const { name,birthday,height,weight } = player;
let base64Name = CryptoJS.enc.Base64.stringify(
CryptoJS.enc.Utf8.parse(name));
let encrypted = CryptoJS.DES.encrypt(
`${base64Name}${birthday}${height}${weight}`,
key,
{ mode: CryptoJS.mode.ECB, padding:CryptoJS.pad.Pkcs7 }
);
return encrypted.toString();
},
},
});
}
这里发现一个 getToken 方法,逻辑也十分清晰,就是将球员的名字、生日、身高、体重经过处理之后再进行 DES 加密,加密密钥就是 key,其值就是 nCQ7ywzJVEqGTTxncPFJzXv8juDWwPMrZAr,DES 加密之后返回即可,具体逻辑可以自行验证。
以上就是 AAEncode 混淆的分析思路和解决方案。
JJEncode 的还原
JJEncode 也是一种 JavaScript 代码混淆算法,其原理和 AAEncode 大同小异,利用它,我们可以将 JavaScript 代码转换成颜文字表示的 JavaScript 代码。
这里有一个示例网站 https://utf-8.jp/public/jencode.html ,打开之后我们便可以看到如图 11-95 所示的样例。
图 11-95 示例网站
可以看到,这个代码中包含了很多 $,看起来可读性也很差,但实际上它也遵循一定的 JavaScript 语法。
接下来,我们再看一个示例网站 htps://spa10.scrape.center/,网站的表现形式和上一个例子完全一样,其源码是经过 JJEncode 混淆的,如图 11-96 所示。
图 11-96 网站源码
其实 JJEncode 混淆的解决方案和 AAEncode 差不多。因为最后可以看到同样也是有一个 (),所以我们同样也把最后的 () 去掉,粘贴到控制台中,如图 11-97 所示。
图 11-97 控制台
运行结果也极其相近,可以看到这也是一个方法。
同样,通过添加 toString 方法的调用也可以将这个方法转化为字符串输出,如图 11-98 所示。
图 11-98 添加 toString 方法的调用
使用同样的方法,我们也可以对代码进行格式化并还原,再看具体的加密流程就可以了。
JSFuck 的还原
JSFuck 也是一种特殊的混淆方案,是基于开源的 JSFuck 库来实现的,其样例可以参考 http://www.jsfuck.com/ ,如图 11-99 所示。
图 11-99 JSFuck 混淆案例
我们可以看到一段 alert(1) 代码被转变为包含 []、()、+、! 的 JavaScript 代码了。
其中 JSFuck 官方也做了说明,它是基于如下几个等价变量实现的:
(省略)
通过如上变量的组合,再加上一些小括号处理优先级,就可以将任意 JavaScript 代码转换为我们所看到的混淆 JavaScript 代码。
但这次不像 AAEncode 和 JJEncode 那样了,这次混淆代码需要稍微花点时间来解混淆。
我们再看一个示例网站 https://spa12.scrape.center/ ,这个网站和前面的网站相比,也是仅仅只有 main.js 不同,其内容是经过 JSFuck 混淆得到的,如图 11-100 所示。
图 11-100 示例网站
但是观察整个代码,发现最后的部分不再是一个小括号了,内容如下:
...[+!+[]+[!+[]+!+[]]])())
可以看到,这里最后的小括号后面还跟了一个小括号,这样我们就没法像 AAEncode 和 JJEncode 那样,将最后的小括号去掉了。
怎么办呢?我们可以稍微退一步,看一下最后的一个右括号匹配的左括号是哪个。首先可以对代码进行格式化,此时可以借助 Beautifier 工具,如图 11-101 所示。
图 11-101 Beautifier 工具
由于小括号和中括号特别多,肉眼非常难观察出其中的规律,这时候可以将格式化后的代码粘贴到 IDE 里面,借助于 IDE 找到括号的匹配规律。
这里我们可以选用 VSCode,新建一个 JavaScript 文件,如 main.js,将代码粘贴进去,然后将光标放在最后一个括号的位置,如图 11-102 所示。
图 11-102 将光标放在最后一个括号的位置
可以发现,最后的一个括号被突出显示,同时在 VSCode 上方也会有另外一个高亮的位置提示它对应的左括号的位置,如图 11-103 所示。
图 11-103 对应的左括号
我们选择将两个括号之间的内容复制出来,粘贴到控制台,如图 11-104 所示。
图 11-104 复制代码到控制台
我们又看到熟悉的代码了,其类型就是一个字符串,这时候就已经成功了一半。
那么剩下的代码是做什么的呢?我们把刚才复制出来的代码从原来的代码里面删除,然后再把剩下的代码粘贴到控制台,如图 11-105 所示。
图 11-105 控制台
可以看到是 undefined,相当于执行成功了,但是没有返回结果。
这时候我们又会想起前面的思路,返回值 undefined 说明返回结果为空,而当前代码最后也带了一个括号,代表执行当前方法,但刚才我们已经把方法的参数(也就是字符串)都已经删除了,也就相当于没有传参数调用,返回值为 undefined 也是有可能的。
既然是方法,那么我们可以尝试得到其方法本身试试。试着去掉最后的一对小括号,重新在控制台运行,如图 11-106 所示。
图 11-106 去掉最后的一对小括号,重新在控制台运行
这时候就可以看到其运行结果了。这是一个 eval 方法,是 JavaScript 中定义的原生方法,传入一段 JavaScript 字符串,利用 eval 就可以执行了。
比如:
eval("console.log('helloworld')");
的执行结果就是: hello world
所以,第一部分的运行结果就是字符串,把它传给 eval 方法,自然就可以执行对应的逻辑了。这样,JSFuck 这种特殊混淆的神秘面纱也被我们揭开了。