对我们开发者而言,在写一些安全性要求较高的项目时,自然不希望自己的代码被别人随意注入修改。比如在 Android 方面,现在已经有很多成熟的 Hook 框架比如 Xposed、xHook、ByteHook、Frida 等可供用户选择。本文将着重介绍一种基于内存搜索的检测方案,经实际测试,能在进程受到注入时绕过 Shamiko 正确检出 Zygisk 和 LSPosed 的存在
实验环境
实现原理
我们知道,一个进程所映射的各种资源(代码、堆栈、动态库等)都会被记录到 /proc/[pid]/maps
这个文件里,虽然 Zygisk / Shamiko 已经尽可能将注入的动态链接库映射成匿名的 memfd,但并没有很好地处理符号隐藏,这就给了我们可乘之机。
通过读取映射表 maps 文件,我们能够得知各个映射在内存中的偏移量及其权限,从中过滤出所有可读可执行的映射,然后在对应的内存区域中搜索关键字,就能把异常的映射文件揪出来。核心代码非常短,只有寥寥 20 行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 bool memory_scan () { bool result = false ; const char *prefix[] = { "zygisk_module_entry" _enc, "org.lsposed.lspd" _enc }; FILE *fp = fopen ("/proc/self/maps" _enc, "r" ); char *left, *right; char perm[8 ], file[512 ]; while (fscanf (fp, "%p-%p %s %*s %*s %*s %[^\n]s" _enc, &left, &right, perm, file) != EOF) { if (perm[0 ] == 'r' && perm[2 ] == 'x' ) { size_t size = right - left; for (auto &pattern : prefix) { if (memmem (left, size, pattern, strlen (pattern))) { char *ptr = file; while (isspace (ptr[0 ]) && ptr < file + sizeof (file)) ptr++; LOGW ("Found \"%s\" in %p-%p (%s)" , pattern, left, right, file); LOGW ( "/proc/%d/map_files/%lx-%lx" , getpid (), reinterpret_cast <uintptr_t >(left), reinterpret_cast <uintptr_t >(right) ); result = true ; } } } } return result; }
其中用户定义字面量 ""_enc
用于在编译期混淆字符串,增加安全性,同时可以避免搜索到自己文件产生误报,下面是检测器的输出:
1 2 3 4 5 6 Found "zygisk_module_entry" in 0x6f67f0b000-0x6f67f17000 (/memfd:jit-cache (deleted)) /proc/16608/map_files/6f67f0b000-6f67f17000 Found "zygisk_module_entry" in 0x6f68001000-0x6f6802c000 (/memfd:jit-cache (deleted)) /proc/16608/map_files/6f68001000-6f6802c000 Found "org.lsposed.lspd" in 0x6f68001000-0x6f6802c000 (/memfd:jit-cache (deleted)) /proc/16608/map_files/6f68001000-6f6802c000
其中两项属于 LSPosed,一项属于「Enhanced mode for Storage Isolation(以下简称 SR)」。值得一提的是,无论是否对目标应用启用存储隔离,SR 都会对其注入链接库,这使得在使用 SR 的设备上第三方应用可以相当容易地检出 Zygisk 😰
另外,如果查看这几个被找到的 map_files 的前几个字节,可以发现 ELF 文件头的一些特征,如果针对这些特征改进搜索,应该能进一步提高查找的效率:
1 2 3 4 5 6 7 8 9 10 ~ # cat /proc/16608/map_files/6f67f0b000-6f67f17000 | head -c 128 | hexdump -C 00000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 |.ELF............| 00000010 03 00 b7 00 01 00 00 00 60 1f 00 00 00 00 00 00 |........`.......| 00000020 40 00 00 00 00 00 00 00 08 c1 00 00 00 00 00 00 |@...............| 00000030 00 00 00 00 40 00 38 00 08 00 40 00 17 00 16 00 |[email protected] ...@.....| 00000040 01 00 00 00 05 00 00 00 00 00 00 00 00 00 00 00 |................| 00000050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000060 60 b9 00 00 00 00 00 00 60 b9 00 00 00 00 00 00 |`.......`.......| 00000070 00 10 00 00 00 00 00 00 01 00 00 00 06 00 00 00 |................| 00000080
奇思妙想🤔
以下是针对上述检测手段想到的一些可能的应对方式,奈何本人水平有限,实在没法确认其可行性,这里就抛砖引玉,留给各位大佬分析了
加大映射的链接库在 maps 文件中的隐藏力度,使其对应条目不出现在文件中。例如 bind mount 一个 FIFO 到 /proc/[pid]/maps
同时开启 inotify,等待读操作时写入我们过滤后的数据(但是如何处理 lseek?
模块开发者处理好字符串和符号的混淆,Zygisk 加载动态链接库后抹除 ELF 文件头和入口函数信息,防止在内存搜索中暴露特征