Maison > Article > Tutoriel système > ptrace sous Linux : un puissant mécanisme de suivi et de contrôle des processus
ptrace est un appel système spécial dans les systèmes Linux. Il permet à un processus de suivre et de contrôler l'exécution d'un autre processus, y compris la lecture et l'écriture de sa mémoire, de ses registres, de ses signaux, etc. ptrace est très puissant et peut être utilisé pour implémenter des outils tels que des débogueurs, des traceurs, des injecteurs, etc. Mais comprenez-vous vraiment comment fonctionne ptrace ? Savez-vous comment utiliser ptrace sous Linux ? Connaissez-vous les avantages et les inconvénients de ptrace ? Cet article vous présentera en détail les connaissances pertinentes de ptrace sous Linux, vous permettant de mieux utiliser et comprendre ce puissant mécanisme de suivi et de contrôle des processus sous Linux.
ptrace offre la possibilité à un processus de contrôler un autre processus, notamment en détectant et en modifiant le code, les données et les registres du processus contrôlé, permettant ainsi les fonctions de définition de points d'arrêt, d'injection de code et de traçage des appels système.
Ici, le processus utilisant la fonction ptrace est appelé traceur, et le processus contrôlé est appelé tracee.
Utilisez la fonction ptrace pour intercepter les appels système
Le système d'exploitation fournit des API standard à la couche supérieure pour effectuer des opérations qui interagissent avec le matériel sous-jacent. Ces API standard sont appelées appels système. Chaque appel système a un numéro d'appel qui peut être interrogé dans unistd.h. Lorsque le processus déclenche un appel système, il place les paramètres dans le registre, puis entre en mode noyau via l'interruption logicielle et exécute le code de l'appel système via le noyau.
Dans la sauvegarde dans ebx, ecx, edx, esi
Par exemple, l'appel système exécuté par l'impression de la console est
write(1,"Hello",5)
est traduit en code assembleur par
mov rax, 1 mov rdi, message mov rdx, 5 syscall message: db "Hello"
Lors de l'exécution d'un appel système, le noyau détecte d'abord si un processus est suivi. Si tel est le cas, le noyau mettra le processus en pause, puis transférera le contrôle au traceur. Après cela, le traceur peut afficher ou modifier les registres du suivi.
L'exemple de code est le suivant
#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
Ce programme crée un sous-processus que nous tracerons (traçons) via fork. Avant d'exécuter execl, le sous-processus informe le noyau qu'il sera tracé via le paramètre PTRACE_TRACEME de la fonction ptrace.
Pour execl, cette fonction déclenchera en fait l'appel système execve. À ce moment, le noyau constate que le processus est suivi, puis le suspend et envoie un signal pour réveiller le traceur en attente (le thread principal de ce programme).
Lorsqu'un appel système est déclenché, le noyau enregistrera le contenu du registre rax contenant le numéro d'appel dans orig_rax, que nous pouvons lire via le paramètre PTRACE_PEEKUSER de ptrace.
ORIG_RAX est le numéro de registre, qui est stocké dans sys/reg.h. Dans un système 64 bits, chaque registre a une taille de 8 octets, donc 8*ORIG_RAX est utilisé ici pour obtenir l'adresse du registre.
Après avoir obtenu le numéro d'appel système, nous pouvons réveiller le processus enfant suspendu via le paramètre PTRACE_CONT de ptrace et le laisser poursuivre son exécution.
paramètres ptrace
long ptrace(enum __ptrace_request request,pid_t pid,void addr, void *data);
La requête de paramètre contrôle le comportement de la fonction ptrace et est définie dans sys/ptrace.h.
Le paramètre pid spécifie le numéro de processus de l'observé.
Les deux paramètres ci-dessus sont requis, et les deux paramètres suivants sont respectivement l'adresse et les données, et leur signification est contrôlée par la demande de paramètre.
Pour les valeurs et significations des paramètres de requête spécifiques, veuillez consulter le document d'aide (entrée console : man ptrace)
Faites attention à la valeur de retour. Le manuel de l'homme indique que la taille des données d'un mot est renvoyée, qui est de 4 octets sur une machine 32 bits et de 8 octets sur une machine 64 bits, tous deux correspondant à la longueur d'un long. . Vous pouvez trouver de nombreux messages irresponsables sur Baidu disant que renvoyer un octet de données est une erreur !
Lire les paramètres d'appel système
Grâce au paramètre PTRACE_PEEKUSER de ptrace, nous pouvons visualiser le contenu de la zone USER, comme la visualisation de la valeur du registre. La zone USER est une structure (structure utilisateur définie dans sys/user.h).
Le noyau stocke la valeur du registre dans cette structure, ce qui permet au traceur de visualiser facilement via la fonction ptrace.
L'exemple de code est le suivant
#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 ***/
Dans le code ci-dessus, nous vérifions les paramètres de l'appel système write (déclenché par la commande ls imprimant du texte sur la console).
Afin de tracer les appels système, nous utilisons le paramètre PTRACE_SYSCALL de ptrace, qui provoquera une pause de l'observé lorsqu'un appel système est déclenché ou se termine, et enverra en même temps un signal au traceur.
Dans l'exemple précédent, nous avons utilisé le paramètre PTRACE_PEEKUSER pour afficher les paramètres de l'appel système. De même, nous pouvons également afficher la valeur de retour de l'appel système stockée dans le registre RAX.
La variable d'état dans le code ci-dessus est utilisée pour détecter si l'observé a fini de s'exécuter et s'il est nécessaire de continuer à attendre que l'observé s'exécute.
Lire les valeurs de tous les registres
Cet exemple montre une méthode simple pour obtenir la valeur du registre
#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; }
Dans cet exemple, toutes les valeurs de registre sont obtenues via le paramètre PTRACE_GETREGS. La structure user_regs_struct est définie dans sys/user.h.
Modifier les paramètres d'appel système
现在我们已经知道如何拦截一个系统调用并查看其参数了,接下来我们来修改它
#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 的强大和灵活。
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!