ptrace学习笔记

Before

User mode & kernel mode

CPU指令集是CPU实现控制硬件的关键,为了防止因用户错误操作或者破坏者攻击导致数据和系统安全异常,CPU提供一种特权级别分层机制(Protection Rings)

提供4层权限级别,由Ring0到Ring3(由里及外)

  • Ring0:权限最高,可以使用全部CPU指令集
  • Ring3:权限最低,只能使用受限的CPU指令集

很明显,用户态和内核态的CPU权限不同,前者Ring3后者Ring0

System Call

在知道两种linux状态后,如果处于用户态如何才能操作系统硬件资源?

答案是通过内核态来代为完成,即系统调用

系统调用用的很频繁,比如读取文件中的open()、read()、close(),这些函数均是linux提供的系统调用,为用户态的程序提供了操作存储在磁盘上文件的入口

因此可以说,系统调用能提供给用户态程序一个操作系统资源的入口

CPU上下文切换:CPU处理过程中CPU寄存器和程序计数器pc数值的切换,1次系统调用会触发2次CPU上下文切换

  • 第一次:用户态到内核态,要保存用户态程序的现场
  • 第二次:内核态到用户态,对用户态的程序进行现场恢复

ptrace

linux最常用的程序调试工具gdb是基于ptrace系统调用实现的

下面是官方文档的翻译学习:

#include <sys/ptrace.h>
long ptrace(enum __ptrace_request op, pid_t pid, void *addr, void *data);

ptrace-process trace,为一个进程提供了观察和控制另一个进程的执行过程的能力,同时也提供检查和改变另一个进程的内存和寄存器

其中被控制的进程称为tracee,控制进程成为tracer

tracee首先需要依附(attached)到tracer上。这里的依附以及随后的操作都是以线程为单位的(ptrace里的pid就是线程单位),因此在多线程进程中,每个线程可以各自依附于不同的tracer,或者不依附因此不被调试。

一个进程可以通过fork来初始化trace,让子进程来做PTRACE_TRACEME;另外,一个进程也可以使用PTRACE_ATTACH或PTRACE_SEIZE来追踪另一个进程

当正在被trace时,tracee会在每次信号发送时停下来。tracer会在下次调用waitpid或wait时被通知,这个调用会返回一个包含tracee被暂停原因的状态值。当tracee被暂停下来时,tracer可以使用各种ptrace操作来检查和修改tracee,接着tracer会让tracee继续运行

当tracer结束tracing时,可以使用PTRACE_DETACH让tracee继续在正常模式下继续执行

Different op

这个参数决定了ptrace要做的操作,下表只列出了在我看来用的多的op,有一些太偏了

