透过一道pwn题看ptrace系统调用

ptrace

NB的系统调用,常用的strace和gdb都是用ptrace实现的

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

参数复杂,其他可以查手册(man 2 ptrace)

手册里,tracer就是调试程序,tracee就是被调试程序,英语一般用-er和-ee来表示主动和被动的关系,例如:employer就是老板,employee就是苦逼的打工人

request指定跟踪的动作

PTRACE_TRACEME:此进程将被父进程跟踪,任何信号(除了 SIGKILL)都会暂停子进程,接着阻塞于 wait() 等待的父进程被唤醒。子进程内部对 exec() 的调用将发出 SIGTRAP 信号,这可以让父进程在子进程新程序开始运行之前就完全控制它。返回信号被wait之类函数捕获到后并不是信号的值,库提供了宏去解析获得,man 2 wait查看 (see also strsignal(3))

image-20231014232059820

PTRACE_ATTACH: 附加到一个指定的进程,使其成为当前进程跟踪的子进程,而子进程的行为等同于它进行了一次 PTRACE_TRACEME 操作

PTRACE_SYSCALL: 调试进程每次进入或者退出系统调用时都会触发一次SIGTRAP信号,

PTRACE_GETREGS:获取通用寄存器值

PTRACE_SETREGS : 设置通用寄存器值

PTRACE_SINGLESTEP:每执行完一次指令之后会触发一次sigtrap,支持获取当前进程的内存/寄存器状态。gdb的单步指令通过该选项实现。

  • 硬件实现单步:处理器支持单步模式(x86通过设置EFLAGS寄存器第8位的TF标志实现)
  • 软件实现单步:每条指令后面都插入一条断点指令(x86 int3[0xcc烫烫烫])
  • 断点的实现可以在指令处插入int3,当被调试的程序运行到断点的时候,产生SIGTRAP信号,把指令位置和调试器维护的断点集合(一般为链表)比较判断SIGTRAP是否由断点产生,把原来指令恢复,设置pc寄存器,就可以继续

PTRACE_CONT:继续运行之前停止的子进程。可同时向子进程交付指定的信号(data参数传递)

pid表示要跟踪的进程pid

addr表示进程的内存地址

data 根据前面设置的requet选项而变化,存放读取出的或者要写入的数据

#include <stdio.h>
#include <stdlib.h>
#include <sys/ptrace.h>
#include <sys/user.h>
#include <unistd.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);
} else {
wait(&status); // 接收被子进程发送过来的 SIGTRAP 信号
//这一次是execve系统调用引起的,调用成功不返回
ptrace(PTRACE_GETREGS, child, 0, &regs);
printf("orig_rax: %llx\t", regs.orig_rax); // 打印rax寄存器的值
printf("rax: %llx\n", regs.rax); // 打印rax寄存器的值
while (1) {
// 1. 发送 PTRACE_SYSCALL 命令给被跟踪进程
// (调用系统调用前,可以获取系统调用的参数)
ptrace(PTRACE_SYSCALL, child, NULL, NULL);

wait(&status); // 接收被子进程发送过来的 SIGTRAP 信号(进入系统调用触发)
if (WIFEXITED(status)) { // 如果子进程退出了, 那么终止跟踪
puts("--------------");
printf("%d\n", status);
break;
}
ptrace(PTRACE_GETREGS, child, 0, &regs); // 获取被跟踪进程寄存器的值
printf("orig_rax: %llx\t", regs.orig_rax); // 打印rax寄存器的值
printf("rax: %llx\t", regs.rax); // 打印rax寄存器的值
// 2. 发送 PTRACE_SYSCALL 命令给被跟踪进程
// (调用系统调用后,可以获取系统调用的返回值)
ptrace(PTRACE_SYSCALL, child, NULL, NULL);
wait(&status); // 接收被子进程发送过来的 SIGTRAP 信号(系统调用返回触发)
//系统调用返回值
ptrace(PTRACE_GETREGS, child, 0, &regs); // 获取被跟踪进程寄存器的值
printf("orig_rax: %llx\t", regs.orig_rax); // 打印rax寄存器的值
printf("rax: %llx\n", regs.rax); // 打印rax寄存器的值
if (WIFEXITED(status)) { // 如果子进程退出了, 那么终止跟踪
puts("++++++++++++++++++++++");
printf("%d\n", status);
break;
}
}
}
return 0;
}

image-20231014223223759

基本上可以和strace输出结果对上

ciscn2022 syscall

首先是父线程分支

通过ptrace(PTRACE_GETREGS, (unsigned int)a1, 0LL, &s);可以判断处s是一个user_regs_struct的结构体定义在#include <sys/user.h>

#include <sys/user.h>
struct user_regs_struct
{
__extension__ unsigned long long int r15;
__extension__ unsigned long long int r14;
__extension__ unsigned long long int r13;
__extension__ unsigned long long int r12;
__extension__ unsigned long long int rbp;
__extension__ unsigned long long int rbx;
__extension__ unsigned long long int r11;
__extension__ unsigned long long int r10;
__extension__ unsigned long long int r9;
__extension__ unsigned long long int r8;
__extension__ unsigned long long int rax;
__extension__ unsigned long long int rcx;
__extension__ unsigned long long int rdx;
__extension__ unsigned long long int rsi;
__extension__ unsigned long long int rdi;
__extension__ unsigned long long int orig_rax;//系统调用号号
__extension__ unsigned long long int rip;
__extension__ unsigned long long int cs;
__extension__ unsigned long long int eflags;
__extension__ unsigned long long int rsp;
__extension__ unsigned long long int ss;
__extension__ unsigned long long int fs_base;
__extension__ unsigned long long int gs_base;
__extension__ unsigned long long int ds;
__extension__ unsigned long long int es;
__extension__ unsigned long long int fs;
__extension__ unsigned long long int gs;
};

定义结构体后代码就清晰明了多了

首先第一个waitpid被子线程里的raise(18)唤醒

image-20231015011436298

这里判断低字节为0x7f是为了判断子线程是否收因信号传递而停止

image-20231015012226004

# define WIFSTOPPED(status)    __WIFSTOPPED (status)
#define __WIFSTOPPED(status) (((status) & 0xff) == 0x7f)