python爬虫之破解javascript-obfuscator的混淆加密
接上一篇有关前端加密达到反爬的文章,是不是觉得用了javascript-obfuscator 就很安全了,那还真不一定啊,还是那句,反爬与反反爬一直在斗争,没有谁能绝对的压制另一方,只有使用者技术的高低。以下就是一个大神的针对javascript-obfuscator库的破解。
本篇文章转载于 : https://mp.weixin.qq.com/s/aZmuc3DwtQvRluvKCKDedw
死代码与花指令
在开始之前,我们先了解一下这种「在代码中插入大量无用代码以混淆视听」的混淆方式吧。这种混淆方式有两种叫法,或者说是两种做法,它们分别是「死代码」和「花指令」。
死代码
死代码一开始是被用来描述一些人写代码时写出的没有用到的代码的,为了编译后的文件尽可能地小,编译器通常会对死代码进行移除处理。
而在不知道什么时候开始,死代码被安全工作者们用来作为一种混淆机制,以将代码量变得极为庞大,使进行逆向工程的人难以找到主要逻辑。
但死代码有个很明显的特征:它虽然看着代码量很大,但实际却完全不会在程序的正常代码中被调用。
如果你有兴趣的话,可以对一些包含了死代码的代码进行聚类分析,你会发现死代码和正常代码之间泾渭分明,正常代码都是互相关联着的,而死代码却是孤零零的一块或者多块,并且正常代码还完全不会与死代码产生关联。
花指令
花指令是以前被大量运用在木马、病毒的免杀上的一种反反汇编手段,花指令中的“指令”通常指的是汇编中的 jmp
、call
之类的调用、跳转指令,而攻击者们会将这些指令巧妙地插入到恶意代码的执行逻辑中,使得静态分析工具在分析到这个位置时无法正常反汇编。
花指令曾经的目的主要有两个,一个是使杀毒软件无法自动分析出恶意代码,达到瞒天过海的效果;一个是给安全工作者在分析恶意软件时设下层层阻拦,使安全工作者需要花费更多的时间才能理清代码逻辑,达到拖延时间的效果。
同样是不知道什么时候开始,花指令也被安全工作者们用来作为一种混淆机制。在这种应用场景下,花指令和死代码其实很类似,它们都是用了大量无用代码来混淆视听,但花指令和死代码最大的区别就是,花指令的无用代码是会被混在正常代码中进行执行的。相比于死代码而言,花指令会造成一些性能损失,但同时也会让进行逆向工程的人更加难以分析。
但花指令也不是无懈可击的,为了不影响程序正常的执行,花指令不能干扰到程序的原有逻辑,举个例子:
a = 1
b = 2
# 花指令开始,对变量进行了一通操作
a += 1
a += b
# 花指令结束,又把变量的值给变回去了
a -= ba -= 1 c(a, b)
所以其实只要你能看出这一通操作没有任何意义,花指令也自然就没法影响到你了。
小结
不管是死代码还是花指令,其实都只需要我们仔细观察就能将其剔除,它们并不是什么很难搞的东西,见得多了之后你甚至都不需要细看就能快速排除掉一些明显不是正常代码的部分,毕竟常见的混淆器中用到的代码其实重合度是很高的,同样的套路见多了之后自然很容易分辨。
更何况,代码混淆是需要考虑性能损耗的,对方不可能为了防你逆向工程而无止尽地对代码进行混淆,要不然人家正常业务也没办法进行了。
实战
基础知识了解完了,我们来进入实战环节。
首先,我们打开 https://obfuscator.io/,这是 Obfuscator 的网页版本,可以快速在网页上进行混淆参数的配置,并且一键生成并导出混淆后的代码。
顺带一提,Obfuscator 是一款非常优秀的 JavaScript 代码混淆工具,但代码结构都是固定的,如果想要更好的混淆效果,可将混淆后的代码进行修改,从而让别人更难分析和调试。
现在,我们用它给出的样例代码来进行混淆。样例代码如下:
// Paste your JavaScript code here function hi() { console.log("Hello World!"); } hi();
注意,我们需要勾选以下选项:
这三个选项的效果分别是:
•Compact code将代码中的换行符全部去掉,使得代码看起来毫无结构性。也就是所谓的代码压缩。•Self Defending在代码中插入自检代码,用来干扰逆向工程的人对代码进行格式化、变量重命名操作,如果代码被格式化了就会无法正常运行。•Dead Code Injection在代码中插入死代码,也就是本文的重点。
配置好参数后点击 Obfuscate 按钮,即可生成按配置混淆后的代码,我生成的代码是这样的(长图警告):
可以看到,原本短短的几行代码,在经过混淆后变成了这么多。而且这个代码还是经过压缩的,完全看不出层级。
当然,这个代码是可以正常运行的,我们用NodeJS跑一遍看看:
看起来混淆并没有影响到正常的代码逻辑,我们再把这一坨代码给格式化一下看看:
果不其然,格式化后的代码直接就没法运行了。在平时我们遇到这种情况时要记住,原代码可以正常运行但格式化之后不行,那么这个报错肯定是跟格式化代码有关系的,至于它报错的内容具体是啥意思其实并不重要。
那么怎么办呢?我们来静态分析一下它的代码就知道了。
先来看看第一段代码:
定义了一个数组并初始化,显然不可能造成什么问题。
接着看看第二段代码(长图警告):
这是一个自执行的函数,没有返回值。但是注意,它的第一个实参是 _0x2831
,也就是之前定义的那个数组,对应的形参是 _0x528cba
。我们可以根据这个来判断它对 _0x2831
做了些什么。
现在我们来一段一段地分析这第二大段代码中的每一段代码,首先是第一段代码:
var _0x1b0e99 = function(_0x5beb46) { while (--_0x5beb46) { _0x528cba['push'](_0x528cba['shift']()); } };
这么短的代码相信大家都应该能看懂,是对 _0x528cba
进行 shift 操作,而 _0x528cba
是自执行函数的形参,实参是 _0x2831
。换句话说,它就是对实参进行 shift 操作。不过这里它只是声明,并没有调用,所以还不会去改变实参。
然后是第二段代码和第三段代码,这里因为代码量太大就不整个贴出来了,之前已经贴过完整代码了。
第二段代码是定义了一个函数,而第三段代码则是调用这个函数,因此我们主要分析这第二段代码即可。
如果你不会分析,可以跳过它声明的语句,它真正开始执行的是这行代码:
var _0x53c9b6 = _0x1d1bc5['updateCookie']();
不要看它这段代码里面既有 setCookie
,又有 getCookie
,其实它跟 cookie 没有半毛钱关系,它只是一个 object 的 key 值,仅此而已。
因此,我们只需要关注它有没有改变实参,有没有改变全局变量。整个代码全局变量只有一个 _0x2831
,它也是实参,也就是说只需要关心这个 _0x2831
即可。
通过上面的分析我们可以知道,它的第一段代码定义了一个函数,确实改变了实参,但是没有调用。因此,我们得找找看它在哪里被调用的,直接搜函数名 _0x1b0e99
,定位到这里:
_0x4c51d1(_0x1b0e99, _0x283138);
这时,_0x1b0e99
是第一个实参,第二个实参 _0x283138
则是自执行函数的形参,它对应的实参是 0x1bf
,是一个整形的数值。这下,我们只需要看看 _0x4c51d1
的函数声明即可:
var _0x4c51d1 = function(_0x3d5743, _0x3c21e0) { _0x3d5743(++_0x3c21e0); };
这么大一段代码,其实真正改变实参的只有这里。我们将这两行代码结合一下,就会变成这样:
_0x1b0e99(++_0x283138);
所以,第二大段代码这个自执行函数中的第二段代码只有这一句是真正改变实参的地方,其他的全部是垃圾代码,直接删除即可。删除后,这个自执行函数就变成了这样:
(function(_0x528cba, _0x283138) { var _0x1b0e99 = function(_0x5beb46) { while (--_0x5beb46) { _0x528cba['push'](_0x528cba['shift']()); } }; _0x1b0e99(++_0x283138); }(_0x2831, 0x1bf));
这样看起来就清爽多了,运行试试看:
还是报同样的错误,接着往下分析第三段代码(长图警告):
这是一个函数,可以看到,引用全局变量 _0x2831
的只有这一行:
var _0x1b0e99 = _0x2831[_0x528cba];
这是一个赋值语句,但是不会改变 _0x2831
这个变量,因此我们只需要重点关注它的返回值 _0x1b0e99
就好。
再来看看它最后赋值的地方,有两处,一处在 if 语句里面:
_0x1b0e99 = _0x1b0e['SmClCt'](_0x1b0e99, _0x283138);
另外一处在 else 语句里面:
_0x1b0e99 = _0x309846;
那它到底是执行的那行代码呢,来看看 if 语句的条件:
_0x309846 === undefined
继续分析上面的代码:
var _0x309846 = _0x1b0e['jZzRvK'][_0x528cba];
以及 _0x1b0e['jZzRvK']
最近的定义的地方:
_0x1b0e['jZzRvK'] = {};
这样就清楚了,_0x309846 === undefined
这个条件是成立的,所以 _0x1b0e99
最后赋值的地方是这里:
_0x1b0e99 = _0x1b0e['SmClCt'](_0x1b0e99, _0x283138);
函数 _0x1b0e['SmClCt']
赋值在这里:
_0x1b0e['SmClCt'] = _0x5beb46;
而实参,则是在 _0x5beb46
函数之前定义过,因此只需要这两行代码提到 _0x5beb46
函数之后即可。注意,这里为了清楚一点,可以这样操作:
if (_0x1b0e['DVdkAf'] === undefined) { ............................ _0x1b0e['SmClCt'] = _0x5beb46; _0x1b0e['jZzRvK'] = {}; _0x1b0e['DVdkAf'] = !![]; } _0x1b0e99 = _0x1b0e['SmClCt'](_0x1b0e99, _0x283138); return _0x1b0e99;
根据上面的思路继续分析 hi
函数,同样也注入了一些不会改变全局变量 _0x2831
的垃圾代码,真正有效的代码只有这一行:
console[_0x1b0e('0xc', '^G6o')](_0x1b0e('0xd', 'Bi36'));
删除掉这些垃圾代码后,我们就可以知道这份代码原来长什么样了。
BOOM!结果就是这么三行代码:
function hi() { console[_0x1b0e('0xc', '^G6o')](_0x1b0e('0xd', 'Bi36')); }
最后我们再运行一下试试看吧:
没有报错,代码成功运行了~
总结
碰到大段代码时不要慌,先试着分析一下,把无用代码剔除掉之后其实最后剩下的可能就只有几行而已。当然实际情况中你往往会碰到混淆参数更复杂的代码,你需要让程序来帮助你进行分析,而想要写出这种程序又会使用到 AST 操作,所以说掌握 AST 操作还是很有必要的,建议学习一下。
以上为转载的内容全文,怎么样,一样可以解出来,傻眼了吧,但是,也不是说javascript-obfuscator就完全没用了,当然有用,必须有用,只是看怎么用,你想一下,如果让这个混淆加密定期换一套呢?我换的速度难道还能比你解密的速度慢吗?然后就算你能解密出来,我再加几道关卡呢?让你解的费劲,恶心你,击垮你的信心,等等的,所以用处是很大的。只是看你怎么用了