ptrace

发布于:2024-08-01 ⋅ 阅读:(166) ⋅ 点赞:(0)

1. 准备知识

进程状态

  • S:Interruptible Sleeping,即可中断睡眠;

  • D:Uninterruptible Sleeping,即不可中断睡眠;

  • R:Running or Runnable,即运行状态;

  • Z:Zombie,即僵尸状态;

  • T:Stopped or Traced,即中止状态(注意是“中止”而非“终止”)。

  • execl语句可以将当前进程替换成一个新进程。在本例中,execl(“/bin/ls”, “ls”, “-l”, “-h”, NULL);语句将原本的子进程替换成了一条ls命令。值得注意的是,如果execl系统调用的进程处于PTRACE_TRACEME状态的话,就会发送一个SIGTRAP信号给父进程,并让自身处于Traced状态。

  • SIGTRAP是一个信号,用于表示调试程序中的断点(breakpoint)。它是由程序中的断点触发或者由调试器发送给正在运行的程序的。它的含义是停止执行程序,以便进行调试操作。

#include <unistd.h>
#include <sys/ptrace.h>
#include <stdio.h>

int main(void){
    pid_t child;
    long orig_rax;
    child = fork();
    if (child == 0){
        printf("Child process id: %d\n", getpid());
        ptrace(PTRACE_TRACEME, 0, NULL, NULL);
        sleep(10);
        execl("/bin/ls", "ls", "-l", "-h", NULL);
    } else {
        sleep(30);
    }
    return 0;
}

运行结果:

[root@localhost ~]# ps -aux | grep 30926
root     30926  0.0  0.0   4216    88 pts/1    S+   15:32   0:00 ./execl_example

# after 10 seconds...
[root@localhost ~]# ps -aux | grep 30926
root     30926  0.0  0.0    404     4 pts/1    t+   15:32   0:00 ls -l -h

子进程从./execl_example变成了ls -l -h,且复用一个PID。而且新的进程进入了t+(Traced stopped)状态,在等待tracer进程对其进行控制。

#include <sys/ptrace.h>
#include <sys/user.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
    pid_t child;
    int status;
    struct user_regs_struct regs;
    int orig_rax;
    child = fork();
    if (child == 0) {
        ptrace(PTRACE_TRACEME, 0, NULL, NULL);
        execl("/bin/ls", "/bin/ls", NULL);
        exit(0);
    } else {
        wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号
        // 1. 发送 PTRACE_SYSCALL 命令给被跟踪进程 (调用系统调用前,可以获取系统调用的参数)
        ptrace(PTRACE_SYSCALL, child, NULL, NULL);
        wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号
        ptrace(PTRACE_GETREGS, child, 0, &regs); // 获取被跟踪进程寄存器的值
        orig_rax = regs.orig_rax; // 获取rax寄存器的值
        printf("orig_rax: %d\n", orig_rax); // 打印rax寄存器的值
        // 2. 发送 PTRACE_SYSCALL 命令给被跟踪进程 (调用系统调用后,可以获取系统调用的返回值)
        ptrace(PTRACE_SYSCALL, child, NULL, NULL);
        wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号
    }
    return 0;
}
  • 调用了两次 ptrace(PTRACE_SYSCALL, child, NULL, NULL),这是因为跟踪系统调用时,需要跟踪系统调用前的环境(比如获取系统调用的参数)和系统调用后的环境(比如获取系统调用的返回值),所以就需要调用两次 ptrace(PTRACE_SYSCALL, child, NULL, NULL)
  • inux系统调用是通过 CPU寄存器 来传递参数的,所以要想获取调用了哪个系统调用,必须获取进程寄存器的值。获取进程寄存器的值,可以通过 ptrace() 系统调用的 PTRACE_GETREGS 命令来实现,
  • PTRACE_GETREGS 命令需要在 data 参数传入类型为 user_regs_struct 结构的指针,user_regs_struct 结构定义如下(在文件 sys/user.h 中)
struct user_regs_struct {
    unsigned long r15,r14,r13,r12,rbp,rbx,r11,r10;
    unsigned long r9,r8,rax,rcx,rdx,rsi,rdi,orig_rax;
    unsigned long rip,cs,eflags;
    unsigned long rsp,ss;
    unsigned long fs_base, gs_base;
    unsigned long ds,es,fs,gs;
};

上面的程序只跟踪了一个系统调用,那么怎么跟踪所有的系统调用呢?很简单,只需要把跟踪的代码放到一个无限循环中即可

#include <sys/ptrace.h>
#include <sys/user.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
struct syscall {
    int  code;
    char *name;
} syscall_table[] = {
    {0, "read"},
    {1, "write"},
    {2, "open"},
    {3, "close"},
    {4, "stat"},
    {5, "fstat"},
    {6, "lstat"},
    {7, "poll"},
    {8, "lseek"},
    ...
    {-1, NULL},
}
 
