VirtualAPP源码解析-Native Hook技术
前言
本篇文章主要介绍VirtualAPP使用的Native Hook技术,不是很深入,因为涉及很多C++,ELF和指令集相关的知识点,很多知识还没有融会贯通,目前只是停留在名词的概念上。后续理解了在进行补充。
应用背景
VirtualAPP中使用了Native Hook技术,主要用于虚拟APP的文件访问重定向。这句话怎么理解和为什么这么做呢,我们先回顾一下VirtualAPP的大致原理。在VirtualAPP中启动一个虚拟APP,大致分为如下几部:
- VirtualAPP通过虚拟服务端启动APP B(虚拟APP)
- 虚拟服务端通过Provider创建APP B对应的进程,同时替换Intent数据指向代理组件
- APP B进程启动,同时将系统服务代理对象,通过动态代理的方式全部替换,指向虚拟服务端
- APP B进程 收到intent数据,将intent中数据解析,重新替换为目标组件。从而实现狸猫换太子。
在上述步骤我们可以看出,虚拟APP是VirtaulAPP的一个子进程。可想而知我们在虚拟app中进行文件存储或者sp操作时,最终的存储路径也是在VirtaulAPP的data目录下,这样就会带来一个问题。如果允许多个app就会可能出现文件访问冲突,同时也没有做到APP间隔离的目的。而VirtualAPP就是通过Native Hook技术解决了该问题。每个APP都有自己各自的文件存储路径。
源码分析
下面我们来简单了解下他是如何实现的,关键方法是VClientImpl的startIOUniformer方法,可以看出进行了存储路径映射,如在子进程当我们访问
/data/data/http://com.xxx/目录时会直接映射到io.virtualapp/virtual/data/user/0/http://com.xxx
NativeEngine.redirectDirectory("/sys/class/net/wlan0/address", wifiMacAddressFile);
NativeEngine.redirectDirectory("/sys/class/net/eth0/address", wifiMacAddressFile);
NativeEngine.redirectDirectory("/sys/class/net/wifi/address", wifiMacAddressFile);
NativeEngine.redirectDirectory("/data/data/" + info.packageName, info.dataDir);
NativeEngine.redirectDirectory("/data/user/0/" + info.packageName, info.dataDir);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
NativeEngine.redirectDirectory("/data/user_de/0/" + info.packageName, info.dataDir);
}
String libPath = VEnvironment.getAppLibDirectory(info.packageName).getAbsolutePath();
String userLibPath = new File(VEnvironment.getUserSystemDirectory(userId), info.packageName + "/lib").getAbsolutePath();
NativeEngine.redirectDirectory(userLibPath, libPath);
NativeEngine.redirectDirectory("/data/data/" + info.packageName + "/lib/", libPath);
NativeEngine.redirectDirectory("/data/user/0/" + info.packageName + "/lib/", libPath);
该方法最终会调用IOUniformer.cpp的startUniformer方法
void IOUniformer::startUniformer(const char *so_path, int api_level, int preview_api_level) {
void *handle = dlopen("libc.so", RTLD_NOW);
if (handle) {
HOOK_SYMBOL(handle, faccessat);
HOOK_SYMBOL(handle, __openat);
HOOK_SYMBOL(handle, fchmodat);
HOOK_SYMBOL(handle, fchownat);
HOOK_SYMBOL(handle, renameat);
HOOK_SYMBOL(handle, fstatat64);
HOOK_SYMBOL(handle, __statfs);
HOOK_SYMBOL(handle, __statfs64);
HOOK_SYMBOL(handle, mkdirat);
HOOK_SYMBOL(handle, mknodat);
HOOK_SYMBOL(handle, truncate);
HOOK_SYMBOL(handle, linkat);
HOOK_SYMBOL(handle, readlinkat);
HOOK_SYMBOL(handle, unlinkat);
HOOK_SYMBOL(handle, symlinkat);
HOOK_SYMBOL(handle, utimensat);
HOOK_SYMBOL(handle, __getcwd);
我们知道android系统是基于Linux内核,文件读写操作也是间接的通过库函数进行系统调用,如我们在应用开发中使用的inputStream与outputStream进行文件读写最终也是调用libc.so库函数提供的方法。
所以需要做到就是将libc库函数的方法进行Hook,将输入参数替换为我们的虚拟app路径,该过程即为native Hook。还有一个疑问点是我们怎么知道要hook哪些函数呢,只能通过查看libc的源码,当然源码也是公开的,可以直接查看如下地址。
https://www.androidos.net.cn/android/9.0.0_r8/xref/bionic/libc/bionic
以faccessat方法为例,我们可以看到方法参数有个pathname我们需要将改方法参数替换掉,然后重新调用系统方法。
extern "C" int ___faccessat(int, const char*, int);
int faccessat(int dirfd, const char* pathname, int mode, int flags)
{
// "The mode specifies the accessibility check(s) to be performed,
// and is either the value F_OK, or a mask consisting of the
// bitwise OR of one or more of R_OK, W_OK, and X_OK."
if ((mode != F_OK) && ((mode & ~(R_OK | W_OK | X_OK)) != 0) &&
((mode & (R_OK | W_OK | X_OK)) == 0)) {
errno = EINVAL;
return -1;
}
具体实现
经过上述步骤我们知道了,需要对libc链接库的方法进行hook,但是如何做到呢,这就不得不提Native Hook的具体实现了,Native Hook的实现方式有两种一个是PLT Hook 与 Inline Hook,实现原理涉及so动态链接过程与ELF文件格式,汇编指令等,这块大家可以百度一下。而罗迪使用的是一个第三方开源项目Cydia Substrate(http://www.cydiasubstrate.com/),该项目即是inline Hook的一种具体实现。爱奇艺开源的xHook则是PLT Hook方案的具体实现。与PLT Hook方案比较,inline Hook实用场景更广泛,能力更强大。而VirualAPP通过灵活的运用宏定义,Hook一个方法只需要两个步骤:
1. Hook调用
HOOK_SYMBOL(handle, faccessat);
2. 定义替换的方法
// int faccessat(int dirfd, const char *pathname, int mode, int flags);
HOOK_DEF(int, faccessat, int dirfd, const char *pathname, int mode, int flags) {
int res;
const char *redirect_path = relocate_path(pathname, &res);
int ret = syscall(__NR_faccessat, dirfd, redirect_path, mode, flags);
return ret;
}
下面我们分析一下 HOOK_SYMBOL与HOOK_DEF宏展开过程
#define HOOK_SYMBOL(handle, func) hook_function(handle, #func, (void*) new_##func, (void**) &orig_##func)
#define HOOK_DEF(ret, func, ...) \
static ret (*orig_##func)(__VA_ARGS__); \
static ret new_##func(__VA_ARGS__)
在编译期间会进行宏替换,HOOK_SYMBOL(handle, faccessat)最终替换为如下格式 ;
hook_function(handle,faccessat,(void*) new_faccessat,(void**)&orig_faccessat)
HOOK_DEF最终会替换为如下格式:
static int (*orig_faccessat)(int dirfd, const char *pathname, int mode, int flags);
static int new_faccessat(int, faccessat, int dirfd, const char *pathname, int mode, int flags) {
int res;
const char *redirect_path = relocate_path(pathname, &res);
int ret = syscall(__NR_faccessat, dirfd, redirect_path, mode, flags);
return ret;
}
可以看出通过宏替换,我们定义了一个函数指针,和一个newfaccessat的替换函数,最终调用hook_function方法实现Hook,hook_function内部调用MSHookFunction函数,该函数即为Cydia Substrate提供的能力。
static inline void hook_function(void *handle, const char *symbol, void *new_func, void **old_func) {
void *addr = dlsym(handle, symbol);
if (addr == NULL) { return; }
MSHookFunction(addr, new_func, old_func);
}
总结
相信大家对Native Hook在整体上有了初步的认识,学习Native Hook不能一蹴而就而是个缓慢的过程。后续文章为大家分享MSHookFunction的具体实现原理