• Android Inline Hook


    最近终于沉下心来对着书把hook跟注入方面的代码敲了一遍,打算写几个博客把它们记录下来。

    第一次介绍一下我感觉难度最大的inline hook,实现代码参考了腾讯GAD的游戏安全入门。

    inline hook的大致流程如下:

    首先将目标指令替换为跳转指令,跳转地址为一段我们自己编写的汇编代码,这段汇编代码先是执行用户指定的代码,如修改寄存器的值,然后执行被替换掉的原指令2,最后再跳转回原指令3处,恢复程序的正常运行。

    为了避开注入过程,我们通过hook自己进程加载的动态连接库进行演示。

    1、实现目标注入程序

    我们将这个程序编译为动态连接库,然后在主程序中加载,作为hook的目标。

    target.h
    #ifndef TARGET_H_INCLUDED
    #define TARGET_H_INCLUDED void target_foo(); #endif // TARGET_H_INCLUDED
    target.c #include "target.h" #include <stdlib.h> #include <stdio.h> #include <unistd.h> void target_foo() { int a = 3; int b = 2; while(a--) { sleep(2); b = a * b; printf("[INFO] b is %d ", b); } b = b + 2; b = b - 1; printf("[INFO] finally, b is %d ", b); }
    Android.mk
    include $(CLEAR_VARS) LOCAL_ARM_MODE := arm LOCAL_MODULE :
    = target LOCAL_CFLAGS += -pie -fPIE -std=c11 LOCAL_LDFLAGS += -pie -fPIE -shared -llog APP_ABI := armeabi-v7a LOCAL_SRC_FILES := target.c include $(BUILD_SHARED_LIBRARY)

    注意Android.mkLOCAL_ARM_MODE := arm代表编译时使用4字节的arm指令集,而不是2字节的thumb指令集。

    2、实现主程序

    在主程序中我们首先加载之前编写的动态链接库,进行hook之后再对其中的函数target_foo进行调用。

    main.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <dlfcn.h>
    #include <unistd.h>
    #include <stdbool.h>
    #include "hook_inline.h"
    
    typedef void (*target_foo)(void);
    void my_func(struct hook_reg *reg) { puts("here we go!"); } void main() { void *handler = dlopen("/data/local/tmp/libtarget.so", RTLD_NOW); target_foo foo = (target_foo)dlsym(handler, "target_foo"); hook_inline_make("/data/local/tmp/libtarget.so", 0xde2, my_func, true); foo(); }
    hook_inline.h #ifndef HOOK_INLINE_H_INCLUDED
    #define HOOK_INLINE_H_INCLUDED #include <stdbool.h> struct hook_reg { long ARM_r0; long ARM_r1; long ARM_r2; long ARM_r3; long ARM_r4; long ARM_r5; long ARM_r6; long ARM_r7; long ARM_r8; long ARM_r9; long ARM_r10;long ARM_r11; long ARM_r12;long ARM_sp; long ARM_lr; long ARM_cpsr; }; typedef void (*hook_func)(struct hook_reg *reg); bool hook_inline_make(const char *library, long address, hook_func func, bool isArm); #endif // HOOK_INLINE_H_INCLUDED

    这里我们hook功能的实现函数为hook_inline_make,4个参数分别为动态库路径,目标地址,用户函数,目标地址处指令集。

    当程序执行到目标地址处时会回调我们传入的用户函数,可通过参数hook_reg来更改寄存器的值(不包括寄存器pc)。因为之前在动态链接库的Android.mk文件指定了使用arm指令集进行编译,所以此处指定最后一个参数为true

    3、实现注入函数

    现在到了最为关键的地方,为了实现这个功能还需要了解几个知识。

    (1)、获取内存中动态链接库的基址

    Linux系统中各个进程的内存加载信息可以在/proc/pid/maps文件中到,通过它我们可以获取到动态链接库在内存中的加载基址。

    long get_module_addr(pid_t pid, const char *module_name)
    {
        char file_path[256];
        char file_line[512];
        if (pid < 0) {
            snprintf(file_path, sizeof(file_path), "/proc/self/maps");
        } else {
            snprintf(file_path, sizeof(file_path), "/proc/%d/maps", pid);
        }
        FILE *fp = fopen(file_path, "r");
        if (fp == NULL) {
            return -1;
        }
        long addr_start = -1, addr_end = 0;
        while (fgets(file_line, sizeof(file_line), fp)) {
            if (strstr(file_line, module_name)) {
                if (2 == sscanf(file_line, "%8lx-%8lx", &addr_start, &addr_end)) {
                    break;
                }
            }
        }
        fclose(fp);
        printf("library :%s %lx-%lx, pid : %d
    ", module_name, addr_start, addr_end, pid);
        return addr_start;
    }

    (2)、更改内存中的二进制代码

    现在的计算机系统中一般对内存进行分段式管理,不同的段有不同的读、写、执行的属性。一般来讲代码段只有读和执行的属性,不允许对代码段进行写操作。Linux系统中通过函数mprotect对内存的属性进行更改,需要注意的一点是需要以内存页的大小进行对齐。

    bool change_addr_writable(long address, bool writable) {
        long page_size = sysconf(_SC_PAGESIZE);
        //align address by page size
        long page_start = (address) & (~(page_size - 1));
        //change memory attribute
        if (writable == true) {
            return mprotect((void*)page_start, page_size, PROT_READ | PROT_WRITE | PROT_EXEC) != -1;
        } else {
            return mprotect((void*)page_start, page_size, PROT_READ | PROT_EXEC) != -1;
        }
    }

    接下来就可以着手实现功能了,inline hook跟指令集密切相关,此处我们先演示arm指令集的情况,之后对thumb指令集进行讨论。这里实现的功能是用户可在自己注册的回调函数中对hook点寄存器的值进行修改。

     

    为了实现32位地址空间的长跳转,我们需要两条指令的长度(8个字节)来实现。一般手机上的arm处理器为3级流水,所以pc寄存器的值总是指向当前执行指令后的第二条指令,因而使用ldr pc, [pc, #-4]来加载该指令之后的跳转地址。当程序跳转到shellcode后,首先对寄存器组进行备份,然后调用用户注册的回调函数,用户可在回调函数中修改备份中各个寄存器(pc寄存器除外)的值,然后从备份中恢复寄存器组再跳转到stubcode,stubcode的功能是执行被hook点的跳转指令替换掉的两条指令,最后跳回原程序。

    shellcode.S
    1
    .global _shellcode_start_s 2 .global _shellcode_end_s 3 .global _hook_func_addr_s 4 .global _stub_func_addr_s 5 .data 6 _shellcode_start_s: 7 @ 备份各个寄存器 8 push {r0, r1, r2, r3} 9 mrs r0, cpsr 10 str r0, [sp, #0xc] 11 str r14, [sp, #0x8] 12 add r14, sp, #0x10 13 str r14, [sp, #0x4] 14 pop {r0} 15 push {r0-r12} 16 @ 此时寄存器被备份在栈中,将栈顶地址作为回调函数的参数(struct hook_reg17 mov r0, sp 18 ldr r3, _hook_func_addr_s 19 blx r3 20 @ 恢复寄存器值 21 ldr r0, [sp, #0x3c] 22 msr cpsr, r0 23 ldmfd sp!, {r0-r12} 24 ldr r14, [sp, #0x4] 25 ldr sp, [r13] 26 ldr pc, _stub_func_addr_s 27 _hook_func_addr_s: 28 .word 0x0 29 _stub_func_addr_s: 30 .word 0x0 31 _shellcode_end_s: 32 .end

    shellcode使用汇编实现,在使用时需要对里边的两个地址进行修复,用户回调函数地址(_hook_func_addr_s)跟stubcode地址(_stub_func_addr_s)。

    接下来我们可以看一下函数hook_inline_make的具体实现了

     1 void hook_inline_make(const char *library, long address, hook_func func)
     2 {
     3     //获取hook点在内存中的地址
     4     long base_addr = get_module_addr(-1, library);
     5     long hook_addr = base_addr + address;
     6     //获取shellcode中的符号地址
     7     extern long _shellcode_start_s;
     8     extern long _shellcode_end_s;
     9     extern long _hook_func_addr_s;
    10     extern long _stub_func_addr_s;
    11     void *p_shellcode_start = &_shellcode_start_s;
    12     void *p_shellcdoe_end = &_shellcode_end_s;
    13     void *p_hook_func = &_hook_func_addr_s;
    14     void *p_stub_func = &_stub_func_addr_s;
    15     //计算shellcode大小
    16     int shellcode_size = (int)(p_shellcdoe_end - p_shellcode_start);
    17     //新建shellcode
    18     void *shellcode = malloc(shellcode_size);
    19     memcpy(shellcode, p_shellcode_start, shellcode_size);
    20     //添加执行属性
    21     change_addr_writable((long)shellcode, true);
    22     //在32bit的arm指令集中,stubcode中的4条指令占用16个字节的空间
    23     //前两条指令为hook点被替换的两条指令
    24     //后两条指令跳转回原程序
    25     void *stubcode = malloc(16);
    26     memcpy(stubcode, (void*)hook_addr, 8);
    27     //ldr pc, [pc, #-4]
    28     //[address]
    29     //手动填充stubcode
    30     char jump_ins[8] = {0x04, 0xF0, 0x1F, 0xE5};
    31     uint32_t jmp_address = hook_addr + 8;
    32     memcpy(jump_ins + 4, &jmp_address, 4);
    33     memcpy(stubcode + 8, jump_ins, 8);
    34     //添加执行属性
    35     change_addr_writable((long)stubcode, true);
    36     //修复shellcode中的两个地址值
    37     uint32_t *shell_hook = shellcode + (p_hook_func - p_shellcode_start);
    38     *shell_hook = (uint32_t)func;
    39     uint32_t *shell_stub = shellcode + (p_stub_func - p_shellcode_start);
    40     *shell_stub = (uint32_t)stubcode;
    41     //为hook点添加写属性
    42     change_addr_writable(hook_addr, true);
    43     //替换hook点指令为跳转指令,跳转至shellcode
    44     jmp_address = (uint32_t)shellcode;
    45     memcpy(jump_ins + 4, &jmp_address, 4);
    46     memcpy((void*)hook_addr, jump_ins, 8);
    47     change_addr_writable(hook_addr, false);
    48     //刷新cache
    49     cacheflush(hook_addr, 8, 0);
    50 }

    注意这里的change_addr_writable函数无论传入false还是true对应地址都会添加上执行属性。由于处理器采用流水线跟多级缓存,在更改代码后我们需要手动刷新cache,即函数cacheflush(第三个参数无意义)。

    4、thumb指令集实现

    由于thumb指令集的功能受到限制,虽然思路上跟arm指令集一致,但在实现上需要用更多条指令,下面是我自己想的一种实现方式,欢迎交流。

    需要注意的是由于每条thumb指令为16bit,所以32位的跳转地址需要占用两条指令的空间,而且跳转时会污染r0寄存器所以要对其进行保护。我在实现程序时将shellcode编译为了arm指令集,所以在原程序、shellcode、stubcode之间相互跳转时需要使用bx指令进行处理器状态切换(需要跳转的地址代码为thumb指令集时,需要将地址的第1个bit位置位)。

  • 相关阅读:
    Linux网络基础配置
    UVA 116 Unidirectional TSP(dp + 数塔问题)
    修改Hosts文件
    倒排索引
    可以把阿里云上面的一些介绍和视频都看看
    练练脑,继续过Hard题目
    explicit的用法
    auto_ptr的使用和注意
    我写的快排程序
    快速排序、查第k大
  • 原文地址:https://www.cnblogs.com/mmmmar/p/8185549.html
Copyright © 2020-2023  润新知