作者:sml2h3
这半年间我和一些有多年开发经验的大牛都有过交流,发现他们中大多数都表示对于简单的加密还可以试着逆向,但是稍微复杂一点的就不行了。其实也不难发现,现在大多数在逆向行业,尤其是前端逆向,活跃的大多是16-25岁的年轻人,反而那些老程序员的身影似乎少了很多,那究竟是为什么呢?
通过观察和交流,我总结了以下几个原因:
•逆向始终是游离在黑白之间,有家庭有事业的人更倾向于更安全更按部就班的开发工作。•对于高难度加密的逆向,需要花费大量的时间及精力,还要冒着极有可能失败的风险,同时对于没有任何资料的高难度加密,需要有很强的耐心以及非常好的记忆力。•逆向代码的思维考验的是分析能力、代码基本功、记忆力以及反向思考的能力,这里可以统称为逆向思维。经历了很多年开发的人,思维固化,可以说很多人已经习惯于顺序开发,再加上前端逆向资料少之又少,基本都是点对点的分析,没有一个系统的资料,越发觉得这个行业越难入门。
没有逆向思维的逆向是没有灵魂的,是事倍功半的。
因此,我把这一节作为本套系列课程的第一篇文章,希望各位读者学习完本节内容后开始尝试转变自己的开发思维,以求无论是从事逆向还是防御的朋友都能在当前的能力上更进一步!
逆向思维其实说难也不是很难,在我的理解中就是心理战的博弈。
在高级的加密中,往往你无法第一时间定位到函数的主要入口,可能你只是通过某一个关键字或者是程序运行中的某一个时刻的状态来阻断程序运行,如通过xhr事件断点在发送xhr请求的前一刻才获得到断点,这个时候很多附加参数都已经完成生成了。高级加密中很少会把加密的过程毫无保留的和请求糅合在一起写,甚至有的js会通过诸如死循环以及逗号表达式的方式或者生成无用参数来达到混淆视听的作用。
在这里,有着良好逆向思维的朋友会不由自主的在心底大胆的提出许多假设,通过换位思考,猜测开发防御系统的作者的心理,然后反向推导数值的生成以及过程中函数的作用。
在我们正常的开发中,受到教程或者学校的影响颇为严重,让我们习惯性用更简洁的代码去完成更复杂的功能,比如说,模块化。
这里,我给大家找到了瑞数防御系统中一段非常重要的代码。
function _$C7() {
var _$Cr = _$Cj(_$kj(_$bX()))(), _$wU = 0, _$hU = {};
_$hU._$wh = _$EI;
_$hU._$hu = _$GA;
function _$GA() {
return _$Jb.call(_$Cr, _$wU);
}
function _$be() {
var _$BU = _$JB.call(_$Cr, _$wU);
if (_$BU >= 40) {
_$wU++;
if (_$BU === 126) _$BU = 58;
return _$BU - 40;
}
var _$Fp = 39 - _$BU;
_$BU = 0;
for (var _$DC = 0; _$DC < _$Fp; _$DC++) {
_$BU *= 86;
var _$Gx = _$JB.call(_$Cr, _$wU + 1 + _$DC);
if (_$Gx === 126) _$Gx = 58;
_$BU += _$Gx - 40;
}
_$wU += _$Fp + 1;
return _$BU + 86;
}
function _$EI() {
var _$Fp = _$be();
var _$DC = _$Jb.call(_$Cr, _$wU, _$Fp);
_$wU += _$Fp;
return _$DC;
}
return _$hU;
}
乍一看,很多朋友已经因为难以阅读代码的意图而关闭了本篇文章。现在我给看完这段代码的朋友好好品一品这段代码。
首先,我们根据代码结构简单理解一下,当运行到这一段代码的时候,可以大概的得到信息是它首先定义了几个局部变量和三个局部_$GA()
,_$be()
,_$EI()
,其中新建了一个新的空对象_$hU
,然后向这个空对象添加了两个对象_$wh
和_$hu
,并分别指向内部的子函数 _$EI
和_$GA
,然后再更进一步看看两个比较长的子函数内容,其中有一段类似的代码引人注目,分别是_$Jb.call(_$Cr, _$wU)
;和_$Jb.call(_$Cr, _$wU, _$Fp)
;如果你有调试经验并调试过相关代码会很轻松的得知_$Jb
代表的是 substr 方法。
在w3school中写到:
substr() 方法可在字符串中抽取从 start 下标开始的指定数目的字符。
再继续看,_$wU
的初始值为 0 ,在执行_$EI()
的时候这个值会发生一定的变化。回过头来我们看看_$C7()
函数是在什么时候调用的,我们可以找到类似于这样的一段代码
_$yA = _$C7()
那么,我们是不是可以理解为_$C7()
实际上等于我们平时开发后端代码的时候的一个 class ,并向外暴露了内部的两个子方法。实例化后赋值给了 _$yA
这个变量。再进一步思考一下,是不是只要我们不重新实例化这个函数,那么随着子函数的调用,_$wU
的值会不断地变化,并保留最后一次调用结束后 _$wU
的最新值,下一次调用的时候 _$wU
可能就不是 0 了,并且由于substr 的切割作用,_$wU
实际上就是下一次切割 _$Cr
变量的一个 start 索引值,又由于_$wU
在代码中是处于一直增长的趋势,是不是我们每一次调用子函数给我们切割返回的内容都是不一样的呢。
看我巴拉巴拉说了半天,其实我只是想表明这就是一个模块化的例子,以后不管什么函数,如果说他的某些参数是在最初就已经被打包在这个 _$Cr
变量中,那么在运行过程中只需要在合适的时机调用子函数就可以取到所需要的数据。
可能到这,有很多朋友会想,说这么多有什么用呢,这和逆向思维有什么关系吗?
当然有,而且非常重要。我们正常的顺序开发中,总会因为其他原因在非加密的阶段调用一些通用的方法,比如说这里的 _$C7()
的子函数。
尤其是这里可能需要在已经调用 N 次的情况再调用才会返回正确的值得情况下,那么东一榔头西一棒槌的顺序开发中可能开发者自己都没有预料到的意外干扰会给我们逆向工作带来麻烦,这就是为什么很多朋友自认为自己已经抠出了加密的关键代码,结果无法正确生成所需要的参数或者生成的结果天差地别的原因。
而逆向思维在这里会给你很大的帮助,你并不是从头来分析整个代码,而是从中间的某一个小片段,如此段函数,去推导出这个函数的作用,发现了其中的陷阱,才能在顺序单步调试的时候避免问题的出现。
逆向思维是一个养成的过程,需要各位不断的去习惯去适应。
给大家一个简单的方法去锻炼自己的逆向思维,当你阻断了正在运行中的程序,你应当先明确自己需要什么,是 cookies 还是某个 param ,亦或是某个 header 参数,然后在通过 callback 回溯代码运行的过程中对不能一眼看穿的代码提出大胆的设想,猜测作者的意图,然后带着这个意图向上或向下进行求证,逐渐你会发现很多问题都会迎刃而解。