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:
- First of all
fork()
.
- CHILD: do
ptrace(PTRACE_TRACEME)
followed by kill(SIGSTOP)
. After this, exec*()
whatever program you want to trace.
- 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.
- 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.
- 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, ®s);
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).