Mctrain's Blog

What I learned in IT, as well as thought about life

Hooking and Hijacking Android Native Code

| Comments

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

先强烈推荐下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

可以看到,里面主要有两个目录:hijackinstruments。其中,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)设置目标进程的寄存器,使得上面的所有修改生效。

这个时候目标进程就开始执行mprotectsc中的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.sodlopen函数的内存地址;
  • 分别获得本进程和目标进程中linker的地址;
  • 通过dlopenlinker的相对偏移一样的原理来计算目标进程中dlopen的真正内存地址。

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

利用ptrace调用attach目标进程;

这个步骤就两句话:

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

至于什么是ptracewaitpid,以及如何使用它们,请参考我之前的一篇博客:系统调用学习笔记 - 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~r3lrpcsp这六个寄存器的值,以及需要加载的库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设置成schijack 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_precallhook_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的具体区别这里就不介绍了,网上这种资料一搜一大堆,有兴趣的还是自己慢慢研究吧。

Comments