说一说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流程

  1. 调用ptrace(PTRACE_ATTACH, tid, 0, 0)
  2. tracee进入STOP状态并发送SIGSTOP信号
  3. 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 内存访问方法

  1. 通过/proc/pid/mem文件

    • 映射整个进程的内存
    • 要求主线程处于STOP状态
  2. 使用系统调用(Linux 3.2+)

    • process_vm_readv
    • process_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 代码注入方式

  1. shellcode注入
  2. 共享库(so)注入

6.2 函数调用流程

  1. 保存tracee当前PC上下文(所有寄存器)
  2. 构造参数,修改PC指针指向自定义代码
  3. 在自定义代码末尾放置TRAP指令
  4. ptrace-cont继续执行
  5. tracer捕获SIGTRAP
  6. 恢复tracee的PC上下文
  7. ptrace-dettach结束

6.3 简洁的注入方法

不需要申请内存或修改代码段:

  1. 修改SP寄存器,抬高栈空间存放参数
  2. 修改PC寄存器为函数地址
  3. 设置参数寄存器(如x86_64的rdi)
  4. 将0压栈(作为伪造的返回地址)
  5. ptrace-cont继续执行

执行RET指令时RIP变为0会触发SIGSEGV,tracer捕获后恢复上下文即可。

示例(调用system("echo 123")):

  1. 修改sp抬高8字节存放"echo 123\0"
  2. 修改pc为system地址
  3. 修改rdi为栈顶地址
  4. 将0压栈
  5. ptrace-cont

7. glibc栈对齐问题

  • x86_64调用libc函数时,SP必须16字节对齐
  • 不对齐会导致movaps等SSE指令SIGSEGV
  • 常见于汇编与C混合调用场景

8. 总结

ptrace使用时需要注意的关键点:

  1. 正确处理多线程,attach所有线程
  2. 正确处理attach过程中的信号
  3. 确保线程处于正确状态再进行内存操作
  4. 多线程环境下断点的全局影响
  5. 代码注入时的上下文保存与恢复
  6. 遵守平台调用约定(如栈对齐)

这些细节处理不当会导致调试器不稳定或被调试进程崩溃。

ptrace技术细节详解 1. ptrace基础概念 ptrace是Linux系统提供的一个强大的系统调用,用于进程调试和跟踪。它允许一个进程(tracer)观察和控制另一个进程(tracee)的执行,检查和修改其寄存器和内存。 2. 多线程处理细节 2.1 线程与进程的关系 在Linux中,线程本质上是轻量级进程(LWP) 进程ID实际上是主线程的ID 每个线程都有自己独立的线程ID 2.2 多线程ptrace操作要点 ptrace操作的最小单位是线程,不是进程 必须attach目标进程的所有线程才能完全控制进程 只attach主线程会导致其他线程继续运行 2.3 获取线程列表的方法 可以通过/proc文件系统查看进程的所有线程: 每个子目录对应一个线程ID 3. ptrace_ attach的正确使用 3.1 attach流程 调用 ptrace(PTRACE_ATTACH, tid, 0, 0) tracee进入STOP状态并发送SIGSTOP信号 tracer需要等待并处理这个信号 3.2 常见错误 错误做法:只等待SIGSTOP信号,丢弃其他信号 3.3 正确做法 如果在SIGSTOP之前收到其他信号,应该将信号发送回去: 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_readv process_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过程中的信号 确保线程处于正确状态再进行内存操作 多线程环境下断点的全局影响 代码注入时的上下文保存与恢复 遵守平台调用约定(如栈对齐) 这些细节处理不当会导致调试器不稳定或被调试进程崩溃。