某某某加固系统分析
依然是四年前的分析总结,时过境迁,应该没啥价值了,留作纪念!
某某某加固系统采取了内外两层native代码模式,外层主要为了保护内层核心代码,从分析来看外层模块主要用来反调试,释放内层模块,维护内存模块的某些运行环境达到防止分离内外模块,另外由于内层模块不是通过系统加载的,所以实现了自主的ELF加载,这样就实现内层模块的加密处理。这些实现后就可以依赖内层模块保护dex。从而达到系统保护目的。
对于某某某加固系统的反调试网上已经有很多资料了,就不再作为重点介绍了。下面主要介绍对内层模块的加载,加密,保护。这些外面资料不多的内容。
外层解密内层核心模块的解密算法是使用zlib库的uncompress函数实现的,不过解密函数解密出来的并不是整个的模块,而是被加密了或者说被移除了四个部分的模块,包含:
program_header_table、.rel.dyn、.rel.plt、Dynamic Segment 。由于是自己的加载系统加载,所以这些被move的部分依赖父模块组装,防止内存直接dump出解密的内层模块。
上面代码只是说明功能,扣出来的,需要自己整理下可能才能执行。
脚本的原理就是根据某某某加固流程,父模块使用uncompress解压后会把解压出来的被偷走的数据重新解密到新的内存地址,在memcpy时得到内存地址和长度,然后等解密出来后dump数据。
另外是根据数据的大小取相关数据的,每个APP可能会不同,需要先跑下看看。
需要说明下,首先跑下hook uncompress后的memcpy hexdump,memcpy加载的新地址数据出现ELF头数据的,表明加载了。然后向上逆推其他数据,这样就能确定每个的数据大小,然后更改脚本,获取数据,并dump下来。
比如本例:
begin:memcpy,len:0xbbff4
0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
c9085589 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 .ELF............
c9085599 03 00 28 00 01 00 00 00 00 00 00 00 34 00 00 00 ..(.........4...
c90855a9 bc fa 0b 00 00 00 00 05 34 00 20 00 08 00 28 00 ........4. ...(.
c90855b9 19 00 18 00 11 a6 dd 35 da cf 22 1a 71 b7 8b 08 .......5..".q...
由于父模块需要内存偏移修正,所以完整的模块需要在后面的一次才能dump。
拿到了需要的so模块数据,我们需要修复这个so模块,否则ida无法分析,用010Editor也会打开失败。下面进入so修复:
使用的工具有010Editor和ELFfix
ELFfix 修复原理具体请参考:
https://bbs.pediy.com/thread-192874.htm
用010Editor打开dump的so模块,会提示错误。
我们已我们已经知道被移除了填充杂乱数据的几个关键的部分。所以肯定不能正常加载。当然修复是需要理解下ELF的文件格式和内存加载原理。这样更便于理解为啥需要这样步骤修复。这个各位自己学习学习了。
首先我们需要还原program_header_table ,这个是系统解析加载的关键数据,用010Editor打开我们刚才dump出来的0x100字节的那个数据
复制并覆盖到dumpso的program_header_table里面,
这样我们就可以正确的打开了,找到这个表(RW_) Dynamic Segment
我们把dump的Dynamic Segment数据也用010Editor打开,就是大小0xd8的那个
找到这个表的位置:
跳转到这个位置:
复制数据并覆盖:
好了,到这里我们完成ELFfix需要的关键数据,保存这个文件,然后可以使用ELFfix工具进行修复了
复制到ELFfix目录下,并执行相关的修复命令会得到修复后的文件:
dump_new_full.so ß修复后的so
再次用010Editor打开这个修复后的so,还是会提示错误。不过打开后section_header_table已经正确了,原来的section_header_table是错误的:
修复后已经正确解析出来了:
同时dynamic_symbol_table 表也出现了,打开看看:
来说下为啥先要修复program_header_table 节和这个段里面的Dynamic Segment,根据ELFfix作者文章里面说明,修复是依赖这两个数据进行解析得到ELF section,所以必须先还原这两个数据块。
下面开始还原rel数据,一个是jmprel,一个是rel
我们打开这两个节:
看到这个长度了吧,我们dump了,用010Editor打开dump的数据,然后010Editor中转到偏移地址中,用dump的解密数据还原。
同样还原下面数据:
到这里内层的so模块被修复还原出来了,IDA加载完全没问题了,得到这个模块我们就可以用IDA分析调试了。当然也可以脱壳使用了。
跟内存调试的比较看看:
注意:目前这个ELFfix不支持64位程序修复。
好了,有了这个就可以详细的分析某某某内层模块的功能,当然也可以patch代码等操作了。
下面来看看某某某加固系统如何来保护内层so功能模块的。比较有特色
某某某加固为了保护内层so不能脱离外层so环境,做了一些防范措施,这种也是,通过移除关键部位的二进制代码,如果脱离了整个环境,那么就会执行错误,被移除的二进制代码丢失。
内层在Java层使用getByte这个函数获取被偷走的二进制代码数据,填充回so的执行中。
被偷的数据:
>dump 0x10004b1
010004B1: 00 00 00 00 00 00 00 00 BD 10 B5 4F F6 75 74 E8 ...........O.ut.
010004C1: BF 10 BD 10 B5 4F F6 76 74 E8 BF 10 BD 10 B5 4F .....O.vt......O
来看看这个是啥函数,原来是:JNIEnv->CallStaticObjectMethodV
这种Java层偷函数二进制执行代码的方式还是比较新颖的。
一般模块的导出函数都是给其他模式引用的接口,某某某这个导出函数却是调用父模块的接口,因为子模块的加载完全是父模块负责的,所以这个接口的填充是父模块做的,所以如果脱离了父模块,这个函数就变成了空的。而且这个函数还是个特别的核心函数,来看看:
获取key,这个函数是获取解密算法rc4的key的,如果没有这个解密key,后面的dex解密都会失败。所以必须到父模块中执行。
通过查询有两个地方调用:
通过分析发现这个函数被填充为父模块的‘_Z9__arm_a_2PcjS_Rii’ 这个导出函数:
这个函数是个多功能函数,根据不同的传入参数,执行不同的功能。很复杂的一个校验,计算,antidebug等集成函数。
这个就是某某某自己实现的类似乱序处理过的多功能集成函数。
来看看key计算的时候参数是啥:
======================= Registers =======================
R0=0x20917ec R1=0x350 R2=0x2024006 R3=0x2024018
R4=0x2024000 R5=0x2024006 R6=0x0 R7=0xcbcca6e8 R8=0x350
R9=0x350 R10=0x2024018 R11=0x202c350 R12=0x80000000 SP=0x7ffad8
LR=0xcbda5859 PC=0xcbdb08fc
======================= Disassembly =====================
0xcbdb08fc: blx r7
020917EC: 22 39 52 52 54 52 52 52 42 52 52 52 13 02 02 19 "9RRTRRRBRRR....
020917FC: 17 0B 60 30 60 31 6A 61 66 67 33 6A 65 61 64 6A ..`0`1jafg3jeadj
0209180C: 62 34 22 39 52 52 5A 52 52 52 53 52 52 52 01 36 b4"9RRZRRRSRRR.6
0209181C: 39 17 3C 26 20 2B 63 22 39 52 52 5E 52 52 52 4E 9.<& +c"9RR^RRRN
0209182C: 52 52 52 33 31 26 3B 24 3B 26 2B 1C 33 3F 37 31 RRR31&;$;&+.3?71
0209183C: 3D 3F 7C 33 3B 35 28 7C 27 3B 7C 30 33 21 37 7C =?|3;5(|';|03!7|
执行下这个函数,得到的key:
>dump 0x2024006
02024006: 67 5E 7F 35 70 37 78 2E 7D 22 75 27 08 56 4A A1 g^.5p7x.}"u'.VJ.
上面的参数哪里来的呢?
分析发现原来是从原始包里面用libz解压出来的:
Executing syscall openat(ffffff9c, 02029000, 00020000, 00000000) at 0xcbc28be4
path:/data/app/xxxxxxxx-1/base.apk
这个解出来就是原始包里面的classes.dex部分:
另外一个地方的调用参数如下:
======================= Registers =======================
R0=0x7ffbf9 R1=0x0 R2=0x2024040 R3=0x7ffb10
R4=0xcbc761c8 R5=0x7ffbf8 R6=0x202b054 R7=0xcbde56df R8=0xcbc761c8
R9=0x202c34c R10=0x0 R11=0x2024000 R12=0x2024040 SP=0x7ffb08
LR=0xcbcca6e8 PC=0xcbdb0982
======================= Disassembly =====================
0xcbdb0982: blx r2
>dump 0x7ffbf9
007FFBF9: 00 DA CB 54 B0 02 02 DF 56 DE CB C8 61 C7 CB 00 ...T....V...a...
007FFC09: 00 00 00 2D 00 00 00 00 60 02 02 00 70 02 02 F0 ...-....`...p...
这是个校验调用,如果脱离父模块就会死在这个调用里面,堆栈会被破坏掉。
当然还有其他类似的这样调用父模块校验:
某某某加固系统最重要的功能应该就是为了对dex文件的保护了,因为一般得到原始的dex后,去掉加固模块就可以直接运行。相当于完整脱壳了。为了达到这个目的,某某某加固系统对dex的保护特别重要。前面的这些对模块的保护最终的目的也是为了保护dex文件。下面来看看他的保护是怎么样的。
某某某加固系统第一次会把原始包中的classes.dex用libz函数解压出来,然后系统会把他重新编译成oat文件,这个文件中的dex头被加密处理了。里面的数据部分也被加密处理。在运行的时候,加固系统直接解内存中加载的classes.oat文件,而不用再次解压原始文件了。这样带来了效率的提升。也保证了没有明文的dex存在磁盘上。
数据的解密用到了rc4算法,算法的key刚才已经说过了是从父模块的函数中获取的,保证了解密的安全性。
rc4算法部分大家可以网上找资料看看,用key生成0x100的解密盒子。然后用这个盒子去解密数据。
nt __fastcall rc4(int result, _BYTE *a2, int a3)
{
int v3; // lr
int v4; // r12
int v5; // r5
if ( a3 )
{
v3 = 0;
v4 = 0;
do
{
--a3;
v3 = (v3 + 1) % 256;
v5 = *(unsigned __int8 *)(result + v3);
v4 = (v4 + v5) % 256;
*(_BYTE *)(result + v3) = *(_BYTE *)(result + v4);
*(_BYTE *)(result + v4) = v5;
*a2++ ^= *(_BYTE *)(result + (unsigned __int8)(*(_BYTE *)(result + v3) + v5));
}
while ( a3 );
}
return result;
}
算法核心部分。
下面是跟踪的截图和注释:这个循环保证解密oat中的所有dex文件:
使用内存中的oat文件,避免出现明文dex在磁盘上。也提高了效率。
下面是查询每个dex的开始地址:
解密出dex头:
进入rc4解密函数,rc4解密其中被加密的数据,而不是整个的dex文件:
下面就已经解密出来加密的数据了:
所有的oat文件中的dex都被解密出来后,需要立即保存下来,否则某某某加固系统会把dex的头再次删除,我们在这个地方先dump下来oat文件。
Dump这个已经解密的oat文件后,我们就可以把其中的dex文件都找出来:
根据oat文件结构知道,dex数据是从0x1000开始的,前面是头,后面接着就是dex文件,从我们刚才跟踪的时候知道,这个oat中有三个dex文件。向下找下看看,也可以根据刚才R5中的偏移找到开始的位置,第一个位置偏移是0x1808,这个里面的dex头保存完整:
从dex结构我们知道,开始的偏移+0x20 后面的4个字节就是长度,第一个长度为:0x60E694,把这个数据保存出来,然后继续这样找其他的dex:
全部找到后我们用解析工具打开看看:
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)