Android inline hook 浅析

简单了解一下 inline hook 技术。

inline hook 技术涉及到很多底层知识,所以我不太 hold 得住。幸好网上能找到很多不错的参考资料,就算不能完全弄懂,也可以了解个大概。先上参考资料:

什么是 inline hook

Inline hooking is a method of intercepting calls to target functions. The general idea is to redirect a function to our own, so that we can perform processing before and/or after the function does its; this could include: checking parameters, shimming, logging, spoofing returned data, and filtering calls.

The hooks are placed by directly modifying code within the target function (inline modification), usually by overwriting the first few bytes with a jump; this allows execution to be redirected before the function does any processing

inline hook 是拦截目标函数调用的一种手段。通常是将原始方法调用重定向到我们指定的新方法,以便在原始方法执行前后可以执行某些操作,包括检查参数、打印日志、过滤某些调用等等。

inline hook 的实现手段是直接修改目标函数的代码(即内联修改),通常是用跳转指令替换目标函数代码的前几个字节。跳转指令让目标函数进行实际处理前被重定向。

inline hook 需要解决的问题:

  • hook 成功后,如何调用原先的旧函数
  • 无限递归问题
  • 多线程场景时的 race conditions
  • 指令修正

Inline hook 的原理

inline hook 由3部分组成:

  • Hook - 为了 hook 目标函数(旧函数),会向其代码中写入一个5个字节的跳转指令(实际跳转指令以及指令大小跟平台相关)
  • Proxy - 用于指定被 hook 的目标函数将要跳转到的函数(新函数)
  • Trampoline - 用于调用旧函数

为什么需要 trampoline

前面提到的两个问题,可以通过 trampoline 来解决

  • hook 成功后,如何调用原先的旧函数
  • 无限递归问题

假设有一个方法 MessageBoxA(),它可以显示一个对话框。假设我们 hook 了该方法,即,将其重定向到某个 Proxy。这个 Proxy 先打印出参数,再调用 MessageBoxA() 显示对话框。但是要注意,由于已经被 hook,所以正常调用 MessageBoxA() 时它又会重定向到 Proxy。这是一个无限递归调用,最后会导致程序因 stack overflow 而中止。

有一个直观的解决方案:在 Proxy 中先 unhook MessageBoxA() (即取消重定向),然后调用 MessageBoxA(),然后再一次 hook MessageBoxA() (即打开重定向)。

上图中提供了另外一个更通用的解决方案:使用 trampoline。先将 MesasgeBoxA() 的头5个字节保存在 trampoline 中,再向 MessageBoxA() 的头5个字节写入 Proxy 的地址。这样一来,hook MessageBoxA() 之后,如果你想正常调用原来的 MessageBoxA(),你可以借助 trampoline 来完成。

Android-Inline-Hook 源码分析

ele7enxxh/Android-Inline-Hook 是 Github 上的一个开源项目。它的几个要点包括:

  • 指令模式判断
  • 跳转指令构造
  • 跳转指令修正
  • 多线程处理及其他细节

这里只看跳转指令构造的大致原理(涉及到太多背景知识和技术细节,只能不求甚解)。

inlineHookItem 结构体

每个被 hook 的函数被记录在 inlineHookItem 结构体中:

1
2
3
4
5
6
7
8
9
10
11
struct inlineHookItem {
uint32_t target_addr; // 目标函数地址
uint32_t new_addr; // 新函数地址
uint32_t **proto_addr;
void *orig_instructions; // 原始指令
int orig_boundaries[4];
int trampoline_boundaries[20];
...
void *trampoline_instructions; // 跳转指令
...
};

inlineHookItem.trampoline_instructions 字段正是上文中提到的 trampoline。

registerInlineHook() 方法

调用 registerInlineHook() 方法开始记录 inlineHookItem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
enum ele7en_status registerInlineHook(uint32_t target_addr, uint32_t new_addr, uint32_t **proto_addr)
{
struct inlineHookItem *item;

...

item = findInlineHookItem(target_addr);
if (item != NULL) {
...
}

item = addInlineHookItem();

item->target_addr = target_addr;
item->new_addr = new_addr;
item->proto_addr = proto_addr;
item->length = TEST_BIT0(item->target_addr) ? 12 : 8;
...

return ELE7EN_OK;
}

该方法几个主要步骤是:

  • 调用 findInlineHookItem() 方法检查是否已有相关记录。有则直接返回,否则进入下一步
  • 调用 addInlineHookItem() 方法为指定的 target_addr 来创建一个新的 inlineHookItem
  • 初始化创建的 inlineHookItem

doInlineHook() 方法

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
static void doInlineHook(struct inlineHookItem *item)
{
...
if (item->proto_addr != NULL) {
*(item->proto_addr) = TEST_BIT0(item->target_addr) ? (uint32_t *) SET_BIT0((uint32_t) item->trampoline_instructions) : item->trampoline_instructions;
}

if (TEST_BIT0(item->target_addr)) {
int i;

i = 0;
if (CLEAR_BIT0(item->target_addr) % 4 != 0) {
((uint16_t *) CLEAR_BIT0(item->target_addr))[i++] = 0xBF00; // NOP
}
((uint16_t *) CLEAR_BIT0(item->target_addr))[i++] = 0xF8DF;
((uint16_t *) CLEAR_BIT0(item->target_addr))[i++] = 0xF000; // LDR.W PC, [PC]
((uint16_t *) CLEAR_BIT0(item->target_addr))[i++] = item->new_addr & 0xFFFF;
((uint16_t *) CLEAR_BIT0(item->target_addr))[i++] = item->new_addr >> 16;
}
else {
((uint32_t *) (item->target_addr))[0] = 0xe51ff004; // LDR PC, [PC, #-4]
((uint32_t *) (item->target_addr))[1] = item->new_addr;
}
...
}

