在逆向开发过程中,我们常常会修改网页源码来调试和跟踪数据流与运行流,那么首先要做的就是将源码格式化,但是当我们逆向瑞数加密的网站时,如果直接格式化源码,运行后就会陷入无限循环,代码不格式化非常影响我们做逆向工作,那么这里博主分析下原因即处理这个无法格式化的问题。
我们找到一个瑞数加密的网站,并获得入口js文件
然后使用浏览器替换,重新打开网站,程序进入无限循环,页面无法显示出来。
静态代码格式化
源码分析
通过比对动态生成的代码,可以确定格式化与否生成的动态代码一致。
既然动态代码一致,那是不是与window.$_ts
变量有关呢,通过对比发现如果格式化后window.$_ts.jf
的值是true,如果改成false则无限循环问题不会出现,因此可以确认代码格式化后无法运行的原因就是window.$_ts.jf
的值导致的。
定位代码
通过插桩方式确认window.$ts.jf
的赋值过程,找到正则匹配代码:_$ht.test(_$_q)
其中_$_q
的值为"function _$dG(){return'\\x74\\x6f\\x53\\x74\\x72\\x69\\x6e\\x67';}"
,可以直接在js文件搜索到,搜索字符串:function _$dG(){return'\x74\x6f\x53\x74\x72\x69\x6e\x67';}
_$ht
的值为/\S+\(\){\S+['|"].+['|"];}/
如果把源码格式化后_$_q
的值为"function _$dG() {\n return '\\x74\\x6f\\x53\\x74\\x72\\x69\\x6e\\x67';\n }"
分析正则串
正则串/\S+\(\){\S+['|"].+['|"];}/
的解释如下:
/
: 正则表达式的开始。\S+
: 匹配一个或多个非空白字符。这部分表示一个函数名或标识符。\(
: 匹配左括号(
。\)
: 匹配右括号)
。{
: 匹配左花括号{
。\S+
: 再次匹配一个或多个非空白字符。这部分可能表示函数体的起始部分。['|"]
: 匹配一个单引号'
或双引号"
。.+
: 匹配一个或多个任意字符。['|"]
: 再次匹配一个单引号'
或双引号"
。;
: 匹配分号;
。}
: 匹配右花括号}
。/
: 正则表达式的结束。
可以看到2和6都是匹配非空白字符,且){
中间是没有空格的,因此只要格式化了就肯定过不了这个正则判断。
然后这个正则串可以在代码中找到:_$ht = new RegExp('\x5c\x53\x2b\x5c\x28\x5c\x29\x7b\x5c\x53\x2b\x5b\x27\x7c\x22\x5d\x2e\x2b\x5b\x27\x7c\x22\x5d\x3b\x7d');
,'\x5c\x53\x2b\x5c\x28\x5c\x29\x7b\x5c\x53\x2b\x5b\x27\x7c\x22\x5d\x2e\x2b\x5b\x27\x7c\x22\x5d\x3b\x7d'
是"\S+\(\){\S+['|"].+['|"];}"
的ascii码的16进制显示
分析函数字符
函数字符比较简单,返回的字符串'\x74\x6f\x53\x74\x72\x69\x6e\x67'
是16进制的ascii码值,解码后就是字符串toString
,或者直接运行_$dG()
也会返回toString
这个字符串,为啥是这么字符串,博主盲猜应该是为了混淆逆向开发者,因为我们要把函数转换为字符串就可以运行代码$dG[$dG()]()
,这样看起来确实有点混淆的感觉,不管调试时鼠标滑在变量上或者运行代码返回的值都是一致的,哈哈哈
动态代码格式化
瑞数的核心代码是动态生成的,也是加入了格式化检测,如果做代码分析,代码格式化会减少我们的逆向复杂度,与外层代码格式化检测不同,动态代码格式化检测是检测的入口函数,入口函数长这样(由于代码是动态生成的,因此直接搜索这个方法是搜不到的);
function _$$I() {
var _$$I, _$ft, _$_0, _$iv, _$j7, _$kb;
if (_$f1._$gS) _$ft = _$bS._$et(), _$$I = _$bS._$et(), _$ft[1] = _$cR[1], _$ft[3] = _$cR[3];else {
_$ft = [], _$$I = new _$at(_$f1._$ic), _$ft[1] = _$cR[1].concat([arguments]), _$ft[3] = _$cR[3].concat([_$$I]), _$_0 = _$f1._$j8;
for (_$iv = 2; _$iv < _$_0.length; _$iv++) _$$I[_$iv] = _$_w(_$_0[_$iv], _$ft);
}
_$ft[0] = arguments, _$ft[2] = _$$I, _$$I[0] = this, _$$I[1] = arguments, _$f1._$$j.charCodeAt ? _$f1._$$j = _$eu(_$f1._$$j) : 0, _$bl(_$f1, 0, _$f1._$$j.length, _$ft), _$j7 = _$ft[4], _$kb = _$ft[5], _$f1._$gS ? (_$bS._$gt(_$ft), _$bS._$gt(_$$I)) : 0;
if (_$j7 === 1) return _$kb;
}
然后通过正则语句/function \S+?\(\){\S+/.test(_$$I.toString())
进行匹配,匹配结果为true则正常执行,为false则进入死循环。
这个正则语句匹配的函数代码前部分如:function _$$I(){var...
因此我们只要找到入口函数并将格式化的入口函数的函数头还原就可以通过格式化检测了
总结
最后总结下,对于外部代码,如果要格式化后代码能正常运行,那我们就要在格式化后搜索_$dG
方法的代码,将这个方法还原成没有格式化的代码,即function _$dG(){return'\x74\x6f\x53\x74\x72\x69\x6e\x67';}
。
对于动态代码,首先要找到入口函数,再将入口函数的代码首部改成function 方法名(){代码
。
两处都修改完后就可以使用格式化后的代码调试开发了,关于格式化代码,中间操作不建议通过复制黏贴方式完成,可能会让二进制数据丢失或者二进制数据并被转化为字符串字面量造成程序运行异常,可以使用babel工具进行处理!
扩展
字符转16进制ascii码:'S'.charCodeAt().toString(16) === '53'
16进制ascii码转字符:String.fromCharCode(parseInt('53', 16)) === 'S'