原文鏈接
在第一部分中我們已經看到ptrace怎么獲取子進程的系統調用以及改變系統調用的參數。在這篇文章中,我們將要研究如何在子進程中設置斷點和往運行中的程序里插入代碼。實際上調試器就是用這種方法來設置斷點和執行調試句柄。與前面一樣,這里的所有代碼都是針對i386平台的。
附着在進程上
在第一部分鍾,我們使用ptrace(PTRACE_TRACEME, …)來跟蹤一個子進程,如果你只是想要看進程是怎么進行系統調用和跟蹤程序的,這個做法是不錯的。但如果你要對運行中的進程進行調試,則需要使用 ptrace( PTRACE_ATTACH, ….)
當 ptrace( PTRACE_ATTACH, …)在被調用的時候傳入了子進程的pid時, 它大體是與ptrace( PTRACE_TRACEME, …)的行為相同的,它會向子進程發送SIGSTOP信號,於是我們可以察看和修改子進程,然后使用 ptrace( PTRACE_DETACH, …)來使子進程繼續運行下去。
下面是調試程序的一個簡單例子
int main() { int i; for(i = 0;i < 10; ++i) { printf("My counter: %d ", i); sleep(2); } return 0; } |
將上面的代碼保存為dummy2.c。按下面的方法編譯運行:
gcc -o dummy2 dummy2.c
./dummy2 &
現在我們可以用下面的代碼來附着到dummy2上。
#include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <linux/user.h> /**//* For user_regs_struct etc. */ int main(int argc, char *argv[]) { pid_t traced_process; struct user_regs_struct regs; long ins; if(argc != 2) ...{ printf("Usage: %s <pid to be traced> ", argv[0], argv[1]); exit(1); } traced_process = atoi(argv[1]); ptrace(PTRACE_ATTACH, traced_process, NULL, NULL); wait(NULL); ptrace(PTRACE_GETREGS, traced_process, NULL, ®s); ins = ptrace(PTRACE_PEEKTEXT, traced_process, regs.eip, NULL); printf("EIP: %lx Instruction executed: %lx ", regs.eip, ins); ptrace(PTRACE_DETACH, traced_process, NULL, NULL); return 0; } |
上面的程序僅僅是附着在子進程上,等待它結束,並測量它的eip( 指令指針)然后釋放子進程。
設置斷點
調試器是怎么設置斷點的呢?通常是將當前將要執行的指令替換成trap指令,於是被調試的程序就會在這里停滯,這時調試器就可以察看被調試程序的信息了。被調試程序恢復運行以后調試器會把原指令再放回來。這里是一個例子:
#include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <linux/user.h>
const int long_size = sizeof(long);
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] = ''; }
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(int argc, char *argv[]) { pid_t traced_process; struct user_regs_struct regs, newregs; long ins; /**//* int 0x80, int3 */ char code[] = ...{0xcd,0x80,0xcc,0}; char backup[4]; if(argc != 2) ...{ printf("Usage: %s <pid to be traced> ", argv[0], argv[1]); exit(1); } traced_process = atoi(argv[1]); ptrace(PTRACE_ATTACH, traced_process, NULL, NULL); wait(NULL); ptrace(PTRACE_GETREGS, traced_process, NULL, ®s); /**//* Copy instructions into a backup variable */ getdata(traced_process, regs.eip, backup, 3); /**//* Put the breakpoint */ putdata(traced_process, regs.eip, code, 3); /**//* Let the process continue and execute the int 3 instruction */ ptrace(PTRACE_CONT, traced_process, NULL, NULL); wait(NULL); printf("The process stopped, putting back " "the original instructions "); printf("Press <enter> to continue "); getchar(); putdata(traced_process, regs.eip, backup, 3); /**//* Setting the eip back to the original instruction to let the process continue */ ptrace(PTRACE_SETREGS, traced_process, NULL, ®s); ptrace(PTRACE_DETACH, traced_process, NULL, NULL); return 0;
} |
上面的程序將把三個byte的內容進行替換以執行trap指令,等被調試進程停滯以后,我們把原指令再替換回來並把eip修改為原來的值。下面的圖中演示了指令的執行過程
 |
 |
1. 進程停滯后 |
2. 替換入trap指令 |
 |
 |
