以下內容選自《深入解析Android5.0系統》,京東,噹噹,亞馬遜上有售。
Hook API的技術由來已久,在操作系統未能提供所需功能的情況下,利用HookAPI的手段來實現某種必需的功能也算是一種不得已的辦法。
筆者瞭解Hook API技術最早是在十幾年前,當時是在Windows平臺下開發電子詞典的光標取詞功能。這項功能就是利用HookAPI的技術把系統的字符串輸出函數替換成了電子詞典中的函數,從而能得到屏幕上任何位置的字符串。無論是16位的Windows95還是32的WindwsNT都有辦法向整個系統或特定的目標進程中“注入”DLL動態庫,並替換掉其中的函數。
Linux平臺中完成HookAPI的方法和Windows上不一樣。Linux由於安全性更高,所以一般的方法難以達到目標,通常是採用ptrace函數來實現HookAPI的目的。但是調用ptrace函數需要root權限,所以開發的軟件作用有限。這也是爲什麼HookAPI技術在Linux上並不流行。
Android上最早使用HookAPI技術的軟件是“xx安全大師”,因爲使用了這項“祕密武器”,所以完成了很多看起來不可思議的功能。當然現在國內市場上差不多所有的安全類軟件都實現了類似的功能。和Linux下一樣,使用這些功能的前提是獲得root授權。
這些安全軟件因爲要截獲系統的binder功能,所以多半是替換掉libc庫的ioctl函數來達到監控binder調用的目的。下面我們也來學習如何用自己開發的動態庫中的ioctl函數替換目標進程中的ioctl函數。
原理看起來並不複雜,但是實現起來卻不是那麼的簡單,向目標進程中注入代碼的步驟是:
1) 用ptrace函數attach上目標進程;
2) 讓目標進程的執行流程跳轉到mmap函數來分配一小段內存空間;
3) 把一段機器碼拷貝到目標進程中剛分配的內存中去;
4) 最後讓目標進程的執行流程跳轉到注入的代碼執行。
下面將詳細的介紹具體的過程。
1. attach目標進程
用ptrace函數attach上目標進程的代碼如下:
ptrace(PTRACE_ATTACH, pid, NULL, 0 );
在繼續操作前,需要先把目標進程的寄存器先保存起來,這樣完成注入後,恢復目標進程的寄存器,目標進程就能不受影響繼續執行了。
structpt_regs old_regs;
ptrace(PTRACE_GETREGS, pid, NULL, &old_regs);
2. 獲取目標進程中函數地址
爲了在目標進程中調用mmap函數,需要得到mmap函數在目標進程中的地址。由於函數地址相對於動態庫的裝載地址是固定不變的,所以本進程mmap函數的地址加上目標進程和本進程的動態庫的裝載地址的差值就等於目標進程的中mmap函數的地址。用公式表達如下:
目標進程函數地址=本進程函數地址 +(本進程libc庫地址-目標進程lib庫地址)
這樣其實只要得到動態庫的裝載地址就能算出目標進程的mmap的地址。一種得到動態庫裝載地址的方法是分析Linux進程的/proc/pid/maps文件,這個文件包含了進程中所有mmap映射的地址。下面我們寫一個獲取動態庫地址的函數,代碼如下:
unsignedlong get_lib_address( pid_t pid, const char* library_name)
{
char filename[256];
if ( pid < 0 ) { //得到當前進程模塊地址時傳入的pid參數爲-1
snprintf(filename, sizeof(filename),"/proc/self/maps");
} else {
snprintf(filename, sizeof(filename), "/proc/%d/maps", pid);
}
FILE *fp = fopen( filename, "r" );
if ( fp != NULL ) {
charline[1024];
while( fgets( line, sizeof(line), fp ) ) {
//在所有的映射行中尋找目標動態庫所在的行
if( strstr( line, library_name ) ) {
char *p = strtok( line, "-" );
unsignedlong addr = strtoul(p, NULL, 16 );
fclose(fp ) ;
returnaddr;
}
}
fclose(fp ) ;
}
return 0;
}
有了這個函數,我們就能算出mmap函數在目標進程中所在的地址了。用前面介紹的方法計算函數地址的代碼如下所示:
unsignedlong local_address = get_lib_address( -1, "/system/lib/libc.so");
unsignedlong remote_adress = get_lib_address( pid, "/system/lib/libc.so");
mmap_addr=(unsigned long)mmap + remote_address - local_address;
3. 調用目標進程中的函數
調用函數是需要傳遞參數的,arm中前4個參數可以使用寄存器傳遞,後面的參數需要通過棧傳遞,mmap有6個參數,所以我們要向目標進程的棧中寫入參數。
long params [] = { //調用mmap函數的參數
0,
MAP_SIZE,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_ANONYMOUS | MAP_PRIVATE,
0,
0
};
//前面四個參數用寄存器傳遞
for ( i =0; i < 4; i ++ ) {
regs->uregs[i] =params[i];
}
regs->ARM_sp -= 2* sizeof(long);
//後面兩個參數放到棧裏
unsignedlong addr = regs->ARM_sp;
ptrace( PTRACE_POKETEXT, pid, addr,params[5]);
ptrace( PTRACE_POKETEXT, pid, addr+sizeof(long),params[6]);
讓目標進程執行函數的方法就是把目標進程的pc寄存器設爲函數的入口地址,然後讓目標進程恢復運行就可以了。但是函數執行完還需要讓本進程繼續控制,爲了達到這個目的,這裏使用的一點技巧:調用mmap時把返回地址設爲0,這樣目標進程執行完mmap返回時會出現地址錯誤,這樣目標進程將被掛起,控制權會回到調試進程手中,現在的調試進程就是我們的應用程序。
regs->ARM_pc = mmap_addr; //設置pc寄存器爲mmap函數的入口
regs->ARM_lr = 0; //把返回地址置爲0
ptrace( PTRACE_SETREGS, pid, NULL, regs); //設置目標進程的寄存器
ptrace( PTRACE_CONT, pid, NULL, 0 ); //讓目標進程繼續運行
waitpid( pid,NULL, WUNTRACED ); //等待目標進程結束
調用結束後,需要讀取目標進程的寄存器,其中寄存器r0保存着返回值,也就是mmap分配的地址:
ptrace(PTRACE_GETREGS, pid, NULL, regs);
unsignedlong remote_mmap_base = (unsignedlong)regs.ARM_r0;
4. 注入代碼並運行
mmap運行完成後,我們已經在目標進程中擁有了一塊內存,現在只需要把準備好的代碼用ptrace寫進去就可以了。因此關鍵的問題是如何準備這段代碼。
需要注入的代碼是用來裝載我們自己的動態庫的,並且裝載完畢後還要能調用其中的函數hook_api來完成替換系統ioctl函數的工作,最後還要將目標進程恢復到初始狀態運行,就好像什麼也沒發生過。
我們需要用匯編來寫這段代碼:
.global_dlopen_addr_s
.global_dlopen_param1_s
.global_dlopen_param2_s
.global_dlsym_addr_s
.global_dlsym_param2_s
.global_saved_cpsr_s
.global_saved_regs_s
.data
_code_start_s:
@執行dlopen函數
ldr r1,_dlopen_param2_s
ldr r0,_dlopen_param1_s
ldr r3,_dlopen_addr_s
blxr3
@調用dlclose函數得到函數hook_api的地址
ldr r1,_dlsym_param2_s
ldr r3,_dlsym_addr_s
blxr3
@調用hook_api函數
blxr0
@恢復原始的cpsr和寄存器值
ldr r1,_saved_cpsr_s
msrcpsr_cf, r1
ldr sp,_saved_r0_pc_s
ldmfd sp,{r0-pc}
_dlopen_addr_s:
.word0xFFFFFFFF
_dlopen_param1_s:
.word0xFFFFFFFF
_dlopen_param2_s:
.word0x2
_dlsym_addr_s:
.word0xFFFFFFFF
_dlsym_param2_s:
.word0xFFFFFFFF
_saved_cpsr_s:
.word0xFFFFFFFF
_saved_regs_s:
.word0xFFFFFFFF
_code_end_s:
.space0x400, 0
.end
這段彙編代碼放在了.data段中,所以定義的並不是函數,只是代碼片段,這樣的好處是把編譯後的二進制代碼注入到目標進程就可以運行。
同時代碼中還定義了一些變量,包括函數dlopen和dlsym的地址也是用變量來表示的,這是因爲我們是在自己的應用中完成這段彙編代碼編譯的,如果注入到目標進程中,函數的地址並不相同,所以把函數地址用變量表示出來,在注入前需要計算出dlopen和dlsym在目標進程中的地址,填在這裏。
變量_dlopen_param1_s用來定義dlopen函數的第一個參數,也就是動態庫的路徑,所以庫的路徑字符串也需要拷貝到目標進程中,因此代碼的最後通過.語句“space0x400,0”開闢了一段空間來存儲路徑字符串等參數。
變量_dsym_param2_s用來定義dlsym函數的第二個參數:需要調用的函數名,它也需要拷貝到目標進程中。
變量_saved_cpsr_s和_saved_regs_s用來保存目標進程原始的cpsr和寄存器值,這樣當dlopen函數返回後,通過恢復cpsr和寄存器就能讓目標進程恢復運行了。
下面的代碼演示瞭如何初始化上面這些變量:
//用前面介紹過的方法得到目標進程中dlopen的地址並填入變量_dlopen_addr_s中
unsignedlong local_handle = get_lib_address( -1, "/system/lib/linker");
unsignedlong remote_handle = get_lib_address( pid, "/system/lib/linker ");
_dlopen_addr_s = (unsigned long)dlopen +remote_handle - local_handle ;
_dlsym_addr_s = (unsigned long)dlsym + remote_handle- local_handle ;
//變量remote_code_ptr存儲的是目標進程中注入代碼的起始地址,
//但是我們要留出一段空間作爲調用dlopen的棧,所以並沒有把起始地址定爲mmap地址的開頭,
//而是加上了一個堆棧的長度
unsignedlong remote_code_ptr = remote_mmap_base +STACK_SIZE;
//變量local_code_ptr指向彙編中的代碼開始地址
unsignedlong local_code_ptr = (unsignedlong)&_cdoe_start_s;
//計算代碼的長度
intlcode_length = (unsigned long)&_code_end_s - (unsignedlong)&_code_start_s;
//變量dlopen_param1_ptr指向彙編中保留的空間,用來存儲動態庫的路徑
unsignedlong dlopen_param1_ptr = local_code_ptr + code_length +0x40;
//變量dlsym_param2_ptr指向彙編中保留的空間,用來存儲函數名字符串
unsignedlong dlsym_param2_ptr = dlopen_param1_ptr +0x100;
//變量saved_regs_ptr指向彙編中保留的空間,用來存儲原始的寄存器
unsigned long saved_regs_ptr = dlsym_param2_ptr +0x100;
//拷貝動態庫的路徑字符串
strcpy((char*)dlopen_param1_ptr, "/system/lib/libhook.so");
//計算路徑字符串在目標進程中的地址
_dlopen_param1_s = dlopen_param1_ptr +remote_code_ptr - local_code_ptr);
//拷貝函數名字符串
strcpy((char*)dlsym_param2_ptr, "hook_api");
//計算函數名字符串在目標進程中的地址
_dlsym_param2_s = dlsym_param2_ptr + remote_code_ptr- local_code_ptr);
//把目標進程原始的cpsr放到變量_saved_cpsr_s中
_saved_cpsr_s =old_regs.ARM_cpsr;
//把目標進程原始的寄存器值r0 ~ r15拷貝到變量saved_regs_ptr中
memcpy((void*)saved_regs_ptr, &(old_regs.ARM_r0),16 * sizeof(long) );
//計算保存寄存器的變量在目標進程中的地址
_saved_regs_s = saved_regs_ptr + remote_code_ptr -local_code_ptr );
初始化這些變量用了很多編程技巧,不太容易理解,所以筆者在這裏每行都做了註釋。
最後,把準備好的代碼“拷貝”到目標進程中並運行:
//ptrace_writedata把一段內存“拷貝”到目標進程的函數
ptrace_writedata(pid, remote_code_ptr,(char*)local_code_ptr,MAP_SIZE-STACK_SIZE);
memcpy( ®s, &old_regs, sizeof(regs) ); //準備寄存器
regs.ARM_sp = (long)remote_code_ptr; //初始化堆棧
regs.ARM_pc = (long)remote_code_ptr; //把PC寄存器設爲代碼的入口地址
ptrace( PTRACE_SETREGS, pid, NULL, ®s);//設置目標進程的寄存器
ptrace_detach( pid ); //退出控制,這樣目標進程就可以恢復運行了
上面的代碼中用ptrace_writedata函數來拷貝一塊內存到目標進程中。這個函數只是封裝了ptrace函數來一次拷貝更多的數據,這裏就不多介紹了。
這樣我們終於在目標進程中裝載進了我們開發的動態庫,這個庫裏有個名爲new_ioctl的函數,它就是我們準備用來替換系統ioctl的函數,下面我們將介紹替換過程。
5. 替換系統的ioctl函數
在前面linker的介紹中讀者應該已經瞭解了,每個動態庫都有自己的全局偏移表GOT。而我們希望完成的是將binder函數所在的庫libbinder.so中的ioctl函數替換掉,打算不打算替換所有動態庫中的ioctl調用,所以我們只要找到libbinder.so的全局偏移表就達到目標了。代碼如下:
boolhook_api() {
//使用打開動態庫的方式得到動態庫的soinfo結構
soinfo* si =(soinfo*)::dlopen("/system/bin/libbinder.so",RTLD_NOW);
if(si == NULL || si->strtab == NULL || si->plt_rel == NULL){
returnfalse;
}
for (uint32_t i = 0; i < si->plt_rel_count;i++) {
//查找重定位表中ioctl所在的項
if(strcmp(si->symtab[ELF32_R_SYM(si->plt_rel[i].r_info)].st_name
+ si->strtab, "ioctl") == 0){
//計算對應的GOT表項的地址
uint32_t* got = (uint32_t*)(si->base +si->plt_rel[i].r_offset);
if(*(got) != new_ioctl) {
//把GOT表項的地址屬性改爲可寫
uint32_tpagesize = sysconf(_SC_PAGE_SIZE);
void*start =(void*)(((uint32_t)got)/pagesize*pagesize);
if(mprotect(start, pagesize * 2, PROT_READ|PROT_WRITE) == -1){
returnfalse;
}
*(got) = new_ioctl; //填入新地址
mprotect(start,pagesize * 2, PROT_READ|PROT_WRITE);
}
return true;
}
}
}
查找ioctl在GOT表項中的地址是通過查找動態庫的函數重定位表來完成的。前面介紹linker模塊時對重定位表已經解釋的很詳細了,這裏的代碼就不用多解釋了。
爲了節省篇幅,本節中的代碼很多都去掉了錯誤判斷和處理語句,讀者如果要借鑑這些代碼,要注意把這部分代碼補全了。這裏介紹了注入部分,其實後面的binder替換函數的編寫,各種系統調用的處理也非常麻煩,需要對Android的Binder機制和framework有深入的瞭解才能完成。