#include <dirent.h>
#include <elf.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ptrace.h>
#include <sys/uio.h>
#include <sys/wait.h>
#include <capstone/capstone.h>
#include "debugger.h"
#include "helpers.h"
static const int PTRACE_OPTIONS =
PTRACE_O_TRACECLONE |
PTRACE_O_TRACEEXEC |
PTRACE_O_TRACESYSGOOD;
static const int PTRACE_CHILD_OPTIONS = PTRACE_OPTIONS | PTRACE_O_EXITKILL;
static const int STOP_ALL_ON_EVENT = 1;
static const useconds_t SCHEDULER_DELAY = 100000;
static const unsigned int BREAKPOINT_INSN = 0xcc;
static struct thread *thread_by_id(struct process *proc, pid_t id) {
struct list *threads = &proc->threads;
for (struct thread *th = threads->head; th != threads->end; th = th->next) {
if (th->id == id) {
return th;
}
}
return NULL;
}
static void free_breakpoints(struct process *proc) {
while (proc->breakpoints.head != proc->breakpoints.end) {
struct breakpoint *b = proc->breakpoints.head;
list_remove(b);
free(b);
}
}
static void install_breakpoints(struct thread *th) {
struct list *breaks = &th->proc->breakpoints;
for (struct breakpoint *b = breaks->head; b != breaks->end; b = b->next) {
if (!b->installed) {
unsigned long word;
word = ptrace(PTRACE_PEEKTEXT, th->id, b->address, NULL);
b->text = word;
word = (word & ~0xff) | BREAKPOINT_INSN;
ptrace(PTRACE_POKETEXT, th->id, b->address, word);
b->installed = 1;
}
}
}
static void uninstall_breakpoints(struct thread *th) {
struct list *breaks = &th->proc->breakpoints;
for (struct breakpoint *b = breaks->tail; b != breaks->end; b = b->prev) {
if (b->installed) {
ptrace(PTRACE_POKETEXT, th->id, b->address, b->text);
b->installed = 0;
}
if (b->enabled < 0) {
struct thread *t;
if (b->tid == 0 ||
((t = thread_by_id(th->proc, b->tid)) && !t->shouldcont)) {
struct breakpoint *del = b;
b = b->next;
list_remove(del);
free(del);
}
}
}
}
static int detect_breakpoint(struct thread *th, int *restart) {
int ret = 0;
*restart = 0;
struct user_regs_struct regs;
struct iovec ivregs = { ®s, sizeof(regs) };
ptrace(PTRACE_GETREGSET, th->id, NT_PRSTATUS, &ivregs);
/* implement with get_breakpoint? */
struct list *breaks = &th->proc->breakpoints;
for (struct breakpoint *b = breaks->tail; b != breaks->end; b = b->prev) {
if (b->installed && (regs.rip - 1 == b->address)) {
regs.rip--;
ptrace(PTRACE_SETREGSET, th->id, NT_PRSTATUS, &ivregs);
b->hits++;
ret = b->user;
if (b->stack != 0 && b->stack != regs.rsp) {
*restart = 1;
}
if (b->tid != 0 && b->tid != th->id) {
*restart = 1;
}
if (!b->enabled) {
*restart = 1;
}
break;
}
}
return ret;
}
static void free_states(struct thread *th) {
while (th->states.head != th->states.end) {
struct state *s = th->states.head;
while (s->maps.head != s->maps.end) {
struct map *m = s->maps.head;
list_remove(m);
free(m->data);
free(m);
}
list_remove(s);
free(s);
}
th->state = NULL;
th->clearstates = 0;
}
static void capture_state(struct process *proc) {
struct list *threads = &proc->threads;
for (struct thread *th = threads->head; th != threads->end; th = th->next) {
if (!th->shouldcont) {
if (th->clearstates) {
free_states(th);
}
struct state *s = xmalloc(sizeof(*s));
struct iovec regs = { &s->regs, sizeof(s->regs) };
ptrace(PTRACE_GETREGSET, th->id, NT_PRSTATUS, ®s);
list_init(&s->maps);
list_insert(th->states.end, s);
th->state = s;
char mapspath[32], entry[512];
snprintf(mapspath, sizeof(mapspath), "/proc/%li/maps", (long)th->id);
FILE *maps = fopen(mapspath, "r");
if (maps) {
while (fgets(entry, sizeof(entry), maps)) {
struct map *m = xmalloc(sizeof(*m));
sscanf(entry, "%lx-%lx ", &m->start, &m->end);
size_t size = m->end - m->start;
m->data = xmalloc(size);
struct iovec loc = { m->data, size };
struct iovec rem = { (void *)m->start, size };
if (process_vm_readv(th->id, &loc, 1, &rem, 1, 0) < 0) {
free(m->data);
free(m);
continue;
}
list_insert(s->maps.end, m);
}
fclose(maps);
}
}
}
}
static void interrupt_all_threads(struct process *proc) {
struct list *threads = &proc->threads;
for (struct thread *th = threads->head; th != threads->end; th = th->next) {
if (!th->stopped) {
ptrace(PTRACE_INTERRUPT, th->id, NULL, NULL);
while (!dbg_wait(th, 0)) {}
}
if (STOP_ALL_ON_EVENT) {
th->shouldcont = 0;
}
}
}
static void continue_all_threads(struct process *proc) {
struct list *threads = &proc->threads;
for (struct thread *th = threads->head; th != threads->end; th = th->next) {
if (th->id > 0) {
ptrace(PTRACE_CONT, th->id, NULL, th->signal);
th->stopped = 0;
th->signal = 0;
th->cont = 0;
th->shouldcont = 2;
strcpy(th->status, "RUNNING");
th->clearstates = 1;
}
}
}
/* 0: stop 1: singlestep 2: cont 3: syscall */
/* this is simplified, surely incorrect, and needs updated once testable */
static void resume_threads(struct process *proc) {
struct list *threads = &proc->threads;
for (struct thread *th = threads->head; th != threads->end; th = th->next) {
if (th->shouldcont) {
install_breakpoints(th);
ptrace(PTRACE_CONT, th->id, NULL, th->signal);
th->stopped = 0;
th->signal = 0;
th->cont = 0;
strcpy(th->status, "RUNNING");
}
}
}
static struct process *new_process(pid_t id, int child) {
struct process *proc = xmalloc(sizeof(*proc));
proc->id = id;
proc->child = child;
list_init(&proc->breakpoints);
list_init(&proc->threads);
memset(proc->status, 0, sizeof(proc->status));
return proc;
}
static struct thread *new_thread(struct process *proc, pid_t id) {
struct thread *th = xmalloc(sizeof(*th));
th->proc = proc;
list_init(&th->states);
th->state = NULL;
th->clearstates = 0;
th->id = id;
th->stopped = 0;
th->signal = 0;
th->cont = 0;
th->shouldcont = 0;
strcpy(th->status, "RUNNING");
return th;
}
struct breakpoint *add_breakpoint(struct process *proc, unsigned long address) {
struct breakpoint *b = xmalloc(sizeof(*b));
b->address = address;
b->text = 0;
b->installed = 0;
b->hits = 0;
b->user = 1;
b->stack = 0;
b->tid = 0;
b->enabled = 1;
list_insert(proc->breakpoints.end, b);
return b;
}
struct breakpoint *get_breakpoint(struct process *proc, unsigned long address) {
struct list *breaks = &proc->breakpoints;
for (struct breakpoint *b = breaks->head; b != breaks->end; b = b->next) {
if (b->address == address) {
return b;
}
}
return NULL;
}
struct process *dbg_attach(pid_t pid, int child) {
const int OPTIONS = (child ? PTRACE_CHILD_OPTIONS : PTRACE_OPTIONS);
char taskpath[32];
snprintf(taskpath, sizeof(taskpath), "/proc/%li/task", (long)pid);
DIR *taskdir = opendir(taskpath);
if (!taskdir) {
return NULL;
}
struct dirent *task;
struct process *proc = new_process(pid, child);
while ((task = readdir(taskdir))) {
pid_t id = strict_strtoul(task->d_name, 0);
if (id != 0) {
if (ptrace(PTRACE_SEIZE, id, NULL, OPTIONS) < 0) {
closedir(taskdir);
dbg_detach(proc);
return NULL;
}
struct thread *th = new_thread(proc, id);
list_insert(proc->threads.end, th);
}
}
closedir(taskdir);
interrupt_all_threads(proc);
capture_state(proc);
return proc;
}
void dbg_detach(struct process *proc) {
/* ptrace requires that threads be stopped before calling PTRACE_DETACH.
* In some cases, a blocked thread can possibly have a pending trap which
* would crash the process if encountered after ptracing. We cycle an extra
* continue/interrupt to consume this trap event ourselves before detaching.
* Threads must actually get scheduled for this to be effective, which is
* the reason for the usleep. A better approach is welcome here. */
interrupt_all_threads(proc);
continue_all_threads(proc);
usleep(SCHEDULER_DELAY);
interrupt_all_threads(proc);
/* Supplement to PTRACE_O_EXITKILL */
if (proc->child) {
kill(proc->id, SIGKILL);
}
while (proc->threads.head != proc->threads.end) {
struct thread *th = proc->threads.head;
if (th->id > 0) {
uninstall_breakpoints(th);
ptrace(PTRACE_DETACH, th->id, NULL, th->signal);
th->id = 0;
}
dbg_free(th);
}
free_breakpoints(proc);
list_remove(proc);
free(proc);
}
int dbg_free(struct thread *th) {
if (th->id <= 0) {
free_states(th);
list_remove(th);
free(th);
return 0;
}
return -1;
}
int dbg_wait(struct thread *th, int primary) {
if (th->id <= 0) {
return -1;
} else if (th->stopped) {
return 1;
}
int status;
if (waitpid(th->id, &status, __WALL | WNOHANG) <= 0) {
return 0;
}
if (WIFEXITED(status)) {
th->id = 0;
th->stopped = 1;
th->signal = WEXITSTATUS(status);
th->cont = 0;
th->shouldcont = 0;
strcpy(th->status, "EXITED");
return 1;
}
if (WIFSIGNALED(status)) {
th->id = 0;
th->stopped = 1;
th->signal = WTERMSIG(status);
th->cont = 0;
th->shouldcont = 0;
strcpy(th->status, "TERMINATED");
return 1;
}
struct thread *newth; // event thread
unsigned long eventmsg;
ptrace(PTRACE_GETEVENTMSG, th->id, NULL, &eventmsg);
/* todo: process status messages */
if (WIFSTOPPED(status)) {
switch (status >> 8) {
/* todo: other ptrace event stops */
case SIGTRAP | (PTRACE_EVENT_CLONE << 8):
newth = new_thread(th->proc, eventmsg);
list_insert(th->proc->threads.end, newth);
while (!dbg_wait(newth, 0)) {}
th->stopped = 1;
th->signal = 0;
th->cont = 0;
th->shouldcont = 0;
strcpy(th->status, "CLONE");
th->state = NULL;
if (primary) {
interrupt_all_threads(th->proc);
uninstall_breakpoints(th);
capture_state(th->proc);
resume_threads(th->proc);
}
return 1;
case SIGTRAP | (PTRACE_EVENT_EXEC << 8):
if ((pid_t)eventmsg != th->id) {
newth = thread_by_id(th->proc, eventmsg);
newth->id = 0;
newth->stopped = 1;
newth->signal = 0;
newth->cont = 0;
newth->shouldcont = 0;
strcpy(newth->status, "EXITED");
}
th->stopped = 1;
th->signal = 0;
th->cont = 0;
th->shouldcont = 0;
strcpy(th->status, "EXECVE");
th->state = NULL;
if (primary) {
interrupt_all_threads(th->proc);
uninstall_breakpoints(th);
capture_state(th->proc);
resume_threads(th->proc);
}
return 1;
case SIGTRAP | (PTRACE_EVENT_STOP << 8):
th->stopped = 1;
th->signal = 0;
th->cont = 0;
strcpy(th->status, "STOPPED");
if (primary) {
interrupt_all_threads(th->proc);
uninstall_breakpoints(th);
capture_state(th->proc);
resume_threads(th->proc);
}
return th->stopped;
case SIGTRAP | 0x80:
th->stopped = 1;
th->signal = 0;
th->cont = 0;
th->shouldcont = 0;
strcpy(th->status, "SYSCALL");
th->state = NULL;
if (primary) {
interrupt_all_threads(th->proc);
uninstall_breakpoints(th);
capture_state(th->proc);
resume_threads(th->proc);
}
return 1;
case SIGTRAP:
th->stopped = 1;
th->signal = 0;
int restart;
int bp = detect_breakpoint(th, &restart);
strcpy(th->status, (bp ? "BREAKPOINT" : "STEP"));
//th->shouldcont = (b && b->enabled == 0);
th->shouldcont = (restart ? 2 : 0);
if (primary) {
interrupt_all_threads(th->proc);
uninstall_breakpoints(th);
capture_state(th->proc);
resume_threads(th->proc);
}
return th->stopped;
default:
th->stopped = 1;
th->signal = WSTOPSIG(status);
th->cont = 0;
th->shouldcont = 0;
strcpy(th->status, "SIGNAL DELIVERY");
th->state = NULL;
if (primary) {
interrupt_all_threads(th->proc);
uninstall_breakpoints(th);
capture_state(th->proc);
resume_threads(th->proc);
}
return 1;
}
}
return -1;
}
int dbg_intr(struct thread *th) {
if (th->id <= 0 || th->stopped) {
return -1;
}
ptrace(PTRACE_INTERRUPT, th->id, NULL, NULL);
th->shouldcont = 0;
return 0;
}
int dbg_cont(struct thread *th) {
if (th->id <= 0 || !th->stopped) {
return -1;
}
ptrace(PTRACE_SINGLESTEP, th->id, NULL, th->signal);
th->stopped = 0;
th->signal = 0;
th->cont = PTRACE_CONT;
th->shouldcont = 1;
strcpy(th->status, "RUNNING");
th->clearstates = 1;
return 0;
}
int dbg_syscall(struct thread *th) {
if (th->id <= 0 || !th->stopped) {
return -1;
}
ptrace(PTRACE_SINGLESTEP, th->id, NULL, th->signal);
th->stopped = 0;
th->signal = 0;
th->cont = PTRACE_SYSCALL;
th->shouldcont = 1;
strcpy(th->status, "RUNNING");
th->clearstates = 1;
return 0;
}
int dbg_stepin(struct thread *th) {
if (!th->stopped) {
return -1;
}
if (th->state != th->states.tail) {
th->state = th->state->next;
return 0;
}
if (th->id <= 0) {
return -1;
}
ptrace(PTRACE_SINGLESTEP, th->id, NULL, th->signal);
th->stopped = 0;
th->signal = 0;
th->cont = PTRACE_SINGLESTEP;
th->shouldcont = 1;
strcpy(th->status, "RUNNING");
th->clearstates = 0;
return 0;
}
int dbg_stepover(struct thread *th) {
if (!th->stopped) {
return -1;
}
if (th->state != th->states.tail) {
th->state = th->state->next;
return 0;
}
if (th->id <= 0) {
return -1;
}
csh handle;
cs_open(CS_ARCH_X86, CS_MODE_64, &handle);
cs_insn *insn = cs_malloc(handle);
uint64_t address = th->state->regs.rip;
size_t size = 128;
const uint8_t *code = deref(th, address, size);
cs_disasm_iter(handle, &code, &size, &address, insn);
int ret = -1;
if (insn->id == X86_INS_CALL) {
struct breakpoint *b = add_breakpoint(th->proc, address);
b->user = 0;
b->stack = th->state->regs.rsp;
b->tid = th->id;
b->enabled = -1;
ret = dbg_cont(th);
} else {
ret = dbg_stepin(th);
}
cs_free(insn, 1);
cs_close(&handle);
return ret;
}
int dbg_stepback(struct thread *th) {
if (!th->stopped) {
return -1;
}
if (th->state != th->states.head) {
th->state = th->state->prev;
return 0;
}
return -1;
}
void *deref(struct thread *th, unsigned long address, size_t size) {
/* todo: size, and mapping breaks */
(void)size;
if (!th->state) {
return NULL;
}
struct list *maps = &th->state->maps;
for (struct map *m = maps->head; m != maps->end; m = m->next) {
if (m->start <= address && address < m->end) {
return (unsigned char *)m->data + (address - m->start);
}
}
return NULL;
}
//int dbg_stepout(struct tracee *dbg) {
// unsigned long bp = dbg->state->regs.rbp;
// unsigned long ra = *(unsigned long *)deref(dbg, bp+8, sizeof(unsigned long));
//
// struct breakpoint *b = xmalloc(sizeof(*b));
// b->address = ra;
// b->stack = 0;
// b->enabled = -1;
// b->active = 0;
// list_insert(dbg->breaks.end, b);
//
// ptrace(PTRACE_SINGLESTEP, dbg->id, NULL, NULL);
// //clear_states(dbg); // keep this?
// dbg->stopped = 0;
// dbg->cont = PTRACE_CONT;
// return 0;
//}