记一次简易的js反混淆
js来源
某次无意间发现了一个能将qmc转换为mp3/flac的网站,而网站本身是调用前端进行解密的,既然如此,当然趁这个机会把qmc的加/解密方式搞到手(虽然把网页离线了也是一个办法~)。果断一波F12,整个HTML文件很小,引用了两个js文件,忽略掉jQuery库,剩下的一个显然是我们想要的解密js了。
反混淆
字符串解密
拿到js,直接浏览器view-source看看长啥样。一看,只有长长的一行,显然代码被软件压缩过了,不过没事,Chrome自带的开发人员工具很强大,左下角一键格式化代码,得到了稍微好看些的代码。代码开头首先是一个str型数组,密文看上去像Base64加密,但是解密后得到的全部是乱码,显然进行了一些其他加密处理,后面紧跟的是对Cookie的一系列操作,但是在程序运行过程中浏览器没有监测到Cookie的变化,暂且认为其未真正起到作用。而注意到前两个函数的函数调用虽然将class.function(params)型的函数调用改为了
1
class['function'](params)
进行调用,但仍然能明显看出到底调用了什么函数,但是从1
var _0x5d98 = function(_0x55979a, _0x1aa978)
后,所有函数调用及大部分字符串都变成了对 _0x5d98 的调用此时可基本确定 _0x5d98 函数用于对加密的字符串进行解密。并且可以确定传入参数大致为
- param1[Int]: 对应的加密字符串
- param2[String]:解密密钥(疑似Rc4)
而解密函数涉及到较多的循环等操作,本着不做无意义工作的精神,决定直接使用console调用解密函数并打印返回值。首先用Python提取所有所有对解密函数的调用。1
2
3
4
5
6
7
8
9
10#coding=utf-8
import re
with open("/Users/~/js_decomplie/qmc.1.js","r") as finp, \
open("/Users/~/js_decomplie/replaceOrigin.txt","w") as out, \
open("/Users/~/js_decomplie/insertLog.js","w") as out2: #用于Console打印调试信息
s = finp.readline()
for t in re.findall("_0x5d98\('(.+?)'\)", s):
out.write("_0x5d98('{}')\n".format(t))
out2.write("console.log(0x5d98('{}'));".format(t))
首先想到的是直接在代码尾部添加console.log()函数进行打印,但是实际操作后发现控制台未输出任何调试信息,而在Chrome的Developer Tools的console中直接调用该函数显示正常,后决定将代码插入位置改为第一次调用解密函数前。
控制台输出正确调试信息。到此字符串加密已经解决,利用Python对被加密字符串进行替换。1
2
3
4
5
6
7
8
9
10
11
12
13#coding=utf-8
in1 = open("/Users/~/js_decomplie/qmc.1.js","r") #原js
in2 = open("/Users/~/js_decomplie/replaceOrigin.txt","r") #替换用 原数据
in3 = open("/Users/~/js_decomplie/replaceData.txt","r") #替换目标数据
out = open("/Users/~/js_decomplie/decorderReplaced.js","w") #输出
for line in in1.readlines():
rpori = in2.readlines()
rpdst = in3.readlines()
for i in range(len(rpori)):
print(rpori[i].strip()+" -> "+rpdst[i].strip())
line = line.replace(rpori[i].strip(),"'"+rpdst[i].strip()+"'")
out.write(line)
替换完成后导入WebStorm进行预览,发现js执行错误,正则表达式出现问题,后发现出错原因为当正则中当‘\’以加密存储时记录为一个字符,而还原时应替换为转义字符’\\‘,替换后,函数正确执行,字符串解密与替换完成。
寻找真实函数
一般的js混淆都喜欢向js中插入无意义代码以达到一部分混淆的作用。现在的目标就是找到对文件解密有意义对代码。
首先,在阅读代码过程中,容易注意到有部分重复出现的代码,如以下代码块:1
2
3
4
5
6
7
8
9
10
11
12
13
14var _0x450c85 = function() {
var _0x59a74b = !![];
return function(_0x4b341d, _0x544d6f) {
var _0x31d6c4 = _0x59a74b ? function() {
if (_0x544d6f) {
var _0x385eb3 = _0x544d6f['apply'](_0x4b341d, arguments);
_0x544d6f = null;
return _0x385eb3;
}
}: function() {};
_0x59a74b = ![];
return _0x31d6c4;
};
}();
通常在编程过程中不会出现仅变量名有所差别的函数(造成浪费),故猜测该代码块为混淆代码,经分析后发现该代码块的作用是使函数继承其本身,没有任何意义。
同样的,类似于以下代码:1
2
3
4
5
6
7
8
9if (!_0x52b7a4()) {
if (!_0x14577b()) {
_0x5eae0a('\x69\x6e\x64\u0435\x78\x4f\x66');
} else {
_0x5eae0a('\x69\x6e\x64\x65\x78\x4f\x66');
}
} else {
_0x5eae0a('\x69\x6e\x64\u0435\x78\x4f\x66');
}
看似一个判断语句,实则无论条件如何都会执行同一无意义操作 _0x5eae0a.indexOf 。
在大量混淆代码存在的情况下,从入口找出解密函数更为方便,注意到HTML元素中有打开文件的部分,在js中定位到接收文件路径的方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14$('#openfile')['bind']('change', function() {
var _0x4b40ea = this['files'];
if (_0x4b40ea['length'] > 0x0) {
for (var _0x1167bf = 0x0; _0x1167bf < _0x4b40ea['length']; _0x1167bf++) {
console['log'](_0x4b40ea[_0x1167bf]);
}
for (var _0x1167bf in _0x4b40ea) {
$('#log')['append']('第' + _0x1efec9 + '个文件名:' + _0x4b40ea[_0x1167bf]['name'] + '\x0d\x0a'); //此处控制网页上的log窗口输出
_0x2ada50[_0x1efec9] = _0x4b40ea[_0x1167bf]['name']['toLowerCase']()['replace']('qmcflac', 'flac')['replace']('qmc3', 'mp3')['replace']('qmc0', 'mp3');
_0x481c3b(new Blob([_0x4b40ea[_0x1167bf]]), _0x1efec9); //此处将文件句柄传向下一个函数_0x481c3b()
_0x1efec9++; //文件计数器
}
}
}
对文件句柄传递函数追踪,可发现对文件进行读取的方法。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16startTime = new Date()['valueOf']();
$('#log')['append']('第' + _0xb85669 + '个文件开始时间:' + startTime + '\x0d\x0a');
var _0x4e592f = new FileReader(); //创建一个文件读取方法
_0x4e592f['readAsBinaryString'](_0x58ca49); //二进制方式打开
_0x4e592f['onload'] = function() {
bstr = _0x47dc75(this['result'], this['result']['length']); //解密关键方法
var _0x239485 = new Blob([bstr]);
var _0x5aa6ad = document['createElement']('a'); //此处对页面添加下载链接元素,说明到此处为止解密工作已经处理完成
_0x5aa6ad['href'] = window['URL']['createObjectURL'](_0x239485);
_0x5aa6ad['innerHTML'] = _0x2ada50[_0xb85669];
_0x5aa6ad['download'] = _0x2ada50[_0xb85669];
$('body')['append'](_0x5aa6ad);
endTime = new Date()['valueOf']();
$('#log')['append']('第' + _0xb85669 + '个文件结束时间:' + endTime + '\x0d\x0a');
$('#log')['append']('第' + _0xb85669 + '个文件耗时:' + (endTime - startTime) + '毫秒\x0d\x0a');
}
显然,_0x47dc75()为解密函数,传入参数分别为密文字符串以及密文长度,返回解密后的字符串。继续跟踪,最终可以找到解密的原始函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function _0x47dc75(_0x4ccae9, _0x1bfb3f) {
u8arr = new Uint8Array(_0x1bfb3f);
for (var _0x10cce6 = 0x0; _0x10cce6 < _0x1bfb3f; _0x10cce6++) {
u8arr[_0x10cce6] = _0x4ccae9[_0x10cce6]['charCodeAt'](0x0) ^ _0x237866(_0x10cce6);
}
return u8arr;
}
function _0x237866(_0x1b18d2) {
if (_0x1b18d2 >= 0x8000) {
return _0x546fd5(_0x1b18d2 % 0x7fff);
} else {
return _0x546fd5(_0x1b18d2);
}
}
function _0x546fd5(_0x23698e) {
var _0x326eb2 = [0x77, 0x48, 0x32, 0x73, 0xde, '...'];//KeyBox过长故省略部分
return _0x326eb2[(_0x23698e * _0x23698e + 0x13c1b) % 0x100];
}
到此,本次js反混淆工作已经完成。最后将其重写为C语言实现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57//
// main.c
// qmcDecrypt
//
// Created by guch8017 on 2019/4/28.
// Copyright © 2019 guch8017. All rights reserved.
//
uint8_t keyBox[] = {0x77, 0x48, 0x32, 0x73, 0xde,...}; //KeyBox过长略去部分
uint8_t keyGen(int i){
return keyBox[(i * i + 0x13c1b) % 0x100];
}
uint8_t xorKey(int i){
if(i >= 0x8000){
return keyGen(i % 0x7fff);
}
else{
return keyGen(i);
}
}
int main(int argc, const char * argv[]) {
FILE * inFp, * outFp;
uint8_t buff;
int i = 0;
if(argc<=1){
printf("Error: No input file\n");
return 0;
}
char targetName[strlen(argv[1]) + 10];
strcpy(targetName, argv[1]);
strcat(targetName, ".out");
if ((inFp = fopen(argv[1], "rb"))==NULL){
printf("Error: Can't not open file %s\n",argv[0]);
return 0;
}
if ((outFp = fopen(targetName, "wb"))==NULL){
printf("Error: Can't not create file %s\n",argv[0]);
return 0;
}
while(!feof(inFp)){
fread(&buff, sizeof(buff), 1, inFp);
buff ^= xorKey(i++);
fwrite(&buff, sizeof(buff), 1, outFp);
}
printf("Target: %s\n",targetName);
fclose(inFp);
fclose(outFp);
}
小结
简易的JS混淆方法
显然,这个js加密是较为简易的,强度也并不高,运用了目前常用的几种混淆方法:
- 变量名混淆
- 插入无意义代码
- 将立即数改写为复杂表达式或函数的返回值
- 对字符串加密,调用时再调用函数对其解密
然而,js本身的性质决定了即使对字符串加密,解密函数仍然要在js代码中实现,这让攻击者可以轻易的调用解密函数对加密进行解密(解密函数都写给别人了还有啥用呢)。目前见到的针对解释性语言的加密,最强的还是PHP中虚拟机加密,但js执行效率不足等等原因限制了该领域的发展,目前暂时没有见到有相关的服务出现。
为何无法在js结尾处打印调试信息
反混淆后,可以发现主函数中有一段1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25var _0x2c575f = _0x450c85(this, function() {
var _0x1995da = function() {};
var _0xb58eb6 = typeof window !== 'undefined' ? window : typeof process === 'object' && typeof require === 'function' && typeof global === 'object' ? global : this;
if (!_0xb58eb6['console']) {
_0xb58eb6['console'] = function(_0x5140de) {
var _0x47f6d1 = {};
_0x47f6d1['log'] = _0x5140de;
_0x47f6d1['warn'] = _0x5140de;
_0x47f6d1['debug'] = _0x5140de;
_0x47f6d1['info'] = _0x5140de;
_0x47f6d1['error'] = _0x5140de;
_0x47f6d1['exception'] = _0x5140de;
_0x47f6d1['trace'] = _0x5140de;
return _0x47f6d1;
}(_0x1995da);
} else {
_0xb58eb6['console']['log'] = _0x1995da;
_0xb58eb6['console']['warn'] = _0x1995da;
_0xb58eb6['console']['debug'] = _0x1995da;
_0xb58eb6['console']['info'] = _0x1995da;
_0xb58eb6['console']['error'] = _0x1995da;
_0xb58eb6['console']['exception'] = _0x1995da;
_0xb58eb6['console']['trace'] = _0x1995da;
}
}
这里可以看到函数获取了窗口的句柄,并将其中的调试相关函数全部替换为 _0x1995da ,即一个空函数,故在主程序加载完成后 console.log 已经变为了一个空函数,无法打印调试信息。