说一说ptrace不可忽略的技术细节
字数 1608 2025-08-24 07:48:22
ptrace技术细节详解
1. ptrace基础概念
ptrace是Linux系统提供的一个强大的系统调用,用于进程调试和跟踪。它允许一个进程(tracer)观察和控制另一个进程(tracee)的执行,检查和修改其寄存器和内存。
2. 多线程处理细节
2.1 线程与进程的关系
- 在Linux中,线程本质上是轻量级进程(LWP)
- 进程ID实际上是主线程的ID
- 每个线程都有自己独立的线程ID
2.2 多线程ptrace操作要点
- ptrace操作的最小单位是线程,不是进程
- 必须attach目标进程的所有线程才能完全控制进程
- 只attach主线程会导致其他线程继续运行
2.3 获取线程列表的方法
可以通过/proc文件系统查看进程的所有线程:
ls /proc/[pid]/task/
每个子目录对应一个线程ID
3. ptrace_attach的正确使用
3.1 attach流程
- 调用
ptrace(PTRACE_ATTACH, tid, 0, 0) - tracee进入STOP状态并发送SIGSTOP信号
- tracer需要等待并处理这个信号
3.2 常见错误
错误做法:只等待SIGSTOP信号,丢弃其他信号
int attach(int tid) {
if (ptrace(PTRACE_ATTACH, tid, 0, 0) == -1) {
printf("attach pid %d fail\n", tid);
return -1;
}
int status = 0;
waitpid(tid, &status, __WALL);
int sig = WSTOPSIG(status);
while (!WIFSTOPPED(status) || sig != SIGSTOP) {
waitpid(tid, &status, __WALL);
}
return 0;
}
3.3 正确做法
如果在SIGSTOP之前收到其他信号,应该将信号发送回去:
int Debugger::attach(int tid) {
if (ptrace(PTRACE_ATTACH, tid, 0, 0) == -1) {
printf("attach pid %d fail\n", tid);
return -1;
}
int status = 0;
waitpid(tid, &status, __WALL);
int sig = WSTOPSIG(status);
while (!WIFSTOPPED(status) || sig != SIGSTOP) {
// 如果在SIGSTOP之前收到其他信号,那么在接收之后要发送回去
tkill(tid, sig);
waitpid(tid, &status, __WALL);
}
return 0;
}
4. 读写tracee内存
4.1 内存读写条件
- 线程必须处于STOP状态
- STOP状态可由以下情况触发:
- ptrace-attach
- ptrace-singlestep
- tracee触发SIGSEGV、SIGTRAP、SIGILL等信号
4.2 内存访问方法
-
通过
/proc/pid/mem文件- 映射整个进程的内存
- 要求主线程处于STOP状态
-
使用系统调用(Linux 3.2+)
process_vm_readvprocess_vm_writev- 这是一种新的IPC机制
5. 断点实现
5.1 断点原理
- 将目标地址的指令修改为TRAP指令(如x86的int3)
- 执行到该指令时产生SIGTRAP信号
- tracer捕获信号并处理
5.2 多线程注意事项
- 代码段是线程间共享的
- 通过线程A设置的断点,线程B/C也可能执行到
- 如果线程B/C未被ptrace attach,执行到TRAP指令会导致进程异常退出
- 必须跟踪新线程产生并及时attach
6. 代码注入技术
6.1 代码注入方式
- shellcode注入
- 共享库(so)注入
6.2 函数调用流程
- 保存tracee当前PC上下文(所有寄存器)
- 构造参数,修改PC指针指向自定义代码
- 在自定义代码末尾放置TRAP指令
- ptrace-cont继续执行
- tracer捕获SIGTRAP
- 恢复tracee的PC上下文
- ptrace-dettach结束
6.3 简洁的注入方法
不需要申请内存或修改代码段:
- 修改SP寄存器,抬高栈空间存放参数
- 修改PC寄存器为函数地址
- 设置参数寄存器(如x86_64的rdi)
- 将0压栈(作为伪造的返回地址)
- ptrace-cont继续执行
执行RET指令时RIP变为0会触发SIGSEGV,tracer捕获后恢复上下文即可。
示例(调用system("echo 123")):
- 修改sp抬高8字节存放"echo 123\0"
- 修改pc为system地址
- 修改rdi为栈顶地址
- 将0压栈
- ptrace-cont
7. glibc栈对齐问题
- x86_64调用libc函数时,SP必须16字节对齐
- 不对齐会导致movaps等SSE指令SIGSEGV
- 常见于汇编与C混合调用场景
8. 总结
ptrace使用时需要注意的关键点:
- 正确处理多线程,attach所有线程
- 正确处理attach过程中的信号
- 确保线程处于正确状态再进行内存操作
- 多线程环境下断点的全局影响
- 代码注入时的上下文保存与恢复
- 遵守平台调用约定(如栈对齐)
这些细节处理不当会导致调试器不稳定或被调试进程崩溃。