首页
论坛
课程
招聘
[原创]一个烂尾的Android内联hook框架
2021-5-23 14:47 8328

[原创]一个烂尾的Android内联hook框架

2021-5-23 14:47
8328

1.引言

  之前因为有某些需求想找一个arm平台的inline hook框架,但是没能找到一个既精简、又支持修改多参数、修改返回值、支持用自定义函数替换掉原函数的框架。所以就决定花点时间自己实现一个,写了几天以后被告知不用做了,于是便有了这个烂尾项目。今天整理文件的时候看到了,与其留着发霉不如分享给坛友作为学习的例子。此代码为没用的草稿,不合规范的地方就不用对其提意见了。

2.需求分析与初步设计

  2.1 精简
    使用一个结构体和一段shellcode维护一个hook
  2.2 支持多线程hook
    每个hook维护一段只属于自己的shellcode
  2.3 支持修改参数
    参数数量小于4的修改r0-r3寄存器,大于4的再修改栈空间
  2.4 支持修改返回值
    覆盖r0寄存器的值
  2.5 支持函数替换
    hook函数执行完以后加载原函数下一条指令地址到lr寄存器
    总体设计为,每个hook维持一个结构体与一段自己的shellcode,初始化hook时直接修改shellcode中的变量值,备份并修改原函数的起始汇编代码,使其直接跳到shellcode执行,完成其余的工作。所有的hook保存在一个链表中进行统一管理。

3.具体实现

    以下为维护hook的结构体:

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct inline_item{
    void* shell_code;           // shelcode的内存起始地址
    int32_t shell_code_len;     // shellcode的长度
    address_t old_fun;          // 原始函数的起始地址
    address_t new_fun;          // hook函数的起始地址
    void* back_code;            // 备份的代码
    int32_t back_code_len;      // 备份代码的长度
    void* reg;                  // 保存寄存器空间
    void* stack;                // 保存栈空间
    list_node_t* param_list;    // 参数链表
    void* ret_value;            // 返回值
} inline_item_t;

    在初始化hook时,首先获取shellcode中变量的内存地址,分配一块内存空间给结构体中的shellcode_code字段,并记录长度,记录原始函数的地址与hook函数的地址,存储原函数的前几条指令作为备份代码,并记录备份代码的长度,param_list中存储有想要修改的参数值,ret_value中存储有想要修改成的返回值。

 

    初始化hook的代码如下:

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
inline_item_t* init_item(address_t* target_fun, address_t* hook_fun, list_node_t* param_list,address_t ret_value)
{
    inline_item_t* p_item = 0;
    address_t temp = 0;
    p_item = (inline_item_t*)malloc( sizeof(inline_item_t) );
    p_item->shell_code_len = (int8_t*)&asm_shellcode_end - (int8_t*)&asm_shellcode_begin;
    p_item->shell_code = (int8_t*)malloc( p_item->shell_code_len);  // 分配shellcode空间
    p_item->old_fun = target_fun;                                   // 保存原始函数地址
    p_item->new_fun = hook_fun;                                     // 存储hook函数地址
    p_item->back_code_len = 12;                                     // 备份12字节指令
    p_item->back_code = malloc(p_item->back_code_len);
    p_item->reg = malloc(0x10 * sizeof(address_t));                 // 分配备份寄存器的空间
    p_item->stack = malloc(0x10 * sizeof(address_t));               // 分配备份栈的空间
    p_item->param_list = 0;
    p_item->ret_value = 0;
    if( param_list != 0){
        p_item->param_list = param_list;                            // 初始化要修改的参数列表
    }
    if( ret_value!=0 ){
        p_item->ret_value = (address_t*)malloc( sizeof(address_t) ); // 初始化要修改的返回值
        ret_value = 10;
        memcpy( p_item->ret_value, &ret_value, sizeof(address_t));
    }
    return p_item;
}

    在备份原函数前几条指令时需要对指令类型进行判断,如果是thumb指令,则需要对地址减1再进行保存。
    判断指令类型的代码如下:

