最近在研究TT的六神算法,目前完成度大概在70%到80%之间吧。六神中的五个部分都相对简单,但Medusa这部分很复杂。其构成大致如下:
在上述参数中,最复杂、最折腾人的就是那段加密的Protobuf数据,共有几十个字段,且大部分都是在进程刚启动时进行初始化。从分析来看,这部分数据应该是用于收集系统环境信息,解密后的内容如下:本文的目标是分析16号字段中'A3xcV-ObJSg3dWOrqH5y6AjA3'字符串的来源。经观察,该字符串在进程启动时保持不变,但在运行几分钟后可能会发生变化。先来看下该固定字符串的生成方式。
逆推trace日志,发现该值存储在一个复杂的数据结构中, 而且这个复杂的数据结构是一个全局变量。查找这个全局变量的交叉引用,发现可能在+0x475c0处进行了初始化。所以我们将上述trace日志转为Frida代码来hook确认一下。从错误日志中能看出,并没有在这里初始化。而且这个库被混淆的特别厉害,所以现在最好的办法就是上调试器下内存写断点。为了调试方便,将Frida代码再转为GDB脚本,作用是从全局变量中读取出目标字符串。再写一个脚本,当指定的库刚被加载时就立马断下。在+0x47608处下断点,命中后打印字符串,根据输出日志在0x7e3ff24b58处下内存写入断点。命中后再次打印,根据输出日志继续下内存写断点。 如此循环操作几次后,发现大概是在+0x127AC8附近进行初始化。 在+0x127AC8处hook,发现被大量调用,这说明现在是在一个通用函数中,并不是真正进行初始化的地方。 对于这种情况最好的办法就是通过栈回溯来找到关键点,但Frida自带的栈回溯功能有些鸡肋,所以还需要自己想想办法。通过静态观察,发现绝大部分函数都比较标准,在函数头有以下特征:
+0x541d8地址所在的函数位于+0x54170处,函数比较复杂,所以我们直接trace。使用Frida hook +0x54170函数,当命中时保存上下文环境并进行trace。 trace完了共一百多万行,先定位到上述的hook点+0x127AC8处,并根据如下代码反推。 最终成功定位到字符串赋值的起始位置,似乎是通过不同的索引查询同一张表得到的。因此,关键点主要集中在两个方面:第一,这张表是如何生成的;第二,这些索引是如何来的。先来看0x7b9c84ae00这张表,它来自于堆上,大小0xab10。 因此,可以利用这个正则7b9c8[4|5][0-9a-zA-Z]{4}\[在trace日志中查找对表赋值的地方。经一番查找,发现了一处特征代码。该代码循环读取0x7e46d92940常量数组中的值作为索引,对一个值取低3位,然后将值存储到表中。0x7e46d92940常量数组如下: 通过对常量数组和算法的特征,能确定这就是Inflate解压算法中用于构建哈夫曼树的部分,其源码如下: 现在既然已经确定了是解压算法,那么只需要找到算法的输入数据就行了。根据对源码的分析,tinf_getbits函数中引用了输入参数,函数逻辑如下:num固定为3 d->source中存储的就是输入参数。 这段代码在trace日志中表现如下,对照源码来看,x25中的地址0x7b9a63e6ca就是原始输入参数。 0x7b9a63e6ca的起始地址是0x7b9a63e6c0,被赋值如下:其中,前4个字节暂不清楚作用,从第4个字节开始就能看到zlib的魔法头0x78 0x1。解压就可以看到是一个json数据,其中包含了目标字符串。 OK,那么现在就应该去分析0x7b9a63e6c0中的数据从何而来。0x7b9a63e6c0的生成算法如下,一眼看出是rc4:异或,模256,状态交换初始化s盒KSA 确定了RSA算法后,只需要找到输入数据和密钥即可。在RC4算法中KSA部分引用密钥,异或部分引用原始数据,因此可以得出0x7bfb738800中存储的是密钥,0x7f0d010600中存储的是原始数据。将原始数据和密钥从trace中读出来,用python来验证,确定是标准的rc4算法。 先来看0x7f0d010600中的原始数据,来自0x7b9c8d1b80,大小0x11c。 0x7b9c8d1b80来自0x7bb39d9400。 0x7bb39d9400来自7bb3daa000。7bb3daa000来自系统调用,调用号0x3f。 不同的内核版本其调用号也不相同,在我的设备上0x3f调用号对应的系统调用是read。 很明显,这段输入数据应该是通过读取文件而来。read函数的第一个参数是文件句柄,在trace日志中的值为0x1ec,它来自于openat系统调用。 openat系统调用第二个参数是文件路径,这里就不继续分析了,直接hook打印参数,发现确实打开的是本地文件,该文件中的内容被rc4加密了。 再来分析0x7bfb738800中的密钥。0x7bfb738800中的值来自to hex string算法,原始hex数据为0x7b40e71840。0x7b40e71840中的值来自0x7e7dec42e8,大小0x10字节。 0x7e7dec42e8来自0x7e7dec41f8。0x7e7dec41f8来自MD5算法,从下面的常量和算法特征中能一眼看出。在MD5算法中,会在原始数据的末尾先填充一个0x80,再填充0x0,直到补齐56字节,最后再填充8字节长度(位数)。所以根据这个特征,只要在trace日志中找到填充的地方,就能找到原始数据。
7b9c8[4|5][0-9a-zA-Z]{4}\[
trace中填充的地方如下,因此能得到原始数据的长度为0x14(0xa0 / 8),原始数据存储在0x7e7dec4208中。 0x7e7dec4208来自0x7b9aadada0。 0x7b9aadada0来自0x7e7dec42e0。 0x7e7dec42e0来自0x7e7dec4220。0x7e7dec4220来自sha1算法,从下面这些常量和右旋操作能看出。同理,也可以根据sha1的填充逻辑来确定原始数据。sha1的填充逻辑与MD5基本相同,区别在于最后填充的8字节长度在MD5中是以小端形式存储,在sha1中是以大端。所以基于这个逻辑,可以得出原始数据为32765f696473,长度为6(0x30 / 8)。32765f696473是一个字符串,以大端格式读取为“sdi_v2”。
经过上述分析,可以确定目标字符串来源于一段 JSON 数据,而这段 JSON 数据来自于一个经过加密的本地文件。该文件采用RC4算法进行加密,其密钥生成过程如下:
写这种算法分析的帖子真的是太无聊了,就先分析到这里吧,后续有机会再来谈下被加密的本地文件是怎么来的!!
自我感觉TT的算法并不难,就是量太大了,所以我想问问有没有感兴趣的朋友来一起分析,欢迎联系。要具备一定的Android逆向经验,水平相当即可。另外,本项目纯粹用于技术交流,不涉及黑灰产,也不以盈利为目的。
[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!