本篇文章主要讲述使用mitmproxy进行代码注入的一种方案,可以解决动态js和加密js的情况,本篇主要使用两个案例来展示。js逆向分析相关的在本篇文章中不会详细说,因为关系不大。本篇文章参考以下内容
1.中国商标网JS调试 - 动态代码注入
2.js逆向案例-rus5逻辑学习
3.某5某秒分析笔记
4.【JS逆向系列】某空气质量监测平台无限 debugger 与 python算法还原
某数的动态js,每次访问返回的都是不同的js,使用无痕模式打开浏览器,打开f12里面,选中事件监听断点中的脚本
此时打开网页可以看到如下的类型的代码
按照前面的思路,那么就是先把代码拿到手,然后进行反混淆,再讲反混淆后的代码设置回响应。这次依然是使用mitmproxy进行拦截,使用ast进行反混淆并添加断点
此时调试代码就比较方便了,也不用再去设置事件监听断点。,下一步可以在eval.call前面找到已经组装好的第二层代码,接下来就是进入vm了。
如果要将这里面的代码进行反混淆,还是一样需要先把代码拿到手,也就是说需要主动执行第一层的代码,然后拿到第二层的代码,此时将代码反混淆后,在第一层代码合适的地方,用反混淆后的代码替换掉原来第二层的代码。
这样就可以反混淆vm里面的代码里面的任意地方加上断点来调试,整体代码如下
def response(flow: http.HTTPFlow):
if 'new_house.html' in flow.request.url:
# 拦截首层html
print('数据拦截', flow.request.url)
with open('new_house.html', 'wb') as f:
f.write(flow.response.content)
# 反混淆第一层,并注入代码,等待生成第二层
os.system('node astfangdi_init')
print('首次处理完成')
# 主动执行生成第二层代码
nodejs = os.popen('node fangdi_init')
jscode = nodejs.read().replace('\n', '')
nodejs.close()
with open('fangdi_init.json', 'w', encoding='utf-8') as f:
f.write(jscode)
print('二层代码获取完成')
# 反混淆第二层代码
os.system('node astfangdi2')
print('二层代码反混淆完成')
# 反混淆第一层代码,并注入前面生成的第二层代码
os.system('node astfangdi')
with open('new_house_decrypt.html', 'rb') as f:
htmlcon = f.read()
flow.response.set_content(htmlcon)
print('首层代码注入完成')
然后是某5秒的加密js,还是使用无痕模式打开浏览器以及选中事件监听断点中的脚本
会断在一个_cf_chl_opt的参数上面,然后会请求一个jsch/v1的接口
返回的是一段带有ob混淆的内容,脚本断点会在解密后执行时断下
这个的拦截就和之前是一样的了,直接可以替换掉。不是说有加密的js吗?那么继续往下看。
后面还会接着请求一个flow/ov1这样的接口,里面的请求体和响应体就是加密了的,当然,准确来说不是加密,后面会说到。
这里解密后会使用function的构造函数进行代码。
这里也是有ob混淆的代码,这里想要拦截反混淆就不是这么简单了,因为返回的是加密了的代码,如果想要反混淆,那么就必须先解密代码。而这里又衍生出一个问题,就算我解密反混淆后,还是不能直接把响应设置进去,因为原本代码是加密的,我直接设置没有加密的代码,那么网页肯定会报错的。
那么这里在原来的基础上,还要多两步,就是把js解密,和重新加密回去。如何找到解密的函数这里就不详细说了,逻辑比较简单,那么就可以直接改成python的代码来解密
data64 = base64.b64decode(flow.response.content)
x = reduce(lambda n, m: n ^ m, [32] + list((cRay + "_0").encode()))
dedata = bytes([((data64[A] & 255) - x - A % 65535 + 65535) % 255 for A in range(len(data64))])
这里的dedata 就是得到的js明文,接下来就可以愉快的对代码进行反混淆了,那么怎么加密回去呢,这里其实就是一个反向运算。举个简单的例子就是解密是减去1,那么加密就是加上。如果解密是乘以x,那么加密就是除以x。加密的代码如下
endata = bytes([((dedata[A]) - 65535 + (A % 65535) + x) % 255 for A in range(len(dedata))])
flow.response.set_content(base64.b64encode(endata))
这样就可以把反混淆的代码设置回去了
这样就可以得到反混淆后的vm代码。调试起来就相对简单了一些,这个是响应体,那请求体也加密了。如果想看看请求的时候提交了什么,或者直接修改请求体的话,那是类似的逻辑,需要先解密,然后再加密才能设置回去。这里主要说一下加密的内容,其实说加密不太准备,应该说只是一个压缩方法。
这里的逻辑其实就是lzstring的压缩算法,python中也有lzstring这个库,但是不能直接使用,因为这里使用的是非标准的base64编码表,需要对源码进行部分修改
这样我们就可以随时对请求体和响应体拦截,不断的对比本地生成的数据和网页提交的数据的差别来生成我们的本地环境,mitmproxy的学习到此就结束啦。