1
2
3
4
5
6
7
8
bool get_type(address_t addr)  
{
#if defined(__arm64__) || defined(__aarch64__)
    return ((int64_t)addr & 0x1);
#else
    return ((int32_t)addr & 0x1);  
#endif
}

    备份指令的函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void back_target(inline_item_t* p_item){
    p_item->back_code_len = BACKLEN;
    //保存old_fun前几条指令
    p_item->back_code = (int8_t*)malloc(p_item->back_code_len);
    memcpy(p_item->back_code, (address_t*)p_item->old_fun, p_item->back_code_len);
    memcpy(p_item->shell_code + asm_pos.back_code_pos, p_item->back_code, p_item->back_code_len);
    //修改old_fun前几条指令
    if( !set_mem_permission(p_item->old_fun,RWX) ){ // 修改内存属性为可读可写可执行
        return ;
    }
    memcpy((address_t*)p_item->old_fun, p_item->shell_code + asm_pos.load_pc_pos,sizeof(address_t));
    memcpy((address_t*)p_item->old_fun + 1, &p_item->shell_code, sizeof(address_t));
    memcpy(p_item->shell_code+asm_pos.load_pc_pos, p_item->back_code, sizeof(address_t)*3);
}

    指令地址为奇数是thumb指令
    在shellcode开始执行之前,需要先对其中的变量进行初始化,以下为需要初始化的值:

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
asm_old_fun_continue:   // 原函数的地址
    .word 0x12345678   
asm_new_fun:            // hook函数的地址           
    .word 0x12345678   
asm_param_num:          //参数的个数
    .word 0x00000010
asm_reg_sp:             // 栈顶值
    .word 0x12345678
asm_reg_lr:             // 返回地址
    .word 0x12345678
callback_modify_param:  // callback_modify_param函数的地址
    .word 0x00000000
callback_modify_return: // callback_modify_return函数的地址
    .word 0x00000000
asm_item_addr:          // 当前hook结构体地址
    .word 0x12345678
asm_reg_addr:         
    .word 0x12345678
asm_stack_addr:
    .word 0x12345678
 
asm_back_code:          // 备份的原始函数代码块
    .word 0x12345678
    .word 0x12345678
    .word 0x12345678
    ldr r15,asm_old_fun_continue    // 跳转到备份代码的下一条指令继续执行

    shellcode在执行时,需要先保存原始上下文,由于r13,r14,r15寄存器比较特殊,在跳转到保存上下文代码块时,寄存器的值已经发生变化,因此需要单独对其进行保存

1
2
3
4
5
6
str r13,asm_reg_sp
str r14,asm_reg_lr
// stmfd sp!,{r0}       // 这个需要注意,保存的pc应当是old_fun的下一条指令地址,
// sub r0,r15,0x        // 所以执行到这里以后要保存当前pc减去已经执行的指令步数*4
// ldmfd sp!,{r0}
bl fun_save_context

    再进行上下文环境保存,代码如下:

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
fun_save_context:
    stmfd sp!,{r12} // ------- 开始保存寄存器
    ldr r12,asm_reg_addr
    stmea r12!,{r0-r11}      //保存r0--r11
    mov r0,r12
    ldmfd sp!,{r12}          //取出r12
    stmea r0!,{r12}          //保存r12
    mrs r1,cpsr              //保存CPSR寄存器
    stmea r0!,{r1}
   // mov r1,sp                //不能直接操作sp
   // stmea r0!,{r1} // ------- 寄存器保存完成
    ldr r0,asm_stack_addr //-- 开始保存栈空间
    mov r2,0x10              //需要保存的栈空间大小
    mov r1,sp
 _loop_save_stack:
    cmp r2,0x00
    ble _loop_save_context_end
    ldr r3,[r1]
    str r3,[r0],0x04
    add r1,r1,0x04
    sub r2,r2,0x01
    b _loop_save_stack
 _loop_save_context_end:  //-- 栈空间保存完成
    bx r14
fun_save_context_end:

    与保存上下文相对应的,以下是恢复上下文的代码块:

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
fun_back_context:
    ldr r0,asm_stack_addr   // 开始恢复栈空间
    mov r1,sp
    mov r2,0x10
 _loop_back_stack:
    cmp r2,0x00
    ble _loop_back_stack_end
    ldr r3,[r0]
    str r3,[r1],0x04
    add r0,r0,0x04
    sub r2,r2,0x01
    b _loop_back_stack
 _loop_back_stack_end:      // 恢复栈空间完成
    ldr r12,asm_reg_addr
    ldmfd r12!,{r0-r11}     // 先取出r0-r11
    stmfd sp!,{r0-r1}
    mov r0,r12
    ldmfd r0!,{r12}         // 取出r12
    ldmfd r0!,{r1}
    msr cpsr,r1
   // ldmfd r0!,{r1}
   // mov sp,r1
    ldmfd sp!,{r0-r1}
    bx r14
