免责声明:本文章中所有内容仅供学习交流,抓包内容、敏感网址、数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,若有侵权,请联系我立即删除!
前言
写作目的:记录手动逆向一个JS高度混淆的网站的整个过程。
网址:aHR0cHM6Ly81NTI0OTY5Ni5jb206Nzc3Ny8=
逆向过程
话不多说,直接开始调试。输入用户名17777777777和密码123456点击登录,弹出验证码:
一般来说网站如果出现复杂验证码都会配合JS参数加密增加防护等级。我们抓包抓到2个XHR请求:
初步推测第一个请求get是获取验证码,第二个请求check是校验验证码。今天的目标就是破解这两个请求的加密参数与返回值。
我们观察这2个接口,发现check请求的参数包含get请求的参数,所以只需要解决check请求的参数就行了。
我们直接看check请求,在Source面板打上XHR断点,断住checkv3.php请求:
在Call Stack中找到一个疑似加密点:
点击进去,可以看到JS代码基本上是高度混淆的:
我们把断点断到拼接请求参数的地方:
逆向URL生成规则
先看下URL的生成,扣出代码:
1 | 'url': _0x15a5d0[_0x59cb56(0x8bc, 0x763, 0xba1, '0jdF', 0x6bd)](_0x15a5d0[_0x59cb56(0x8ea, 0x8b8, 0x9b2, 'm*3l', 0xcfc)], _0x36b61f), |
我们再取出_0x15a5d0[_0x59cb56(0x8bc, 0x763, 0xba1, '0jdF', 0x6bd)]放到console上输出,发现其是一个函数:
点击进去,看到它是一个花指令,就是把两个参数相加:
所以URL参数的生成实际上是调用一个加法,把两个参数相加。我们再看这个加法传入的2个参数。_0x15a5d0[_0x59cb56(0x8ea, 0x8b8, 0x9b2, 'm*3l', 0xcfc)]和_0x36b61f。
``_0x15a5d0[_0x59cb56(0x8ea, 0x8b8, 0x9b2, ‘m*3l’, 0xcfc)]`是一个固定的地址。
_0x36b61f是一个13位数的时间戳。
所以URL实际上就是一个固定的地址拼接一个13位的时间戳,即/yzmtest/checkv3.php?t={13位时间戳}。
逆向data生成规则
先扣出data生成那一部分代码:
1 | 'data': _0xf828e5[_0x59cb56(0x7c7, 0x4a1, 0x425, 'F^5Z', 0x1f3)](JSON[_0x2e7c5c(0x9ff, 0x5fa, 0x813, 'Ra[M', 0x821) + _0x160f59(0x921, 0x690, 0x2a4, 'ZP*j', 0x657)]( |
在console面板上,很容易看出,username是我们输入的账号17777777777前面拼接了e5。
password则是明文:
所以上面的代码可以加以简化为:
1 | 'data': _0xf828e5[_0x59cb56(0x7c7, 0x4a1, 0x425, 'F^5Z', 0x1f3)](JSON[_0x2e7c5c(0x9ff, 0x5fa, 0x813, 'Ra[M', 0x821) + _0x160f59(0x921, 0x690, 0x2a4, 'ZP*j', 0x657)]( |
调试下_0x59cb56(0x7c7, 0x4a1, 0x425, 'F^5Z', 0x1f3)
再调试下_0x2e7c5c(0x9ff, 0x5fa, 0x813, 'Ra[M', 0x821) + _0x160f59(0x921, 0x690, 0x2a4, 'ZP*j', 0x657):
把上面的代码简化下:
1 | 'data': _0xf828e5.enc.JSON.stringify( |
可以看到,这里是用了某种加密算法对{“d”: xxx, “username”: “xxx”, “password”: “xxx”}进行加密从而得到data。这个加密算法到底是什么加密呢?我们console面板上输入_0xf828e5.enc然后点击进去:
看到了整个方法有2个分支,我们分别对2个分支打上断点,发现只进去else了。如果对JS常见的加密有过了解的话,这里看到iv,mode,padding这三个关键字立马会想到用的是AES加密算法。第一个参数_0x4d04a3是待加密的字符串:
第二个参数this[_0x19e2aa(0x93, 0x454, ‘cu5X’, 0x139, 0x43d) + ‘e’]是密钥。我们打印出其内容:
1 | var key = { |
第三个参数是AES加密的一些参数,mode一般是CBC,padding不重要可以不传。最后我们在console上输出iv的值:
1 | var iv = { |
有了AES加密的这几个参数我们就可以很简单的还原出解密算法了。代码如下:
1 | var CryptoJS = require("crypto-js"); |
还记得前面我们提到过抓到2个XHR请求吗?一个是请求验证码的,一个是进行验证码验证的。我们看第一个请求验证码的请求。
这个返回值c是不是就是用的AES加密呢?我们用上边的解码程序试验一下。果不其然,可以正确反解出加密内容:
里面的一串JSON正好是我们刚才出现的验证码的内容:
1 | [ |
记住这个JSON,我们待会还有用。我们接着回到data破解的思路上去。data的生成代码进一步简化:
1 | 'data': AES.encode( |
接下来看看最核心的d的生成规则。_0x15a5d0[_0x2e7c5c(0x2ce, 0x61d, 0xa5a, ‘4[E4’, 0x33d)]是一个加法的花指令。
_0x15a5d0[_0x59cb56(0x92b, 0x683, 0x968, ‘F^5Z’, 0x9a7)]也是一个加法的花指令:
_0x15a5d0[_0x59cb56(0x92b, 0x683, 0x968, ‘F^5Z’, 0x9a7)]也是加法花指令。
_0x15a5d0[_0x160f59(0x11f, 0x403, 0x29d, ‘AVXK’, 0x518)]依旧是一个加法花指令。
_0x4fd69b[_0x8eb3(0xc7a, 0x728, 0x704, ‘OBf4’, 0xc4c)]是内置的join方法。
_0x4fd69b是个字符串:
_0x8eb3(0x136, 0x44f, 0x748, ‘HZxj’, 0x2a3) + ‘en’是字符串$strlen
_0xf828e5[_0x8eb3(0x136, 0x44f, 0x748, ‘HZxj’, 0x2a3) + ‘en’]则是取_0xf828e5这个object的$strlen属性,这里的值是3。
_0x556be9(0x5cf, 0x870, 0x50b, ‘Ra[M’, 0x3af) + ‘r’这里是substr。
_0x36b61f是13位的时间戳。
-(0x2670 + -0x77d * -0x2 + -0x3568 * 0x1)是固定的常量,-2。
_0x160f59(0xae3, 0x5f9, 0x92d, ‘NUpf’, 0x8a5)是字符串$ver。这个ver其实在我们刚才解码第一个请求的返回值的里面就有了,值为3587,跟这里的吻合。
最终简化代码如下:
1 | 'data': AES.encode( |
到这里d的生成算法基本上一目了然了。d=字符串(这里是4c2c8bc886b8bf8d)+_0xf828e5.$strlen(这里是3)+13位时间戳的最后2位(这里是17)+版本号(第一个请求接口有返回为3587)=4c2c8bc886b8bf8d3173587。扣出d的生成代码,在console上输出,验证下:
完全吻合。现在唯一的问题是_0x4fd69b这个字符串怎么来的以及_0xf828e5.$strlen这个值怎么算出来的,解决了这两个问题,d的生成规则就破解了。
我们手动搜索_0x4fd69b这个字符串,总共找到三处:
扣出第二处的代码如下:
1 | _0x4fd69b[_0x219591('FZa9', 0x79c, 0x826, 0x6a9, 0x45f)](_0xf828e5[_0x59b553('W]B)', 0xd30, 0x809, 0x809, 0x2c4)][_0x19f5d9[_0x3ebe03('nCyg', 0xb6c, 0x868, 0x4ee, 0xd78)](parseInt, _0x1bf4a9[_0x59b553('#og4', 0xbb9, 0x8a2, 0x953, 0x441) + 'ce'](_0x19f5d9[_0x3ebe03('F^5Z', 0x448, 0xc1, -0x2ba, 0x8c)], ''))]['d']); |
按照上边提供的方法,在console上分段调试代码含义,代码反混淆如下(为节省篇幅,从这里开始,代码反混淆过程都不会写了,直接给出反混淆的结果):
1 | _0x4fd69b.push(_0xf828e5.$list[parseInt("btncanv_3".replace("btncanv_", ""))]['d']); |
这个代码的意思就是取上边我们提到的验证码数组中索引为3的值,即4c2c8bc886b8bf8d,把这个值push到数组_0x4fd69b(这里虽然取得是d这个属性,但是实际上d属性跟id属性的值是一样的,_0x281004[‘d’] = _0x3aa5c6[‘id’])。这里的这个btncanv_3恰好是验证码的答案,即正确答案的元素的id。
那么这个btncanv_3是怎么确定的呢?是我们手动点选验证码图案的时候选中的,我们手动选中了验证码图案,JS代码会根据我们选中的图案,拿到它的id(如果有选中了多个,也只取第一个),然后进行JS加密,后端会根据相应的解密算法,拿到我们上传的那个验证码。我们开篇提到过“一般来说网站如果出现复杂验证码都会配合JS参数加密增加防护等级”,这里就验证了这句话。我们这里虽然破解了验证码验证接口表单数据的加密算法,但是验证码的点选,我们还需要辅助相应的验证码识别的算法,帮助我们完成验证码的识别与点选。这里主要是讲解手动反混淆方案,验证码后边会有专门的专题文章进行介绍,先埋一个坑后边补上。
接下来看下另外一个_0xf828e5.$strlen的生成规则。我们文件中全局搜索’en’(为什么要搜索这个?因为前面$strlen的字符串混淆是_0x8eb3(0x136, 0x44f, 0x748, ‘HZxj’, 0x2a3) + ‘en’),果然被我们找到了。代码如下:
1 | this[_0x2a85c2(0x8a2, 0x60c, 0xa3d, 0x7b0, '0NjW') + 'en'] = _0x15a5d0[_0x19b874(0xc8f, 0xe5a, 0x13b9, 0xb8b, 'W]B)')](Math[_0x26b2f7(0xf44, 0xc7b, 0xbc1, 0x1077, 'o4oN')](_0x15a5d0[_0x26b578(0xfa3, 0xcfb, 0x820, 0x11b8, 'm*3l')](-0x1bcd + 0x1 * 0x1b23 + -0xaf * -0x1, Math[_0x2a85c2(0x11fb, 0xf8b, 0xd69, 0x137a, '*EQ3') + 'm']())), -0x727 * 0x2 + 0x24d7 + -0x1f * 0xba) |
代码反混淆之后整理如下:
1 | this.$strlen = Math.floor(5 * Math.random()) + 3 |
至此,整个data数据生成过程调试完了。最终的算法伪代码整理如下:
1 | let _0xf828e5.$strlen = Math.floor(5 * Math.random()) + 3; |
逆向clientid生成规则
扣出clientid生成相关的代码:
1 | this[_0x26b2f7(0x9fc, 0xa9b, 0xe1c, 0x6b2, '%jat') + _0x34ce5a(0x1321, 0xfb2, 0x11d0, 0x109c, 'm*3l')] = _0x1c3499[_0x2a85c2(0xf23, 0xc3d, 0x99d, 0xf50, 'OBf4')]('')[_0x26b2f7(0xf47, 0xb49, 0xe76, 0xc02, 'W]B)') + 'r'](0x4f * -0x1 + 0x1779 + -0x172a, -0x4ff * 0x3 + -0x3 * -0x75c + 0x1 * -0x70d) |
代码反混淆如下:
1 | this.$clientid = _0x1c3499.join("").substr(0, 10) |
$clientid的生成规则依赖_0x1c3499,我继续往下看,扣出_0x1c3499的相关代码:
1 | _0x1c3499 = [] |
代码反混淆如下:
1 | _0x1c3499 = []; |
可以看到__0x1c3499的值又依赖_0x3e49dd和_0x3badf1。我们再扣出相应的代码:
1 | _0x3e49dd = _0x15a5d0[_0x2a85c2(0x240, 0x6a9, 0x570, 0x230, 'ZP*j')](Number, _0x15a5d0[_0x26b578(0x8cc, 0xaab, 0xa86, 0xf1d, 'NUpf')](Math[_0x19b874(0x12ba, 0xd7c, 0xb35, 0xe72, 'o4oN') + 'm']()[_0x34ce5a(0xffa, 0xa91, 0x802, 0xe92, 'uUCz') + _0x19b874(0x174, 0x684, 0x9d7, 0x737, 'G0Im')]()[_0x26b2f7(0xaa8, 0x6bd, 0x562, 0x35a, 'F^5Z') + 'r'](0x10 * 0xc2 + 0x6d * -0x5 + -0x9 * 0x11c, 0xb3 * 0x35 + 0x13 * 0x1a5 + -0x444a * 0x1), Date[_0x26b2f7(0xbf3, 0xd8c, 0xaf2, 0xacf, 'g(lc')]()))[_0x26b578(0x326, 0x69f, 0xaa3, 0x78d, '*EQ3') + _0x26b2f7(0x8dc, 0x900, 0x8f0, 0xa3a, 'hROy')](0x143b + 0x53c + -0x1953) |
代码反混淆后结果如下:
1 | _0x3e49dd = Number(Math.random().toString().substr(3, 4) + Date.now().toString()).toString(36) |
_0x3577fe的值是固定的三个元素的数组,如下:
1 | _0x3577fe = [-0x2090 + -0x1b * 0x139 + 0x4197, 0x959 + -0x248c + 0x45 * 0x65, 0x2 * -0x1ea + 0x229f + -0x1ec3]; // 4 6 8 |
到这里为止,$clientid的生成规则就全部反混淆出来了,最终的代码整理如下:
1 | let _0x3577fe = [4, 6, 8]; |
真是一层一层剥开你的心🥴
逆向token生成规则
扣出token生成的相关代码:
1 | 'token': _0xf828e5[_0x2e7c5c(0x3c9, 0x4c, -0x2fc, 'hROy', -0x1f8)](_0x15a5d0[_0x2e7c5c(0x78e, 0x6f8, 0x927, '(e@x', 0x3d0)](_0x15a5d0[_0x59cb56(0x398, 0x6f8, 0x7eb, '(e@x', 0x571)](_0x15a5d0[_0x160f59(0xbca, 0xad5, 0x7c2, 'FZa9', 0xa2c)](_0xf828e5[_0x2e7c5c(0x56b, 0x25a, 0x1e6, 'g(lc', -0x22c) + _0x556be9(0x527, 0x2b, 0x30c, 'su5h', 0x58f)], _0xf828e5[_0x8eb3(0x1bf, 0x17c, 0x58, 'Ra[M', -0xec) + _0x2e7c5c(0x203, 0x237, -0x41, 'Qm)6', 0x40c)]), _0xf828e5[_0x2e7c5c(0xd48, 0x8de, 0x717, 'j[vi', 0xd5e) + _0x59cb56(0x301, 0x4c3, 0x1e5, '0jdF', 0x8fb)]), _0x15a5d0[_0x59cb56(-0x1ee, 0xb3, 0x35, 'OBf4', 0x524)])) |
反混淆之后的最终代码如下:
1 | 'token': _0xf828e5["sign"](_0xf828e5.$clientid + _0xf828e5.$username + _0x15a5d0.sGZgF)) |
庆幸的是,这里的_0x15a5d0.sGZgF是一个固定的字符串,内容为”x045783”。所以重点在于破解这个加密方法sign。我们扣出相应的代码:
1 | _0x58b6e0[_0x339e58(0x61a, -0x24f, 0x94, 'nCyg', 0x267) + _0x18c3fa(0x813, 0x825, 0x36f, 'TEE1', 0x6be)][_0x1d90d1(0x18a, 0x8b2, -0x158, 'Qm)6', 0x348)] = function(_0x1c6621) { |
看到这么大一段代码不要慌!!!我们慢慢开始剥洋葱。🙃,剥到最后,很简单。
1 | _0x58b6e0.prototype.sign = function(_0x1c6621) { |
其实就是对传入的参数做了一个md5的加密,然后进行乱序处理。
到这里我们两个接口的所有请求参数加密算法,以及接口的返回值的解密算法都已经破解了。我们接下来简单验证下是否正确。
验证
由于check接口需要机器学习对验证码进行识别,所以这里只验证get接口的参数。用NodeJS的Express框架搭建好获取token的服务,供Python调用。
运行结果如下:
总结
本文通过一个网站,手动对AST混淆代码进行了一个反混淆。在JS代码安全防护原理——AST混淆原理中提到的几种混淆原理基本都出现过了。比如数组混淆,数组逆向,花指令,流程平坦化,逗号表达式混淆,字符串加密,常量加密等。可以看到,手动混淆的过程是极其容易出错,工作量非常大且十分痛苦的。接下来会写相关文章,介绍如何通过工具对AST混淆代码进行自动反混淆,不过,手动混淆这种能力也是必须要掌握的,万一你使用的工具失效了,或者说遇到一些更加特殊的网站,只能通过手动混淆呢?如果想获取本文的完整代码,扫码关注公众号,然后公众号内回复关键字02即可获取。