手把手教你如何 Hook Native 方法
经常写 C
的代码的同学肯定对 xHook
的大名有所耳闻吧,它是一个 hook
Native
方法的库,比如说我们可以 hook
malloc
、 realloc
和 calloc
等和内存分配相关的方法,来追踪 Native
的内存分配的栈,可以通过这样的方法来分析内存的问题。除了监控内存的分配以外,还可以有很多其他的功能,发挥自己的想象力吧。
本篇文章就是一步一步来实现 hook
内存分配的方法 malloc
方法。在这之前我希望你对 ELF
文件有基本的了解,如果有了解对他的实现的原理会有更加深刻的认识,我之前有写过相应的文章,需要的同学自取:
关于 ELF 格式文件的笔记(一)
关于 ELF 格式文件的笔记(二)
关于 ELF 格式文件的笔记(三)
理论知识
在 .dynamic
Section
中定义了当前的可执行文件或者库(库还可以依赖其他的库,很好理解吧)中依赖了哪些其他的库,在执行当前程序或者加载当前库时,linker
会再去加载这些依赖的库到内存中,如果这些库已经加载就不用再加载了。
库是加载好了,但是我们怎样去调用库中的方法呢(或者使用库中的全局变量)?首先我们的库他会在 .dynsym
Section
中声明它所用到的外部库的符号,所谓的符号也就是方法名,变量名等等。可以认为就是一个字符串的标识,通过这个我们肯定不能够调用方法的,如果要调用方法需要通过方法对应的机器码所存放的地址去调用才行。那么问题就来了,如何获取这个地址呢?我们需要通过 linker
去拿到这个地址,在被调用的库中它也会在 .dynsym
中声明它对外暴露的符号和所对应的地址,这样 linker
就可以拿到了。在 Android
中都是饥饿加载。
linker
加载依赖的符号的地址有两种方式,一种是饥饿加载,也就在库被加载到内存中时就会通过 .plt
Section
去请求 linker
去加载所需符号的地址,然后把这个对应的地址写入到 .got
Section
中;另一种是懒加载,在加载库的时候不会去加载它所依赖的地址,只有在调用对应的符号的时候,.plt
发现对应的符号地址没有加载到 .got
中才会请求 linker
去加载,如果已经加载就直接使用就好了。
linker
虽然能够找到对应的符号的地址,但是还有一个问题,这个符号的地址要写入到 .got
中的哪个位置呢?在 .rela.plt
(还有很多别的重定位 Section
,名字都是 .rela.xxx
这样的,大家可以去查查他们的具体功能,这里不说了) 重定位 Section
中记录了这个地址。
在复习了如何调用依赖库的方法后,然后我们回到我们的标题,那么要如何去 hook
一个方法呢?哈哈,对了就是通过修改 .got
中的外部库的地址来实现,我们只需要把这个地址修改成我们 hook
的方法的地址就行了。这里要注意是每个库可或者执行文件他们都有一个单独的 .got
文件来描述他们自己的依赖的外部符号的地址。假如有两个库 a.so
和 b.so
他们都调用了外部的 malloc
方法,我如果只需要 hook
a.so
中的 malloc
方法,我就修改 a.so
加载到内存中对应的 .got
地址就好了,修改后也就是只有 a.so
的 malloc
函数会被 hook
,而 b.so
是不会受影响的。这里要注意一下。
写一个简单的库
我们先忘记 hook
这件事,写一个简单的库先。
libwaithook.so
头文件如下:
#ifndef STACKTRACE_WAITHOOK_H
#define STACKTRACE_WAITHOOK_H
typedef struct Message {
char *chars;
int charsLen;
} Message;
Message* allocMessage(int size);
void sayHello(Message *msg);
void freeMessage(Message * msg);
#endif //STACKTRACE_WAITHOOK_H
实现如下:
#include <cstdlib>
#include "waithook.h"
Message* allocMessage(int size) {
auto *msg = static_cast<Message *>(malloc(sizeof(Message)));
char *chars = static_cast<char *>(malloc(size));
msg->chars = chars;
msg->charsLen = size;
return msg;
}
void freeMessage(Message* msg) {
char* chars = msg->chars;
msg->chars = nullptr;
free(chars);
free(msg);
}
void sayHello(Message* msg) {
sprintf(msg->chars, "Hello, World!!");
}
实现非常简单,allocMessage
分配一个 Message
; freeMessage
释放 Message
的内存;sayHello
在 Message.chars
中写入 Hello, World!!
字符串。
libhook.so
libhook.so
中就是简单调用 libwaithook.so
库中的方法。
extern "C" JNIEXPORT jstring JNICALL
Java_com_tans_stacktrace_MainActivity_sayHello(
JNIEnv* env,
jobject act /* this */) {
auto msg = allocMessage(20);
sayHello(msg);
auto jString = env->NewStringUTF(msg->chars);
freeMessage(msg);
return jString;
}
就是简单的调用 libwaithook.so
中的方法,然后通过 jni
放回到 Java
层,最终 Android
会通过 Toast
显示出来。
hook libwaithook.so
中的 malloc
方法
Tips:我是 hook
的 arm64
的 libwaithook
,其他的架构我没有适配,所以运行应该有问题。
我们上面说到如果要 hook
malloc
方法,那就需要找到对应 .got
中储存 malloc
函数地址的地方,而 .rela.plt
重定向中有记录这个地址。所以我们来通过 readelf
来查看一下它当中的内容:
Relocation section '.rela.plt' at offset 0x538 contains 7 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000001bf0 000100000402 R_AARCH64_JUMP_SL 0000000000000000 __cxa_finalize@LIBC + 0
000000001bf8 000200000402 R_AARCH64_JUMP_SL 0000000000000000 __cxa_atexit@LIBC + 0
000000001c00 000300000402 R_AARCH64_JUMP_SL 0000000000000000 __register_atfork@LIBC + 0
000000001c08 000400000402 R_AARCH64_JUMP_SL 0000000000000000 malloc@LIBC + 0
000000001c10 000500000402 R_AARCH64_JUMP_SL 0000000000000000 free@LIBC + 0
000000001c18 000600000402 R_AARCH64_JUMP_SL 0000000000000000 __vsprintf_chk@LIBC + 0
000000001c20 000700000402 R_AARCH64_JUMP_SL 0000000000000000 __stack_chk_fail@LIBC + 0
malloc@LIBC
就是对应的 malloc
函数的符号,然后在 .got
中的地址就是 0x1c08
。这个地址是在 ELF
文件中的相对地址,不是加载在内存中的绝对地址,所以在运行时还需要获取到 libwaithook.so
加载到内存中的起始地址,通过这个起始地址再加上 0x1c08
就能够得到 malloc
在 .got
中运行时得到的地址。
OK,首先我们来看看如何获取一个库的加载到内存中的起始地址(我是在 libhook.so
中 去 hook
libwaithook.so
):
int phdr_callback(struct dl_phdr_info* info, size_t size, void* data) {
if (strstr(info->dlpi_name, "libwaithook.so")) {
auto *baseAddr = static_cast<unsigned long *>(data);
*baseAddr = info->dlpi_addr;
}
return 0;
}
void hookMalloc() {
unsigned long waitHookLibBaseAddr = 0;
dl_iterate_phdr(phdr_callback, &waitHookLibBaseAddr);
if (waitHookLibBaseAddr != 0) {
LOGD("Find libwaithook.so base address: 0x%016lx", waitHookLibBaseAddr);
} else {
LOGE("Don't find libwaithook.so base address.");
return;
}
// ...
}
我们需要通过 dl_iterate_phdr
去遍历所有的用到的库的基准地址,第一个参数是回调函数指针,第二个参数是回调函数的自定义参数。
我是找到了 libwaithook.so
库后,然后去把它的基准地址写入到 waitHookLibBaseAddr
中。
然后我们就可以通过这个基准地址然后再加上 0x1c08
就能够得到 malloc
在 .got
中储存地址的地方了。我们就可以对这个地址中的内容进行修改,让它指向我们指定方法的地址了。但是这里还有一个问题,.got
中的地址是没有写权限的,我们需要修改这个地址的权限,修改地址的权限是以页为单位,我们需要找到我们的要修改的地址对应的页,通常在 Android
中页的大小为 4KB,如果对页的概念不懂的同学,可以看看我之前的文章:聊聊虚拟内存
我们继续看看如何修改内存页中的权限:
void hookMalloc() {
// ...
void ** mallocGotAddr = reinterpret_cast<void **>(waitHookLibBaseAddr + 0x1c08);
auto pageStart = (PAGE_MASK & (unsigned long)mallocGotAddr);
auto pageEnd = pageStart + PAGE_SIZE;
//add write permission
mprotect((void *)pageStart, PAGE_SIZE, PROT_READ | PROT_WRITE);
*(mallocGotAddr) = (void *)my_malloc;
// ...
}
首先我通过上面的到的基准地址然后加上 0x1c08
后得到了,我们要修改的地址,然后得到了对应地址的页。然后调用 mprotect
为这个地址页添加写的权限,最后把这个地址中的值指向了我自定义的函数 my_malloc
的地址。这样就完成修改了。
这样我们就大功告成了吗??,呃呃呃呃,还差最后一步,因为原来的 malloc
函数的地址在寄存器中可能还存在缓存数据,我们需要把这个缓存数据清除,让它重新加载我们设置的地址。
通过以下方法来清除寄存器中的缓存:
void hookMalloc() {
// ...
__builtin___clear_cache(static_cast<char *>((void *) pageStart), static_cast<char *>((void *) pageEnd));
}
清除缓存,同样也是以内存页为单位的。
好了这里我在贴出一下完整的 libhook.so
代码:
#include <jni.h>
#include <link.h>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <cstdlib>
#include <cstdio>
#include <sys/mman.h>
#include "waithook.h"
#include "../android_log.h"
static jobject mainAct = nullptr;
static JNIEnv *jniEnv = nullptr;
void *my_malloc(size_t s) {
if (mainAct != nullptr && jniEnv != nullptr) {
auto clazz = jniEnv->FindClass("com/tans/stacktrace/MainActivity");
auto methodId = jniEnv->GetMethodID(clazz, "hookMessage", "(Ljava/lang/String;)V");
char *chars = static_cast<char *>(malloc(50));
sprintf(chars, "Alloc %d bytes", s);
auto jString = jniEnv->NewStringUTF(chars);
jniEnv->CallVoidMethod(mainAct, methodId, jString);
LOGD("my_alloc: %d", s);
}
return malloc(s);
}
int phdr_callback(struct dl_phdr_info* info, size_t size, void* data) {
if (strstr(info->dlpi_name, "libwaithook.so")) {
auto *baseAddr = static_cast<unsigned long *>(data);
*baseAddr = info->dlpi_addr;
}
return 0;
}
void hookMalloc() {
unsigned long waitHookLibBaseAddr = 0;
dl_iterate_phdr(phdr_callback, &waitHookLibBaseAddr);
if (waitHookLibBaseAddr != 0) {
LOGD("Find libwaithook.so base address: 0x%016lx", waitHookLibBaseAddr);
} else {
LOGE("Don't find libwaithook.so base address.");
return;
}
void ** mallocGotAddr = reinterpret_cast<void **>(waitHookLibBaseAddr + 0x1c08);
auto pageStart = (PAGE_MASK & (unsigned long)mallocGotAddr);
auto pageEnd = pageStart + PAGE_SIZE;
//add write permission
mprotect((void *)pageStart, PAGE_SIZE, PROT_READ | PROT_WRITE);
*(mallocGotAddr) = (void *)my_malloc;
//clear instruction cache
__builtin___clear_cache(static_cast<char *>((void *) pageStart), static_cast<char *>((void *) pageEnd));
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_tans_stacktrace_MainActivity_sayHello(
JNIEnv* env,
jobject act /* this */) {
jniEnv = env;
mainAct = act;
auto msg = allocMessage(20);
sayHello(msg);
auto jString = env->NewStringUTF(msg->chars);
freeMessage(msg);
jniEnv = nullptr;
mainAct = nullptr;
return jString;
}
extern "C" JNIEXPORT void JNICALL
Java_com_tans_stacktrace_MainActivity_hook(
JNIEnv* env,
jobject /* this */) {
hookMalloc();
}
my_alloc
中会通过 jni
通知 Java
层,我甚至还可以拿到请求分配内存的方法栈,如果获取 Native
的方法栈可以看看我的这篇文章:手把手教你如何 Dump Native 线程栈和监听崩溃信号
最后我放一下我自己的测试 Demo 截图:
Demo 的源码我都放在 Github上了,需要的自取:Stacktrace。
最后
xHook
非常神奇,在阅读本篇文章后我希望你能够知道它神奇的原理。