char *find_syscall_symbol(int code) {
    struct syscall *sc;
    for (sc = syscall_table; sc->code >= 0; sc++) {
        if (sc->code == code) {
            return sc->name;
        }
    }
    return NULL;
}
 
int main(int argc, char *argv[])
{
    pid_t child;
    int status;
    struct user_regs_struct regs;
    int orig_rax;
 
    child = fork();
    if (child == 0) {
        ptrace(PTRACE_TRACEME, 0, NULL, NULL);
        execl("/bin/ls", "/bin/ls", NULL);
        exit(0);
    } else {
        wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号
 
        while (1) {
            // 1. 发送 PTRACE_SYSCALL 命令给被跟踪进程 (调用系统调用前,可以获取系统调用的参数)
            ptrace(PTRACE_SYSCALL, child, NULL, NULL);
 
            wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号
            if(WIFEXITED(status)) { // 如果子进程退出了, 那么终止跟踪
                break;
            }
 
            ptrace(PTRACE_GETREGS, child, 0, &regs); // 获取被跟踪进程寄存器的值
 
            orig_rax = regs.orig_rax; // 获取rax寄存器的值
 
            printf("syscall: %s()\n", find_syscall_symbol(orig_rax)); // 打印系统调用
 
            // 2. 发送 PTRACE_SYSCALL 命令给被跟踪进程 (调用系统调用后,可以获取系统调用的返回值)
            ptrace(PTRACE_SYSCALL, child, NULL, NULL);
 
            wait(&status); // 接收被子进程发送过来的 SIGCHLD 信号
            if(WIFEXITED(status)) { // 如果子进程退出了, 那么终止跟踪
                break;
            }
        }
    }
 
    return 0;
}

输出结果

[root@localhost liexusong]$ ./strace
syscall: brk()
syscall: mmap()
syscall: access()
syscall: open()
syscall: fstat()
syscall: mmap()
syscall: close()
syscall: open()
syscall: read()
syscall: fstat()
syscall: mmap()
syscall: mprotect()
syscall: mmap()
syscall: mmap()
syscall: close()
...

2 一个使用示例

#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <stdio.h>
 
int main()
{   pid_t child;
    struct user_regs_struct regs;
 
    child = fork();  // 创建一个子进程
    if(child == 0) { // 子进程
        ptrace(PTRACE_TRACEME, 0, NULL, NULL); // 表示当前进程进入被追踪状态
        execl("/bin/ls", "ls", NULL);          // 执行 `/bin/ls` 程序
    } 
    else { // 父进程
        wait(NULL); // 等待子进程发送一个 SIGCHLD 信号
        ptrace(PTRACE_GETREGS, child, NULL, &regs); // 获取子进程的各个寄存器的值
        printf("Register: rdi[%ld], rsi[%ld], rdx[%ld], rax[%ld], orig_rax[%ld]\n",
                regs.rdi, regs.rsi, regs.rdx,regs.rax, regs.orig_rax); // 打印寄存器的值
        ptrace(PTRACE_CONT, child, NULL, NULL); // 继续运行子进程
        sleep(1);
    }
    return 0;
}

输出结果

Register: rdi[0], rsi[0], rdx[0], rax[0], orig_rax[59]
ptrace  ptrace.c

第一行是由父进程输出的,主要是打印了子进程执行 /bin/ls 程序后各个寄存器的值。而第二行是由子进程输出的,主要是打印了执行 /bin/ls 程序后输出的结果

执行顺序

  • 主进程调用 fork() 系统调用创建一个子进程。
  • 子进程调用 ptrace(PTRACE_TRACEME,…) 把自己设置为被追踪状态,并且调用 execl() 执行 /bin/ls 程序。
  • 被设置为追踪(TRACE)状态的子进程执行 execl() 的程序后,会向父进程发送 SIGCHLD 信号,并且暂停自身的执行。
  • 父进程通过调用 wait() 接收子进程发送过来的信号,并且开始追踪子进程。
  • 父进程通过调用 ptrace(PTRACE_GETREGS, child, …) 来获取到子进程各个寄存器的值,并且打印寄存器的值。
  • 父进程通过调用 ptrace(PTRACE_CONT, child, …) 让子进程继续执行下去。

3 爸爸查看儿子的信息 PTRACE_PEEKUSER