op操作说明
PTRACE_TRACEME0指明当前进程(tracee)要被父进程来追踪pid、addr、data都忽略;是tracee使用的唯一ptrace操作
PTRACE_PEEKTEXT1读取tracee 内存addr处的一个字并作为结果返回两个操作实际一样;data忽略
PTRACE_PEEKDATA2同上同上
PTRACE_PEEKUSER3读取tracee 用户区偏移addr处的一个字并作为结果返回这个字中包含了进程注册信息
PTRACE_POKETEXT4复制data(一个字)到tracee内存addr处两个操作实际一样
PTRACE_POKEDATA5同上同上
PTRACE_POKEUSER6复制data(一个字)到tracee 用户区偏移addr处
PTRACE_GETREGS12复制tracee的通用寄存器值到tracer的dataaddr忽略;Intel386特有
PTRACE_GETFPREGS14复制tracee的浮点寄存器值到tracer的dataaddr忽略;Intel386特有
PTRACE_GETREGSET0x4204读取tracee的寄存器使用NT_PRSTATUS(数值为1)作为addr的值可以读取通用寄存器。如果CPU具有浮点或向量寄存器,可以通过将addr设置为NT_foo常量来读取;data指向结构体iovec,该结构体描述了目标缓冲区的位置和大小
PTRACE_SETREGS13修改tracee的通用寄存器
PTRACE_SETFPREGS15修改tracee的浮点寄存器
PTRACE_SETREGSET0x4205修改tracee的寄存器同PTRACE_GETREGSET
PTRACE_SETOPTIONS0x4200根据data设置ptrace选项addr忽略;data是位掩码,标志如下
PTRACE_O_EXITKILL当tracer退出时发送SIGKILL信号到tracee;有助于确保tracee不能逃脱tracer的控制
PTRACE_O_TRACECLONE跟踪tracee clone的调用(在tracee下次调用clone时停下,并自动追踪新克隆的进程;新进程的PID可以使用PTRACE_GETEVENTMSG获取)
PTRACE_O_TRACEEXEC跟踪tracee execve的调用
PTRACE_O_TRACEEXIT跟踪tracee exit的调用
PTRACE_O_TRACEFORK跟踪tracee fork的调用
PTRACE_O_TRACESYSGOOD用来区分syscall-stops 和其他类型的ptrace-stops
PTRACE_O_TRACEVFORK跟踪tracee vfork的调用
PTRACE_O_TRACEVFORKDONE当vfork完成时停下tracee
PTRACE_O_TRACESECCOMP当seccomp SECCOMP_RET_TRACE触发时停下tracee
PTRACE_O_SUSPEND_SECCOMP暂停tracee的seccomp保护
PTRACE_GETEVENTMSG0x4201获取刚发生的ptrace事件消息并置于data对于PTRACE_EVENT_EXIT是tracee的退出状态;对于PTRACE_EVENT_FORK、PTRACE_EVENT_VFORK、PTRACE_EVENT_VFORK_DONE、PTRACE_EVENT_CLONE是新进程的PID,对于PTRACE_EVENT_SECCOMP是seccomp
PTRACE_CONT7重启停止的tracee会在附加的情况下让程序继续运行
PTRACE_SYSCALL24重启停止的tracee,但进行系统调用跟踪继续执行tracee直到下次进入或退出系统调用
PTRACE_SINGLESTEP9重启停止的tracee,但进行系统调用跟踪继续执行tracee直到执行一条指令后停止
PTRACE_LISTEN0x4208重启停止的tracee但阻止其执行只在PTRACE_SEIZE附加的tracee上起作用
PTRACE_KILL8向tracee发送SIGKILL来终止它已弃用
PTRACE_INTERRUPT0x4207停止tracee
PTRACE_ATTACH16附加到指定进程向tracee发送一个SIGSTOP,使用waitpid来等待tracee停止;addr、data忽略
PTRACE_SEIZE0x4206附加到指定进程和PTRACE_ATTACH不同,PTRACE_SEIZE不停止进程
PTRACE_DETACH17重启停止的tracee但首先和它分离addr忽略
PTRACE_GET_SYSCALL_INFO0x420e获取导致停止的系统调用信息ptrace_syscall_info结构体指针存入data,addr存储结构体大小

Other useful funcs

wait

#include <sys/types.h> 
#include <sys/wait.h>
pid_t wait(int *status);

进程一旦调用了wait就阻塞自己,由wait自动分析当前进程的某个子进程已经退出,如果找到了一个已经变成僵尸的进程就会销毁该进程,反之则阻塞在这里直到有一个出现为止,status宏定义同下waitpid。如果成功返回值为被收集的子进程PID,如果调用进程没有子进程调用就会失败,返回-1

waitpid

#include <sys/types.h> 
#include <sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);

当调用waitpid时,当指定等待的子进程已经停止运行或结束了,则waitpid会立即返回;否则调用waitpid的父进程则会被阻塞,暂停运行

  • pid:要等待的子进程识别码
参数值说明
pid<-1等待进程组号为pid绝对值的任何子进程
pid=-1等待任何子进程,此时waitpid等价于wait,waitpid(-1, status, 0)=wait()
pid=0等待进程组号与目前进程相同的任何子进程,也就是和调用waitpid的进程在同一个进程组的进程
pid>0等待进程号为pid的子进程
  • status:保存子进程的状态信息,父进程可以根据该status值判断子进程的状态

在sys/wait.h中定义了好几个宏来解析这个状态信息如下:

说明
WIFEXITED(status)如果子进程正常结束返回真
WEXITSTATUS(status)如果WIFEXITED(status)为真,返回子进程exit()返回的结束码
WIFSIGNALED(status)如果子进程因为一个未捕获的信号而终止返回真
WTERMSIG(status)如果WIFSIGNALED(status)为真,返回导致子进程终止的信号代码
WIFSTOPPED(status)如果子进程被暂停了返回真
WSTOPSIG(status)如果WIFSTOPPED(status)为真,返回导致子进程暂停的信号代码
  • options:控制waitpid行为
参数说明
WNOHANG如果pid指定的子进程没有结束则waitpid立即返回0,而不是阻塞在这个函数上等待;反之返回子进程的进程号
WUNTRACED如果pid指定的子进程进入暂停状态,则立马返回

这些参数可以用|连接起来使用

wait和waitpid区别

  • wait直接阻塞调用者,waitpid有选择项WNOHANG可以不阻塞调用者
  • waitpid并不等待第一个终止的子进程,它有若干个选择项,可以控制所等待的特定进程
  • wait实际上是waitpid的一个特例,wait(-1,status,0)

fork

#include <unistd.h>
pid_t fork(void);

