阅读了看雪上一篇对 android VMP 壳的反混淆思路文章:
[原创]android so文件攻防实战反混淆
对于壳的功能作者并没有进行详细的阐述,看后感觉意犹未尽,而且这家厂商的壳分析文章在网上挺多的,以前简单分析过它的免费版,这次正好拿来作为 VMP 壳入门。因为内容过多,需要分为上下两篇。
分析环境
设备:Nexus 5系统:Android 6架构:armeabi-v7a
java层入口
先看有没有实现 application实现了 application 就去看看它的 attachBaseContext ,加载了so那么接下来就是分析so
init函数
从初始化函数开始分析,F5 失败,0xD0688 的地方分析失败
0xD0688 处调用了一个函数,进去后 F5 看到参数个数有点多,可能是函数参数声明不正确
结合上层函数的传参情况,对该函数的参数声明修改成如下形式
1void __fastcall sub_D0C30(_DWORD *a1, int a2, int a3)
然后对 sub_D0C30 这个函数 F5 会出现如下错误,这是因为函数中存在破坏堆栈的指令
解决办法参考下面这篇文章:
[推荐]IDA sp-analysis failed 不能F5的 解决方案之(二)
ida 中打开 Options -> General -> Stack pointer ,这样方便查看堆栈情况,根据函数首尾入栈与出栈找到破坏堆栈的一句指令,把它 nop 掉即可
回到 init 函数,现在可以 F5 了,两个函数的主要内容都在注释中写出
1234567void init_proc(){ int v0; // r0 sub_D0F50((int)""); // mmap内存,把段seg000的内容拷贝过去 sub_D0C30("", (int)&dword_103D4, v0); // 解密}
进入 sub_D0C30 ,在该函数结尾的两个 mprotect 下断点,运行到断点处就能 dump 出解密后内存数据,第一个 mprotect 是修改代码段的权限,第二个 mprotect 是修改数据段的权限(包括 GOT 表),所以能够 dump 出解密后的代码段和数据段。
1234567891011void __fastcall sub_D0C30(_DWORD *a1, int a2, int a3){ //...... syscall_mprotect( (void *)((v12 + v23[2] - 4095) & 0xFFFFF000), ((v23[5] + 4095 + v12 + v23[2]) & 0xFFFFF000) - ((v12 + v23[2] - 4095) & 0xFFFFF000), 3); sub_D0ADC((_DWORD *)(v12 + a1[6]), v12 + a1[4], a1[5], v12); syscall_mprotect((void *)v12, v14, 7); syscall_munmap((void *)a3, v15);}
后面还有一个 init 函数,自己调试了一下,逻辑就是判断当前进程是不是 dex2oat ,不太重要就不细说了
JNI_OnLoad函数
OLLVM混淆JNI_Onload 做了平坦化混淆,网上有很多应对 OLLVM 的方法,我一般就直接在 ida 中 trace 出指令日志,然后分析日志
上一篇文章用 ida 提供的 trace 功能,出现跑飞的情况,而且我不想跟进函数调用,所以参考这位大佬的 trace 脚本自己改了一份:
[原创]细说arm反类ollvm混淆-基本思想
trace 脚本跟踪 JNI_Onload 的所有指令,效果如下:
12345678( libDexHelper.so[0xAEF38000])[2D E9 F0 4F ]0x00028868: PUSH.W {R4-R11,LR}( libDexHelper.so[0xAEF38000])[AD F6 AC 4D ]0x0002886C: SUBW SP, SP, #0xCAC( libDexHelper.so[0xAEF38000])[8D 4C ]0x00028870: LDR R4, =(_GLOBAL_OFFSET_TABLE_ - 0xAEF6087C)( libDexHelper.so[0xAEF38000])[00 22 ]0x00028872: MOVS R2, #0 ...( libDexHelper.so[0xAEF38000])[11 D0 ]0x0002AC20: BEQ loc_AEF62C46( libDexHelper.so[0xAEF38000])[0D F6 AC 4D ]0x0002AC46: ADDW SP, SP, #0xCAC( libDexHelper.so[0xAEF38000])[BD E8 F0 8F ]0x0002AC4A: POP.W {R4-R11,PC}
接下来如果函数比较简单就分析日志找到关键的块下断点,复杂的话就用脚本从执行过的块中尽可能过滤掉虚假块,对剩下的真实块逐个分析,基本能把逻辑分析出来。具体就不展开了,绕过 OLLVM 不是本篇的内容。像下面这样把真实块打印出来,index 表示执行的顺序,ea 表示块的偏移地址,接下来就是对这些块进行漫长分析.
12345678-----relevant-block------index: 0, start ea: 0x28868index: 2, start ea: 0x288aeindex: 3, start ea: 0x288b2...index: 116, start ea: 0x2a5aeindex: 117, start ea: 0x2ac16index: 118, start ea: 0x2ac46
注册java方法0x2A024 处调用 RegisterNatives 注册 com/secneo/apkwrapper/H 的所有 native 方法,整理出 java 方法对应的 native 函数
123456789101112131415161718"attach": sub_2775C"b": sub_17210"c": sub_18B10"d": sub_20384"e": sub_224A0"f": sub_1CE90"g": sub_1CA30"h": sub_1C420"n": sub_1BF1C"j": sub_1BC9C"k": sub_199CC"l": sub_1996C"m": sub_1991C"bb": sub_17B48"o": sub_18874"p": sub_126B0"q": sub_153C4"mu": sub_154BC
加载原dex0x2A030 处调用 sub_265E8 函数,在这里面完成 dex 的加载,同样是做了混淆
解密dex内存数据
在 0x2721C 处调用 sub_49604 ,该函数内部通过反射类 java/lang/Class 获取 dexCache 对象,再反射获取 java/lang/DexCache 的 dexFile 对象,再调用 GetIntField 将其转成在 native 层中的指针,在 android 6 上,该指针加 4 字节处指向当前壳 dex 在内存中的位置,最后通过匹配特征找到 dex 中的 injected_data 。
在 0x27366 处调了一个函数,观察第二个参数指向 injected_data 的某个位置,当函数跑完后该位置加密数据变成 dex ,说明调用解密函数解密 dex ,这里可以 dump 出 dex 。
来到 0x26E6A ,调用 access 函数查看 v1filter.jar 是否存在于 .cache 目录,若不存在则在 0x26D26 处解密并释放 v1filter.jar 到 .cache ,解密数据来自于 apk 包 assets 目录下的 resthird.data
加载dex
0x273C6 处调用 sub_3FA6D ,这个函数在 0x44A52 处 hook 了 art::DexFileVerifier::verify 函数,hook 后 verify 函数开头指令如下图所示:可以看到明显的 hook 特征,使用 LDR 指令跳转到函数 sub_AEF31EBC ,开头的 BX 指令的意义可能是为了保证 ldr 指令是 4 字节,这样可以固定使用 LDR PC,[PC - 4] 指令来取地址,不知道是否正确。sub_AEF31EBC 给 R0 赋值为 1 就返回了,也就是恒定返回 true回到 sub_3FA6D ,在 0x426BA 处 hook art::ClassLinker::OpenDexFilesFromOat ,hook 原理一样,fake 函数是 sub_433E8 ,这个函数留到下面再讲。sub_3FA6D 最后调用 dlsym 获取 art::DexFile::OpenMemory 地址然后作为返回值返回,后面要用到这个函数加载 dex
0x26756 处调用 sub_1A7C4 ,这个函数的逻辑是对每一个 classes.dex ,在 .cache 下创建文件,写 4 字节的 dex magic 到文件中,这个 app 一共 10 个 dex 。然后在 0x1AE44 处调用 com/secneo/apkwrapper/H 的 native 方法 f ,经过分析,该方法调用 makeDexElements 加载 dex 并且替换原 classloader 的 dexPathList 中的 dexElements 。调用 makeDexElements 所传入的 dex 路径是就是 10 个 dex 所在的 .cache 目录,而 dex 只有 4 字节,直接加载肯定是失败的,那么前面 hook OpenDexFilesFromOat 的 fake 函数 sub_433E8 在这里起到作用,其逻辑如下:
在 0x4345A 判断加载的是否是 v1Filter.jar ,若是就在 0x435EE 处调用原 OpenDexFilesFromOat 做文件加载,v1Filter.jar 里面就是个工具类 dex
在 0x4350C 处判断是否加载 classes.dex ,若是就在 0x4352E 处找到相应的 dex 数据,然后在 0x43568 处调用 OpenMemory 完成内存加载
接下来,在 OpenMemory 处下断点即可 dump 出所有 dex ,为了方便分析,可以使用这个工具把 dex 合并为 apk :dex2apk
此外,分析过程中没有发现对 dex 做指令还原,后来我又确认了一下,在 dex 加载过后查看 DefineClass 、LoadMethod 和 LinkMethod 等函数开头有没有 hook 指令特征,然而一直到 jni_onload 跑完都没看到变化。去年分析的免费版也不是抽取壳,也是在 OpenMemory 处 dump 出 dex 。
框架检测和反调试magisk检测0x2AB2C 处查看当前进程名是否是 :bbs:com.secneo.apkwrapper.r.S ,这个可能是检测 magisk 的 android 服务,不过我没在手机上找到这个服务,不知道具体如何检测。
1234567891011121314int __fastcall is_magisk_check_process(int a1){ getpid(a1); sub_15270((int)v6); // 获取进程名 ...... decrypt_str((int)v5, 30, 201); // 解密":bbs:com.secneo.apkwrapper.r.S" ...... if ( !strncmp_0(v6, v5, v1) ) { result = 1; } result = 0; return result;}
回到 JNI_Onload ,0x2A142 处的 BLX R5 指令跳转到 sub_5f5c8 ,这个函数是实现反调试以及 frida 检测逻辑的地方。
frida检测在 0x5fd12 处调用 sub_5A088 做 frida 检测
从 /proc/self/task/
从 /proc/self/task/
调用 readlink 读取 /proc/self/fd/
123456789101112131415161718192021222324252627282930313233343536373839404142int __fastcall sub_5A088(int a1){ // ... v1 = opendir_1(&v29); // "/proc/self/task" if ( v1 ) { while ( 1 ) { v3 = readdir_0(v1); v6 = open_1(&v73, 0x80000); // "/proc/self/task/
反调试在 0x5fd00 和 0x5fd06 处调用 pipe 函数,创建两个管道用于父子进程通信0x5f8ee 处 fork 进程,父进程逻辑如下:
0x5fb50 处 hook ptrace ,hook 后的 fake 函数是 sub_5B044 ,该函数通过 svc 调用 kill 掉进程
0x5fb5c 处开线程,线程函数 sub_59970 调用 inotify_add_watch 函数监控文件 /proc/self/mem
0x5fb56 处开线程,线程函数 sub_60ce0 调用 fopen 打开 /proc/self/status ,fgets 读取文件内容找到 “State:” ,然后读取值,再调用 strcasestr 匹配子字符串 "T (stopped)" 、"(zombie)" 以及 "t (tracing stop)" ,匹配到了程序会退出。接着打开 /proc/self/task/
最后往可写 pipe 写入 0xFF ,再从可读 pipe 读取一字节,这个字节如果不是 0xFF ,就做 svc 调用 kill 掉自己
除了不 hook ptrace,子进程逻辑跟父进程一样,而且它也会用管道跟父进程通信,在读出的字节不是 0xFF 会 kill 掉自身。
其它JNI_Onload 跑完后,回 java 层看到 com/secneo/apkwrapper/AW 的 attachBaseContext 调用 com/secneo/apkwrapper/H 类的 attach 方法,前面说过这个方法被 RegisterNatives 注册到 sub_2775C ,到这里一般是做资源替换和 application 替换,就不详细分析了。
结束感觉现在大多数壳都是用 makeDexElements 加载磁盘上 dex 然后在 java 层做 dexElements 合并,磁盘上的 dex 不是掏空就是加密,然后在加载 dex 的调用链上找个函数 hook 完成解密,比如 OpenCommon ,恢复抽取的方法指令就 hook loadMethod 。对于这个壳的反 frida ,亲测使用 hluda 的 spawn 模式可以过掉。然后因为有 ptrace 反调试,用 frida 或者调试器 attach 上去会崩,其它反调试可以编译修改内核绕过。除了上面分析的加固技术,还有防篡改技术,需要看看这篇文章:防篡改剖析