ptrace使用方法
基础
操作系统通过一个叫做“系统调用”的标准机制来对上层提供服务,他们提供了一系列标准的API来让上层应用程序获取底层的硬件和服务,比如文件系统。当一个进程想要进行一个系统调用的时候,它会把该系统调用所需要用到的参数放到寄存器里,然后执行软中断指令0x80. 这个软中断就像是一个门,通过它就能进入内核模式,进入内核模式后,内核将会检查系统调用的参数,然后执行该系统调用。
在 i386 平台下(本文所有代码都基于 i386), 系统调用的编号会被放在寄存器 %eax 中,而系统调用的参数会被依次放到 %ebx,%ecx,%edx,%exi 和 %edi中,比如说,对于下面的系统调用:
write(2, "Hello", 5)
编译后,它最后大概会被转化成下面这样子:
movl $4, %eax movl $2, %ebx movl $hello,%ecx movl $5, %edx int $0x80
其中 $hello 指向字符串 "Hello"。
这里是基于32位系统的寄存器名称,64位系统的寄存器名称为rax,rbx
看完上面简单的例子,现在我们来看看 ptrace 又是怎样执行的。首先,我们假设进程 A 要 ptrace 进程 B。在 ptrace 系统调用真正开始前,内核会检查一下我们将要 trace 的进程 B 是否当前已经正在被 traced 了,如果是,内核就会把该进程 B 停下来,并把控制权交给调用进程 A (任何时候,子进程只能被父进程这唯一一个进程所trace),这使得进程A有机会去检查和修改进程B的寄存器的值。
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),可以让子进程重新恢复运行。
#include<stdio.h> #include<unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <stdlib.h> #include <sys/ptrace.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> // #include <linux/user.h> #include <sys/user.h> #include <sys/reg.h> #include <sys/syscall.h> const int long_size = sizeof(long); #define LONG_SIZE 8 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 * LONG_SIZE, 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 * LONG_SIZE, NULL); memcpy(laddr, data.chars, j); } str[len] = '\0'; // printf("getdata str=%s\n", str); } 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 * LONG_SIZE, data.val); ++i; laddr += long_size; } j = len % long_size; if(j != 0) { memcpy(data.chars, laddr, j); ptrace(PTRACE_POKEDATA, child, addr + i * LONG_SIZE, data.val); } } int main(int argc, char *argv[]) { int fd = 0; char acBuf[4096] = {0}; fd = open("./model", O_WRONLY | O_TRUNC); if (-1 == fd) { printf("open error!\n"); return 0; } sprintf(acBuf, "test model"); write(fd, acBuf, strlen(acBuf)); close(fd); } int main(int argc, char *argv[]) { pid_t child; child = fork(); if (child == 0) { ptrace(PTRACE_TRACEME, 0, NULL, NULL); printf("before\n"); execl("/bin/ls", "ls", NULL); printf("after\n"); } else { long orig_eax; long params[3]; int status; char *str, *laddr; int toggle = 0; printf("1\n"); while (1) { wait(&status); printf("status=%d\n", status); if (WIFEXITED(status)) break; struct user_regs_struct regs; ptrace(PTRACE_GETREGS,child,NULL,®s); printf("Write called with %ld,%ld,%ld,%ld\n",regs.orig_rax,regs.rbx,regs.rcx,regs.rdx); orig_eax = ptrace(PTRACE_PEEKUSER, child, LONG_SIZE * ORIG_RAX, NULL); if (orig_eax == SYS_write) { printf("2\n"); if (toggle == 0) { toggle = 1; params[0] = ptrace(PTRACE_PEEKUSER, child, LONG_SIZE * RBX, NULL); params[1] = ptrace(PTRACE_PEEKUSER, child, LONG_SIZE * RSI, NULL); params[2] = ptrace(PTRACE_PEEKUSER, child, LONG_SIZE * RDX, NULL); printf("%d,%d,%d\n", params[0],params[1],params[2]); str = (char *)calloc((params[2] + 1), sizeof(char)); getdata(child, params[1], str, params[2]); printf("str=%s\n", str); reverse(str); putdata(child, params[1], str, params[2]); } else { toggle = 0; } } ptrace(PTRACE_SYSCALL, child, NULL, NULL); } } return 0; }
这里做的操作是,子进程会调用ls命令,而父进程会将子进程的结果反转后输出。
参考:https://www.cnblogs.com/catch/p/3476280.html
https://omasko.github.io/2018/04/19/ptrace%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0I/