返回值:在父进程中返回的是子进程的PID,子进程中返回的是0。如果失败父进程返回-1,没有子进程创建

fork通过系统调用创建一个与原来进程几乎完全相同的子进程,但根据初始参数或传入变量不同,两个进程可以作不同的事

父子进程在独立的内存空间中运行,在fork时二者内存空间内容一致

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

int main() {
    pid_t pid = fork();
    if (pid) {
        printf("I'm parent process, PID: %d, Child Pid: %d\n", getpid(), pid);
    } else {
        printf("I'm child process, PID: %d\n", getpid());
        exit();
    }
}
Child created successfully, PID: 350297
I'm child process, PID: 350297

知识点:

  1. 父子进程是两个独立的进程,内存空间是独立的,但变量中指向的外部数据是相同的
  2. 父子进程都会从fork的地方继续向下执行代码
  3. 子进程不会继承父进程原有的lock、timer、signal处理,即一切与process相关的设置恢复默认值
  4. 子进程终止时会回传一个SIGCHLD的signal给父进程,父进程接收后会读取子进程的退出状态,有了子进程的终止状态后系统才会将子进程从进程表中删除并释放资源(回收)。如果child终止后SIGCHLD没有被父进程接收,就会变成僵尸进程(无法kill)

使用top可以查看几个僵尸进程

![image-2025070210411050

使用ps -A -ostat,ppid,pid,cmd |grep -e '^[Zz]'可以定位僵尸进程

image-20250702104243193.png

使用kill -HUP 僵尸进程父ID来杀死僵尸进程父进程,从而杀死僵尸进程image-20250702104110507.png

  1. 父进程比子进程先终止,则child变为孤儿进程,会被系统进程init或systemd自动接收为子进程(收养,re-parenting);通常也会有刻意使得进程成为孤儿进程,使其与用户会话脱钩,转至后台运行,这一做法常用于长时间运行的程序,可以用nohup完成这样的操作

zombie process

僵尸进程的危害很明显,当大量僵尸进程留存在系统中,会消耗PCB系统资源,但系统资源有限,当达到一定数目系统会崩溃

上面给的案例实际上也有僵尸进程产生,但存在时间过短无法观测;因此在父进程中添加sleep

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

int main() {
    pid_t pid = fork();
    if (pid) {
        printf("I'm parent process, PID: %d, Child Pid: %d\n", getpid(), pid);
        sleep(120);
    } else {
        printf("I'm child process, PID: %d\n", getpid());
        exit();
    }
}

如下图即可发现僵尸进程,等待2min父进程结束后,僵尸进程成为孤儿进程被init收养,然后init调用wait来回收所有僵尸进程

image-20250702111146250.png

避免僵尸进程的方法是使用wait来接收SIGCHLD

#include<unistd.h>
#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>

int main() {
    pid_t pid = fork();
    int status;
    if (pid) {
        printf("I'm parent process, PID: %d, Child Pid: %d\n", getpid(), pid);
        wait(&status);
        printf("Child process exited with status: %d\n", WEXITSTATUS(status));
        sleep(60);
    } else {
        printf("I'm child process, PID: %d\n", getpid());
        exit(0);
    }
}
I'm parent process, PID: 351235, Child Pid: 351240
I'm child process, PID: 351240
Child process exited with status: 0

image-20250702111711703.png

还有一种使用signal来杀死僵尸进程,见下节signal

orphan process

孤儿进程与僵尸进程不同的是结束会立即被init进程收养并回收,不会像僵尸进程那样占用系统资源,损害系统

这里可以联想到把僵尸进程变为孤儿进程,借助init杀死僵尸进程

一个很妙的办法就是连续fork两次,把子进程变为孤儿进程,父进程变为init进程,通过init进程可以处理僵尸进程

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        printf("First child process, PID: %d\n", getpid());
        pid = fork();
        if (pid == 0) {
            printf("Second child process, PID: %d\n", getpid());
            sleep(3);
            printf("Second child process exiting.\n");
            exit(0);
        } else if (pid > 0) {
            printf("First child process exiting after second child.\n");
            exit(0); 
        }
    }
    printf("Parent process, PID: %d, waiting for children to exit.\n", getpid());
    waitpid(pid, NULL, 0);    // 处理回收第一个子进程
    sleep(20);
    exit(0);
    return 0;
}

可以看到在parent sleep期间,没有僵尸进程了

image-20250704084216396.png

image-20250704084201248.png

signal

#include <signal.h>
typedef typeof(void (int))  *sighandler_t;
sighandler_t signal(int signum, sighandler_t handler);

signal设置一个函数来处理信号,即带有sig参数的信号处理程序

  • signum:在信号处理程序中作为变量使用的信号码
