您的位置:首页 > 移动开发 > Android开发

Android Arm Inline Hook

2017-04-27 09:04 399 查看


什么是Inline Hook

Inline Hook即内部跳转Hook,通过替换函数开始处的指令为跳转指令,使得原函数跳转到自己的函数,通常还会保留原函数的调用接口。与GOT表Hook相比,Inline Hook具有更广泛的适用性,几乎可以Hook任何函数,不过其实现更为复杂,考虑的情况更多,并且无法对一些太短的函数Hook。

其基本原理请参阅网上其他资料。


需要解决的问题

Arm模式与Thumb模式的区别
跳转指令的构造
PC相关指令的修正
线程处理
其他一些细节

下面我将结合源码对这几个问题进行解决。


Arm模式与Thumb模式的区别

本文讨论的对象为基于32位的Arm架构的Inline Hook,在Arm版本7及以上的体系中,其指令集分为Arm指令集和Thumb指令集。Arm指令为4字节对齐,每条指令长度均为32位;Thumb指令为2字节对齐,又分为Thumb16、Thumb32,其中Thumb16指令长度为16位,Thumb32指令长度为32位。

在对一个函数进行Inline Hook时,首先需要判断当前函数指令是Arm指令还是Thumb指令,指令使用目标地址值的bit[0]来确定目标地址的指令类型。bit[0]的值为1时,目标程序为Thumb指令;bit[0]值为0时,目标程序为ARM指令。其相关实现代码为以下宏:

// 设置bit[0]的值为1

#define SET_BIT0(addr) (addr | 1)

// 设置bit[0]的值为0

#define CLEAR_BIT0(addr) (addr & 0xFFFFFFFE)

// 测试bit[0]的值,若为1则返回真,若为0则返回假

#define TEST_BIT0(addr) (addr & 1)


跳转指令的构造

跳转指令主要分为以下两种:

B系列指令:B、BL、BX、BLX
直接写PC寄存器

Arm的B系列指令跳转范围只有4M,Thumb的B系列指令跳转范围只有256字节,然而大多数情况下跳转范围都会大于4M,故我们采用
LDR PC, [PC, ?]
构造跳转指令。另外Thumb16指令中并没有合适的跳转指令,如果单独使用Thumb16指令构造跳转指令,需要使用更多的指令完成,并且在后续对PC相关指令的修正也更加繁琐,故综合考虑下,决定放弃对ARMv5的支持。

另外,Arm处理器采用3级流水线来增加处理器指令流的速度,也就是说程序计数器R15(PC)总是指向“正在取指”的指令,而不是指向“正在执行”的,即PC总是指向当前正在执行的指令地址再加2条指令的地址。比如当前指令地址是0×8000, 那么当前pc的值,在thumb下面是0×8000 + 2 2, 在arm下面是0×8000 + 4 2。

对于Arm指令集,跳转指令为:

LDR PC, [PC, #-4]addr
LDR PC, [PC, #-4]
对应的机器码为:0xE51FF004,
addr
为要跳转的地址。该跳转指令范围为32位,对于32位系统来说即为全地址跳转。

对于Thumb32指令集,跳转指令为:

LDR.W PC, [PC, #0]addr
LDR.W PC, [PC, #0]
对应的机器码为:0x00F0DFF8,
addr
为要跳转的地址。同样支持任意地址跳转。

其相关实现代码为:

// Arm Mode

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;

}

// Thumb Mode

else {
((uint32_t *) (item->target_addr))[0] = 0xe51ff004;
// LDR PC, [PC, #-4]
((uint32_t *) (item->target_addr))[1] = item->new_addr;

}

首先通过TEST_BIT0宏判断目标函数的指令集类型,其中若为Thumb指令集,多了下面一个额外处理:

123
if (CLEAR_BIT0(item->target_addr) % 4 != 0) {	((uint16_t *) CLEAR_BIT0(item->target_addr))[i++] = 0xBF00;  // NOP}
对bit[0]的值清零,若其值4字节不对齐,则添加一个2字节的
NOP
指令,使得后续的指令4字节对齐。这是因为在Thumb32指令中,若该指令对PC寄存器的值进行了修改,则该指令必须是4字节对齐的,否则为非法指令。


PC相关指令的修正

不论是Arm指令集还是Thumb指令集,都存在很多的与PC值相关的指令,例如:B系列指令、literal系列指令等。原有函数的前几个被跳转指令替换的指令将会被搬移到trampoline_instructions中,此时PC值已经变动,所以需要对PC相关指令进行修正(所谓修正即为计算出实际地址,并使用其他指令完成同样的功能)。相关修正代码位于relocate.c文件中。其中
INSTRUCTION_TYPE
描述了需要修正的指令,限于篇幅,这里仅阐述Arm指令的修正过程,对应的代码为
relocateInstructionInArm
函数。

函数原型如下:

/*

target_addr: 待Hook的目标函数地址,即为当前PC值,用于修正指令

orig_instructions:存放原有指令的首地址,用于修正指令和后续对原有指令的恢复

length:存放的原有指令的长度,Arm指令为8字节;Thumb指令为12字节

trampoline_instructions:存放修正后指令的首地址,用于调用原函数

orig_boundaries:存放原有指令的指令边界(所谓边界即为该条指令与起始地址的偏移量),用于后续线程处理中,对PC的迁移

trampoline_boundaries:存放修正后指令的指令边界,用途与上相同

count:处理的指令项数,用途与上相同

*/

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);

具体实现中,首先通过函数
getTypeInArm
判断当前指令的类型,本函数通过类型,共分为4个处理分支:

BLX_ARM、BL_ARM、B_ARM、BX_ARM
ADD_ARM
ADR1_ARM、ADR2_ARM、LDR_ARM、MOV_ARM
其他指令


BLX_ARM、BL_ARM、B_ARM、BX_ARM指令的修正

即为B系列指令(
BLX <label>
BL
<label>
B <label>
BX
PC
)的修正,其中
BLX_ARM
BL_ARM
需要修正LR寄存器的值,相关代码为:

123
if (type == BLX_ARM || type == BL_ARM) {	trampoline_instructions[trampoline_pos++] = 0xE28FE004;	// ADD LR, PC, #4}
接下来构造相应的跳转指令,即为:

1
trampoline_instructions[trampoline_pos++] = 0xE51FF004;  	// LDR PC, [PC, #-4]
最后解析指令,计算实际跳转地址
value
,并将其写入
trampoline_instructions
,相关代码为:

12345678910111213141516171819
if (type == BLX_ARM) {	x = ((instruction & 0xFFFFFF) << 2) | ((instruction & 0x1000000) >> 23);}else if (type == BL_ARM || type == B_ARM) {	x = (instruction & 0xFFFFFF) << 2;}else {	x = 0;}top_bit = x >> 25;imm32 = top_bit ? (x | (0xFFFFFFFF << 26)) : x;if (type == BLX_ARM) {	value = pc + imm32 + 1;}else {	value = pc + imm32;}trampoline_instructions[trampoline_pos++] = value;
如此便完成了B系列指令的修正,关于指令的字节结构请参考Arm指令手册。


ADD_ARM指令的修正

ADD_ARM
指的是
ADR
Rd, <label>
格式的指令,其中
<label>
与PC相关。

首先通过循环遍历,得到Rd寄存器,代码如下:

1234567891011121314
int rd;int rm;int r;// 解析指令得到rd、rm寄存器rd = (instruction & 0xF000) >> 12;rm = instruction & 0xF;// 为避免冲突,排除rd、rm寄存器,选择一个临时寄存器Rrfor (r = 12; ; --r) {	if (r != rd && r != rm) {		break;	}}
接下来是构造修正指令:

1234567891011
// PUSH {Rr},保护Rr寄存器值trampoline_instructions[trampoline_pos++] = 0xE52D0004 | (r << 12);// LDR Rr, [PC, #8],将PC值存入Rr寄存器中trampoline_instructions[trampoline_pos++] = 0xE59F0008 | (r << 12);// 变换原指令`ADR Rd, <label>`为`ADR Rd, Rr, ?`trampoline_instructions[trampoline_pos++] = (instruction & 0xFFF0FFFF) | (r << 16);//POP {Rr},恢复Rr寄存器值trampoline_instructions[trampoline_pos++] = 0xE49D0004 | (r << 12);// ADD PC, PC,跳过下一条指令trampoline_instructions[trampoline_pos++] = 0xE28FF000;trampoline_instructions[trampoline_pos++] = pc;


ADR1_ARM、ADR2_ARM、LDR_ARM、MOV_ARM

分别为
ADR Rd, <label>
ADR
Rd, <label>
LDR Rt, <label>
MOV
Rd, PC


同样首先解析指令,得到
value
,相关代码如下:

123456789101112131415161718192021222324252627282930
int r;uint32_t value;r = (instruction & 0xF000) >> 12;if (type == ADR1_ARM || type == ADR2_ARM || type == LDR_ARM) {	uint32_t imm32;		imm32 = instruction & 0xFFF;	if (type == ADR1_ARM) {		value = pc + imm32;	}	else if (type == ADR2_ARM) {		value = pc - imm32;	}	else if (type == LDR_ARM) {		int is_add;				is_add = (instruction & 0x800000) >> 23;		if (is_add) {			value = ((uint32_t *) (pc + imm32))[0];		}		else {			value = ((uint32_t *) (pc - imm32))[0];		}	}}else {	value = pc;}
最后构造修正指令,代码如下:

12345
// LDR Rr, [PC]trampoline_instructions[trampoline_pos++] = 0xE51F0000 | (r << 12);// 跳过下一条指令trampoline_instructions[trampoline_pos++] = 0xE28FF000;	// ADD PC, PCtrampoline_instructions[trampoline_pos++] = value;


其他指令

事实上,还有些指令格式需要修正,例如:
PUSH {PC}
PUSH
{SP}
等,虽然这些指令被Arm指令手册标记为deprecated,但是仍然为合法指令,不过在实际汇编中并未发现此类指令,故未做处理,相关代码如下:

12
// 直接将指令存放到trampoline_instructions中trampoline_instructions[trampoline_pos++] = instruction;
处理完所有待处理指令后,最后加入返回指令:

// LDR PC, [PC, #-4]trampoline_instructions[trampoline_pos++] = 0xe51ff004;trampoline_instructions[trampoline_pos++] = lr;
Thumb指令的修正,大家可以参考这里的思路,自行阅读源码。


线程处理

一个完善的Inline Hook方案必须要考虑多线程环境,即要考虑线程恰好执行到被修改指令的位置。在Window下,使用
GetThreadContext
SetThreadContext
枚举所有线程,迁移context到搬迁后的指令中。然而在Linux+Arm环境下,并没有直接提供相同功能的API,不过可以使用
ptrace
完成,主要流程如下:

解析/proc/self/task目录,获取所有线程id
创建子进程,父进程等待。子进程枚举所有线程,PTRACE_ATTACH线程,迁移线程PC寄存器,枚举完毕后,子进程给自己发SIGSTOP信号,等待父进程唤醒
父进程检测到子进程已经SIGSTOP,完成Inline Hook工作,向子进程发送SIGCONT信号,同时等待子进程退出
子进程枚举所有线程,PTRACE_DETACH线程,枚举完毕后,子进程退出
父进程继续其他工作

这里使用子进程完成线程处理工作,实际上是迫不得已的。因为,如果直接使用本进程
PTRACE_ATTACH
线程,会出现operation not permitted,即使赋予root权限也是同样的错误,具体原因不得而知。

具体代码请参考
freeze
unFreeze
两个函数。


其他一些细节

页保护

页面大小为4096字节,使用
mprotect
函数修改页面属性,修改为
PROT_READ
| PROT_WRITE | PROT_EXEC

刷新缓存

对于ARM处理器来说,缓存机制作用明显,内存中的指令已经改变,但是cache中的指令可能仍为原有指令,所以需要手动刷新cache中的内容。采用
cacheflush
即可实现。
一个已知的BUG

虽然本库已经把大部分工作放在了
registerInlineHook
函数中,但是在
inlineHook
inlineUnHook
函数中还是不可避免的使用了部分libc库的API函数,例如:
mprotect
memcpy
munmap
free
cacheflush
等。如果使用本库对上述API函数进行Hook,可能会失败甚至崩溃,这是因为此时原函数的指令已经被破坏,或者其逻辑已经改变。解决这个Bug有两个方案,第一是采用其他Hook技术;第二将本库中的这些API函数全部采用内部实现,即不依赖于libc库,可采用静态链接libc库,或者使用汇编直接调相应的系统调用号。


ARM平台backtrace与inlineHook多线程安全浅析

本文主要讨论了Android+ARM平台的backtrace原理以及inlineHook中涉及到的多线程安全的问题。

本文关于Arm Inline Hook线程处理的解决方案已经过时,新方案点这里


前言

在之前的Android inline hook项目中,在复杂环境下,如果遇到下面两个场景可能导致异常,甚至引起被hook进程的crash。
待hook的函数正在执行;
待hook的函数处于函数调用栈中;

举个例子,代码为:
#include <stdio.h>#include <pthread.h>#include "inlineHook.h"int new_thread(){	while(1) {		printf("new_a\n");		sleep(1);	}}int thread(){	while(1) {		printf("a\n");		sleep(1);	}}int main(){	int err;	pthread_t tid;		err = pthread_create(&tid, NULL, thread, NULL);	if (err) {		return -1;	}	pthread_join(tid, NULL);	return 0;}

当我们对要hook的代码前8-10个字节变动的时候,如果子线程刚好执行到此处,或者子线程的函数调用栈包含此处地址,那么有一定几率会导致异常甚至crash。


backtrace

为了防止上述情况发生,我们在hook之前需要对当前进程的所有线程做检测,以确保hook的函数不在当前的函数调用栈中。我们可以利用backtrace机制,获取线程的每层调用地址与我们需要hook的函数地址做比较,来实现该检测。

栈回溯(backtrace)是指程序运行时打印出当前的调用栈,在程序发生崩溃时,系统常常会打印出栈回溯信息。linux+arm平台下,编译器通过unwind实现栈回溯。



上面是在Android平台通过kill
-3 pid命令打印出的调用栈,包含了调用的函数、具体偏移地址以及现场保存的寄存器信息。我们只需要其中的每一层的调用具体地址即可。不走运的是,NDK中并没有提供直接backtrace的接口函数,查看源码,在dalvik/vm/interp/Stack.cpp的dvmDumpNativeStack函数实现了backtrace的功能,dvmDumpNativeStack源码如下:

void dvmDumpNativeStack(const DebugOutputTarget* target, pid_t tid){#ifdef HAVE_ANDROID_OS    const size_t MAX_DEPTH = 32;    backtrace_frame_t backtrace[MAX_DEPTH];    ssize_t frames = unwind_backtrace_thread(tid, backtrace, 0, MAX_DEPTH);    if (frames > 0) {        backtrace_symbol_t backtrace_symbols[MAX_DEPTH];        get_backtrace_symbols(backtrace, frames, backtrace_symbols);        for (size_t i = 0; i < size_t(frames); i++) {            char line[MAX_BACKTRACE_LINE_LENGTH];            format_backtrace_line(i, &backtrace[i], &backtrace_symbols[i],                    line, MAX_BACKTRACE_LINE_LENGTH);            dvmPrintDebugMessage(target, "  %s\n", line);        }        free_backtrace_symbols(backtrace_symbols, frames);    } else {        dvmPrintDebugMessage(target, "  (native backtrace unavailable)\n");    }#endif}

dvmDumpNativeStack函数功能为打印指定线程的backtrace,这里是直接将打印信息输出,与需求不符。查阅system/core/libcorkscrew/backtrace.c中的unwind_backtrace_thread函数:

ssize_t unwind_backtrace_thread(pid_t tid, backtrace_frame_t* backtrace,        size_t ignore_depth, size_t max_depth) {    if (tid == gettid()) {        return unwind_backtrace(backtrace, ignore_depth + 1, max_depth);    }    ALOGV("Unwinding thread %d from thread %d.", tid, gettid());    // TODO: there's no tgkill(2) on Mac OS, so we'd either need the    // mach_port_t or the pthread_t rather than the tid.#if defined(CORKSCREW_HAVE_ARCH) && !defined(__APPLE__)    struct sigaction act;    struct sigaction oact;    memset(&act, 0, sizeof(act));    act.sa_sigaction = unwind_backtrace_thread_signal_handler;    act.sa_flags = SA_RESTART | SA_SIGINFO | SA_ONSTACK;    sigemptyset(&act.sa_mask);    pthread_mutex_lock(&g_unwind_signal_mutex);    map_info_t* milist = acquire_my_map_info_list();    ssize_t frames = -1;    if (!sigaction(SIGURG, &act, &oact)) {        g_unwind_signal_state.map_info_list = milist;        g_unwind_signal_state.backtrace = backtrace;        g_unwind_signal_state.ignore_depth = ignore_depth;        g_unwind_signal_state.max_depth = max_depth;        ...

这里由于函数比较长,只贴出了前部分。其中判断如果线程id为当前线程id,则直接调用unwind_backtrace函数,而unwind_backtrace函数通过调用_Unwind_Backtrace、__Unwind_Backtrace、__gnu_Unwind_Backtrace解析.ARM.extab和.ARM.exidx节(具体解析过程实在有点麻烦,不再深入研究),将每层调用栈的信息存存入类型为backtrace_frame_t的backtrace结构体中,贴出backtrace_frame_t定义:

typedef struct {    uintptr_t absolute_pc;     /* absolute PC offset */    uintptr_t stack_top;       /* top of stack for this frame */    size_t stack_size;         /* size of this stack frame */} backtrace

其中absolute_pc即为调用地址,unwind_backtrace_thread的返回值则为调用栈的层数。

最后给出具体的方案:
遍历/proc/pid/task目录,获得进程下的所有线程id号;
通过tkill函数向所有线程发SIGSTOP信号;
通过dlopen、dlsym调用libcorkscrew.so的unwind_backtrace_thread函数,获得所有线程的函数调用栈信息;
遍历所有线程的函数调用栈信息,将每层调用地址与需要hook的函数作判断,若调用地址均不位于待hook的函数内,则可以安全的进行inline hook,否则停止;
通过tkill函数向所有线程发生SIGCONT信号;
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: