ptrace浅析
浅析ptrace系统调用
一、ptrace系统调用基础
1. 核心使命:观察和控制线程执行
在类 Unix 操作系统中,ptrace(process trace,进程追踪)系统调用是一项基础而强大的机制。它为单个进程(被称为”追踪者“,tracer)提供了一种能力,使其能够深入地观察并控制另一个进程(被称为”被追踪者“,tracee)的执行。
ptrace 的核心功能涵盖了:
- 检查并修改被追踪者的内存和寄存器
- 拦截其发起的系统调用
- 管理投递给它的信号
这一机制的主要且合法的应用场景包括:
- 实现用户空间的调试器(如 GDB)
- 系统调用追踪工具(如 strace)
2. 追踪者-被追踪者关系:一种基于线程的范式
在 Linux 系统中,一个关键且常常被误解的概念是:ptrace 的操作对象是线程(thread),而非进程(process)。
- 在 ptrace 的语境下,”tracee” 始终指代一个独立的线程ID(TID),绝非一个可能包含多个线程的进程ID(PID)
- 在一个多线程进程中,每个线程都可以被独立地附着到一个(可能不同的)追踪者,或者完全不被追踪
这种基于线程的粒度赋予了 ptrace 极大的灵活性,使其能够支持复杂的调试场景,例如在多线程应用中只追踪一个出现问题的特定线程,而让其他线程不受干扰地继续运行。
3. 基础交互循环
所有基于 ptrace 的工具都遵循一个基础的交互模型:
- 追踪者通过
ptrace()系统调用向内核发出指令 - 但它不通过 ptrace() 的返回值来获知被追踪者状态变化
- 而是依赖 waitpid() 来接收来自内核的通知
当追踪相关事件在被追踪者身上发生时:
- 内核立即暂停被追踪线程执行,将其置于”追踪停止”(ptrace-stop)状态
- 内核通过
waitpid()唤醒追踪者,并返回状态值 - 追踪者解析状态值,理解停止原因
在被追踪者处于停止状态的时间窗口内:
- 追踪者可以安全地使用其他 ptrace 请求检查寄存器、读写内存
- 完成操作后,必须调用重启类 ptrace 请求(如
PTRACE_CONT或PTRACE_SYSCALL)恢复执行
核心交互循环:
ptrace(重启) → waitpid(等待事件) → ptrace(检查/修改)
这种交互模型揭示了 ptrace 的根本特性:
- 它是一种同步的、事件驱动的机制
- 但其事件粒度相对较粗
- 每次交互都强制被追踪线程进入”停止-运行”循环
- 性能问题根源:频繁的上下文切换
对于一个被拦截的系统调用,至少需要四次上下文切换:
- 被追踪者进入内核
- 内核切换到追踪者
- 追踪者返回内核
- 内核恢复被追踪者
二、 建立与管理追踪关系
1. 主动追踪:使用 PTRACE_TRACEME 的 fork/execve 模型
这是追踪新进程的经典模型,常用于 strace ./program 这样的场景:
- 追踪者(父进程)调用
fork()创建子进程 - 子进程调用
ptrace(PTRACE_TRACEME, 0, NULL, NULL)向内核表明它愿意被其父进程追踪 - 子进程调用
execve()加载并执行新程序 - 内核在新程序第一条指令执行前发送
SIGTRAP信号 - 追踪者通过
waitpid()捕获此事件,获得介入机会
2. 被动追踪:附着到正在运行的进程
这种模型被gdb --pid=<xxx> 等工具广泛运用
(1) 经典方法:PTRACE_ATTACH
1 | ptrace(PTRACE_TRACEME, pid, NULL, NULL); |
- 向目标进程发送
SIGSTOP信号,强制进入停止状态 - 追踪者必须调用
waitpid()等待停止事件 - 侵入性:立即且无条件暂停目标线程执行
(2) 现代方法:PRTACE_SEIZE(Linux 3.4 起)
- 关键区别:不会发送
SIGSTOP,目标继续正常运行 - 追踪者需显式使用
PTRACE_INTERRUPT或发送信号暂停目标 - 非侵入式:适合实现基于事件的监控,无需初始中断
(3) 分析对比使用场景
| 特性 | PTRACE_ATTACH | PTRACE_SEIZE |
|---|---|---|
| 附着行为 | 立即暂停目标进程 | 静默附着,目标继续运行 |
| 使用场景 | 传统 暂停-调试 模式的调式 | 需要监控但不干扰的场景 |
| 事件处理 | 通过 SIGSTOP 识别 | 使用 PTRACE_EVENT_STOP 语义更加清晰 |
| 设计理念 | 面向调试 | 面向通用进程监管 |
设计演进:PTRACE_SEIZE 将”附着”和”停止”解耦,允许追踪者像”潜伏者”一样静默建立监控,仅在需要时介入。
3. 解除关系
1 | ptrace(PTRACE_DETACH, pid, null, signal); |
- 终止追踪关系
- 被追踪者从停止状态恢复,继续执行
- 关键技巧可通过 signal 参数注入信号
ptrace(PTRACE_DETACH, pid, NULL, SIGSTOP)可使进程保持暂停状态
- 恢复被追踪者原有的父子关系
三、 X86 架构下的 ptrace 参数详解
ptrace 系统调用原型:
1 | long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data); |
第一个参数:request(请求类型)
在 X86 架构中,ptrace 请求类型有明确的数值定义。这是最关键的部分,特别是在汇编调用时必须使用正确的数值。
X86 架构下 ptrace 请求常量与数值对应表
| 请求常量 | 十六进制值 | 调用者 | PID 参数 | ADDR 参数 | DATA 参数 | 备注 | 内核版本 |
|---|---|---|---|---|---|---|---|
| PTRACE_TRACEME | 0X0 | Tracee | 忽略 | 忽略 | 忽略 | 声明本进程可以被父进程追踪,必须在 execve前调用 |
1.0 |
| PTRACE_PEEKTEXT | 0X1 | Tracer | Tracee TID | Tracee 代码段地址 | 忽略 | 读取 staree 代码段内存,返回值为读取的数据 | 1.0 |
| PTRACE_PEEKDATA | 0X2 | Tracer | Tracee TID | Tracee 数据段地址 | 忽略 | 读取 tracee 数据段内存,返回值为读取的数据 | 1.0 |
| PTRACE_PEEKUSER | 0X3 | Tracer | Tracee TID | USER 区偏移 | 忽略 | 读取 tracee USER 区域(寄存器等) | 1.0 |
| PTRACE_POKETEXT | 0X4 | Tracer | Tracee TID | Tracee 代码段地址 | 写入的数据 | 写入 tracee 代码段内存,成功返回0 | 1.0 |
| PTRACE_POKEDATA | 0X5 | Tracer | Tracee TID | Tracee 数据段地址 | 写入的数据 | 写入 tracee 数据段内存,成功返回0 | 1.0 |
| PTRACE_POKEUSER | 0X6 | Tracer | Tracee TID | USER 区偏移 | 写入的数据 | 写入 tracee USER 区域(寄存器等) | 1.0 |
| PTRACE_CONT | 0X7 | Tracer | Tracee TID | 忽略 | 注入信号 | 恢复 tracee 执行,0表述不发送信号 | 1.0 |
| PTRACE_KILL | 0X8 | Tracer | Tracee TID | 忽略 | 忽略 | 终止 tracee ,发送SIGKILL | 1.0 |
| PTRACE_SINGLESTEP | 0X9 | Tracer | Tracee TID | 忽略 | 注入信号 | 单步执行 tracee ,执行一条指令后暂停 | 2.2 |
| PTRACE_GETREGS | 0XC | Tracer | Tracee TID | 忽略 | struct user_regs_struct* | 获取通用寄存器,tracee 必须停止 | 2.2 |
| PTRACE_SETREGS | 0XD | Tracer | Tracee TID | 忽略 | struct user_regs_strust* | 设置通用寄存器,reacee 必须停止 | 2.2 |
| PTRACE_GETFPREGS | 0XE | Tracer | Tracee TID | 忽略 | struct user_fpregs_struct* | 获取浮点寄存器,tracee 必须停止 | 2.2 |
| PTRACE_SETFPREGS | 0XF | Tracer | Tracee TID | 忽略 | struct iser_fpregs_struct* | 设置浮点寄存器,tracee 必须停止 | 2.2 |
| PTRACE_ATTACH | 0X10 | Tracer | Tracee TID | 忽略 | 忽略 | 附着到正在运行的进程,会发送SIGSTOP | 1.0 |
| PTRACE_DETACH | 0X11 | Tracer | Tracee TID | 忽略 | 注入信号 | 解除追踪关系,0 表示不发送信号,SIGSTOP保持暂停 | 1.0 |
| PTRACE_GETFPXREGS | 0X12 | Tracer | Tracee TID | 忽略 | struct user_fpxregs_struct* | 获取扩展浮点寄存器 | 2.2 |
| PTRACE_SETFPXREGS | 0X13 | Tracer | Tracee TID | 忽略 | struct user_fpxregs_struct* | 设置扩展浮点寄存器 | 2.2 |
| PTRACE_SYSCALL | 0X18 | Tracer | Tracee TID | 忽略 | 注入信号 | 继续执行,系统调用入口/出口暂停 | 1.0 |
| PTRACE_SETOPTIONS | 0X4200 | Tracer | Tracee TID | 忽略 | PTRACE_O_*位掩码 | 设置追踪选项 | 2.4.6 |
| PTRACE_GETEVENTMSG | 0X4201 | Tracer | Tracee TID | 忽略 | unsigned long* | 获取事件消息(如新进程 PID) | 2.5.46 |
| PTRACE_GETINFO | 0X4202 | Tracer | Tracee TID | 忽略 | siginfo_t* | 获取信号详细信息 | 2.3.99 |
| PTRACE_SETINFO | 0X4203 | Tracer | Tracee TID | 忽略 | siginfo_t* | 设置信号详细信息 | 2.3.99 |
| PTRACE_GETREGSET | 0X4204 | Tracer | Tracee TID | NT_*类型 | iovec* | 获取寄存器集合(更通用) | 2.6.34 |
| PTRACE_SETREGSET | 0X4205 | Tracer | Tracee TID | NT_*类型 | iovec* | 设置寄存器集合(更通用) | 2.6.34 |
| PTRACE_SIEIZE | 0X4206 | Tracer | Tracee TID | 忽略 | PTRACE_O_*位掩码 | 非侵入式附着,不会暂停进程 | 3.4 |
| PTRACE_INTERRUPT | 0X4207 | Tracer | Tracee TID | 忽略 | 忽略 | 中断正在运行的 tracee | 3.4 |
| PTRACE_LISTEN | 0X4208 | Tracer | Tracee TID | 忽略 | 忽略 | 监听 tracee 状态,不发送信号 | 3.4 |
第二个参数:PID(进程/线程ID)
类型:pid_t 32位系统位4字节,64位系统位8字节
关键特性:
- 在 ptrace 语境中,pid 始终表示线程 ID(TID),而非进程ID
- 如果传入的是某进程的ID,那么 ptrace 的实际操作进程的主线程
- 无法直接操作整个进程,只能操作单个线程
第三个参数:ADDR(地址参数)
类型: void*(在系统调用中解释为long)
用途:
- 内存相关操作中指定 teacee地址空间的目标地址
- 寄存器相关操作中通常被忽略或表示寄存器偏移
- 信号操作中通常被忽略
X86 特定:在 PEEK/POKE 请求中,addr 是 tracee 的虚拟内存地址
第四个参数:DATA(数据参数)
类型:void*(在系统调用中解释为long)
用途:
- 输入:向内核提供数据(写入内存的内容)
- 输出:向内核接收数据(读读取内存结果)
- 混合:某些请求中既是输入又是输出
X86 特定:在 GETREGS/SETREGS 请求中,data 是指向
user_regs_struct的指针
相关数据结构定义:
寄存器结构体
1. user_regs_struct(通用寄存器)
用途:通过 PTRACE_GETREGS/PTRACE_SETREGS 获取/设置通用寄存器
X86-64 架构定义
1 | struct user_regs_struct |
关键字段说明:
orig_rax:系统调用号(在系统调用入口处)rip:当前指令指针(程序计数器)rsp:栈指针rax:函数返回值/系统调用号
X86 架构定义
1 | struct user_regs_struct |
2.user_fpregs_struct(浮点寄存器)
用途:通过 PTRACE_GETFPREGS/PTRACE_SETFPREGS 获取/设置传统浮点寄存器
X86-64 架构定义
1 | struct user_fpregs_struct |
X86 架构定义
1 | struct user_fpregs_struct |
关键字段说明:
cwd:控制字寄存器swd:状态字寄存器twd:标记字寄存器fip:指令指针foo:操作数指针st_space:实际的浮点寄存器栈
3.user_fpxregs_struct(扩展浮点寄存器)
用途:通过 PTRACE_GETFPXREGS/PTRACE_SETFPXREGS 获取/设置扩展浮点寄存器(SSE等)
X86-64 架构定义
1 | struct user_fpxregs_struct |
X86 架构定义
1 | struct user_fpxregs_struct |
关键字段说明:
mxcsr:SSE 控制/状态寄存器xmm_space:XMM 寄存器(SSE指令使用)
信号信息结构体
siginfo_t
用途:通过 PTRACE_GETSIGINFO/PTRACE_SETSIGINFO 获取/设置信号详细信息
1 | typedef struct siginfo |
常用字段:
si_signo:信号编号(如 SIGSEGV=11)si_code:信号来源代码(如 SI_USER, SI_KERNEL, SI_QUEUE)si_pid:发送信号的进程IDsi_uid:发送信号进程的真实UIDsi_addr:导致错误的内存地址(对SIGSEGV等)
I/O向量结构
iovec
用途:通过 PTRACE_GETREGSET/PTRACE_SETREGSET 指定数据区域
1 | struct iovec |
使用方式
1 | struct iovec iov; |
NT* 寄存器集常量
用途:通过 PTRACE_GETREGSET/PTRACE_SETREGSET 指定寄存器集类型
| 常量 | 值 | 用途 |
|---|---|---|
NT_PRSTATUS |
1 | 进程状态信息 |
NT_PRFPREG |
2 | 浮点寄存器 |
NT_PRPSINFO |
3 | 进程信息 |
NT_AUXV |
6 | 辅助向量 |
NT_X86_XSTATE |
0x202 | x86 扩展状态(包括 AVX 寄存器) |
NT_ARM_VFP |
0x400 | ARM VFP/NEON 寄存器 |
NT_ARM_TLS |
0x401 | ARM TLS 寄存器 |
NT_S390_HIGH_GPRS |
0x300 | s390 上半部分寄存器 |
使用示例
1 | // 获取 x86 扩展状态(包括 AVX 寄存器) |
信号值对应表
| 信号值 | 信号名称 | 说明 | 与 TRACE 的关系 |
|---|---|---|---|
| 1 | SIGHUP | 挂起信号(终端断开连接) | 通常会被追踪者转发给 tracee |
| 2 | SIGINT | 中断信号(Ctrl+C) | 通常会被追踪者捕获并处理 |
| 3 | SIGQUIT | 退出信号(Ctrl+\) | 通常会被追踪者转发 |
| 4 | SIGILL | 非法指令 | 可能表示 tracee 执行了无效代码 |
| 5 | SIGTRAP | 跟踪/断点陷阱 | ptrace 的核心信号,用于通知追踪事件 |
| 6 | SIGABRT | 程序异常终止 | 可能由 tracee 主动调用 abort() 产生 |
| 7 | SIGBUS | 总线错误 | 通常与内存访问错误相关 |
| 8 | SIGFPE | 浮点异常 | 算术运算错误 |
| 9 | SIGKILL | 强制终止 | 无法被捕获或忽略,追踪者可使用 PTRACE_KILL 发送 |
| 10 | SIGUSR1 | 用户定义信号1 | 可用于自定义通信 |
| 11 | SIGSEGV | 段错误 | 内存访问违规 |
| 12 | SIGUSR2 | 用户定义信号2 | 可用于自定义通信 |
| 13 | SIGPIPE | 管道错误 | 向已关闭的管道写入数据 |
| 14 | SIGALRM | 定时器信号 | alarm() 定时器到期 |
| 15 | SIGTERM | 终止请求 | 请求进程正常退出 |
| 16 | SIGSTKFLT | 栈错误 | x86 架构特定的信号 |
| 17 | SIGCHLD | 子进程状态变化 | 追踪子进程时关键信号 |
| 18 | SIGCONT | 继续执行 | 用于恢复被停止的进程 |
| 19 | SIGSTOP | 停止执行 | 无法被捕获或忽略,用于暂停进程 |
| 20 | SIGTSTP | 终端停止信号(Ctrl+Z) | 通常会被追踪者捕获 |
| 21 | SIGTTIN | 后台进程尝试从终端读取 | 与终端 I/O 相关 |
| 22 | SIGTTOU | 后台进程尝试向终端写入 | 与终端 I/O 相关 |
示例题目分析:
NCTF2022 ezshellcode
先查看题目基本信息


程序非常简单,首先输出了当前进程号,然后mmap了一段可读可写可执行内存并读入shellcode,接着开启沙箱,并关闭了stdin、stdout、stderr,然后执行shellcode

沙箱禁用了socket、connect、bind、listen这四个系统调用,阻止用户通过网络通信手段发送flag
首先输出了进程号,而且题目提供的docker start.h如下
1 |
|
可以看到其中有 echo 0 > /proc/sys/kernel/yama/ptrace_scope
ptrace_scope是 Linux 内核 Yama 安全模块中的一个关键参数,专门用于控制系统中ptrace系统调用的安全策略。
ptrace_scope的取值与含义
值 安全级别 允许的操作 0 最宽松 任何进程都可以被相同 UID 的进程跟踪(传统Linux行为) 1 默认限制 仅允许父进程跟踪子进程,或进程需有 CAP_SYS_PTRACE能力2 严格限制 仅允许有 CAP_SYS_PTRACE能力的进程使用ptrace(禁用普通用户调试)3 最严格 完全禁用 ptrace (除非内核特殊配置)
正好本题泄露了PID,所以我们可以在一个进程中调用 ptrace 然后 attach 到另一个卡在 read 系统调用的进程上,然后被在 ptraee 读完 shellcode 之后,直接通过 prtace 来设置寄存器,直接跳转到 shellcode ,从而绕过关闭流。
exp如下
1 | from pwn import * |
下面来简单解释一下exp
首先 attach 上对应的进程(tracee) shellcode = shellcraft.ptrace(0x10, pid, 0, 0) # PTRACE_ATTACH,然后让 tracee 在 read 系统调用出口处暂停 shellcode += shellcraft.ptrace(0x18, pid, 0, 0) # PTRACE_SYSCALL,然后通过获取寄存器来检测 ptraee 是否成功读入 shellcode ,若成功读入则设置子进程的 rip 为 shellcode 的地址,未成功读入则回到 PTRACE_SYSCALL,最后将寄存器发送给 tracee、detach tracee 就能成功 getshell。

NepCTF 2025 smallbox
查看题目基本信息


题目逻辑同样很简单,首先 mmap 了一段可读可写可执行的内存,然后fork了一个子进程,并使子进程进入无限死循环,然后读入 shellcode 并加载 沙箱,最后执行shellcode
沙箱信息如下,只允许了ptrace系统调用

但是我们注意到,子进程陷入了无限循环,并没有加载沙箱,所以这道题很明显是要我们利用父进程通过ptrace来劫持子进程的流程
exp如下
1 | from pwn import * |