信号
SIGABRTsignal abort,程序异常终止
SIGFPEsignal floating-point exception,算术运算错误(例如除0或溢出)
SIGILLsignal illegal instruction,非法函数映像,如非法指令
SIGINTsignal interupt,中断信号,如ctrl+c
SIGSEGVsignal segmentation violation,非法访问存储器,如访问不存在的内存单元
SIGTERMsignal terminate,发送给本程序的终止请求信号
  • handler:指向函数的指针,可以自定义也可以下面的预定义函数之一
函数说明
SIG_DFL默认的信号处理程序
SIG_IGN忽视信号

这里用signal实现了个ctrl+c检测

#include<signal.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

void sighandler(int signum) {
    printf("Received signal %d\n", signum);
    exit(1);
}

int main() {
    signal(SIGINT, sighandler);
    while (1) {
        printf("Running... Press Ctrl+C to stop.\n");
        sleep(1);
    }
    return 0;
}

image-20250702160429728.png

回到之前说僵尸进程也可以通过设置signal来,根据子进程终止时会回传一个SIGCHLD的signal给父进程原理,我们可以写个处理SIGCHLD的handler,如下

#include<signal.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>

void sighandler(int signum) {
    printf("Received SIGCHLD signal, child process terminated.\n");
    wait(0);
}

int main() {
    signal(SIGCHLD, sighandler);    // 当然也可以直接signal(SIGCHLD,SIG_IGN);忽略,即接收它但不做任何处理
    pid_t pid = fork();
    int status;
    if (pid) {
        sleep(1); // 确保子进程先执行
        printf("I'm parent process, PID: %d, Child Pid: %d\n", getpid(), pid);
        unsigned int remain = sleep(10);  // 会被打断
        printf("Parent sleep 10s, remaining: %u seconds\n", remain);
        sleep(5);  // 不会被打断
        printf("Parent sleep 5s\n");
    } else {
        printf("I'm child process, PID: %d\n", getpid());
        sleep(2);  // 模拟子进程工作
        printf("Child sleep 2s.\n");
        exit(0);
    }
}

比较奇妙的点在于父进程中的sleep(10)并没有完全执行,原因如下:

  • 当子进程sleep 2s后退出,父进程的signal收到了来自子进程的SIGCHLD信号,此时父进程还处于sleep(10)的系统调用阻塞状态,该状态被SIGCHLD信号打断,即sleep终止,转而去调用和执行sighandler回收僵尸进程
  • 可以看到remain正好8s

image-20250702162558843.png

如果多个子进程同时被创建时并终止时,上面代码中父进程能把这些僵尸进程都杀死吗?

测试代码如下

#include<signal.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>

void sighandler(int signum) {
    printf("Received SIGCHLD signal, child process terminated.\n");
    wait(0);
}

int main() {
    signal(SIGCHLD, sighandler);
    for (int i = 0; i < 5; i++) {
        pid_t pid = fork();
        int status;
        if (pid == 0) {
            printf("I'm child process %d, PID: %d\n", i, getpid());
            sleep(2);  // 模拟子进程工作
            printf("Child PID: %d existing after 2s.\n");
            exit(0);
        }
    }
    sleep(1); // 确保子进程先执行
    printf("I'm parent process, PID: %d\n", getpid());
    // 由于可能被打断,用循环几次sleep测试
    for (int i = 0; i < 5; i++) {
        unsigned int remain = sleep(5);
        printf("Parent sleep 5s, remaining: %u seconds\n", remain);
    }
    return 0;
}

测试结果如下,可以发现只有3个僵尸进程被杀死

image-20250702164730219.png

image-20250702164717848.png

为什么没能杀死全部僵尸子进程?

SIGCHLD是一种标准信号,而不是实时信号,对于标准信号,Linux内核处理方式如下:

  • 信号不排队:如果一个信号已被发送给某个进程,且该进程还没有响应该信号(即信号处于待处理/pengding状态),此时再向该进程发送同意信号,后面的信号将被丢弃

但可以确定的是收到SIGCHLD必然有子进程退出,前面知道了waitpid可以不阻塞调用者,因此可以用waitpid替换wait(使用WNOHANG告诉waitpid有尚未终止的子进程在运行时不要阻塞)

#include<signal.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>

void sighandler(int signum) {
    int status;
    pid_t pid;

    // 使用循环,只要还有僵尸进程就一直回收
    // waitpid 在有僵尸进程时返回 > 0,没有时返回 0,出错时返回 -1
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        printf("Handler reaped child PID: %d\n", pid);
    }
}

