记一次简易的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 | #coding=utf-8 |
替换完成后导入WebStorm进行预览,发现js执行错误,正则表达式出现问题,后发现出错原因为当正则中当‘\’以加密存储时记录为一个字符,而还原时应替换为转义字符’\\‘,替换后,函数正确执行,字符串解密与替换完成。
寻找真实函数
一般的js混淆都喜欢向js中插入无意义代码以达到一部分混淆的作用。现在的目标就是找到对文件解密有意义对代码。
首先,在阅读代码过程中,容易注意到有部分重复出现的代码,如以下代码块:
1 | var _0x450c85 = function() { |
通常在编程过程中不会出现仅变量名有所差别的函数(造成浪费),故猜测该代码块为混淆代码,经分析后发现该代码块的作用是使函数继承其本身,没有任何意义。
同样的,类似于以下代码:
1 | if (!_0x52b7a4()) { |
看似一个判断语句,实则无论条件如何都会执行同一无意义操作 _0x5eae0a.indexOf 。
在大量混淆代码存在的情况下,从入口找出解密函数更为方便,注意到HTML元素中有打开文件的部分,在js中定位到接收文件路径的方法:
1 | $('#openfile')['bind']('change', function() { |
对文件句柄传递函数追踪,可发现对文件进行读取的方法。
1 | startTime = new Date()['valueOf'](); |
显然,_0x47dc75()为解密函数,传入参数分别为密文字符串以及密文长度,返回解密后的字符串。继续跟踪,最终可以找到解密的原始函数:
1 | function _0x47dc75(_0x4ccae9, _0x1bfb3f) { |
到此,本次js反混淆工作已经完成。最后将其重写为C语言实现
1 | // |
小结
简易的JS混淆方法
显然,这个js加密是较为简易的,强度也并不高,运用了目前常用的几种混淆方法:
- 变量名混淆
- 插入无意义代码
- 将立即数改写为复杂表达式或函数的返回值
- 对字符串加密,调用时再调用函数对其解密
然而,js本身的性质决定了即使对字符串加密,解密函数仍然要在js代码中实现,这让攻击者可以轻易的调用解密函数对加密进行解密(解密函数都写给别人了还有啥用呢)。目前见到的针对解释性语言的加密,最强的还是PHP中虚拟机加密,但js执行效率不足等等原因限制了该领域的发展,目前暂时没有见到有相关的服务出现。
为何无法在js结尾处打印调试信息
反混淆后,可以发现主函数中有一段
1 | var _0x2c575f = _0x450c85(this, function() { |
这里可以看到函数获取了窗口的句柄,并将其中的调试相关函数全部替换为 _0x1995da ,即一个空函数,故在主程序加载完成后 console.log 已经变为了一个空函数,无法打印调试信息。