fun_back_context_end:

    执行hook的代码:

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
    ldr r13,asm_reg_sp
 
    bl fun_back_context     // 保存上下文
    ldr r0,asm_new_fun
    blx r0                  // 带返回跳转到hook函数
 
    stmfd sp!,{r0,r1}     
    ldr r1,callback_modify_param
    cmp r1,0x00
    beq not_modofy_param    // 判断是否需要修改参数
    ldr r0,asm_item_addr
    blx r1                  // 如果需要修改参数,以hook结构体作为参数带返回跳转callback_modify_param
not_modofy_param:
    ldmfd sp!,{r0,r1}
 
    ldr r13,asm_reg_sp
    bl fun_back_context
    bl asm_back_code         // 执行原函数
 
    stmfd sp!,{r0,r1}
    ldr r1,callback_modify_return
    cmp r1,0x00
    beq not_modofy_return    // 执行完原函数后,判断是否需要修改返回值
    ldr r0,asm_item_addr     // 如果需要修改返回值,以hook结构体作为参数带返回跳转
    blx r1
not_modofy_return:
    ldmfd sp!,{r0,r1}
 
    bl fun_back_context     // 恢复上下文
    ldr r14,asm_reg_lr      // 将old_fun的lr地址填充回去
    bx r14                  // 跳转回正常执行流程

    以上代码中,callback_modify_param被填充为以下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void modify_param(inline_item_t* p_item ){
    int n;
    int index;
    address_t data;
    list_node_t* param_list ;
    if(!p_item){
        return ;
    }
    param_list = p_item->param_list;  // 参数存储在结构体的一个链表中
    while(param_list != 0){           // 遍历整个链表
        index = ((param_t*)param_list->data)->index;
        data = ((param_t*)param_list->data)->data;
        if( index <=3 ){    // 如果参数小于3个,修改寄存器的值
            memcpy(p_item->reg + sizeof(address_t)*index, &data, sizeof(address_t) );
        }
        else {              // 参数大于3个,修改栈顶值
            memcpy(p_item->stack + sizeof(address_t)*(index-4), &data, sizeof(address_t));
        }
        param_list = param_list->next;
    }
}

    callback_modify_return的值被填充为以下函数的地址:

1
2
3
4
5
6
7
void modify_return(inline_item_t*p_item){
    if(!p_item)    {
        return ;
    }
    //覆盖r0的值修改返回值
    memcpy(p_item->reg, p_item->ret_value, sizeof(address_t));
}

    指令修复部分还没完成。

 

    最后附上Makefile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
NDK_ROOT=~/Android/Sdk/ndk/21.0.6113669
TOOL_CHAIN=$(NDK_ROOT)/lone_toolchain_23
CC=$(TOOL_CHAIN)/bin/armv7a-linux-androideabi23-clang
CFLAG=-g -m32
TARGET=sniper
RM=rm -f
DIR=.
OBJ_DIR=$(DIR)/obj
SRC_DIR=$(DIR)/src
SRC=$(wildcard $(DIR)/*.c)
SRC+=$(wildcard $(DIR)/*.asm)
OBJ=$(patsubst %.asm,%.o,$(patsubst %.c,%.o,$(SRC)))
all:$(TARGET)
$(TARGET):$(OBJ)
    $(CC) $(CFLAG) -o $@ $^
$(DIR)/%.o:$(DIR)/%.c
    $(CC) $(CFLAG) -c $<
$(DIR)/%.o:$(DIR)/%.asm
    $(CC) $(CFLAG) -c $<
clean:
    $(RM) *.o
    $(RM) *.s
install:
    adb push $(TARGET) /sdcard

4.结语

    总体来讲arm上的inline hook框架要比x86难实现一点,传参方式使用寄存器加栈顶空间,还需要对两种不同类型的指令进行判断和修复。目前的代码只能说是写了一个开头,要想做一个完整的框架,还是需要一点工作量的。


[2022夏季班]《安卓高级研修班(网课)》月薪三万班招生中~

最后于 2021-5-23 14:53 被某警官编辑 ,原因:
收藏
点赞3
打赏
分享
最新回复 (3)
雪    币: 2484
活跃值: 活跃值 (1786)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
TUGOhost 活跃值 2021-5-23 22:21
2
0
作者有考虑dobby吗?
雪    币: 19
活跃值: 活跃值 (2454)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
不吃早饭 活跃值 2021-5-24 01:43
3
0
作者有考虑Substrate吗?
雪    币: 754
活跃值: 活跃值 (541)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
Himeko 活跃值 2021-5-25 15:05
4
0
不吃早饭 作者有考虑Substrate吗?
饭哥啥时候搓个文章让大伙长长见识
游客
登录 | 注册 方可回帖
返回