int main() {
    signal(SIGCHLD, sighandler);
    for (int i = 0; i < 5; i++) {
        pid_t pid = fork();
        int status;
        if (pid == 0) {
            printf("I'm child process %d, PID: %d\n", i, getpid());
            sleep(2);  // 模拟子进程工作
            printf("Child PID: %d existing after 2s.\n", getpid());
            exit(0);
        }
    }
    sleep(1); // 确保子进程先执行
    printf("I'm parent process, PID: %d\n", getpid());
    // 由于可能被打断,用循环几次sleep测试
    for (int i = 0; i < 5; i++) {
        unsigned int remain = sleep(5);
        printf("Parent sleep 5s, remaining: %u seconds\n", remain);
    }
    return 0;
}

输出如下:可以看到所有终止的子进程均被signal的Handler回收

image-20250702174148570.png

image-20250702174125436.png

getpid/getppid

#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
  • getpid:返回调用进程的PID
  • getppid:返回调用进程父进程的PID(如果父进程已被终止,PID就是init或者其他subreaper进程【用户态通过prctl让进程像init一样来收养孤儿程序,成为subreaper进程】)

Show me your code

有了这么多op操作,不得去实现些东西

Anti-debug with PTRACE_TRACEME

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

int main() {
    if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) == -1) {
        printf("Debugger detected! Exiting...\n");
        exit(1);
    }
    printf("No debugger detected. Running normally.\n");
    sleep(5);
    return 0;
}

很简洁的代码,下图分别展示了没被调试以及被调试时的状态

image-20250630162208470.png

image-20250630162150452.png

System call trace in child process with PTRACE_SYSCALL

在gemini给的一份案例基础上做了些修改,通过PTRACE_SYSCALL来实现子进程系统调用的追踪

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/reg.h>

// 根据系统调用号返回对应的函数名字符串
const char* get_syscall_name(long syscall_num) {
    // 这里只列举了一部分常见的系统调用作为演示
    switch (syscall_num) {
        // --- 文件操作 ---
        case 0: return "read";
        case 1: return "write";
        case 2: return "open";
        case 3: return "close";
        case 4: return "stat";
        case 5: return "fstat";
        case 6: return "lstat";
        case 8: return "lseek";
        case 257: return "openat";

        // --- 内存管理 ---
        case 9: return "mmap";
        case 10: return "mprotect";
        case 11: return "munmap";
        case 12: return "brk";

        // --- 进程控制 ---
        case 57: return "fork";
        case 59: return "execve";
        case 60: return "exit";
        case 62: return "kill";
        case 231: return "exit_group";
  
        // --- 其他 ---
        case 16: return "ioctl";
        case 17: return "pread64";
        case 21: return "access";
        case 33: return "access";
        case 79: return "getcwd";
        case 262: return "newfstatat";

        default: return "Unknown Syscall";
    }
}

int main() {
    pid_t child_pid = fork();

    if (child_pid == -1) {
        perror("fork");
        return 1;
    }

    if (child_pid == 0) {
        // --- 子进程 ---
        // 1. 请求被父进程追踪
        if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) == -1) {
            perror("ptrace PTRACE_TRACEME");
            exit(1);
        }
  
        // 2. 加载并执行新程序 (ls)
        // 子进程会在这里暂停,等待父进程接管
        execl("/bin/ls", "ls", NULL);
        // 如果 execl 成功,下面的代码不会执行
        perror("execl");
        exit(1);

    } else {
        // --- 父进程 ---
        int status;
  
        // 首次等待,接管子进程
        waitpid(child_pid, &status, 0);
        printf("Attach Finished, Child PID: %d\n", child_pid);

        // 设置 ptrace 选项,以便更好地区分不同的事件
        ptrace(PTRACE_SETOPTIONS, child_pid, 0, PTRACE_O_TRACESYSGOOD);
  
        while (WIFSTOPPED(status)) {
            // 让子进程继续执行,直到下一次事件(如系统调用)
            ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL);
      
            // 等待子进程的下一次暂停
            waitpid(child_pid, &status, 0);

            if (WIFSTOPPED(status) && WSTOPSIG(status) & 0x80) {
                // 这是一个系统调用事件
                long syscall_num = ptrace(PTRACE_PEEKUSER, child_pid, 8 * ORIG_RAX, NULL);
                printf("[syscall]: %ld %s\n", syscall_num, get_syscall_name(syscall_num));
            }
        }
  
        printf("Child Process Exit\n");
    }

    return 0;
}

Tracer control tracee with PTRACE_ATTACH and PTRACE_CONT

  • 父进程利用PTRACE_ATTACH附加到子进程,waitpid检查子进程暂停状态
  • 父进程利用PTRACE_CONT让子进程继续运行,waitpid检查子进程退出状态
