对我们开发者而言,在写一些安全性要求较高的项目时,自然不希望自己的代码被别人随意注入修改。比如在 Android 方面,现在已经有很多成熟的 Hook 框架比如 Xposed、xHook、ByteHook、Frida 等可供用户选择。本文将着重介绍一种基于内存搜索的检测方案,经实际测试,能在进程受到注入时绕过 Shamiko 正确检出 Zygisk 和 LSPosed 的存在


实验环境

  • 设备:Mi 10 Lite Zoom|MIUI V12.5.4.0.RJVCNXM|API 30

  • 环境:Magisk v25.1 (25100)|LSPosed v1.8.3 (6552)|Shamiko v0.5.1 (115)

实现原理

  我们知道,一个进程所映射的各种资源(代码、堆栈、动态库等)都会被记录到 /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 文件入手

  加大映射的链接库在 maps 文件中的隐藏力度,使其对应条目不出现在文件中。例如 bind mount 一个 FIFO 到 /proc/[pid]/maps 同时开启 inotify,等待读操作时写入我们过滤后的数据(但是如何处理 lseek?

  • 从映射的 so 库入手

  模块开发者处理好字符串和符号的混淆,Zygisk 加载动态链接库后抹除 ELF 文件头和入口函数信息,防止在内存搜索中暴露特征