Heim > Artikel > System-Tutorial > ptrace unter Linux: ein leistungsstarker Prozessverfolgungs- und Kontrollmechanismus
ptrace ist ein spezieller Systemaufruf in Linux-Systemen. Er ermöglicht es einem Prozess, die Ausführung eines anderen Prozesses zu verfolgen und zu steuern, einschließlich des Lesens und Schreibens seines Speichers, seiner Register, Signale usw. ptrace ist sehr leistungsstark und kann zur Implementierung von Tools wie Debuggern, Tracern, Injektoren usw. verwendet werden. Aber verstehen Sie wirklich, wie Ptrace funktioniert? Wissen Sie, wie man ptrace unter Linux verwendet? Kennen Sie die Vor- und Nachteile von Ptrace? In diesem Artikel werden Ihnen die relevanten Kenntnisse von Ptrace unter Linux im Detail vorgestellt, damit Sie diesen leistungsstarken Prozessverfolgungs- und Kontrollmechanismus unter Linux besser nutzen und verstehen können.
ptrace bietet einem Prozess die Möglichkeit, einen anderen Prozess zu steuern, einschließlich der Erkennung und Änderung des Codes, der Daten und Register des gesteuerten Prozesses, wodurch die Funktionen zum Festlegen von Haltepunkten, zum Einfügen von Code und zum Verfolgen von Systemaufrufen ermöglicht werden.
Hier wird der Prozess, der die Ptrace-Funktion verwendet, als Tracer bezeichnet, und der gesteuerte Prozess wird als Tracee bezeichnet.
Verwenden Sie die Ptrace-Funktion, um Systemaufrufe abzufangen
Das Betriebssystem stellt der oberen Ebene Standard-APIs zur Verfügung, um Vorgänge auszuführen, die mit der zugrunde liegenden Hardware interagieren. Diese Standard-APIs werden als Systemaufrufe bezeichnet. Jeder Systemaufruf verfügt über eine Aufrufnummer, die in unistd.h abgefragt werden kann. Wenn der Prozess einen Systemaufruf auslöst, legt er die Parameter im Register ab, wechselt dann über den Soft-Interrupt in den Kernel-Modus und führt den Code des Systemaufrufs über den Kernel aus.
Im Speichern in ebx, ecx, edx, esi
Zum Beispiel lautet der durch Konsolendruck ausgeführte Systemaufruf
write(1,"Hello",5)
wird als
in Assembler-Code übersetztmov rax, 1 mov rdi, message mov rdx, 5 syscall message: db "Hello"
Beim Ausführen eines Systemaufrufs erkennt der Kernel zunächst, ob ein Prozess verfolgt wird, und übergibt dann die Kontrolle an den Tracer, der dann die Register des Traceers anzeigen oder ändern kann.
Der Beispielcode lautet wie folgt
#include #include #include #include #include #include #include int main() { pid_t child; long orig_rax; child = fork(); if(child == 0) { ptrace(PTRACE_TRACEME,0,NULL,NULL); execl("/bin/ls","ls",NULL); } else { wait(NULL); orig_rax = ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL); printf("the child made a system call %ld\n",orig_rax); ptrace(PTRACE_CONT,child,NULL,NULL); } return 0; } //输出:the child made a system call 59
Dieses Programm erstellt einen Unterprozess, den wir über fork verfolgen (verfolgen) werden. Bevor execl ausgeführt wird, informiert der Unterprozess den Kernel darüber, dass er über den Parameter PTRACE_TRACEME der Funktion ptrace verfolgt wird.
Für execl löst diese Funktion tatsächlich den execve-Systemaufruf aus. Zu diesem Zeitpunkt stellt der Kernel fest, dass der Prozess verfolgt wird, hält ihn dann an und sendet ein Signal, um den wartenden Tracer (den Hauptthread in diesem Programm) aufzuwecken.
Wenn ein Systemaufruf ausgelöst wird, speichert der Kernel den Inhalt des Rax-Registers mit der Rufnummer in orig_rax, den wir über den Parameter PTRACE_PEEKUSER von ptrace lesen können.
ORIG_RAX ist die Registernummer, die in sys/reg.h gespeichert ist. In einem 64-Bit-System hat jedes Register eine Größe von 8 Bytes, daher wird hier 8*ORIG_RAX verwendet, um die Registeradresse zu erhalten.
Nachdem wir die Systemaufrufnummer erhalten haben, können wir den PTRACE_CONT-Parameter von ptrace verwenden, um den angehaltenen untergeordneten Prozess aufzuwecken und ihn mit der Ausführung fortzusetzen.
Ptrace-Parameter
long ptrace(enum __ptrace_request request,pid_t pid,void addr, void *data);
Die Parameteranforderung steuert das Verhalten der Ptrace-Funktion und ist in sys/ptrace.h definiert.
Parameter pid gibt die Prozessnummer des verfolgten Prozesses an.
Die beiden oben genannten Parameter sind erforderlich, und die nächsten beiden Parameter sind Adresse bzw. Daten. Ihre Bedeutung wird durch die Parameteranforderung gesteuert.
Informationen zu den Werten und Bedeutungen bestimmter Anforderungsparameter finden Sie im Hilfedokument (Konsoleneingabe: man ptrace)
Achten Sie auf den Rückgabewert. Im Man-Handbuch heißt es, dass die Datengröße eines Wortes zurückgegeben wird, die auf einer 32-Bit-Maschine 4 Bytes und auf einer 64-Bit-Maschine 8 Bytes beträgt, was beides der Länge eines Longs entspricht . Auf Baidu finden Sie viele unverantwortliche Beiträge, die besagen, dass die Rückgabe eines Datenbytes falsch ist!
Systemaufrufparameter lesen
Über den PTRACE_PEEKUSER-Parameter von ptrace können wir den Inhalt des USER-Bereichs anzeigen, beispielsweise den Wert des Registers. Der USER-Bereich ist eine Struktur (Benutzerstruktur definiert in sys/user.h).
Der Kernel speichert den Registerwert in dieser Struktur, die für den Tracer praktisch ist, um ihn über die Ptrace-Funktion anzuzeigen.
Der Beispielcode lautet wie folgt
#include #include #include #include #include #include #include #include #include int main() { pid_t child; long orig_rax,rax; long params[3]={0}; 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_rax = ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL); //printf("the child made a system call %ld\n",orig_rax); if(orig_rax == SYS_write) { if(insyscall == 0) { insyscall = 1; params[0] = ptrace(PTRACE_PEEKUSER,child,8*RDI,NULL); params[1] = ptrace(PTRACE_PEEKUSER,child,8*RSI,NULL); params[2] = ptrace(PTRACE_PEEKUSER,child,8*RDX,NULL); printf("write called with %ld, %ld, %ld\n",params[0],params[1],params[2]); } else { rax = ptrace(PTRACE_PEEKUSER,child,8*RAX,NULL); printf("write returned with %ld\n",rax); insyscall = 0; } } ptrace(PTRACE_SYSCALL,child,NULL,NULL); } } return 0; } /*** 输出: write called with 1, 25226320, 65 ptrace_1.c ptrace_2.c ptrace_3.C ptrace_4.C ptrace_5.c test.c write returned with 65 ***/
Im obigen Code überprüfen wir die Parameter des Systemaufrufs write (ausgelöst durch den Befehl ls, der Text auf der Konsole ausgibt).
Um Systemaufrufe zu verfolgen, verwenden wir den Parameter PTRACE_SYSCALL von ptrace, der bewirkt, dass Tracee anhält, wenn ein Systemaufruf ausgelöst wird oder endet, und gleichzeitig ein Signal an Tracer sendet.
Im vorherigen Beispiel haben wir den Parameter PTRACE_PEEKUSER verwendet, um die Systemaufrufparameter anzuzeigen. Ebenso können wir den im RAX-Register gespeicherten Systemaufrufrückgabewert anzeigen.
Die Statusvariable im obigen Code wird verwendet, um zu erkennen, ob der Tracee die Ausführung abgeschlossen hat und ob weiterhin auf die Ausführung des Tracees gewartet werden muss.
Lesen Sie die Werte aller Register
Dieses Beispiel zeigt eine einfache Methode zum Erhalten des Registerwerts
#include #include #include #include #include #include #include #include int main() { pid_t child; long orig_rax ,rax; long params[3] = {0}; int status = 0; 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_rax = ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL); if(orig_rax == SYS_write) { if(insyscall == 0) { insyscall = 1; ptrace(PTRACE_GETREGS,child,NULL,®s); printf("write called with %llu, %llu, %llu\n",regs.rdi,regs.rsi,regs.rdx); } else { ptrace(PTRACE_GETREGS,child,NULL,®s); printf("write returned with %ld\n",regs.rax); insyscall = 0; } } ptrace(PTRACE_SYSCALL,child,NULL,NULL); } } return 0; }
In diesem Beispiel werden alle Registerwerte über den Parameter PTRACE_GETREGS abgerufen. Die Struktur user_regs_struct ist in sys/user.h definiert.
Systemaufrufparameter ändern
现在我们已经知道如何拦截一个系统调用并查看其参数了,接下来我们来修改它
#include #include #include #include #include #include #include #include #include #define LONG_SIZE 8 //获取参数 char* getdata(pid_t child,unsigned long addr,unsigned long len) { char *str =(char*) malloc(len + 1); memset(str,0,len +1); union u{ long int val; char chars[LONG_SIZE]; }word; int i, j; for(i = 0,j = len/LONG_SIZE; iif(word.val == -1) perror("trace get data error"); memcpy(str+i*LONG_SIZE,word.chars,LONG_SIZE); } j = len % LONG_SIZE; if(j != 0) { word.val = ptrace(PTRACE_PEEKDATA,child,addr + i*LONG_SIZE,NULL); if(word.val == -1) perror("trace get data error"); memcpy(str+i*LONG_SIZE,word.chars,j); } return str; } //提交参数 void putdata(pid_t child,unsigned long addr,unsigned long len, char *newstr) { union u { long val; char chars[LONG_SIZE]; }word; int i,j; for(i = 0, j = len/LONG_SIZE; iif(ptrace(PTRACE_POKEDATA, child, addr+i*LONG_SIZE,word.val) == -1) perror("trace error"); } j = len % LONG_SIZE; if(j !=0 ) { memcpy(word.chars,newstr+i*LONG_SIZE,j); ptrace(PTRACE_POKEDATA, child, addr+i*LONG_SIZE,word.val); } } //修改参数 void reserve(char *str,unsigned int len) { int i,j; char tmp; for(i=0,j=len-2; imain() { pid_t child; child = fork(); if(child == 0) { ptrace(PTRACE_TRACEME,0,NULL,NULL); execl("/bin/ls","ls",NULL); } else { struct user_regs_struct regs; int status = 0; int toggle = 0; while(1) { wait(&status); if(WIFEXITED(status)) break; memset(®s,0,sizeof(struct user_regs_struct)); if(ptrace(PTRACE_GETREGS,child,NULL,®s) == -1) { perror("trace error"); } if(regs.orig_rax == SYS_write) { if(toggle == 0) { toggle = 1; //in x86_64 system call ,pass params with %rdi, %rsi, %rdx, %rcx, %r8, %r9 //no system call has over six params printf("make write call params %llu, %llu, %llu\n",regs.rdi,regs.rsi,regs.rdx); char *str = getdata(child,regs.rsi,regs.rdx); printf("old str,len %lu:\n%s",strlen(str),str); reserve(str,regs.rdx); printf("hook str,len %lu:\n%s",strlen(str),str); putdata(child,regs.rsi,regs.rdx,str); free(str); } else { toggle = 0; } } ptrace(PTRACE_SYSCALL,child,NULL,NULL); } } return 0; } /*** 输出: make write call params 1, 9493584, 66 old str,len 66: ptrace ptrace2 ptrace3 ptrace4 ptrace5 test test.s hook str,len 66: s.tset tset 5ecartp 4ecartp 3ecartp 2ecartp ecartp s.tset tset 5ecartp 4ecartp 3ecartp 2ecartp ecartp make write call params 1, 9493584, 65 old str,len 65: ptrace_1.c ptrace_2.c ptrace_3.C ptrace_4.C ptrace_5.c test.c hook str,len 65: c.tset c.5_ecartp C.4_ecartp C.3_ecartp c.2_ecartp c.1_ecartp c.tset c.5_ecartp C.4_ecartp C.3_ecartp c.2_ecartp c.1_ecartp ***/
这个例子中,综合了以上我们提到的所有知识。进一步得,我们使用了ptrace的PTRACE_POKEDATA参数来修改系统调用的参数值。
这个参数和PTRACE_PEEKDATA参数的作用相反,它可以修改tracee指定地址的数据。
单步调试
接下来介绍一个调试器中常用的操作,单步调试,它就用到了ptrace的PTRACE_SINGLESTEP参数。
#include #include #include #include #include #include #include #include #define LONG_SIZE 8 void main() { pid_t chid; chid = fork(); if(chid == 0) { ptrace(PTRACE_TRACEME,0,NULL,NULL); //这里的test是一个输出hello world的小程序 execl("./test","test",NULL); } else { int status = 0; struct user_regs_struct regs; int start = 0; long ins; while(1) { wait(&status); if(WIFEXITED(status)) break; ptrace(PTRACE_GETREGS,chid,NULL,®s); if(start == 1) { ins = ptrace(PTRACE_PEEKTEXT,chid,regs.rip,NULL); printf("EIP:%llx Instuction executed:%lx\n",regs.rip,ins); } if(regs.orig_rax == SYS_write) { start = 1; ptrace(PTRACE_SINGLESTEP,chid,NULL,NULL); }else{ ptrace(PTRACE_SYSCALL,chid,NULL,NULL); } } } }
通过rip寄存器的值来获取下一条要执行指令的地址,然后用PTRACE_PEEKDATA读取。
这样,就可以看到要执行的每条指令的机器码。
通过本文,你应该对 Linux 下的 ptrace 有了一个深入的了解,知道了它的定义、原理、用法和优点。你也应该明白了 ptrace 的适用场景和注意事项,以及如何在 Linux 下正确地使用 ptrace。我们建议你在需要跟踪和控制进程的执行时,使用 ptrace 来实现你的目标。同时,我们也提醒你在使用 ptrace 时要注意一些潜在的风险和问题,如权限、安全、兼容性等。希望本文能够帮助你更好地使用 Linux 系统,让你在 Linux 下体验 ptrace 的强大和灵活。
Das obige ist der detaillierte Inhalt vonptrace unter Linux: ein leistungsstarker Prozessverfolgungs- und Kontrollmechanismus. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!