#include <stdio.h>
#include <stdlib.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
    pid_t pid;
    int status;

    pid = fork();
    if (pid == 0) {
        int cnt = 0;
        while (cnt < 5) {
            printf("%d\n", cnt++);
            sleep(2);
        }
        exit(0);
    } else {
        sleep(1);
        printf("Parent attaching to child...\n");
        if(ptrace(PTRACE_ATTACH, pid, NULL, NULL) == -1) {
            perror("ptrace ATTACH");
            return 1;
        }

        waitpid(pid, &status, 0);
        if (WIFSTOPPED(status)) {
            printf("Child process has stopped, PID: %d\n", pid);
        } else {
            printf("Child process did not stop as expected.\n");
            return 1;
        }

        printf("I'll restart subprocess after 5 seconds\n");
        unsigned int remain = sleep(5);
        printf("Parent sleep 5s, remaining: %u seconds\n", remain);

        ptrace(PTRACE_CONT, pid, NULL, NULL);   // 让子进程继续执行

        waitpid(pid, &status, 0);   // 等待子进程最终结束
        if (WIFEXITED(status)) {
            printf("Child process exited with status: %d\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("Child process was killed by signal: %d\n", WTERMSIG(status));
        } else {
            printf("Child process did not exit normally.\n");
        }
    }
}

image-20250704094702685.png

Get process status with PTRACE_GETREGS

同样用了上面的PTRACE_SYSCALL来使子进程断在每次系统调用上,然后PTRACE_GETREGS获取当前状态下寄存器值,并检查是否常见系统调用如读写、打开文件、执行等操作,是则打印对应信息

#include <stdio.h>
#include <stdlib.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <sys/syscall.h>
#include <fcntl.h>

#define STANDARD_SYSCALL(x)  \
    (\
        x == __NR_read  || \
        x == __NR_write || \
        x == __NR_open  || \
        x == __NR_execve|| \
        x == __NR_fork  || \
        x == __NR_openat   \
    )

int main() {
    pid_t pid;
    struct user_regs_struct regs;
    int status;

    pid = fork();
    if(pid == 0) {
        int cnt=0;
        int k;
        char buffer[0x20];
        while(cnt < 2){
            int fd=open("/etc/passwd", O_RDONLY);
            read(fd, buffer, 0x20);
            printf("buffer: %s\n", buffer);
            sleep(2);
            cnt++;
        }
        exit(0);
    }
    else{
        ptrace(PTRACE_ATTACH,pid,NULL,NULL);
        for(;;){
            long rax, rdi, rsi, rdx, rcx, rip;
            ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
            wait(&status);
            if(WIFEXITED(status)){
                printf("process exit\n");
                break;
            }
            ptrace(PTRACE_GETREGS, pid, NULL, &regs);
            rax = regs.orig_rax;
            rcx=regs.rcx;
            rdx=regs.rdx;
            rdi=regs.rdi;
            rsi=regs.rsi;
            rip=regs.rip;
            if(!STANDARD_SYSCALL(rax))
                continue;
            printf("syscall %ld %p %p %p rip=%p\n", rax, rcx, rdx, rdi, rsi, rip);
        }
    }
}

可以发现rax值正是系统调用号

image-20250704101637712.png

此外,我们还会发现每个系统调用都执行了两次,可以看到打印buffer处前后都有个write调用,这是因为PTRACE_SYSCALL会在每一次系统调用的入口和出口各暂停一次子进程,可以对入口出口进行判断

Read process memory with PTRACE_PEEKDATA

下面这个代码可以获取子进程内存里存储的读取文件内容

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <errno.h>

#define STANDARD_SYSCALL(x)  \
    ( \
        x == __NR_read  || \
        x == __NR_write || \
        x == __NR_openat  || \
        x == __NR_execve \
    )

// 从子进程读取一个以 null 结尾的字符串
void read_string(pid_t pid, unsigned long long addr, char *buffer, size_t size) {
    memset(buffer, 0, size);
    for (size_t i = 0; i < size; i += sizeof(long)) {
        long word = ptrace(PTRACE_PEEKDATA, pid, addr + i, NULL);
        if (word == -1 && errno != 0) {
            perror("ptrace PEEKDATA read_string");
            break;
        }
        memcpy(buffer + i, &word, sizeof(long));
        // 正确的检查方式:在读取到的8字节中查找'\0'
        if (memchr(&word, '\0', sizeof(long)) != NULL) {
            break;
        }
    }
}

// 从子进程读取指定长度的字节
void read_bytes(pid_t pid, unsigned long long addr, char *buffer, size_t nbytes) {
    memset(buffer, 0, nbytes + 1); // 清空缓冲区
    for (size_t i = 0; i < nbytes; i += sizeof(long)) {
        long word = ptrace(PTRACE_PEEKDATA, pid, addr + i, NULL);
        if (word == -1 && errno != 0) {
            perror("ptrace PEEKDATA read_bytes");
            break;
        }
        // 注意处理边界,不要多复制
        size_t bytes_to_copy = (nbytes - i < sizeof(long)) ? (nbytes - i) : sizeof(long);
        memcpy(buffer + i, &word, bytes_to_copy);
    }
}

void print_buffer(const char* buffer, size_t len) {
    for (size_t i = 0; i < len; i++) {
        if (buffer[i] >= 0x20 && buffer[i] < 0x7F) {
            putchar(buffer[i]);
        } else {
            putchar('.');
        }
    }
    printf("\n");
}

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        char buffer[0x21] = {0};
        int fd = open("flag", O_RDONLY);
        if (fd != -1) {
            read(fd, buffer, 0x20);
            printf("%s\n", buffer);
            close(fd);
        }

        execl("/bin/cat", "cat", "flag", NULL);
        exit(0);

    } else {
        int status;
        ptrace(PTRACE_ATTACH, pid, NULL, NULL);
        int in_syscall = 0;
        struct user_regs_struct regs;
  
        // 等待子进程的 TRACEME
        wait(&status);
        if (WIFEXITED(status)) return 0;
  
        printf("Parent attached. Tracing...\n");

        for (;;) {
            ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
            wait(&status);

            if (WIFEXITED(status)) {
                printf("Process exited.\n");
                break;
            }

            ptrace(PTRACE_GETREGS, pid, NULL, &regs);
      
            if (in_syscall == 0) { // 系统调用入口
                long syscall_num = regs.orig_rax;
                if (STANDARD_SYSCALL(syscall_num)) {
                    printf("[Syscall Entry] ID=%ld, RDI=0x%llx, RSI=0x%llx, RDX=0x%llx\n",
                           syscall_num, regs.rdi, regs.rsi, regs.rdx);
              
                    char buffer[256];
                    // 在入口处读取输入参数
                    if (syscall_num == __NR_openat) {
                        read_string(pid, regs.rsi, buffer, sizeof(buffer));
                        printf("  -> openat(dfd=%d, path=\"%s\", flags=0x%llx)\n", (int)regs.rdi, buffer, regs.rdx);
                    } else if (syscall_num == __NR_write) {
                        read_bytes(pid, regs.rsi, buffer, regs.rdx);
                        printf("  -> write(fd=%d, count=%lld):\n", (int)regs.rdi, regs.rdx);
                        print_buffer(buffer, regs.rdx);
                    } else if (syscall_num == __NR_execve) {
                        read_string(pid, regs.rdi, buffer, sizeof(buffer));
                        printf("  -> execve(path=\"%s\", ...)\n", buffer);
                    }
                }
                in_syscall = 1;
            } else { // 系统调用出口
                long syscall_num = regs.orig_rax;
                if (syscall_num == __NR_read) {
                    // 在出口处读取 read 的结果
                    if ((long long)regs.rax > 0) { // regs.rax 是返回值
                        long long bytes_read = regs.rax;
                        char buffer[256];
                        read_bytes(pid, regs.rsi, buffer, bytes_read);
                        printf("[Syscall Exit] read(fd=%d, count=%lld) -> %lld bytes read:\n", (int)regs.rdi, regs.rdx, bytes_read);
                        print_buffer(buffer, bytes_read);
                    }
                }
                in_syscall = 0;
            }
        }
    }
    return 0;
}

可以看到,不管是open+read+printf和cat中间各种系统调用以及传参值,同时获取了内存中的文件数据

image-20250704112602088.png

Write process memory with PTRACE_POKEDATA

在上一个读取代码上简化了些,实现了打开文件后修改内存flag值,printf出的是rot13后的结果

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <errno.h>

#define STANDARD_SYSCALL(x)  \
    ( \
        x == __NR_read  || \
        x == __NR_write || \
        x == __NR_openat  || \
        x == __NR_execve \
    )

// 从子进程读取一个以 null 结尾的字符串
void read_string(pid_t pid, unsigned long long addr, char *buffer, size_t size) {
    memset(buffer, 0, size);
    for (size_t i = 0; i < size; i += sizeof(long)) {
        long word = ptrace(PTRACE_PEEKDATA, pid, addr + i, NULL);
        if (word == -1 && errno != 0) {
            perror("ptrace PEEKDATA read_string");
            break;
        }
        memcpy(buffer + i, &word, sizeof(long));
        // 正确的检查方式:在读取到的8字节中查找'\0'
        if (memchr(&word, '\0', sizeof(long)) != NULL) {
            break;
        }
    }
}

