手把手教你如何 Hook Native 方法

809 阅读9分钟

手把手教你如何 Hook Native 方法

经常写 C 的代码的同学肯定对 xHook 的大名有所耳闻吧,它是一个 hook Native 方法的库,比如说我们可以 hook mallocrealloccalloc 等和内存分配相关的方法,来追踪 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.sob.so 他们都调用了外部的 malloc 方法,我如果只需要 hook a.so 中的 malloc 方法,我就修改 a.so 加载到内存中对应的 .got 地址就好了,修改后也就是只有 a.somalloc 函数会被 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 分配一个 MessagefreeMessage 释放 Message 的内存;sayHelloMessage.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:我是 hookarm64libwaithook,其他的架构我没有适配,所以运行应该有问题。

我们上面说到如果要 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 截图:

Screenshot_20231130_175204.png

Demo 的源码我都放在 Github上了,需要的自取:Stacktrace

最后

xHook 非常神奇,在阅读本篇文章后我希望你能够知道它神奇的原理。