1

Let's say I have the following assembly code which I'd like to single-step through:

.globl _start
_start:
    nop
    mov $60, %eax
    syscall

What would be the simplest way I could attach a ptrace to this to run this with single-stepping? I usually do this in gdb but curious how to manually do this in the crudest way possible (with no error handling or anything except the above case) to see what occurs behind the scenes. Any language is fine (assembly might be the best though).

Marco Bonelli
  • 63,369
  • 21
  • 118
  • 128
David542
  • 104,438
  • 178
  • 489
  • 842

2 Answers2

6

For simplicity, I added an int3 which triggers a breakpoint trap. In real usage, you'd want to trace the exec call and put a software or hardware breakpoint at the entry address you parsed out of the ELF header. I have assembled the target program into a.out and it looks like:

00000000004000d4 <_start>:
  4000d4:   cc                      int3   
  4000d5:   90                      nop
  4000d6:   b8 3c 00 00 00          mov    $0x3c,%eax
  4000db:   0f 05                   syscall 

A simple program demonstrating single stepping:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ptrace.h>
#include <sys/user.h>

int main() {
    int pid;
    int status;
    if ((pid = fork()) == 0) {
        ptrace(PTRACE_TRACEME, 0, NULL, NULL);
        execl("./a.out", "a.out", NULL);
    }
    printf("child: %d\n", pid);
    waitpid(pid, &status, __WALL);
    ptrace(PTRACE_CONT, pid, NULL, NULL);
    while(1) {
        unsigned long rip;
        waitpid(pid, &status, __WALL);
        if (WIFEXITED(status)) return 0;
        rip = ptrace(PTRACE_PEEKUSER, pid, 16*8, 0);    // RIP is the 16th register in the PEEKUSER layout
        printf("RIP: %016lx opcode: %02x\n", rip, (unsigned char)ptrace(PTRACE_PEEKTEXT, pid, rip, NULL));
        ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL);
    }
}

Sample output:

$ ./singlestep 
child: 31254
RIP: 00000000004000d5 opcode: 90
RIP: 00000000004000d6 opcode: b8
RIP: 00000000004000db opcode: 0f
Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Jester
  • 56,577
  • 4
  • 81
  • 125
  • `PEEKUSER` with `16*8` is a magic constant for the offset of RIP in a regs struct, I assume? GP integer register first, then RIP, then more stuff I guess? – Peter Cordes Sep 23 '20 at 17:14
  • Yeah, `GETREGS` is a better choice as that has a `struct` you can use. – Jester Sep 23 '20 at 17:35
  • 1
    Fortunately Marco's answer has an example of doing it that way. I added a comment for the magic constant in your version. – Peter Cordes Sep 23 '20 at 17:50
4

Here's a cleaner solution if you don't want to manually insert a debugger interrupt (int3) in the target program.

What you want to do is:

  1. First of all fork().
  2. CHILD: do ptrace(PTRACE_TRACEME) followed by kill(SIGSTOP). After this, exec*() whatever program you want to trace.
  3. PARENT: wait() for the child, then proceed with ptrace(PTRACE_SYSCALL) + wait(). Execution of the child will resume and immediately stop again when the kill syscall ends.
  4. PARENT: do another two ptrace(PTRACE_SYSCALL) + wait(), one will stop when the child is entering execve and one will stop right after execve is completed.
  5. PARENT: continue with ptrace(PTRACE_SINGLESTEP) as much as you want.
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/user.h>

void hexdump_long(unsigned long long addr, long data) {
        printf("[parent] 0x%016llx: ", addr);

        for (unsigned i = 0; i < 64; i += 8)
                printf("%02x ", ((unsigned long)data >> i) & 0xff);
        putchar('\n');
}