// 从子进程读取指定长度的字节
void read_bytes(pid_t pid, unsigned long long addr, char *buffer, size_t nbytes) {
    memset(buffer, 0, nbytes + 1); // 清空缓冲区
    for (size_t i = 0; i < nbytes; i += sizeof(long)) {
        long word = ptrace(PTRACE_PEEKDATA, pid, addr + i, NULL);
        if (word == -1 && errno != 0) {
            perror("ptrace PEEKDATA read_bytes");
            break;
        }
        // 注意处理边界,不要多复制
        size_t bytes_to_copy = (nbytes - i < sizeof(long)) ? (nbytes - i) : sizeof(long);
        memcpy(buffer + i, &word, bytes_to_copy);
    }
}

// 从子进程读取并修改指定长度的字节
void write_bytes(pid_t pid, unsigned long long addr, char *buffer, size_t nbytes) {
    memset(buffer, 0, nbytes + 1); // 清空缓冲区
    for (size_t i = 0; i < nbytes; i += sizeof(long)) {
        long word = ptrace(PTRACE_PEEKDATA, pid, addr + i, NULL);
        if (word == -1 && errno != 0) {
            perror("ptrace PEEKDATA read_bytes");
            break;
        }
        // 注意处理边界,不要多复制
        size_t bytes_to_copy = (nbytes - i < sizeof(long)) ? (nbytes - i) : sizeof(long);
        memcpy(buffer + i, &word, bytes_to_copy);
    }
    for (size_t i = 0; i < nbytes; i++) {
        if (buffer[i] <= (char)'z' && buffer[i] >= (char)'a') {
            buffer[i] = (char)((buffer[i] - 97 + 13) % 26 + 97); // 修改字节
        }
    }
    for (size_t i = 0; i < nbytes; i += sizeof(long)) {
        long word = 0;
        memcpy(&word, buffer + i, sizeof(long));
        if (ptrace(PTRACE_POKEDATA, pid, addr + i, word) == -1) {
            perror("ptrace POKEDATA write_bytes");
            break;
        }
    }
}

int main() {
    pid_t pid = fork();
    if (pid == 0) {
        char buffer[0x21] = {0};
        int fd = open("flag", O_RDONLY);
        if (fd != -1) {
            read(fd, buffer, 0x20);
            printf("%s\n", buffer);
            close(fd);
        }
        exit(0);

    } else {
        int status;
        ptrace(PTRACE_ATTACH, pid, NULL, NULL);
        int in_syscall = 0;
        struct user_regs_struct regs;
  
        // 等待子进程的 TRACEME
        wait(&status);
        if (WIFEXITED(status)) return 0;
  
        printf("Parent attached. Tracing...\n");

        for (;;) {
            ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
            wait(&status);

            if (WIFEXITED(status)) {
                printf("Process exited.\n");
                break;
            }

            ptrace(PTRACE_GETREGS, pid, NULL, &regs);

            long syscall_num = regs.orig_rax;
            if (STANDARD_SYSCALL(syscall_num)) {
                char buffer[256];
                // 在入口处读取输入参数
                if (syscall_num == __NR_openat) {
                    read_string(pid, regs.rsi, buffer, sizeof(buffer));
                    printf("  -> openat(dfd=%d, path=\"%s\", flags=0x%llx)\n", (int)regs.rdi, buffer, regs.rdx);
                } else if (syscall_num == __NR_write) {
                    write_bytes(pid, regs.rsi, buffer, regs.rdx);
                    printf("  -> write(fd=%d, count=%lld):\n", (int)regs.rdi, regs.rdx);
                } else if (syscall_num == __NR_read) {
                    read_bytes(pid, regs.rsi, buffer, regs.rdx);
                    printf("read: %s\n", buffer);
                    printf("  -> read(fd=%d, count=%lld):\n", (int)regs.rdi, regs.rdx);
                }
            }
        }
    }
    return 0;
}

image-20250704210941957.png

小结

这两周断断续续抽空学,总算基本学会了ptrace,顺带学了很多unix核心系统函数。后续结合逆向再加些好玩的代码,比如hook啥的

参考

  1. https://zhuanlan.zhihu.com/p/653385264
  2. https://man7.org/linux/man-pages/man2/ptrace.2.html
  3. https://blog.csdn.net/Roland_Sun/article/details/32084825
  4. https://burweisnote.blogspot.com/2017/09/fork.html
  5. https://www.cnblogs.com/Anker/p/3271773.html
  6. https://xia0ji233.pro/2023/12/03/Ptrace/index.html
最后修改:2025 年 07 月 04 日
如果觉得我的文章对你有用,请随意赞赏