ptrace ialah panggilan sistem khas dalam sistem Linux Ia membenarkan satu proses menjejak dan mengawal pelaksanaan proses lain, termasuk membaca dan menulis ingatannya, daftar, isyarat, dsb. ptrace sangat berkuasa dan boleh digunakan untuk melaksanakan alat seperti penyahpepijat, pengesan, penyuntik, dll. Tetapi, adakah anda benar-benar faham bagaimana ptrace berfungsi? Adakah anda tahu cara menggunakan ptrace di bawah Linux? Adakah anda tahu kelebihan dan kekurangan ptrace? Artikel ini akan memperkenalkan anda kepada pengetahuan berkaitan ptrace di bawah Linux secara terperinci, membolehkan anda menggunakan dan memahami mekanisme penjejakan dan kawalan proses yang berkuasa ini dengan lebih baik di bawah Linux.
ptrace menyediakan keupayaan untuk satu proses untuk mengawal proses lain, termasuk mengesan dan mengubah suai kod, data dan daftar proses terkawal, dengan itu membolehkan fungsi menetapkan titik putus, kod suntikan dan mengesan panggilan sistem.
Di sini, proses menggunakan fungsi ptrace dipanggil tracer, dan proses terkawal dipanggil tracee.
Gunakan fungsi ptrace untuk memintas panggilan sistem
Sistem pengendalian menyediakan API standard ke lapisan atas untuk melaksanakan operasi yang berinteraksi dengan perkakasan asas ini dipanggil panggilan sistem. Setiap panggilan sistem mempunyai nombor panggilan yang boleh ditanya dalam unistd.h. Apabila proses mencetuskan panggilan sistem, ia akan memasukkan parameter ke dalam daftar, kemudian memasuki mod kernel melalui gangguan lembut, dan melaksanakan kod panggilan sistem melalui kernel.
Dalam Simpan dalam ebx, ecx, edx, esi
Sebagai contoh, panggilan sistem yang dilaksanakan oleh percetakan konsol ialah
write(1,"Hello",5)
diterjemahkan ke dalam kod pemasangan sebagai
mov rax, 1 mov rdi, message mov rdx, 5 syscall message: db "Hello"
Apabila melaksanakan panggilan sistem, kernel mula-mula mengesan sama ada proses adalah tracee. Jika ya, kernel akan menjeda proses dan kemudian memindahkan kawalan kepada Tracer kemudiannya boleh melihat atau mengubah suai daftar tracee.
Kod contoh adalah seperti berikut
#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
Atur cara ini mencipta subproses yang akan kami kesan (jejak) melalui fork Sebelum melaksanakan execl, subproses memberitahu kernel bahawa ia akan dikesan melalui parameter PTRACE_TRACEME fungsi ptrace.
Untuk execl, fungsi ini sebenarnya akan mencetuskan panggilan sistem execve Pada masa ini, kernel mendapati bahawa proses adalah tracee, kemudian menggantungnya, dan menghantar isyarat untuk membangunkan pengesan menunggu (benang utama dalam program ini).
Apabila panggilan sistem dicetuskan, kernel akan menyimpan kandungan daftar rax yang memegang nombor panggilan dalam orig_rax, yang boleh kita baca melalui parameter PTRACE_PEEKUSER ptrace.
ORIG_RAX ialah nombor daftar, yang disimpan dalam sys/reg.h Dalam sistem 64-bit, setiap daftar mempunyai saiz 8 bait, jadi 8*ORIG_RAX digunakan di sini untuk mendapatkan alamat daftar.
Selepas kami memperoleh nombor panggilan sistem, kami boleh membangunkan proses anak yang digantung melalui parameter PTRACE_CONT ptrace dan biarkan ia meneruskan pelaksanaan.
parameter ptrace
long ptrace(enum __ptrace_request request,pid_t pid,void addr, void *data);
Permintaan parameter mengawal kelakuan fungsi ptrace dan ditakrifkan dalam sys/ptrace.h.
Parameter pid menyatakan nombor proses jejak.
Dua parameter di atas diperlukan, dan dua parameter seterusnya ialah alamat dan data masing-masing, dan maknanya dikawal oleh permintaan parameter.
Untuk nilai dan makna parameter permintaan tertentu, sila lihat dokumen bantuan (input konsol: man ptrace)
Perhatikan nilai pulangan Manual lelaki menyatakan bahawa saiz data perkataan dikembalikan, iaitu 4 bait pada mesin 32-bit dan 8 bait pada mesin 64-bit, kedua-duanya sepadan dengan panjang panjang. . Anda boleh menemui banyak siaran yang tidak bertanggungjawab di Baidu yang mengatakan bahawa mengembalikan satu bait data adalah salah!
Baca parameter panggilan sistem
Melalui parameter PTRACE_PEEKUSER ptrace, kita boleh melihat kandungan kawasan PENGGUNA, seperti nilai daftar. Kawasan USER ialah struktur (struktur pengguna ditakrifkan dalam sys/user.h).
Inti menyimpan nilai daftar dalam struktur ini, yang memudahkan pengesan untuk melihat melalui fungsi ptrace.
Kod contoh adalah seperti berikut
#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 ***/
Dalam kod di atas, kami menyemak parameter panggilan sistem tulis (dicetuskan oleh teks percetakan arahan ls ke konsol).
Untuk mengesan panggilan sistem, kami menggunakan parameter PTRACE_SYSCALL ptrace, yang akan menyebabkan tracee berhenti seketika apabila panggilan sistem dicetuskan atau ditamatkan, dan pada masa yang sama menghantar isyarat kepada pengesan.
Dalam contoh sebelumnya, kami menggunakan parameter PTRACE_PEEKUSER untuk melihat parameter panggilan sistem Begitu juga, kami juga boleh melihat nilai pulangan panggilan sistem yang disimpan dalam daftar RAX.
Pembolehubah status dalam kod di atas digunakan untuk mengesan sama ada tracee telah selesai melaksanakan dan sama ada perlu untuk terus menunggu tracee untuk dilaksanakan.
Baca nilai semua daftar
Contoh ini menunjukkan kaedah mudah untuk mendapatkan nilai daftar
#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; }
Dalam contoh ini, semua nilai daftar diperoleh melalui parameter PTRACE_GETREGS. Struktur user_regs_struct ditakrifkan dalam sys/user.h.
Ubah suai parameter panggilan sistem
现在我们已经知道如何拦截一个系统调用并查看其参数了,接下来我们来修改它
#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 的强大和灵活。
Atas ialah kandungan terperinci ptrace di bawah Linux: mekanisme penjejakan dan kawalan proses yang berkuasa. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!