3.斷點成功,控制權交給了調試器 |
4. 繼續運行,將原指令替換回來並將eip復原 |
在了解了斷點的機制以后,往運行中的程序里面添加指令也不再是難事了,下面的代碼會使原程序多出一個”hello world”的輸出
這時一個簡單的”hello world”程序,當然為了我們的特殊需要作了點修改:
void main() { __asm__(" jmp forward backward: popl %esi # Get the address of # hello world string movl $4, %eax # Do write system call movl $2, %ebx movl %esi, %ecx movl $12, %edx int $0x80 int3 # Breakpoint. Here the # program will stop and # give control back to # the parent forward: call backward .string "Hello World\n"" ); } |
使用 gcc –o hello hello.c來編譯它。 (自己調試時還有錯誤,雙引號中的雙引號問題不知該如何解決,還望高人指點,下面就直接用現成的機器碼了)
在backward和forward之間的跳轉是為了使程序能夠找到”hello world” 字符串的地址。
使用GDB我們可以得到上面那段程序的機器碼。啟動GDB,然后對程序進行反匯編:
(gdb) disassemble main Dump of assembler code for function main: 0x80483e0 <main>: push %ebp 0x80483e1 <main+1>: mov %esp,%ebp 0x80483e3 <main+3>: jmp 0x80483fa <forward> End of assembler dump. (gdb) disassemble forward Dump of assembler code for function forward: 0x80483fa <forward>: call 0x80483e5 <backward> 0x80483ff <forward+5>: dec %eax 0x8048400 <forward+6>: gs 0x8048401 <forward+7>: insb (%dx),%es:(%edi) 0x8048402 <forward+8>: insb (%dx),%es:(%edi) 0x8048403 <forward+9>: outsl %ds:(%esi),(%dx) 0x8048404 <forward+10>: and %dl,0x6f(%edi) 0x8048407 <forward+13>: jb 0x8048475 0x8048409 <forward+15>: or %fs:(%eax),%al 0x804840c <forward+18>: mov %ebp,%esp 0x804840e <forward+20>: pop %ebp 0x804840f <forward+21>: ret End of assembler dump. (gdb) disassemble backward Dump of assembler code for function backward: 0x80483e5 <backward>: pop %esi 0x80483e6 <backward+1>: mov $0x4,%eax 0x80483eb <backward+6>: mov $0x2,%ebx 0x80483f0 <backward+11>: mov %esi,%ecx 0x80483f2 <backward+13>: mov $0xc,%edx 0x80483f7 <backward+18>: int $0x80 0x80483f9 <backward+20>: int3 End of assembler dump. |
我們需要使用從man+3到backward+20之間的字節碼,總共41字節。使用GDB中的x命令來察看機器碼。
(gdb) x/40bx main+3 <main+3>: eb 15 5e b8 04 00 00 00 <backward+6>: bb 02 00 00 00 89 f1 ba <backward+14>: 0c 00 00 00 cd 80 cc <forward+1>: e6 ff ff ff 48 65 6c 6c <forward+9>: 6f 20 57 6f 72 6c 64 0a |
已經有了我們想要執行的指令,還等什么呢?只管把它們根前面那個例子一樣插入到被調試程序中去!
代碼:
int main(int argc, char *argv[]) { pid_t traced_process; struct user_regs_struct regs, newregs; long ins; int len = 41; char insertcode[] = "\xeb\x15\x5e\xb8\x04\x00" "\x00\x00\xbb\x02\x00\x00\x00\x89\xf1\xba" "\x0c\x00\x00\x00\xcd\x80\xcc\xe8\xe6\xff" "\xff\xff\x48\x65\x6c\x6c\x6f\x20\x57\x6f" "\x72\x6c\x64\x0a\x00"; char backup[len]; if(argc != 2) ...{ printf("Usage: %s <pid to be traced> ", argv[0], argv[1]); exit(1); } traced_process = atoi(argv[1]); ptrace(PTRACE_ATTACH, traced_process, NULL, NULL); wait(NULL); ptrace(PTRACE_GETREGS, traced_process, NULL, ®s); getdata(traced_process, regs.eip, backup, len); putdata(traced_process, regs.eip, insertcode, len); ptrace(PTRACE_SETREGS, traced_process, NULL, ®s); ptrace(PTRACE_CONT, traced_process, NULL, NULL); wait(NULL); printf("The process stopped, Putting back " "the original instructions "); putdata(traced_process, regs.eip, backup, len); ptrace(PTRACE_SETREGS, traced_process, NULL, ®s); printf("Letting it continue with " "original flow "); ptrace(PTRACE_DETACH, traced_process, NULL, NULL); return 0; } |
將代碼插入到自由空間
在前面的例子中我們將代碼直接插入到了正在執行的指令流中,然而,調試器可能會被這種行為弄糊塗,所以我們決定把指令插入到進程中的自由空間中去。通過察看/proc/pid/maps可以知道這個進程中自由空間的分布。接下來這個函數可以找到這個內存映射的起始點:
long freespaceaddr(pid_t pid) { FILE *fp; char filename[30]; char line[85]; long addr; char str[20]; sprintf(filename, "/proc/%d/maps", pid); fp = fopen(filename, "r"); if(fp == NULL) exit(1); while(fgets(line, 85, fp) != NULL) ...{ sscanf(line, "%lx-%*lx %*s %*s %s", &addr, str, str, str, str); if(strcmp(str, "00:00") == 0) break; } fclose(fp); return addr; }
在/proc/pid/maps中的每一行都對應了進程中一段內存區域。主函數的代碼如下:
int main(int argc, char *argv[]) { pid_t traced_process; struct user_regs_struct oldregs, regs; long ins; int len = 41; char insertcode[] = "\xeb\x15\x5e\xb8\x04\x00" "\x00\x00\xbb\x02\x00\x00\x00\x89\xf1\xba" "\x0c\x00\x00\x00\xcd\x80\xcc\xe8\xe6\xff" "\xff\xff\x48\x65\x6c\x6c\x6f\x20\x57\x6f" "\x72\x6c\x64\x0a\x00"; char backup[len]; long addr; if(argc != 2) ...{ printf("Usage: %s <pid to be traced> ", argv[0], argv[1]); exit(1); } traced_process = atoi(argv[1]); ptrace(PTRACE_ATTACH, traced_process, NULL, NULL); wait(NULL); ptrace(PTRACE_GETREGS, traced_process, NULL, ®s); addr = freespaceaddr(traced_process); getdata(traced_process, addr, backup, len); putdata(traced_process, addr, insertcode, len); memcpy(&oldregs, ®s, sizeof(regs)); regs.eip = addr; ptrace(PTRACE_SETREGS, traced_process, NULL, ®s); ptrace(PTRACE_CONT, traced_process, NULL, NULL); wait(NULL); printf("The process stopped, Putting back " "the original instructions "); putdata(traced_process, addr, backup, len); ptrace(PTRACE_SETREGS, traced_process, NULL, &oldregs); printf("Letting it continue with " "original flow "); ptrace(PTRACE_DETACH, traced_process, NULL, NULL); return 0; }
|
ptrace的幕后工作
那么,在使用ptrace的時候,內核里發生了聲么呢?這里有一段簡要的說明:當一個進程調用了 ptrace( PTRACE_TRACEME, …)之后,內核為該進程設置了一個標記,注明該進程將被跟蹤。內核中的相關原代碼如下:
Source: arch/i386/kernel/ptrace.c if (request == PTRACE_TRACEME) { /* are we already being traced? */ if (current->ptrace & PT_PTRACED) goto out; /* set the ptrace bit in the process flags. */ current->ptrace |= PT_PTRACED; ret = 0; goto out; } |
一次系統調用完成之后,內核察看那個標記,然后執行trace系統調用(如果這個進程正處於被跟蹤狀態的話)。其匯編的細節可以在 arh/i386/kernel/entry.S中找到。
現在讓我們來看看這個sys_trace()函數(位於 arch/i386/kernel/ptrace.c )。它停止子進程,然后發送一個信號給父進程,告訴它子進程已經停滯,這個信號會激活正處於等待狀態的父進程,讓父進程進行相關處理。父進程在完成相關操作以后就調用ptrace( PTRACE_CONT, …)或者 ptrace( PTRACE_SYSCALL, …), 這將喚醒子進程,內核此時所作的是調用一個叫wake_up_process() 的進程調度函數。其他的一些系統架構可能會通過發送SIGCHLD給子進程來達到這個目的。
小結:
ptrace函數可能會讓人們覺得很奇特,因為它居然可以檢測和修改一個運行中的程序。這種技術主要是在調試器和系統調用跟蹤程序中使用。它使程序員可以在用戶級別做更多有意思的事情。已經有過很多在用戶級別下擴展操作系統得嘗試,比如UFO,一個用戶級別的文件系統擴展,它使用ptrace來實現一些安全機制。