首先声明这是一篇中文博客。
先强烈推荐下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
指针所指向的那个地址。但是这里有一点需要注意的,在我们执行这条语句的时候:
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的具体区别这里就不介绍了,网上这种资料一搜一大堆,有兴趣的还是自己慢慢研究吧。