IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    Hooking and Hijacking Android Native Code

    Liu Yutao发表于 2015-11-15 15:04:00
    love 0

    首先声明这是一篇中文博客。

    先强烈推荐下Zheng Min大神的安卓动态调试七种武器系列文章,里面已经有两篇介绍我这次所要介绍的hooking的内容了,而且应该会比我这篇的内容更丰富。然而,任性的我还是要写这篇博客,原因除了自己太久没写博客了有点不好意思之外,更重要的是希望在写的过程中来理解这个技术。

    当然了,这篇博文主要还是代码分析,采用的是Collin Mulliner的这个项目adbi。当然他自己也有一个专门的slide来介绍里面用到的技术。

    好了,开始进入正文。

    首先clone这个项目:

    $ git clone https://github.com/crmulliner/adbi.git
    

    在具体分析代码之前先简单介绍下这个项目的目的、用法、以及流程吧:

    目的

    对android中的某个进程所使用的某个native库lib中的某个函数func进行劫持,使得当这个进程调用到这个函数的时候,会首先进入我们的hook函数,在hook函数中做一些其它的事情,比如打印一些log之类的,然后再调用真正的函数func。

    用法

    • 对项目进行编译,会生成一个可执行文件hijack和一个链接库文件libexample.so,将其放到/data/local/tmp目录下:

      $ adb push hijack/libs/armeabi/hijack /data/local/tmp/
      $ adb push instruments/example/libs/armeabi/libexample.so /data/local/tmp/
      
    • 然后进入android的adb shell里面,运行:

      $ adb shell
      $ su
      # cd /data/local/tmp
      # ./hijack -d -p PID -l /data/local/tmp/libexample.so
      

    它的作用是劫持pid为PID的进程的epoll_wait()库函数调用,每当该函数被调用,就会进到libexample.so中的my_epoll_wait() hook函数,打印一行内容,并调用真正的epoll_wait()函数。

    流程

    上面这整个hijacking和hooking的流程是这样的:

    • 在hijack的过程中,会将一段hijack code放在目标进程的栈上,调用mprotect将栈设置为可执行,并且将mprotect调用的返回值设置成这段hijack code的地址,因此,在mprotect返回时,就开始执行这段hijack code;
    • 这段hijack code所做的事情就是调用dlopen,加载libexample.so链接库;
    • 在libexample.so库的初始化函数中,对目标进程所调用的libc库中的epoll_wait函数进行hook;
    • 之后,只要目标进程一调用epoll_wait函数,就会首先进入hook函数。

    好了,这个流程看上去很简单,但是里面用到了很多Linux相关的知识,是一个很不错的介绍如何对进程进行hook和hijack的实例,接下来的篇幅就主要来介绍这整个流程是如何通过几百行C代码实现的。

    代码结构

    这是adbi项目的代码结构:

    |-hijack
      |-jni
        |-Android.mk
      |-hijack.c
    |-instruments
      |-base
        |-jni
          |-Android.mk
          |-Application.mk
        |-base.c
        |-base.h
        |-hook.c
        |-hook.h
        |-util.c
        |-util.h
      |-example
        |-jni
          |-Android.mk
        |-epoll.c
        |-epoll_arm.c
    |-README.md
    |-build.sh
    |-clean.sh
    

    可以看到,里面主要有两个目录:hijack和instruments。其中,hijack主要作用就是之前流程里面说的第一步,即:

    • 将一段hijack code放在目标进程的栈上,调用mprotect将栈设置为可执行,并且将mprotect调用的返回值设置成这段hijack code的地址,因此,在mprotect返回时,就开始执行这段hijack code。

    而instruments目录中包含了两个子目录,一个是base,主要是一些可以被调用的库函数,它最终会被编译成libbase.a静态链接库;另外一个是example,它用了一个非常简单的例子来展示如何利用libbase.a做hook,即之前流程里面的第三步:

    • 在libexample.so库的初始化函数中,对目标进程所调用的libc库中的epoll_wait函数进行hook。

    hijack

    hijack目录中只有一个代码文件:hijack.c,以及一个和编译相关的文件:jni/Android.mk。

    我们先来看这个Android.mk:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
     LOCAL_PATH := $(call my-dir)
    
     include $(CLEAR_VARS)
    
     LOCAL_MODULE    := hijack
     LOCAL_SRC_FILES := ../hijack.c
     LOCAL_ARM_MODE := arm
     LOCAL_CFLAGS := -g
    
     include $(BUILD_EXECUTABLE)
    

    其实这就是一个很典型的Android应用的jni的编译文件,表示它要用../hijack.c这个源文件编译一个可执行文件($(BUILD_EXECUTABLE))hijack。

    关于hijack.c这个文件,我们先来看一下main函数:

    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
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    
    int main(int argc, char *argv[])
    {
      ...
    
      while ((opt = getopt(argc, argv, "p:l:dzms:Z:D:")) != -1) {
        ...
      }
    
      ...
    
      if (!nomprotect) {
        if (0 > find_name(pid, "mprotect", &mprotectaddr)) {
          exit(1);
        }
      }
    
      void *ldl = dlopen("libdl.so", RTLD_LAZY);
      if (ldl) {
        dlopenaddr = (unsigned long)dlsym(ldl, "dlopen");
        dlclose(ldl);
      }
      unsigned long int lkaddr;
      unsigned long int lkaddr2;
      find_linker(getpid(), &lkaddr);
      find_linker(pid, &lkaddr2);
      dlopenaddr = lkaddr2 + (dlopenaddr - lkaddr);
    
    
      // Attach 
      if (0 > ptrace(PTRACE_ATTACH, pid, 0, 0)) {
        printf("cannot attach to %d, error!\n", pid);
        exit(1);
      }
      waitpid(pid, NULL, 0);
    
      if (appname) {
        ...
      }
    
      if (zygote) {
        ...
      }
    
      sprintf(buf, "/proc/%d/mem", pid);
      fd = open(buf, O_WRONLY);
      ptrace(PTRACE_GETREGS, pid, 0, &regs);
    
      sc[11] = regs.ARM_r0;
      sc[12] = regs.ARM_r1;
      sc[13] = regs.ARM_r2;
      sc[14] = regs.ARM_r3;
      sc[15] = regs.ARM_lr;
      sc[16] = regs.ARM_pc;
      sc[17] = regs.ARM_sp;
      sc[19] = dlopenaddr;
    
      // push library name to stack
      libaddr = regs.ARM_sp - n*4 - sizeof(sc);
      sc[18] = libaddr;
    
      if (stack_start == 0) {
        stack_start = (unsigned long int) strtol(argv[3], NULL, 16);
        stack_start = stack_start << 12;
        stack_end = stack_start + strtol(argv[4], NULL, 0);
      }
    
      // write library name to stack
      if (0 > write_mem(pid, (unsigned long*)arg, n, libaddr)) {
        printf("cannot write library name (%s) to stack, error!\n", arg);
        exit(1);
      }
    
      // write code to stack
      codeaddr = regs.ARM_sp - sizeof(sc);
      if (0 > write_mem(pid, (unsigned long*)&sc, sizeof(sc)/sizeof(long), codeaddr)) {
        printf("cannot write code, error!\n");
        exit(1);
      }
    
      // calc stack pointer
      regs.ARM_sp = regs.ARM_sp - n*4 - sizeof(sc);
    
      // call mprotect() to make stack executable
      regs.ARM_r0 = stack_start; // want to make stack executable
      regs.ARM_r1 = stack_end - stack_start; // stack size
      regs.ARM_r2 = PROT_READ|PROT_WRITE|PROT_EXEC; // protections
    
      // normal mode, first call mprotect
      if (nomprotect == 0) {
        regs.ARM_lr = codeaddr; // points to loading and fixing code
        regs.ARM_pc = mprotectaddr; // execute mprotect()
      }
      // no need to execute mprotect on old Android versions
      else {
        regs.ARM_pc = codeaddr; // just execute the 'shellcode'
      }
    
      // detach and continue
      ptrace(PTRACE_SETREGS, pid, 0, &regs);
      ptrace(PTRACE_DETACH, pid, 0, (void *)SIGCONT);
    
      return 0;
    }
    

    这里主要有几个重要的步骤:

    • parse传进来的参数,这个这里就不解释了;
    • 定位目标进程中mprotect函数的内存地址;
    • 定位目标进程中dlopen函数的内存地址;
    • 利用ptrace调用attach目标进程;
    • 构建hijack所需要的context,这里是一个数据结构sc;
    • 将sc写到栈上;
    • 利用之前得到的mprotect将栈设置成可执行,并将mprotect的返回值设置成sc数据结构中的code首地址;
    • 利用ptrace(PTRACE_SETREGS)设置目标进程的寄存器,使得上面的所有修改生效。

    这个时候目标进程就开始执行mprotect和sc中的code代码了。

    接下来我们来逐一介绍各个步骤:

    定位目标进程中mprotect函数的内存地址;
    1
    
    find_name(pid, "mprotect", &mprotectaddr)
    

    我们来看一下find_name:

    1
    2
    3
    4
    5
    6
    7
    8
    
    static int
    find_name(pid_t pid, char *name, unsigned long *addr)
    {
      load_memmap(pid, mm, &nmm);
      find_libc(libc, sizeof(libc), &libcaddr, mm, nmm);
      load_symtab(libc);
      lookup_func_sym(s, name, addr);
    }
    

    里面主要分为四个步骤:

    • load_memmap:主要是通过读取特定/proc/PID/maps文件,获得该进程打开的所有动态链接库的地址和其它相关内存地址(如栈的地址),并将所有这些信息存储在mm这个数据结构中;
    • find_libc:在mm中查找libc,并将其首地址填到libcaddr变量中;
    • load_symtab:打开libc对应的库文件,根据elf格式将里面的symbol table解析出来,并且填入数据结构symtab_t中,并返回;
    • lookup_func_sym:在这一堆的symbol table里面找到对应的函数名,并且写入变量addr中。

    通过以上四个步骤,即可得到进程中mprotect的内存地址。

    定位目标进程中dlopen函数的内存地址;

    获取dlopen的方法和之前获取mprotect的方法不太一样,主要原因是在于dlopen所在的库libdl.so在程序运行时是不会显示在该进程对应的/proc/PID/maps中的,因此需要先在本进程中先用dlopen开启libdl.so,然后通过相对地址的计算方法来获得目标进程中dlopen的内存地址,具体步骤如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
      void *ldl = dlopen("libdl.so", RTLD_LAZY);
      if (ldl) {
        dlopenaddr = (unsigned long)dlsym(ldl, "dlopen");
        dlclose(ldl);
      }
      unsigned long int lkaddr;
      unsigned long int lkaddr2;
      find_linker(getpid(), &lkaddr);
      find_linker(pid, &lkaddr2);
      dlopenaddr = lkaddr2 + (dlopenaddr - lkaddr);
    
    • 首先在本进程调用dlopen打开libdl.so(dlopen的用法可参照这里);
    • 利用dlsym获得libdl.so中dlopen函数的内存地址;
    • 分别获得本进程和目标进程中linker的地址;
    • 通过dlopen和linker的相对偏移一样的原理来计算目标进程中dlopen的真正内存地址。

    获得linder的内存地址的方法和获得mprotect函数内存地址的方法类似,这里就不阐述了,主要代码在find_linker_mem和find_linker这两个函数中。

    利用ptrace调用attach目标进程;

    这个步骤就两句话:

    1
    2
    3
    
     // Attach 
      ptrace(PTRACE_ATTACH, pid, 0, 0);
      waitpid(pid, NULL, 0);
    

    至于什么是ptrace和waitpid,以及如何使用它们,请参考我之前的一篇博客:系统调用学习笔记 - Ptrace和wait,这里就不详细说了。

    构建hijack所需要的context,这里是一个数据结构sc;

    其实sc就是一个长度为20的unsigned int数组:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    unsigned int sc[] = {
    0xe59f0040, //        ldr     r0, [pc, #64]   ; 48 <.text+0x48>
    0xe3a01000, //        mov     r1, #0  ; 0x0
    0xe1a0e00f, //        mov     lr, pc
    0xe59ff038, //        ldr     pc, [pc, #56]   ; 4c <.text+0x4c>
    0xe59fd02c, //        ldr     sp, [pc, #44]   ; 44 <.text+0x44>
    0xe59f0010, //        ldr     r0, [pc, #16]   ; 30 <.text+0x30>
    0xe59f1010, //        ldr     r1, [pc, #16]   ; 34 <.text+0x34>
    0xe59f2010, //        ldr     r2, [pc, #16]   ; 38 <.text+0x38>
    0xe59f3010, //        ldr     r3, [pc, #16]   ; 3c <.text+0x3c>
    0xe59fe010, //        ldr     lr, [pc, #16]   ; 40 <.text+0x40>
    0xe59ff010, //        ldr     pc, [pc, #16]   ; 44 <.text+0x44>
    0xe1a00000, //        nop                     r0
    0xe1a00000, //        nop                     r1 
    0xe1a00000, //        nop                     r2 
    0xe1a00000, //        nop                     r3 
    0xe1a00000, //        nop                     lr 
    0xe1a00000, //        nop                     pc
    0xe1a00000, //        nop                     sp
    0xe1a00000, //        nop                     addr of libname
    0xe1a00000, //        nop                     dlopenaddr
    };
    

    其中,sc[0]~sc[10]是一段汇编指令,而sc[11]~sc[19]则是保存了r0~r3、lr、pc和sp这六个寄存器的值,以及需要加载的库libname的地址,和dlopen函数的内存地址。

    其中,这六个寄存器的值是通过:

    1
    
    ptrace(PTRACE_GETREGS, pid, 0, &regs);
    

    从目标进程中获得的,而dlopenaddr就是之前获得的dlopen的地址。libaddr的获得是通过这段代码获得的:

    1
    2
    
    libaddr = regs.ARM_sp - n*4 - sizeof(sc);
    sc[18] = libaddr;
    

    其中n*4是需要加载的库(即/data/local/tmp/libexample.so)的文件名长度,所以,/data/local/tmp/libexample.so这个字符串就被放在了sc数据结构的下方。

    有了以上的值,我们来具体看看这段汇编指令到底在做什么:

    1
    2
    
    0xe59f0040, //        ldr     r0, [pc, #64]   ; 48 <.text+0x48>
    0xe3a01000, //        mov     r1, #0  ; 0x0
    

    这里有一个trick需要先解释一下,即如何通过pc来进行寻址。pc即表示当前程序运行指令的内存地址,[pc, #n]则表示pc+n指针所指向的那个地址。但是这里有一点需要注意的,在我们执行这条语句的时候:

    1
    
    ldr     r0, [pc, #64]
    

    pc已经不再是当前指令的内存地址了,而是自动被加了8,即这里的pc其实是pc+8那条指令的内存地址,所以[pc, #64]其实指向的是和当前指令内存地址偏移72 bytes的地址,如果你算一下会发现是

    1
    
    0xe1a00000, //        nop                     addr of libname
    

    所以,r0的值就是指向/data/local/tmp/libexample.so这个字符串的地址。而r1的值是1,即RTLD_LAZY的值。

    而接下来的这两条指令:

    1
    2
    
    0xe1a0e00f, //        mov     lr, pc
    0xe59ff038, //        ldr     pc, [pc, #56]   ; 4c <.text+0x4c>
    

    首先将pc值(pc+8的地址)付给了lr,即调用完函数之后的返回值,然后同样利用pc的寻址方式将dlopenaddr的值赋给了pc,因此,接下来就会调用dlopen函数,第一个参数是r0的值,即指向/data/local/tmp/libexample.so字符串的指针,第二个参数是r1的值,即RTLD_LAZY。

    当dlopen返回之后,程序的执行流会跳到lr指向的内存地址,即接下来的这段代码:

    1
    2
    3
    4
    5
    6
    7
    
    0xe59fd02c, //        ldr     sp, [pc, #44]   ; 44 <.text+0x44>
    0xe59f0010, //        ldr     r0, [pc, #16]   ; 30 <.text+0x30>
    0xe59f1010, //        ldr     r1, [pc, #16]   ; 34 <.text+0x34>
    0xe59f2010, //        ldr     r2, [pc, #16]   ; 38 <.text+0x38>
    0xe59f3010, //        ldr     r3, [pc, #16]   ; 3c <.text+0x3c>
    0xe59fe010, //        ldr     lr, [pc, #16]   ; 40 <.text+0x40>
    0xe59ff010, //        ldr     pc, [pc, #16]   ; 44 <.text+0x44>
    

    它的作用就是恢复这6个寄存器,最后会恢复pc,因此程序重新回到原来的执行流中。

    总结一下,sc里面的hijack code的主要作用就是调用一下dlopen加载/data/local/tmp/libexample.so,然后回到正常的执行流中。

    将sc写到栈上;
    1
    2
    3
    4
    5
    6
    
      // write code to stack
      codeaddr = regs.ARM_sp - sizeof(sc);
      if (0 > write_mem(pid, (unsigned long*)&sc, sizeof(sc)/sizeof(long), codeaddr)) {
        printf("cannot write code, error!\n");
        exit(1);
      }
    

    其中,write_mem的实现非常简单,就是调用了ptrace(PTRACE_POKETEXT)将数据写到目标进程的内存空间中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    /* Write NLONG 4 byte words from BUF into PID starting
       at address POS.  Calling process must be attached to PID. */
    static int
    write_mem(pid_t pid, unsigned long *buf, int nlong, unsigned long pos)
    {
      unsigned long *p;
      int i;
    
      for (p = buf, i = 0; i < nlong; p++, i++)
        if (0 > ptrace(PTRACE_POKETEXT, pid, (void *)(pos+(i*4)), (void *)*p))
          return -1;
      return 0;
    }
    
    利用之前得到的mprotect将栈设置成可执行,并将mprotect的返回值设置成sc数据结构中的code首地址;

    代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
      // calc stack pointer
      regs.ARM_sp = regs.ARM_sp - n*4 - sizeof(sc);
    
      // call mprotect() to make stack executable
      regs.ARM_r0 = stack_start; // want to make stack executable
      regs.ARM_r1 = stack_end - stack_start; // stack size
      regs.ARM_r2 = PROT_READ|PROT_WRITE|PROT_EXEC; // protections
    
      // normal mode, first call mprotect
      if (nomprotect == 0) {
        regs.ARM_lr = codeaddr; // points to loading and fixing code
        regs.ARM_pc = mprotectaddr; // execute mprotect()
      }
      // no need to execute mprotect on old Android versions
      else {
        regs.ARM_pc = codeaddr; // just execute the 'shellcode'
      }
    

    主要就是计算出栈的首地址和长度,然后将目标进程的pc设置成mprotectaddr,将返回地址lr设置成sc中hijack code的起始地址。这样在调用完mprotect之后就能直接执行hijack code了。

    利用ptrace(PTRACE_SETREGS)设置目标进程的寄存器,使得上面的所有修改生效。
    1
    2
    3
    
      // detach and continue
      ptrace(PTRACE_SETREGS, pid, 0, &regs);
      ptrace(PTRACE_DETACH, pid, 0, (void *)SIGCONT);
    

    至此,hijack的全部功能就实现了,现在/data/local/tmp/libexample.so已经被加载到了目标进程的内存空间中,接下来就要看下这个库里面到底是如何实现特定函数的hook的。

    hook

    在instruments这个目录下有两个子目录,其中base相当于是一个函数库,它会被编译成静态链接库libbase.a,我们可以看下instruments/base/jni/Android.mk这个文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    LOCAL_PATH := $(call my-dir)
    
    include $(CLEAR_VARS)
    
    LOCAL_MODULE    := base
    LOCAL_SRC_FILES := ../util.c ../hook.c ../base.c
    LOCAL_ARM_MODE := arm
    
    include $(BUILD_STATIC_LIBRARY)
    

    而example里面的代码会将libbase.a静态链接进来,然后生成一个动态链接库libexample.so,可以从其编译文件instruments/example/jni/Android.mk看出来:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    LOCAL_PATH := $(call my-dir)
    
    include $(CLEAR_VARS)
    LOCAL_MODULE := base
    LOCAL_SRC_FILES := ../../base/obj/local/armeabi/libbase.a
    LOCAL_EXPORT_C_INCLUDES := ../../base
    include $(PREBUILT_STATIC_LIBRARY)
    
    
    include $(CLEAR_VARS)
    LOCAL_MODULE    := libexample
    LOCAL_SRC_FILES := ../epoll.c  ../epoll_arm.c.arm
    LOCAL_CFLAGS := -g
    LOCAL_SHARED_LIBRARIES := dl
    LOCAL_STATIC_LIBRARIES := base
    include $(BUILD_SHARED_LIBRARY)
    

    这个example非常简单,它也只有一个文件(epoll.c),里面只有几十行代码,我们先来看下这个库的初始化函数my_init,这个函数会在该库被加载的时候运行一次:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    void my_init(void)
    {
      counter = 3;
    
      log("%s started\n", __FILE__)
    
      set_logfunction(my_log);
    
      hook(&eph, getpid(), "libc.", "epoll_wait", my_epoll_wait_arm, my_epoll_wait);
    }
    

    里面主要是调用了libbase.a提供的hook函数(源文件为instruments/base/hook.c):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    int hook(struct hook_t *h, int pid, char *libname, char *funcname, void *hook_arm, void *hook_thumb)
    {
      find_name(pid, funcname, libname, &addr);
      strncpy(h->name, funcname, sizeof(h->name)-1);
    
      if (addr % 4 == 0) {
        h->thumb = 0;
        h->patch = (unsigned int)hook_arm;
        h->orig = addr;
        h->jump[0] = 0xe59ff000; // LDR pc, [pc, #0]
        h->jump[1] = h->patch;
        h->jump[2] = h->patch;
        for (i = 0; i < 3; i++)
          h->store[i] = ((int*)h->orig)[i];
        for (i = 0; i < 3; i++)
          ((int*)h->orig)[i] = h->jump[i];
      } else {
        ...
      }
      hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jumpt));
      return 1;
    }
    

    这个函数其实是区分了ARM指令集和THUMB指令集的,为了简化,我们暂时只考虑ARM指令,即这里的(addr % 4 == 0)的情况。

    首先,这里先找到需要被hook的目标库(libname)的目标函数(funcname)的内存地址,这里需要注意的,由于libexample.so这个库已经是在目标进程的进程空间中运行了,所以其获得的地址即为目标函数在目标进程中的地址。这里的find_name所用到的技术和hijack.c里面用到的技术基本是一样的,这里就不详述了。

    在获得目标函数代码的首地址之后,将其赋值给h->orig这个变量,将这个该函数的前三条指令保存在h->store这个数组中,并将以下三条指令覆盖(overwrite)目标函数的前三条指令:

    1
    2
    3
    
        h->jump[0] = 0xe59ff000; // LDR pc, [pc, #0]
        h->jump[1] = h->patch;
        h->jump[2] = h->patch;
    

    其中,h->patch即为hook函数的地址,在example里面是my_epoll_wait。同样的,这里又一次用到了利用pc进行寻址的技术,可以看前面的内容,这里也不详述了。

    最后,调用了一个hook_cacheflush函数:

    1
    
    hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jumpt));
    

    这个函数的主要作用就是刷新指令的缓存。因为虽然前面的操作修改了内存中的指令,但有可能被修改的指令已经被缓存起来了,再执行的话,CPU可能会优先执行缓存中的指令,使得修改的指令得不到执行。所以我们需要使用一个隐藏的系统调用来刷新一下缓存。

    至此,目标进程目标函数的hook工作也就完成了。最后我们来看一下这个hook函数my_epoll_wait做了什么:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    int my_epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
    {
      orig_epoll_wait = (void*)eph.orig;
    
      hook_precall(&eph);
      int res = orig_epoll_wait(epfd, events, maxevents, timeout);
      if (counter) {
        hook_postcall(&eph);
        log("epoll_wait() called\n");
        counter--;
        if (!counter)
          log("removing hook for epoll_wait()\n");
      }
    
      return res;
    }
    

    其实这个函数非常简单,就是在前count次调用epoll_wait的时候打印一下。这里面有两个libbase.a中的函数:hook_precall和hook_postcall。我们来分别看一下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    void hook_precall(struct hook_t *h)
    {
      int i;
    
      if (h->thumb) {
        unsigned int orig = h->orig - 1;
        for (i = 0; i < 20; i++) {
          ((unsigned char*)orig)[i] = h->storet[i];
        }
      }
      else {
        for (i = 0; i < 3; i++)
          ((int*)h->orig)[i] = h->store[i];
      }
      hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jumpt));
    }
    

    hook_precall的主要作用是恢复目标函数的前三条指令,这里同样对ARM指令和THUMB指令做了区分。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    void hook_postcall(struct hook_t *h)
    {
      int i;
    
      if (h->thumb) {
        unsigned int orig = h->orig - 1;
        for (i = 0; i < 20; i++)
          ((unsigned char*)orig)[i] = h->jumpt[i];
      }
      else {
        for (i = 0; i < 3; i++)
          ((int*)h->orig)[i] = h->jump[i];
      }
      hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jumpt));
    }
    

    而hook_postcall则是重新用hook函数覆盖目标函数的前三条指令。


    好了,到这里,adbi里面的代码基本上就分析完了。最后简单描述下什么是ARM指令和THUMB指令吧。

    ARM vs. THUMB

    在传统的RISC模式的指令集中,指令都是定长的,比如ARM指令的长度都是32-bits。定长的好处在于处理器处理起来效率高,但是缺点也是显而易见的,即浪费空间。所以又引入了THUMB指令。

    THUMB指令可以看作是ARM指令压缩形式的子集,所谓子集,即THUMB指令集中的所有指令都可以被32-bits的ARM指令所替代,而并非所有ARM指令都有对应的THUMB指令。

    所以可以说THUMB模式是ARM在时间和空间中的一个权衡,因此,在普通的ARM可执行文件中,ARM指令和THUMB指令是同时存在的,所以在做诸如分析、攻击等操作的时候需要同时考虑两种模式的存在,这也是adbi为什么会需要区分对待ARM和THUMB的原因吧。

    ARM和THUMB的具体区别这里就不介绍了,网上这种资料一搜一大堆,有兴趣的还是自己慢慢研究吧。



沪ICP备19023445号-2号
友情链接