doInlineHook() 方法是 inline hook 关键之一。其作用包括:

  • 处理对原始方法的访问 - 将 trampoline_instructions 强制转型为 void *proto_addr。如果想调用原始方法,调用 proto_addr 指向的方法即可
  • 修改原始方法,写入跳转指令

relocateInstruction() 方法

relocateInstruction() 方法是 inline hook 的关键之一。其作用是将原始函数的被跳转指令替换的那几个指令拷贝到 trampoline_instructions 中,此时 PC 值已经变动,所以还需要对相关指令进行修正。 PC 值相关的指令由 INSTRUCTION_TYPE 描述。

relocateInstruction() 方法用于按不同的平台分为:

  • relocateInstructionInThumb()
    • relocateInstructionInThumb16()
    • relocateInstructionInThumb32()
  • relocateInstructionInArm()

这里只看 relocateInstructionInArm() 方法。它的参数包括:

  • target_addr - 待Hook的目标函数地址,即当前 PC 值,用于修正指令
  • orig_instructions - 存放原有指令的首地址,用于修正指令和后续对原有指令的恢复
  • length - 存放的原有指令的长度,Arm 指令为8字节;Thumb 指令为12字节
  • trampoline_instructions - 存放修正后指令的首地址,用于调用原函数
  • orig_boundaries - 存放原有指令的指令边界(所谓边界即为该条指令与起始地址的偏移量),用于后续线程处理中,对 PC 的迁移
  • trampoline_boundaries - 存放修正后指令的指令边界,用途与上相同
  • count - 处理的指令项数,用途与上相同

精简后的代码如下:

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
static void relocateInstructionInArm(uint32_t target_addr, uint32_t *orig_instructions, int length, uint32_t *trampoline_instructions, int *orig_boundaries, int *trampoline_boundaries, int *count)
{
for (orig_pos = 0; orig_pos < length / sizeof(uint32_t); ++orig_pos) {
uint32_t instruction;
int type;
instruction = orig_instructions[orig_pos];
type = getTypeInArm(instruction);
if (type == BLX_ARM || type == BL_ARM || type == B_ARM || type == BX_ARM) {
...
trampoline_instructions[trampoline_pos++] = value;
} else if (type == ADD_ARM) {
...
trampoline_instructions[trampoline_pos++] = 0xE52D0004 | (r << 12); // PUSH {Rr}
trampoline_instructions[trampoline_pos++] = 0xE59F0008 | (r << 12); // LDR Rr, [PC, #8]
trampoline_instructions[trampoline_pos++] = (instruction & 0xFFF0FFFF) | (r << 16);
trampoline_instructions[trampoline_pos++] = 0xE49D0004 | (r << 12); // POP {Rr}
trampoline_instructions[trampoline_pos++] = 0xE28FF000; // ADD PC, PC
trampoline_instructions[trampoline_pos++] = pc;
} else {
trampoline_instructions[trampoline_pos++] = instruction;
}
}

trampoline_instructions[trampoline_pos++] = 0xe51ff004; // LDR PC, [PC, #-4]
trampoline_instructions[trampoline_pos++] = lr;
}

这里的略去了技术细节,建议参考 Android Arm Inline Hook

Demo

ele7enxxh/Android-Inline-Hook: thumb16 thumb32 arm32 inlineHook in Android 近期没有更新,它仍然使用 ndk-build 编译,且提供的 example 真机验证不便。

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
33
34
35
36
37
38
39
40
#include <stdio.h>

#include "inlineHook.h"

int (*old_puts)(const char *) = NULL;

int new_puts(const char *string)
{
old_puts("inlineHook success");
}

int hook()
{
if (registerInlineHook((uint32_t) puts, (uint32_t) new_puts, (uint32_t **) &old_puts) != ELE7EN_OK) {
return -1;
}
if (inlineHook((uint32_t) puts) != ELE7EN_OK) {
return -1;
}

return 0;
}

int unHook()
{
if (inlineUnHook((uint32_t) puts) != ELE7EN_OK) {
return -1;
}

return 0;
}

int main()
{
puts("test");
hook();
puts("test");
unHook();
puts("test");
}

所以我动手写了另一个 demo,完整代码见 Inline hook demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class HelloJni extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_hello_jni);

findViewById(R.id.btnCallOriginal).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
callHookTestCallOriginal();
}
});

}

public native void callHookTestCallOriginal();

static {
System.loadLibrary("hello-jni");
}
}

源码结构如下:

NDK 编译配置如下:

1
2
3
4
5
6
7
8
9
cmake_minimum_required(VERSION 3.4.1)

add_library(hello-jni SHARED
hello-jni.c inlinehook/inlineHook.c inlinehook/relocate.c inlinehook/hooktest.c)

# Include libraries needed for hello-jni lib
target_link_libraries(hello-jni
android
log)