关于在Android Mapping中隐藏 Native 库映射

缘起

在Android APP中隐藏部分native so文件在 /proc/self/maps 中的展示是个非常有趣的课题。前段时间在和某dalao讨论某恶意应用就存在着这种行为,当时我脑子中第一个想法就是将原本通过dlopen等方式打开的native so文件重新映射为匿名内存。但是这只是一个很模糊的想法,很多细节比如能否将匿名内存重新mapping到原本的基地址、在匿名内存中一些JNI接口是否还能够被正常的调用到等等都不明了。

恰巧不久之后,github就给我推送了Android-Library-Remap-Hide项目,正好可以基于这个项目探究下隐藏native so mapping信息的原理并且扩展一些对抗策略的研究

老样子本文涉及到的代码可以下面的链接中获取:

实现原理

remapping的核心功能全部在 RemapTools.h 中实现;

首先是从/proc/self/maps中获取当前进程目标native lib的内存映射以及读写权限:

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
29
30
31
32
std::vector<ProcMapInfo> ListModulesWithName(std::string name) {
std::vector<ProcMapInfo> returnVal;

char buffer[512];
FILE *fp = fopen("/proc/self/maps", "re");
if (fp != nullptr) {
while (fgets(buffer, sizeof(buffer), fp)) {
if (strstr(buffer, name.c_str())) {
ProcMapInfo info{};
char perms[10];
char path[255];
char dev[25];

sscanf(buffer, "%lx-%lx %s %ld %s %ld %s", &info.start, &info.end, perms, &info.offset, dev, &info.inode, path);

//Process Perms
if (strchr(perms, 'r')) info.perms |= PROT_READ;
if (strchr(perms, 'w')) info.perms |= PROT_WRITE;
if (strchr(perms, 'x')) info.perms |= PROT_EXEC;
if (strchr(perms, 'r')) info.perms |= PROT_READ;

//Set all other information
info.dev = dev;
info.path = path;

LOGI("Line: %s", buffer);
returnVal.push_back(info);
}
}
}
return returnVal;
}

完成目标内存映射信息的采集后,就可以依次remapping掉对应的内存部分完成隐藏了

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
void RemapLibrary(std::string name) {
std::vector<ProcMapInfo> maps = ListModulesWithName(name);

for (ProcMapInfo info : maps) {
void *address = (void *)info.start;
size_t size = info.end - info.start;
void *map = mmap(0, size, PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); // 1

if ((info.perms & PROT_READ) == 0) {
LOGI("Removing protection: %s", info.path);
mprotect(address, size, PROT_READ); // 2
}

if (map == nullptr) {
LOGE("Failed to Allocate Memory: %s", strerror(errno));
}
LOGI("Allocated at address %p with size of %zu", map, size);

//Copy to new location
std::memmove(map, address, size); // 3
mremap(map, size, size, MREMAP_MAYMOVE | MREMAP_FIXED, info.start); // 4

//Reapply protection
mprotect((void *)info.start, size, info.perms); // 5
}
}

大体上分成了以下5个步骤:

  1. 申请一块与remapping区域相同大小的内存空间;

  2. 判断下remapping区域是否可读,如果不可读就通过mprotect将其设置为可读区域,确保这部分区域可以被拷贝到匿名内存中;

  3. 拷贝remapping区域到申请的匿名内存中;

  4. 通过mremap 将申请的匿名内存映射到需要remapping的地址;

  5. 恢复原本内存段的读写权限;

代码非常的简洁明了,封装成函数后,调用就可以将native lib隐藏到匿名内存的同时也不影响lib的正常运行;

值得关注的还有之前没怎么用到过的mremap , 这是个libc中的标准函数,功能描述如下:

**mremap**() expands (or shrinks) an existing memory mapping,
potentially moving it at the same time (controlled by the *flags*
argument and the available virtual address space).
*old_address* is the old address of the virtual memory block that
you want to expand (or shrink).  Note that *old_address* has to be
page aligned.  *old_size* is the old size of the virtual memory
block.  *new_size* is the requested size of the virtual memory
block after the resize.  An optional fifth argument, *new_address*,
may be provided; see the description of **MREMAP_FIXED** below.
If the value of *old_size* is zero, and *old_address* refers to a
shareable mapping (see [mmap(2)](https://man7.org/linux/man-pages/man2/mmap.2.html) **MAP_SHARED**), then **mremap**() will
create a new mapping of the same pages.  *new_size* will be the
size of the new mapping and the location of the new mapping may
be specified with *new_address*; see the description of
**MREMAP_FIXED** below.  If a new mapping is requested via this
method, then the **MREMAP_MAYMOVE** flag must also be specified.
The *flags* bit-mask argument may be 0, or include the following
flags:

测试

frida hook

虽然一些恶意的程序可以通过remapping匿名化一些native lib在proc maps中的展示,但是我更想知道这种操作会不会影响到frida去hook里面的函数。

在验证之前,我的猜想是remapping并不会让frida hook不到目标函数,因为remapping后程序是能够继续调用到原本native lib中的函数的,而且从原理上来看原本库中所有的函数地址都没有发生变化;但是可能会需要额外想办法去定位下隐藏的native lib的映射基地址。然而测试结果却让我大为震惊。

我在一台Android 12的Pixel 6上,使用Frida 15.1.14做的验证

在Demo APP中实际上是隐藏了两个native lib,一个是包含JNI接口的libRevenyInjector.so, 另一个是通过dlopen加载的libTest.so

在隐藏了这两个native lib之后,挂载frida,我惊奇的发现Process.getModuleByName仍然可以找到它们:

1.png

笑死,居然只是个障眼法….

既然都能展示Module信息也就一定可以正常的hook或是主动调用了。原本想的getModuleByName没办法定位隐藏模块的基址的应对方案也就没有深入测试下去的必要了,也就只是两个思路罢了:

  • remapping的前提是需要知道目标内存段的起始&终止地址,这些需要从/proc/self/maps中获取;frida & ebpf hook文件的读写不是难事,在读取的maps中也一定会有被隐藏的native lib信息;

  • hook libc中的mremap函数,这是隐藏功能实现的核心函数,调用的参数就有native lib的基地址;即使通过svc直接实现mremap,通过Frida & ebpf 仍然可以hook,最多是多几行代码;

小结

通过remapping的方式, 确实能混淆安全研究员对一些关键库的关注;但是对frida hook基本没有任何的影响。当然也可能是remapping的方法不行,还是有些新招式能够影响到frida的。同时也说明了frida很可能并不是通过 /proc/self/maps 来获取modules列表的;

除此之外,这种招式对于APP进程本身检查自身的加载native lib也会有些影响,比如Xposed模块 & 注入的frida so完全可以用这种方式隐藏下,来规避一些检测;

总之,这个问题仍然还有许多继续深入研究下去的点,就等之后有机会再说吧。

参考