本文作者/focus
HOOK通過在系統調用或函數調用前以替換的方式改變程序中原有的函數功能,實現更改原有函數的功能。
利用LD_PRELOAD進行HOOK
Linux提供了一個名為LD_PRELOAD的環境變量。這個環境變量允許用戶指定一個或多個共享鏈接庫文件的路徑。當程序啟動時,動態加載器會在加載C語言運行庫之前,首先加載LD_PRELOAD所指定的共享鏈接庫。這種加載方式被稱為預裝載。
預裝載機制使得用戶可以在程序執行前插入自定義的共享鏈接庫,從而改變或擴展程序的行為。這些自定義的共享鏈接庫可以包含重寫的函數定義,當程序嘗試調用這些函數時,動態加載器會優先加載并執行預裝載的庫中的函數定義,而不是默認的庫中的定義。結合LD_PRELOAD和預裝載機制,我們可以實現對函數的HOOK。
首先我們編寫一個目標程序代碼,這個程序會等待用戶的輸入,從而處于阻塞狀態。
?
#include?int?main() { ??printf("please input a number: "); ??int?val = 0; ??scanf("%d", &val); ??printf("already recv your number! "); ??return?0; }
?
然后編寫HOOK函數,該函數重寫了scanf函數,打印出一句話,從而使目標程序便能夠無需等待用戶輸入而繼續執行。
?
#include?int?main() { ??printf("please input a number: "); ??int?val = 0; ??scanf("%d", &val); ??printf("already recv your number! "); ??return?0; }
?
通過以下命令分別編譯目標程序和用于HOOK的so文件。
?
gcc ./target.c -o target gcc --shared hook.c -o hook.so -fPIC
?
執行下述命令實現HOOK。可以看到scanf函數由原先的等待用戶輸入,變成了輸出一句話。
?
LD_PRELOAD=./hook.so ./target
?
利用ptrace進行HOOK
然而上述方法只能對未運行的程序進行HOOK,對于已經運行的程序可以利用ptrace系統調用進行HOOK。
ptrace允許一個進程監控和控制另一個進程的執行,是GDB等調試器實現的基礎。利用ptrace進行HOOK的步驟如下所示:
?
1.HOOK程序利用ptrace附加到已經運行的目標程序,獲取目標進程運行的上下文,保存原寄存器數據; 2.查找目標程序的link_map鏈表的指針,根據函數名稱遍歷查找函數的真實地址。link_map地址存在于.got.plt區節中,該區節的加載地址可以從DYNAMIC段DT_PLTGOT區域得到。在此我們主要查找dlopen函數地址; 3.通過修改目標程序的寄存器和堆棧使目標程序調用dlopen函數,從而將hook.so加載到目標程序中; 4.將要HOOK的原函數地址,替換為hook.so中重寫后的新函數地址。因為hook.so在上一步被dlopen加載到目標內存空間中,所以重寫后的新函數地址可以通過步驟2得到; 5.恢復目標程序原寄存器的內容,傳入PTRACE_DETACH參數結束對目標程序的附加。
?
接下來詳細介紹具體的實現:
利用ptrace附加目標程序,傳入user_regs_struct結構體用于保存目標程序的寄存器信息。
?
void?ptrace_attach(pid_t?pid, struct user_regs_struct *regs) { ????if(ptrace(PTRACE_ATTACH, pid, NULL, NULL) < 0) ????{ ????????printf("ptrace_attach error "); ????} ???? ????waitpid(pid, NULL, WUNTRACED); ???? ????if(ptrace(PTRACE_GETREGS, pid, NULL, regs)) ????{ ????????printf("ptrace_getregs error! "); ????} }
?
這里省略了查找link_map鏈表指針,該鏈表可以通過解析ELF文件結構獲取到。主要看如何遍歷link_map鏈表查找指定函數的地址。
在find_symbol函數中通過link_map鏈表指針獲取link_map結構體內容,根據link_map中l_name字段判斷該動態鏈接庫是否有效。
在find_symbol_in_linkmap函數中,通過解析link_map中l_ld字段,獲取動態鏈接庫的符號表等信息,與指定函數名稱進行對比,獲取該函數的地址。
?
Elf_Addr find_symbol(int?pid, Elf_Addr lm_addr, char?*sym_name) { ????struct?link_map?lmap;//存儲lmap的內容 ????unsigned?int?nlen = 0; ????while?(lm_addr) ????{ ????????// 有了link_map指針后,根據指針獲取link_map結構體的內容 ????????ptrace_getdata(pid, lm_addr, &lmap, sizeof(struct link_map)); ????// 獲取下一個link_map結構體的指針 ????????lm_addr = (Elf_Addr)(lmap.l_next); ????????// 判斷動態鏈接庫是有否有效 ????????if?(0?== lmap.l_name) ????????{ ????????????continue; ????????} ????????Elf_Addr sym_addr = find_symbol_in_linkmap(pid, &lmap, sym_name); ????????if?(sym_addr) ????????{ ????????????return?sym_addr; ????????} ????} ????return?0; }
?
通過上一步的find_symbol函數,可以得到dlopen的函數地址,模擬調用dlopen函數,設置棧空間將要加載的so庫絕對路徑寫入棧地址,調用dlopen加載so庫到目標地址中。
?
int?inject_code(pid_t?pid, unsigned?long?dlopen_addr, char?*libc_path) { ????char?sbuf1[STRLEN], sbuf2[STRLEN]; ????struct?user_regs_struct?regs, saved_regs; ????int?status; ????ptrace_getregs(pid, ®s);//獲取所有寄存器值 ????ptrace_getdata(pid, regs.rsp + STRLEN, sbuf1, sizeof(sbuf1)); ????ptrace_getdata(pid, regs.rsp, sbuf2, sizeof(sbuf2)); ????/*用于引發SIGSEGV信號的ret內容*/ ????unsigned?long?ret_addr = 0x666; ????ptrace_setdata(pid, regs.rsp, (char?*)&ret_addr, sizeof(ret_addr)); ????ptrace_setdata(pid, regs.rsp + STRLEN, libc_path, strlen(libc_path) + 1); ????memcpy(&saved_regs, ®s, sizeof(regs)); ????regs.rdi = regs.rsp + STRLEN; ????regs.rsi = RTLD_NOW|RTLD_GLOBAL|RTLD_NODELETE; ????regs.rip = dlopen_addr+2; ????if?(ptrace(PTRACE_SETREGS, pid, NULL, ®s) < 0) ????{ ????????printf("inject_code:PTRACE_SETREGS 1 failed!"); ????} ????if?(ptrace(PTRACE_CONT, pid, NULL, NULL) < 0) ????{ ????????printf("inject_code:PTRACE_CONT failed!"); ????} ????waitpid(pid, &status, 0); ????if?(ptrace(PTRACE_SETREGS, pid, 0, &saved_regs) < 0) ????{ ????????printf("inject_code:PTRACE_SETREGS 2 failed!");; ????} ????ptrace_setdata(pid, saved_regs.rsp + STRLEN, sbuf1, sizeof(sbuf1)); ????ptrace_setdata(pid, saved_regs.rsp, sbuf2, sizeof(sbuf2)); ????return?0; }
?
接下來查找目標程序的got表,修改目標函數的got表內容為新函數的地址。
?
Elf_Addr find_sym_in_rel(int?pid, char?*sym_name) { ????Elf_Rel *rel = (Elf_Rel *) malloc(sizeof(Elf_Rel)); ????Elf_Sym *sym = (Elf_Sym *) malloc(sizeof(Elf_Sym)); ????int?i; ????char?str[STRLEN] = {0}; ????unsigned?long?ret; ????struct?lmap_result?*lmret?= get_dyn_info(pid); ????for?(i = 0; inrelplts; i++) ????{ ????????ptrace_getdata(pid, lmret->jmprel + i*sizeof(Elf_Rela), rel, sizeof(Elf_Rela)); ????????ptrace_getdata(pid, lmret->symtab + ELF64_R_SYM(rel->r_info) * sizeof(Elf_Sym), sym, sizeof(Elf_Sym)); ????????int?n = ptrace_getstr(pid, lmret->strtab + sym->st_name, str, STRLEN); ????????if?(strcmp(str, sym_name) == 0) ????????????break; ????} ????if?(i == lmret->nrelplts) ????????ret = 0; ????else ????????ret = rel->r_offset; ????free(rel); ????return?ret; }
?
在這里我們修改一下目標程序,循環10次,每次接收控制臺輸入,并打印出來。
?
#include?#include? int?main() { ??int?val = 10; ??while?(val--) ??{ ????????sleep(2); ????printf("please input a number: "); ????int?val = 0; ????scanf("%d", &val); ????printf("your val is %d ", val); ??} ??return?0; }
?
在HOOK代碼中,自動為val變量賦值。
?
#include?#include? int?num = 1; int?hookscanf(const?char?*format,...) { ????va_list ap; ????int?retval; ????va_start(ap, format); ????int* pval = va_arg(ap, int*); ??printf("自動輸入:%d ", num); ????*pval = num++; ????return?0; }
?
編譯運行,效果如下,不需要在控制臺進行輸入,自動為val賦值。
小結
本文介紹了兩種linux系統下常見的HOOK方法,第一種方法比較簡單但無法對已經運行的程序進行HOOK,第二種方法相較于第一種會更加復雜,需要對ELF文件格式以及相關結構有更深入的了解。
審核編輯:黃飛
?
評論
查看更多