#include <stdio.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/reg.h>   /* For constants ORIG_RAX etc */
int main(){
   pid_t child;
   long orig_rax;
   child=fork();
   // 0是儿子 非0是爸爸(当前进程到id)
   // 儿子直接执行,而爸爸会调用wait等待儿子执行完毕
   if(child==0){
	  printf("1.1 child:%d\n",child);
	  //trace me:允许爸爸查看我到信息(告诉内核,我是被父进程附加的)
      ptrace(PTRACE_TRACEME,0,NULL,NULL);
      printf("2. /bin/ls\n");
       //如果execl系统调用的进程处于PTRACE_TRACEME状态的话,就会发送一个SIGTRAP信号给父进程,并让自身处于Traced状态。
      //子进程运行到这里暂停,由父进程到PTRACE_CONT恢复,这个时候寄存器已经保存了子进程到信息
       //execl系统调用给父进程发送SIGTRAP信号后,父进程怎样处理这个信号?
      execl("/bin/ls","ls",NULL);
   }else{
	printf("1.0 child:%d\n",child);
	//等待子进程停下来(execl那里),参数为null,不在乎子进程是什么状态
        //这里说一下他们到生死关系,首先是执行fork分出了一个爸爸,然后儿子执行了traceme告诉内核
        //我是可以被爸爸控制到,然后运行到execl方法企图执行但还未执行,这个时候儿子停止了,
        //同时爸爸被激活.执行wait后到语句
       //wait系统调用是一个用来进行进程控制的系统调用,它可以用来阻塞父进程,当父进程接收到子进程传来信号或者子进程退出时,父进程才会继续运行。
       //所以这里的wait系统调用很显然用来接收子进程调用execl时产生的SIGTRAP信号。
       //父进程接收到SIGTRAP信号,就意味着子进程执行execl系统调用已成功。也就意味着现在子进程已经进入了Traced状态,在等待父进程对其进行控制。
        wait(NULL);
        //peek user:查看儿子到信息(查看子进程中寄存器到内容,ORIG_RAX寄存器值的保存地址)
       //PTRACE_PEEKUSER作为操作类型,这个操作类型的作用官方是这样描述的:读取tracee进程的USER字段中相关偏移量位置的值
        orig_rax = ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);
        printf("3. The child made a system call %ld\n",orig_rax);
        //cont: 让子进程恢复运行
        ptrace(PTRACE_CONT,child,NULL,NULL);
   }
}

4查看儿子所有到信息 PTRACE_GETREGS

/*
 ptrace之读取目标进程寄存器到值到user_regs_struct 
 */
#include <stdio.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/reg.h>
#include <sys/user.h> //user_regs_struct regs寄存器的头部
#include <sys/syscall.h>
#include <unistd.h>
#include <sys/types.h>
 
int main(){
	pid_t child;
	long orig_rax;
	int status;
	int iscalling=0;
	struct user_regs_struct regs;
	//fork出两个进程
	child=fork();
	if(child==0){
		//子进程 child为0
	    printf("1.0 child:%d\n",child);
	    // 告诉内核,本子进程可以被父进程查看/修改
		ptrace(PTRACE_TRACEME,0,NULL,NULL);
		//运行ls -l -h
		execl("/bin/ls","ls","-l","-h",NULL);
	}else{
		//父进程 child为子进程pid
		printf("1.1 child:%d\n",child);
		while(1){
			// 如果status不为null,status将返回子进程到是否结束到状态
			// 如果其所有子进程都还在运行,则阻塞
			// 如果一个子进程已经终止,正等待父进程获取其终止状态,则获取该子进程的终止状态然后立即返回
			// 如果没有任何子进程,则立即出错返回
			wait(&status);// 接收被子进程发送过来的 SIGCHLD 信号
			
			// WIFEXITED宏可以用来检测接收到的信号是否标志着子进程退出
            //检查子进程是暂停还准备退出
			// 如若正常结束子进程返回的状态,则为真
			if(WIFEXITED(status))
				break;
			//读取ORIG_RAX的内容,peek user:查看儿子到信息(查看子进程中寄存器的内容,ORIG_RAX:保存了系统调用号)
			orig_rax=ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);
			//printf("1.2 orig_rax:%ld\n",orig_rax);
			if(orig_rax==SYS_write){
				//GET REGS:获取child进程寄存器到结构体user_regs_struct中
				ptrace(PTRACE_GETREGS,child,NULL,&regs);
				if(!iscalling){
					iscalling=1;
					// 打印子进程寄存器的值
					printf("0. SYS_write call with %lld, addr:%lld, len:%lld\n",regs.rdi,regs.rsi,regs.rdx);
				}else{
					// 打印系统调用号的值
					printf("1. SYS_write call return %lld\n",regs.rax);
					iscalling=0;
				}
			}
			// PTRACE_SYSCALL:和PTRACE_CONT一样使暂停的子进程继续执行,唯一不同到是syscall会在child进程下一次执行系统掉调用到时候再次让child进程暂停(SINTRAP信号)。
			ptrace(PTRACE_SYSCALL,child,NULL,NULL);
			//ptrace(PTRACE_CONT,child,NULL,NULL);
		}
	}
	return 0;
}