cptrace

Intercept only syscall with PTRACE_SINGLESTEP


We have a school project where we need to re-code "strace".

We have to only intercept syscall like write and read, but we cannot use PTRACE_SYSCALL. I'm looking for a way to do that using PTRACE_SINGLESTEP, I've already coded a way to print the syscall and when I'm using PTRACE_SYSCALL it works fine, but when I use PTRACE_SINGLESTEP I can't find a way to only print the syscalls.

Here is the code I use, maybe someone can help me figure out what's wrong with it:

pid_t child;
long orig_eax;
user_regs_struct regs;

child = fork();
if (child == 0) {
    ptrace(PTRACE_TRACEME, 0, 0, 0);
    execve("/home/architek/a.out", {"/home/architek/a.out", NULL}, envp);
} else {
    waitpid(child, &status, 0);
    while (WIFSTOPPED(status)) {
        orig_eax = ptrace(PTRACE_PEEKUSER, child, 8 * ORIG_RAX, NULL);
        ptrace(PTRACE_GETREGS, child, NULL, &regs);
        call_printer(&regs, child);
        ptrace(PTRACE_SINGLESTEP, child, 0, 0);
        waitpid(child, &status, 0);
    }
}

Solution

  • If you cannot use PTRACE_SYSCALL to stop the child right before/after a syscall, then you will have to manually detect when one is about to happen. I doubt that checking the source code of strace would help, since strace is most likely using PTRACE_SYSCALL, no reason to manually decode instructions.

    Assuming you are working on x86-64, here's how you can do it:

    1. Using PTRACE_SINGLESTEP, keep stepping one instruction at a time.
    2. Each instruction, use PTRACE_PEEKTEXT to fetch the next instruction pointed by the instruction pointer.
    3. Check if the instruction is a syscall instruction by comparing the bytes with the opcode of syscall, which is two bytes: 0x0f 0x05. Since x86 is little endian, this means checking whether the return value of ptrace(PTRACE_PEEKDATA, ...) has the two least significant bytes set to 0x050f.

    NOTE: If you are on another architecture, or if you also want to detect 32-bit syscalls, you can simply check for different/more values on step 3. On Linux x86-64, there are multiple ways to issue a syscall, with different opcodes. For example, 32-bit syscalls on Linux are done through int 0x80 (opcode 0xcd 0x80). Check this other answer of mine for a list.

    Here's an example:

    #include <errno.h>
    
    long opcode;
    
    // ...
    
    waitpid(child, &status, 0);
    
    while (WIFSTOPPED(status)) {
        ptrace(PTRACE_GETREGS, child, NULL, &regs);
    
        errno = 0;
        opcode = ptrace(PTRACE_PEEKTEXT, child, regs.rip, 0);
        if (opcode == -1 && errno != 0) {
            perror("ptrace(PTRACE_PEEK_DATA) failed");
            exit(1);
        }
    
        if (((unsigned long)opcode & 0xffff) == 0x050f) {
            // Child about to execute a syscall instruction,
            // check the registers to know more...
        }
    
        ptrace(PTRACE_SINGLESTEP, child, 0, 0);
        waitpid(child, &status, 0);
    }