动手打造Android7.0以上的注入工具
在不使用Xposed的一些场景下,想要Hook进入目标APK的方法,最直接有效的方法是注入代码到目标APK,进而完成Hook操作。
面临的挑战
编写注入工具的原理是借助安卓系统的ptrace
接口,操纵目标进程的内存,修改进程空间的数据与代码,然后调用dlopen()
、dlsym()
等接口加载与使用动态库,完成达到注入so的目的。
ptrace
接口是Linux层面的东西,在网络上可以大量找到这个API的使用介绍与方法,这里不打算深研它的基础原理与使用方法,而是把目光聚集在安卓系统上其特定使用场景上。
把so注入到了目标进程中后,并没有就此完事,而是需要做更多有意义的事情,比如Hook目标APK的代码,so的代码Hook这里不讲,Java层的话,就需要获取其VM环境上下文,从而调用Java的API,手动的在目标进程中加载DEX或APK,最后再执行Hook这个操作。
从安卓7.0对系统的限制,以及注入工具的使用流程上看,我们面临着如下的挑战:
- 7.0系统的限制与绕过。7.0系统不允许调用很多私有或限制的API,很多函数调用受到了阻碍,再者,SeLinux的限制,让so动态库的注入与加载也遇到了问题,并不能直接加载一个不受系统信任的so动态库到目标不进程中去。
- 编写注入器与注入代码。如何编写一个通用的框架,可以与注入工具配合好在目标APK中加载so与APK文件,你想好了么?
- 注入系统进程。有时候为了选择Hook多个目标APK的方法,会选择一劳永逸的注入它们的系统父进程,比如SystemServer与com.android.phone进程。注入这些进程与普通进程有区别吗?
- NDK编译系统隐藏API。在编写注入工具时,会调用到很多安卓系统中使用频繁,但NDK中却没有提供的接口。这个时候就需要想办法来调用它们了。
- 代码混淆。最后,作为功能的增强,可以加入OLLVM的自动化编译,对目标so进行代码混淆。
开发实战
这里使用了低版本的NDK r10e进行开发。代码的编译通过命令行完成,编写使用Visual Studio Code。
7.0系统的限制与绕过
首先是7.0系统的限制与绕过。不解决这个问题,后面的开发工作无从谈起。
dlopen()
与dlsym()
的调用限制网络上有一个优雅的绕过方法。代码仓库是:https://github.com/avs333/Nougat_dlfunctions。核心代码位于jni/fake_dlfcn.c文件中,fake_dlopen()
与fake_dlsym()
可以代替dlopen()
与dlsym()
来使用,它的原理是在当前进程的内存中,搜索符号表的方式,在内存中找到函数的内存地址。当然,它是有限制的:只能dlopen()
已经加载进入内存的so,即系统或自己预先加载的动态库,并且参数flags
加载标志被忽略。
以上解决了调用系统限制API的问题,但加载外部so的限制却还在那。SeLinux的强制实施,使得dlopen()
外部的so动态库有可能会失败返回。SeLinux会检测so动态库的label标签与权限是否满足可加载的要求,不满足就会无情的拒绝!为了解决这个问题,需要调用setxattr()
修改so的属性信息。这里封装的代码如下:
int setxattr(const char *path, const char *value) {
if (!file_exists("/sys/fs/selinux")) {
return 0;
}
return syscall(__NR_setxattr, path, "security.selinux", value, strlen(value),
0);
}
当我们注入so前,可以插入如下代码来解决第三方so加载的问题:
snprintf(so_path, sizeof(so_path), "/data/local/tmp/libsvr.so");
...
setxattr(so_path, "u:object_r:system_file:s0");
编写注入器
注入器是一个ELF格式的安卓可执行文件,使用Android.mk配置好它的开发信息如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_C_INCLUDES := $(LOCAL_PATH)
LOCAL_MODULE := inject
LOCAL_LDLIBS := -ldl -llog
LOCAL_CFLAGS := -std=c99
# 基于pie
LOCAL_CFLAGS += -fvisibility=hidden
LOCAL_CFLAGS += -fPIE
LOCAL_LDFLAGS += -pie -fPIE
LOCAL_SRC_FILES := inject/inject.c
include $(BUILD_EXECUTABLE)
注入器的代码网络上流传了一个inject。早先的一个版本是由古河放出,后来github上也有了很多的版本。例如https://github.com/shutup/libinject2。当然,它们很多都年久失修,并不能在新的系统上运行起来,需要对代码做一些修正。
其中一处是对ptrace_attach()
的处理,如zygote
进程,很多时候是不能一次attach成功的,需要进行更加细致的处理。这里修正代码如下:
int ptrace_attach(pid_t pid, bool is_zygote) {
struct pt_regs regs;
int status = 0;
if (ptrace(PTRACE_ATTACH, pid, NULL, 0) < 0) {
perror("ptrace_attach");
return -1;
}
if (is_zygote) {
while (waitpid(pid, &status, __WNOTHREAD) == -1 && (EINTR == errno)) {
LOGI("waitpid EINTR, status = %d\n", status);
}
int times = 50;
while ((times--) != 0) {
if (ptrace(PTRACE_SYSCALL, pid, NULL, 0) < 0) {
perror("ptrace_syscall");
ptrace_detach(pid);
kill(pid, SIGCONT);
return -1;
}
while (waitpid(pid, &status, __WNOTHREAD) == -1 && (EINTR == errno)) {
LOGI("waitpid EINTR, status = %d\n", status);
}
ptrace_getregs(pid, ®s);
bool is_async_syscall = false;
#if defined(__arm__)
if (regs.ARM_r7 <= NR_faccessat) {
if ((NR_ioctl == regs.ARM_r7) ||
(NR__newselect == regs.ARM_r7) ||
(NR_poll == regs.ARM_r7)) {
is_async_syscall = true;
}
} else {
//if (regs.ARM_r7 > NR_epoll_pwait) {
// is_async_syscall = false;
//}
#define _BYTE unsigned char
#define BYTEn(x, n) (*((_BYTE*)&(x)+n))
#define LOBYTE(x) BYTEn(x, 0)
is_async_syscall = regs.ARM_r7 > NR_epoll_pwait ? 0 :
((1 << (LOBYTE(regs.ARM_r7) - 0x4F)) & 0x803) != 0;
}
#elif defined(__aarch64__)
//FIXME aarch64 ptrace_attach
is_async_syscall = false;
#endif
if (ptrace_continue(pid) < 0) {
ptrace_detach(pid);
kill(pid, SIGCONT);
return -1;
}
if (!is_async_syscall)
usleep(100000u);
kill(pid, SIGSTOP);
while (waitpid(pid, &status, __WNOTHREAD) == -1 && (EINTR == errno)) {
LOGI("waitpid EINTR, status = %d\n", status);
}
if (is_async_syscall) {
return 0;
}
}
return 0;
} else {
status = ptrace_wait_for_signal(pid, SIGSTOP);
LOGI("ptrace_wait_for_signal: %d %d\n", __LINE__, status);
return 0;
}
}
然后有一个非常需要注意的地方,是ptrace_call()
进行系统调用时,对于libc.so中的函数调用,需要对lr寄存器进行修正,更新为libc.so的起始地址,不然,有些函数是被调用失败的,比如mmap()
。代码如下:
int ptrace_call(pid_t pid, uintptr_t addr, long *params, int num_params,
struct pt_regs *regs) {
int i;
#if defined(__arm__)
int num_param_registers = 4;
#elif defined(__aarch64__)
int num_param_registers = 8;
#endif
for (i = 0; i < num_params && i < num_param_registers; i++) {
regs->uregs[i] = params[i];
}
//
// push remained params into stack
//
if (i < num_params) {
regs->ARM_sp -= (num_params - i) * sizeof(long);
ptrace_writedata(pid, (void *)regs->ARM_sp, (uint8_t *)¶ms[i],
(num_params - i) * sizeof(long));
}
regs->ARM_pc = addr;
if (regs->ARM_pc & 1) // thumb
{
regs->ARM_pc &= (~1u);
regs->ARM_cpsr |= CPSR_T_MASK;
} else // arm
{
regs->ARM_cpsr &= ~CPSR_T_MASK;
}
regs->ARM_lr = 0;
long lr_val = 0;
char sdk_ver[32];
memset(sdk_ver, 0, sizeof(sdk_ver));
__system_property_get("ro.build.version.sdk", sdk_ver);
if (atoi(sdk_ver) <= 23) {
lr_val = 0;
} else { // Android 7.0
static long start_ptr = 0;
if (start_ptr == 0) {
char map_buf[MAX_PATH];
char name_buf[0x400];
char line[0x400];
memset(map_buf, 0, sizeof(map_buf));
memset(name_buf, 0, sizeof(name_buf));
memset(line, 0, sizeof(line));
sprintf(map_buf, "/proc/%d/maps", pid);
FILE *fd = fopen(map_buf, "r");
if (fd) {
while (fgets(line, sizeof(line), fd)) {
if (strstr(line, "libc.so")) {
if (!fgets(line, sizeof(line), fd) )
break;
long start_addr;
long end_addr;
char ownship[8];
long ll;
char ss[8];
long ll2;
sscanf(line, "%lx-%lx %4s %lx %5s %ld %s",
&start_addr, &end_addr, ownship, &ll, ss, &ll2, name_buf);
if (ownship[2] != 'x') {
start_ptr = start_addr;
}
}
}
fclose(fd);
}
}
lr_val = start_ptr;
}
regs->ARM_lr = lr_val;
if (ptrace_setregs(pid, regs) == -1 || ptrace_continue(pid) == -1) {
return -1;
}
int stat = 0;
waitpid(pid, &stat, WUNTRACED);
while (stat != 0xb7f) {
if (ptrace_continue(pid) == -1) {
LOGE("error\n");
return -1;
}
waitpid(pid, &stat, WUNTRACED);
}
return 0;
}
注入SystemServer与com.android.phone
注入进程的流程是一样的,只是判断是zygote进程时,需要做一些特别的处理。
inject_remote_process()
为注入的核心,代码如下:
int inject_remote_process(pid_t target_pid, const char *library_path,
const char *function_name, void *param,
size_t param_size, bool is_zygote) {
int ret = -1;
void *dlopen_addr, *dlsym_addr, *dlclose_addr, *dlerror_addr;
void *malloc_addr;
uint8_t *map_base;
struct pt_regs regs, old_regs;
long parameters[10];
LOGD("[+] Injecting process: %d, %s, %s, %s\n", target_pid, library_path,
function_name, (const char*)param);
if (get_module_base(target_pid, library_path) != 0) {
LOGI("[+] target process[%d] injected already\n", target_pid);
return EXIT_SUCCESS;
}
if (ptrace_attach(target_pid, is_zygote) == -1) {
LOGI("[+] target process[%d] ptrace_attach returned.\n", target_pid);
return EXIT_SUCCESS;
}
if (ptrace_getregs(target_pid, ®s) == -1)
goto exit;
// save original registers
memcpy(&old_regs, ®s, sizeof(regs));
malloc_addr = get_remote_addr(target_pid, libc_path, (void *)malloc);
LOGD("[+] Remote malloc address: 0x%p\n", malloc_addr);
// call malloc
parameters[0] = 0x4000;
if (ptrace_call_wrapper(target_pid, "malloc", malloc_addr, parameters, 1, ®s) == -1)
goto exit;
LOGD("[+] get mmap retval\n");
map_base = (uint8_t*)ptrace_retval(®s);
dlopen_addr = get_remote_addr(target_pid, linker_path, (void *)dlopen);
dlsym_addr = get_remote_addr(target_pid, linker_path, (void *)dlsym);
dlclose_addr = get_remote_addr(target_pid, linker_path, (void *)dlclose);
dlerror_addr = get_remote_addr(target_pid, linker_path, (void *)dlerror);
LOGI("[+] Get imports: dlopen: 0x%p, dlsym: 0x%p, dlclose: 0x%p, dlerror: 0x%p\n",
dlopen_addr, dlsym_addr, dlclose_addr, dlerror_addr);
ptrace_writedata(target_pid, map_base, (uint8_t*)library_path, strlen(library_path) + 1);
parameters[0] = (long)map_base;
parameters[1] = RTLD_NOW | RTLD_GLOBAL;
// dlopen(path)
if (ptrace_call_wrapper(target_pid, "dlopen", dlopen_addr, parameters, 2, ®s) == -1)
goto exit;
void *sohandle = (void*)ptrace_retval(®s);
if (!sohandle) {
// dlerror()
if (ptrace_call_wrapper(target_pid, "dlerror", dlerror_addr, 0, 0, ®s) == -1)
goto exit;
LOGE("start ptrace_retval");
uint8_t *errret = (uint8_t*)ptrace_retval(®s);
LOGE("stop ptrace_retval");
uint8_t errbuf[100];
LOGE("start ptrace_readdata");
ptrace_readdata(target_pid, errret, errbuf, 100);
LOGE("stop ptrace_readdata");
//LOGE("[+] dlopen failed. error code[0x%X], error msg[%s]", *errret, errbuf);
LOGE("[+] dlopen failed. ");
goto exit;
}
#define FUNCTION_NAME_ADDR_OFFSET 0x100
#define FUNCTION_PARAM_ADDR_OFFSET 0x200
ptrace_writedata(target_pid, map_base + FUNCTION_NAME_ADDR_OFFSET,
(uint8_t*)function_name, strlen(function_name) + 1);
parameters[0] = (long)sohandle;
parameters[1] = (long)map_base + FUNCTION_NAME_ADDR_OFFSET;
// dlsym(handle, function_name)
if (ptrace_call_wrapper(target_pid, "dlsym", dlsym_addr, parameters, 2, ®s) == -1)
goto exit;
void *hook_entry_addr = (void*)ptrace_retval(®s);
LOGI("[+] hook_entry_addr = %p\n", hook_entry_addr);
ptrace_writedata(target_pid, map_base + FUNCTION_PARAM_ADDR_OFFSET, param, param_size + 1);
// hook_entry(param)
parameters[0] = (long)map_base + FUNCTION_PARAM_ADDR_OFFSET;
if (ptrace_call_wrapper(target_pid, function_name, hook_entry_addr, parameters, 1, ®s) == -1)
goto exit;
ptrace_setregs(target_pid, &old_regs);
ptrace_detach(target_pid);
ret = 0;
exit:
LOGE("exit %d",ret);
return ret;
}
inject_com_android_phone()
代码如下:
void inject_com_android_phone(const char *path) {
char apk_path[512];
char so_path[512];
pid_t pid = find_pid_of("com.android.phone", false);
if (pid > 0) {
memset(apk_path, 0, sizeof(apk_path));
memset(so_path, 0, sizeof(so_path));
//snprintf(so_path, sizeof(so_path), "%s/%s", path, SRV_SO_NAME);
snprintf(so_path, sizeof(so_path), "/data/local/tmp/libsvr.so");
//snprintf(apk_path, sizeof(apk_path), "%s/%s", path, SRV_APK_NAME);
snprintf(apk_path, sizeof(apk_path), "/data/local/tmp/svr.apk");
if (!file_exists(apk_path)) {
return;
}
setxattr(apk_path, "u:object_r:system_file:s0");
if (!file_exists(so_path)) {
return;
}
setxattr(so_path, "u:object_r:system_file:s0");
inject_remote_process(pid, so_path, SRV_INIT_FUNC_NAME, apk_path, strlen(apk_path), false);
}
}
inject_system_server()
的代码与它一样,只是调用inject_remote_process()
的最后一个参数不同。
最后,封装一下接口,编写main()
如下:
int main(int argc, char *argv[]) {
int type;
char path[0x1000];
const char *dir;
type = atoi(argv[1]);
memset(path, 0, 0x1000);
readlink("/proc/self/exe", path, 4096uLL);
dir = dirname(path);
strcpy(path, dir);
symlink_odex(path);
switch (type) {
case 1:
inject_system_server(path);
break;
case 2:
inject_com_android_phone(path);
break;
case 3:
inject_zygote(path);
break;
default:
printf("error\n");
break;
}
return 0;
}
编写注入代码
编写注入代码libsrv.so,主要的目的是要在目标APK中加载APK或DEX。它需要一个init初始方法,在加载时就执行这个操作。代码如下:
pthread_t gThread;
__attribute__ ((visibility ("default"))) void clientInit(const char* jarpath) {
pthread_create(&gThread, NULL, (void *(*)(void*))_clientInit, (void*)jarpath);
}
新开一个_clientInit
的线程,它传入的参数jarpath即为要加载的APK。实现代码如下:
int _clientInit(const char* jarpath) {
JNIEnv* env = NULL;
JavaVM *javaVM = android::AndroidRuntime::mJavaVM;
LOGE("use mJavaVM returned %p\n", javaVM);
if (javaVM) {
jint result = javaVM->AttachCurrentThread(&env, 0);
if ((result == JNI_OK) && (env != NULL)) {
LOGE("attach ok. clientInit JavaVM : 0x%p, JNIEnv : 0x%p\n", javaVM, env);
// FIXME hook_loadNativeLobrary();
// System.setProperty("process_arch", "64");
load_dex_and_run(env, jarpath);
javaVM->DetachCurrentThread();
LOGE("DetachCurrentThread all finished!");
} else {
LOGE("NOTE: attach of thread failed\n");
return -1;
}
}
/*
if (sdk_ver > 23) {
#if defined(__arm__) || defined(__i386__)
void *librt_dso = fake_dlopen("/system/lib/libandroid_runtime.so", RTLD_NOW);
#elif defined(__aarch64__) || defined(__x86_64__)
void *librt_dso = fake_dlopen("/system/lib64/libandroid_runtime.so", RTLD_NOW);
#endif
LOGE("fake_dlopen for libandroid_runtime.so returned %p\n", librt_dso);
void *pVM = fake_dlsym(librt_dso, "_ZN7android14AndroidRuntime7mJavaVME");
LOGE("fake_dlsym for android::AndroidRuntime::mJavaVM returned %p\n", *(void**)pVM);
javaVM = (JavaVM *)*(void**)pVM;
} else {
javaVM = android::AndroidRuntime::getJavaVM();
LOGE("use getJavaVM() returned %p\n", javaVM);
}
*/
return 0;
}
android::AndroidRuntime::mJavaVM
是一个导出的符号,它表示当前系统运行环境的JavaVM实例,可以通过它来创建JAVA环境,加载APK。注意注释掉的代码,这是尝试通过代码调用libandroid_runtime.so
的方式来获取JavaVM实例,这两种方法都是可取的。
如何获取这个系统隐藏的导出符号我们下面再讲,先看看如何加载一个APK。load_dex_and_run()
是关键,它在AttachCurrentThread()
成功调用后,会返回一个JNIEnv环境,有了它就可以顺序的调用Java层的loadClass()
来加载APK了。代码如下:
void load_dex_and_run(JNIEnv *env, const char *jarpath) {
jclass clzClassLoader = env->FindClass("java/lang/ClassLoader");
LOGI("java/lang/ClassLoader 0x%p\n", clzClassLoader);
jmethodID mdgetSystemClassLoader = env->GetStaticMethodID(clzClassLoader, "getSystemClassLoader", "()Ljava/lang/ClassLoader;");
LOGI("java/lang/ClassLoader.getSystemClassLoader method 0x%p\n", mdgetSystemClassLoader);
jobject systemClassLoader = env->CallStaticObjectMethod(clzClassLoader, mdgetSystemClassLoader);
LOGI("java/lang/ClassLoader.getSystemClassLoader 0x%p\n", systemClassLoader);
if (NULL == systemClassLoader) {
LOGE("getSystemClassLoader failed!!!");
return;
}
char cmdline[1024];
const char* entryName;
get_cmdline_from_pid(getpid(), cmdline, sizeof(cmdline));
if(strstr(cmdline, "system_server") != NULL) {
entryName = "entryServer";
} else if(strstr(cmdline, "com.android.phone") != NULL) {
entryName ="entryPhone";
} else if (strstr(cmdline, "zygote") != NULL) {
entryName = "entryZygote";
} else {
LOGE("wrong cmdLine %s",cmdline);
return;
}
jclass clzPathClassLoader = env->FindClass("dalvik/system/PathClassLoader");
LOGI("java/lang/ClassLoader 0x%p\n", clzClassLoader);
jmethodID mdinitPathCL = env->GetMethodID(clzPathClassLoader, "<init>", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/ClassLoader;)V");
LOGI("PathClassLoader loading jarpath[%s]\n", jarpath);
jstring jarpath_str = env->NewStringUTF(jarpath);
jobject myClassLoader = env->NewObject(clzPathClassLoader, mdinitPathCL, jarpath_str, NULL, systemClassLoader);
env->DeleteLocalRef(jarpath_str);
LOGI("myClassLoader 0x%p\n", myClassLoader);
if (NULL != myClassLoader) {
jclass entry_class = findClassFromLoader(env, myClassLoader, "com.app.service.EntryClass");
if (NULL != entry_class) {
LOGI("Entry Class 0x%p\n", entry_class);
/*
char dirpath[1024];
memset(dirpath, 0, sizeof(dirpath));
strcpy(dirpath, jarpath);
strcpy(dirpath, dirname(dirpath));
LOGI("PathClassLoader loading dirpath[%s]\n", dirpath);
*/
jmethodID entry_method = env->GetStaticMethodID(entry_class, entryName, "()Ljava/util/List;");
if (NULL != entry_method) {
//jstring dirpath_str = env->NewStringUTF(dirpath);
jobject arr = env->CallStaticObjectMethod(entry_class, entry_method/*, dirpath_str*/);
//env->DeleteLocalRef(dirpath_str);
if (arr != NULL) {
//class ArrayList
jclass cls_arraylist = env->GetObjectClass(arr);
//method in class ArrayList
jmethodID arraylist_get = env->GetMethodID(cls_arraylist,"get","(I)Ljava/lang/Object;");
jmethodID arraylist_size = env->GetMethodID(cls_arraylist,"size","()I");
jint len = env->CallIntMethod(arr,arraylist_size);
LOGI("get java ArrayList<User> object by C++ , then print it...../n");
for (int i = 0; i < len; i += 2) {
jobject obj_user = env->CallObjectMethod(arr,arraylist_get,i);
jobject obj_user1 = env->CallObjectMethod(arr,arraylist_get,i+1);
enable(env, NULL, obj_user, obj_user1);
}
} else {
LOGE("return binder arr null");
}
}
}
}
}
好了,代码就帖这么多了,流程上已经明了。
文章内容到这里截断了。。知乎发文居然不能到4k字。。好吧,大家上微信公众号看吧。
小结
本篇主要介绍了如何开发一款支持Android 7.0以上版本的so注入工具,并讲解了开发中可能遇到的坑,以及应对它们的措施,当然,要想开发一款兼容性很好的注入工具,还需要进行大量的测试与实践,这后续的工作就交给大家了。
文章精美排版PDF与代码,知识星球会员可以在知识星球:【软件安全与逆向分析】(ID: 86753808)中下载。
https://t.zsxq.com/rRR3juN (二维码自动识别)
更多精彩内容,欢迎关注微信公众号【feicong_sec】