轉自http://www.cnblogs.com/catch/p/3476280.htmlhtml
[本文翻譯自這裏: http://www.linuxjournal.com/article/6100?page=0,0,做者:Pradeep Padaia]linux
你是否曾經想過怎樣才能攔截系統調用?你是否曾經想過經過修改一下系統調用的參數來耍一把內核?你是否想過調試器是怎樣把一個進程停下來,而後把控制權轉移給你的?若是你覺得這些都是經過複雜的內核編程來實現的,那你就錯了,事實上,Linux 提供了一種很優雅的方式來實現上述全部行爲:ptrace 系統調用。ptrace 提供了一種機制使得父進程能夠觀察和控制子進程的執行過程,ptrace 還能夠檢查和修改該子進程的可執行文件在內存中的鏡像及該子進程所使用的寄存器中的值。這種用法一般來講,主要用於實現對進程插入斷點和跟蹤子進程的系統調用。編程
在本篇文章中,咱們將學習怎麼去攔截一個系統調用而且修改該系統調用的參數,在後續一篇文章中,咱們將繼續探討 ptrace 的一些更深刻的技術,如設置斷點,在運行的子進程中插入代碼等。咱們將會查看進程的寄存器和數據段,並去修改其中的內容。咱們還會介紹一種方式來在進程中插入代碼,使得該進程能停下來,並執行咱們插入的任意代碼。學習
基礎spa
操做系統經過一個叫作「系統調用」的標準機制來對上層提供服務,他們提供了一系列標準的API來讓上層應用程序獲取底層的硬件和服務,好比文件系統。當一個進程想要進行一個系統調用的時候,它會把該系統調用所須要用到的參數放到寄存器裏,而後執行軟中斷指令0x80. 這個軟中斷就像是一個門,經過它就能進入內核模式,進入內核模式後,內核將會檢查系統調用的參數,而後執行該系統調用。操作系統
在 i386 平臺下(本文全部代碼都基於 i386), 系統調用的編號會被放在寄存器 %eax 中,而系統調用的參數會被依次放到 %ebx,%ecx,%edx,%exi 和 %edi中,好比說,對於下面的系統調用:翻譯
write(2,
"Hello"
, 5)
|
編譯後,它最後大概會被轉化成下面這樣子:debug
movl $4, %eax movl $2, %ebx movl $hello,%ecx movl $5, %edx int $0x80
其中 $hello 指向字符串 "Hello"。調試
看完上面簡單的例子,如今咱們來看看 ptrace 又是怎樣執行的。首先,咱們假設進程 A 要 ptrace 進程 B。在 ptrace 系統調用真正開始前,內核會檢查一下咱們將要 trace 的進程 B 是否當前已經正在被 traced 了,若是是,內核就會把該進程 B 停下來,並把控制權交給調用進程 A (任什麼時候候,子進程只能被父進程這惟一一個進程所trace),這使得進程A有機會去檢查和修改進程B的寄存器的值。code
下面咱們用一個例子來講明:
#include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <linux/user.h> /* For constants ORIG_EAX etc */ int main() { pid_t child; long orig_eax; child = fork(); if(child == 0) { ptrace(PTRACE_TRACEME, 0, NULL, NULL); execl("/bin/ls", "ls", NULL); } else { wait(NULL); orig_eax = ptrace(PTRACE_PEEKUSER, child, 4 * ORIG_EAX, NULL); printf("The child made a " "system call %ld\n", orig_eax); ptrace(PTRACE_CONT, child, NULL, NULL); } return 0; }
當把上面這段代碼編譯執行後,終端上除了命令 ls 的輸出外,還會輸出下面一行:
The child made a system call 11
根據上面的輸出,咱們知道,在執行 ls 命令的時候,第11號系統調用被執行了,它是子進程中執行的第一個系統調用。若是想查看一下各個系統調用編號對應的名字,能夠參考頭文件:/usr/include/asm/unistd.h.
正如你在上面的例子中所看到,ptrace 的使用流程通常是這樣的:父進程 fork() 出子進程,子進程中執行咱們所想要 trace 的程序,在子進程調用 exec() 以前,子進程須要先調用一次 ptrace,以 PTRACE_TRACEME 爲參數。這個調用是爲了告訴內核,當前進程已經正在被 traced,當子進程執行 execve() 以後,子進程會進入暫停狀態,把控制權轉給它的父進程(SIG_CHLD信號), 而父進程在fork()以後,就調用 wait() 等子進程停下來,當 wait() 返回後,父進程就能夠去查看子進程的寄存器或者對子進程作其它的事情了。
當系統調用發生時,內核會把當前的%eax中的內容(即系統調用的編號)保存到子進程的用戶態代碼段中(USER SEGMENT or USER CODE),咱們能夠像上面的例子那樣經過調用Ptrace(傳入PTRACE_PEEKUSER做爲第一個參數)來讀取這個%eax的值,當咱們作完這些檢查數據的事情以後,經過調用ptrace(PTRACE_CONT),可讓子進程從新恢復運行。
ptrace的參數
ptrace 總共有 4 個參數:
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
其中第一個參數決定ptrace的行爲也決定了接下來其它3個參數是怎樣被使用的,第1個參數能夠取如下任意一個值:
PTRACE_TRACEME, PTRACE_PEEKTEXT, PTRACE_PEEKDATA, PTRACE_PEEKUSER, PTRACE_POKETEXT, PTRACE_POKEDATA, PTRACE_POKEUSER, PTRACE_GETREGS, PTRACE_GETFPREGS, PTRACE_SETREGS, PTRACE_SETFPREGS, PTRACE_CONT, PTRACE_SYSCALL, PTRACE_SINGLESTEP, PTRACE_DETACH
本文接下來會解釋這些參數有什麼不一樣的地方。
讀取系統調用的參數
經過調用ptrace並傳入PTRACE_PEEKUSER做爲第一個參數,咱們能夠檢查子進程中,保存了該進程的寄存器的內容(及其它一些內容)的用戶態內存區域(USER area)。內核把寄存器的內容保存到這塊區域,就是爲了可以讓父進程經過ptrace來讀取,下面舉一個例子來講明一下:
#include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <linux/user.h> #include <sys/syscall.h> /* For SYS_write etc */ int main() { pid_t child; long orig_eax, eax; long params[3]; int status; int insyscall = 0; child = fork(); if(child == 0) { ptrace(PTRACE_TRACEME, 0, NULL, NULL); execl("/bin/ls", "ls", NULL); } else { while(1) { wait(&status); if(WIFEXITED(status)) break; orig_eax = ptrace(PTRACE_PEEKUSER, child, 4 * ORIG_EAX, NULL); if(orig_eax == SYS_write) { if(insyscall == 0) { /* Syscall entry */ insyscall = 1; params[0] = ptrace(PTRACE_PEEKUSER, child, 4 * EBX, NULL); params[1] = ptrace(PTRACE_PEEKUSER, child, 4 * ECX, NULL); params[2] = ptrace(PTRACE_PEEKUSER, child, 4 * EDX, NULL); printf("Write called with " "%ld, %ld, %ld\n", params[0], params[1], params[2]); } else { /* Syscall exit */ eax = ptrace(PTRACE_PEEKUSER, child, 4 * EAX, NULL); printf("Write returned " "with %ld\n", eax); insyscall = 0; } } ptrace(PTRACE_SYSCALL, child, NULL, NULL); } } return 0; }
編譯執行上面的代碼,獲得的輸出和前一個例子的輸出有些相似:
ppadala@linux:~/ptrace > ls a.out dummy.s ptrace.txt libgpm.html registers.c syscallparams.c dummy ptrace.html simple.c ppadala@linux:~/ptrace > ./a.out Write called with 1, 1075154944, 48 a.out dummy.s ptrace.txt Write returned with 48 Write called with 1, 1075154944, 59 libgpm.html registers.c syscallparams.c Write returned with 59 Write called with 1, 1075154944, 30 dummy ptrace.html simple.c Write returned with 30
在這個例子中,咱們追蹤了 write() 這個系統調用,由上面的輸出咱們能夠看出,ls這個程序總共調用了3次 write().
調用 ptrace 並傳入參數:PTRACE_SYSCALL, 會使得子進程在每次進行系統調用及結束一次系統調用時都會被內核停下來,這一個過程就至關於作了一個ptrace(PTRACE_CONT) 調用,而後在每次系統調用前和系統調用後就停下來。在前面一個例子中,咱們用 PTRACE_PEEKUSER 來讀取系統調用的參數,當系統調用結束後,該調用的返回值會被放在%eax中,像上面的例子展現的那樣,這個值也是能夠被讀取的。
至於上面的例子中出現的調用:wait(&status),這是個典型的用於判斷子進程是被 ptrace 停住仍是已經運行結束了的用法,變量 status 用於標記子進程是否已經結束退出,關於這個 wait() 和 WIFEXITED 的更多細節,讀者能夠自行查看一下manual(man 2).
讀取寄存器的值
若是你想在系統調用開始前或結束後讀取多個寄存器的值,上面的代碼實現起來會比較麻煩,ptrace提供了另外一種方式來一次性讀取全部的寄存器的內容,這就是參數:PTRACE_GETREGS的做用。參看下面的例子:
#include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <linux/user.h> #include <sys/syscall.h> int main() { pid_t child; long orig_eax, eax; long params[3]; int status; int insyscall = 0; struct user_regs_struct regs; child = fork(); if(child == 0) { ptrace(PTRACE_TRACEME, 0, NULL, NULL); execl("/bin/ls", "ls", NULL); } else { while(1) { wait(&status); if(WIFEXITED(status)) break; orig_eax = ptrace(PTRACE_PEEKUSER, child, 4 * ORIG_EAX, NULL); if(orig_eax == SYS_write) { if(insyscall == 0) { /* Syscall entry */ insyscall = 1; ptrace(PTRACE_GETREGS, child, NULL, ®s); printf("Write called with " "%ld, %ld, %ld\n", regs.ebx, regs.ecx, regs.edx); } else { /* Syscall exit */ eax = ptrace(PTRACE_PEEKUSER, child, 4 * EAX, NULL); printf("Write returned " "with %ld\n", eax); insyscall = 0; } } ptrace(PTRACE_SYSCALL, child, NULL, NULL); } } return 0; }
這個例子和前面一個例子幾乎是如出一轍的,除了讀取寄存器的地方換成了PTRACE_GETREGS.在這裏咱們用到了user_regs_struct這個結構體,它被定義在<linux/user.h>中。
作點有趣的事情
好,有了前面的基礎,如今咱們能夠來嘗試作些有趣的事情了。下面咱們將把子進程調用 write 時,傳給 write() 的參數都給反轉過來,看看會獲得怎樣的結果。
#include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <linux/user.h> #include <sys/syscall.h> const int long_size = sizeof(long); void reverse(char *str) { int i, j; char temp; for(i = 0, j = strlen(str) - 2; i <= j; ++i, --j) { temp = str[i]; str[i] = str[j]; str[j] = temp; } } void getdata(pid_t child, long addr, char *str, int len) { char *laddr; int i, j; union u { long val; char chars[long_size]; }data; i = 0; j = len / long_size; laddr = str; while(i < j) { data.val = ptrace(PTRACE_PEEKDATA, child, addr + i * 4, NULL); memcpy(laddr, data.chars, long_size); ++i; laddr += long_size; } j = len % long_size; if(j != 0) { data.val = ptrace(PTRACE_PEEKDATA, child, addr + i * 4, NULL); memcpy(laddr, data.chars, j); } str[len] = '\0'; } void putdata(pid_t child, long addr, char *str, int len) { char *laddr; int i, j; union u { long val; char chars[long_size]; }data; i = 0; j = len / long_size; laddr = str; while(i < j) { memcpy(data.chars, laddr, long_size); ptrace(PTRACE_POKEDATA, child, addr + i * 4, data.val); ++i; laddr += long_size; } j = len % long_size; if(j != 0) { memcpy(data.chars, laddr, j); ptrace(PTRACE_POKEDATA, child, addr + i * 4, data.val); } } int main() { pid_t child; child = fork(); if(child == 0) { ptrace(PTRACE_TRACEME, 0, NULL, NULL); execl("/bin/ls", "ls", NULL); } else { long orig_eax; long params[3]; int status; char *str, *laddr; int toggle = 0; while(1) { wait(&status); if(WIFEXITED(status)) break; orig_eax = ptrace(PTRACE_PEEKUSER, child, 4 * ORIG_EAX, NULL); if(orig_eax == SYS_write) { if(toggle == 0) { toggle = 1; params[0] = ptrace(PTRACE_PEEKUSER, child, 4 * EBX, NULL); params[1] = ptrace(PTRACE_PEEKUSER, child, 4 * ECX, NULL); params[2] = ptrace(PTRACE_PEEKUSER, child, 4 * EDX, NULL); str = (char *)calloc((params[2]+1) * sizeof(char)); getdata(child, params[1], str, params[2]); reverse(str); putdata(child, params[1], str, params[2]); } else { toggle = 0; } } ptrace(PTRACE_SYSCALL, child, NULL, NULL); } } return 0; }
上面的代碼編譯運行後,將獲得這樣的相似下面的結果:
ppadala@linux:~/ptrace > ls a.out dummy.s ptrace.txt libgpm.html registers.c syscallparams.c dummy ptrace.html simple.c ppadala@linux:~/ptrace > ./a.out txt.ecartp s.ymmud tuo.a c.sretsiger lmth.mpgbil c.llacys_egnahc c.elpmis lmth.ecartp ymmud
有趣吧!這個例子使用到了咱們前面提到過的全部概念。在這當中,咱們經過在 ptrace 中使用 PTRACE_POKEDATA 參數來改變子進程中的數據。這個 PTRACE_POKEDATA 用起來和 PTRACE_PEEKDATA 是同樣的,不一樣之處只在於 PTRACE_POKEDATA 不只能夠讀數據,還能往子進程裏寫數據。
單步執行
ptrace 提供了一種手段使得咱們能夠像 debugger 同樣單步執行子進程的代碼,很酷?調用一下 ptrace(PTRACE_SINGLESTEP) 就能完成這樣的事情,這個調用會告訴內核,在子進程每執行完一條子令以後,就停一下。
下面的代碼演示了怎麼讀取子進程中當前正在被執行的子令,爲了讓讀者更好的理解發生了什麼事情,我本身寫了一個很簡單的dummy程序來方便你們理解。
下面是一小段彙編代碼:
.data hello: .string "hello world\n" .globl main main: movl $4, %eax movl $2, %ebx movl $hello, %ecx movl $12, %edx int $0x80 movl $1, %eax xorl %ebx, %ebx int $0x80 ret
咱們用命令把它編譯成可執行文件:
gcc -o dummy1 dummy1.s
而後咱們將單步執行這個程序:
#include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <linux/user.h> #include <sys/syscall.h> int main() { pid_t child; const int long_size = sizeof(long); child = fork(); if(child == 0) { ptrace(PTRACE_TRACEME, 0, NULL, NULL); execl("./dummy1", "dummy1", NULL); } else { int status; union u { long val; char chars[long_size]; }data; struct user_regs_struct regs; int start = 0; long ins; while(1) { wait(&status); if(WIFEXITED(status)) break; ptrace(PTRACE_GETREGS, child, NULL, ®s); if(start == 1) { ins = ptrace(PTRACE_PEEKTEXT, child, regs.eip, NULL); printf("EIP: %lx Instruction " "executed: %lx\n", regs.eip, ins); } if(regs.orig_eax == SYS_write) { start = 1; ptrace(PTRACE_SINGLESTEP, child, NULL, NULL); } else ptrace(PTRACE_SYSCALL, child, NULL, NULL); } } return 0; }
編譯運行上面的代碼,輸出的結果是:
hello world EIP: 8049478 Instruction executed: 80cddb31 EIP: 804947c Instruction executed: c3
想要看明白這裏作了什麼事情,你可能須要先查一下 Intel 的手冊,弄明白那些指令是幹什麼的。對程序執行更復雜的單步操做,如加入斷點等,咱們還須要寫一些更細緻更復雜的代碼,在下一篇文章中,咱們會展現一下怎麼對程序加入斷點。