int main(int argc, char **argv) {
        int status;
        pid_t pid;

        if ((pid = fork()) == 0) {
                char *child_argv[] = {"./prog", NULL};
                char *child_envp[] = {NULL};

                ptrace(PTRACE_TRACEME, 0, 0, 0);
                kill(getpid(), SIGSTOP); // Don't use libc `raise` because it does more syscalls.

                execve(child_argv[0], child_argv, child_envp);
                perror("[child ] execve failed");
                return 1;
        }

        // Wait for child to stop
        wait(&status);

        // Exit kill syscall
        ptrace(PTRACE_SYSCALL, pid, 0, 0);
        wait(&status);

        // Enter execve syscall
        ptrace(PTRACE_SYSCALL, pid, 0, 0);
        wait(&status);

        // Exit execve syscall
        ptrace(PTRACE_SYSCALL, pid, 0, 0);
        wait(&status);

        // Child is now running the new program, trace one step at a time.
        // Trace up to 1000 steps or until the program exits/receives a signal.
        unsigned steps = 1000;

        while(WIFSTOPPED(status)) {
                struct user_regs_struct regs;
                long code;

                steps--;
                if (steps == 0) {
                        ptrace(PTRACE_CONT, pid, 0, 0);
                        break;
                }

                ptrace(PTRACE_GETREGS, pid, 0, &regs);
                code = ptrace(PTRACE_PEEKTEXT, pid, regs.rip, 0);

                hexdump_long(regs.rip, code);

                ptrace(PTRACE_SINGLESTEP, pid, 0, 0);
                wait(&status);
        }

        if (steps == 0)
                wait(&status);

        if (WIFEXITED(status))
                printf("[parent] Child exited with status %d.\n", WEXITSTATUS(status));
        else
                puts("[parent] Child didn't exit, something else happened.");

        return 0;
}

Test program (just exit(0)):

_start:
    mov rdi, 0x0
    mov rax, 0x3c
    syscall

Result:

$ ./trace
[parent] 0x0000000000400080: bf 00 00 00 00 b8 3c 00
[parent] 0x0000000000400085: b8 3c 00 00 00 0f 05 00
[parent] 0x000000000040008a: 0f 05 00 00 00 00 00 00
[parent] Child exited with status 0.

NOTE: the hexdump_long() function only dumps a long, but x86 instructions can be longer or shorter. This is just an example. In order to compute the real sizes of x86 instructions you would need an instruction decoder (here is an example for x86 32bit).

Marco Bonelli
  • 63,369
  • 21
  • 118
  • 128
  • 1
    Dumping 8 or 16 bytes of "upcoming" machine code is very reasonable, and will help you find your place if looking at disassembly. Trying to be smarter is complicated by branches; you don't always get RIP samples at the start and end of an instruction, and a short jump forward (or any jump) could fool the naive heuristic you suggested. [Get size of assembly instructions](https://stackoverflow.com/q/23788236) has Ira Baxter's x86 32-bit instruction-length decoder in C. – Peter Cordes Sep 23 '20 at 16:05
  • @PeterCordes holy moly that's an impressive answer. Thanks for the link I'll update mine. – Marco Bonelli Sep 23 '20 at 16:40
  • 1
    Pretty sure Ira had that already written for commercial reasons and just chose to share it, which is still cool. But note that it's only 32-bit mode, not handling REX prefixes (Mostly simple except for movq vs. movabs, I think) or the different meaning of the address-size prefix in 64-bit mode which is length-changing only for the `moffs` al/ax/eax/rax form of mov, otherwise not in 64-bit mode. – Peter Cordes Sep 23 '20 at 17:12
  • @PeterCordes yeah I saw. x86 is kind of... overwhelming from that point of view :') – Marco Bonelli Sep 23 '20 at 17:48
  • Yeah, x86 is a mess. I also didn't look to see if Ira's code supported AVX VEX prefixes, or AVX512 EVEX. (Both of which overlap with invalid encodings of other instructions in 32-bit mode). More and more new instructions get added with new AVX512 extensions, some with 1-byte immediates and some without. – Peter Cordes Sep 23 '20 at 17:53
  • @PeterCordes not to mention stuff like https://github.com/XlogicX/irasm – Marco Bonelli Sep 23 '20 at 18:29