This blog has tags: debug hooks linux windows
Setting breakpoints on yourself could be useful because it’s an easy way to dynamically notify a debugger when something happens. Perhaps it’s a condition that shouldn’t happen and you want to break there so that a debugger can catch it.
For Windows, there’s DebugBreak or __debugbreak__.
For Linux, you’d use a compiler intrinsic or inline assembly
via gcc, clang, or whatever other compiler you like.
__builtin_break is specifically defined fro gcc.
If you want to insert a breakpoint when it wasn’t set before, you can write your own memory or even another process’s memory if you have permission.
On Windows, WriteProcessMemory can be used on yourself.
On Linux, /proc/self/mem can also be used.
Note that I mention these specifically because they bypass page protections so even a page that is readonly or even read/execute can be written to with this, even with W^X policies like DEP.
After that, write the instruction that does the breakpoint.
// Writing x86 breakpoint on Windows
WriteProcessMemory(GetCurrentProcess(), &fn, "\xcc", 1, NULL);
// Writing x86 breakpoint on Linux
int fd = open("/proc/self/mem", O_RDWR);
lseek64(fd, (off64_t)&fn, SEEK_SET);
write(fd, "\xcc", 1);
Hardware breakpoints are especially tricky to set on yourself.
First off, not all threads have the same hardware breakpoints. This is actually a cool feature because you can break or watch the data access on a single thread, whereas software breakpoints are forced to break for all threads. However, it makes it harder to broadcast this for all threads.
Note that threads do not inherit hardware breakpoints so you often have to reserve a hardware breakpoint for thread creation if you really want a true global hardware breakpoint.
For Windows, GetThreadContext and SetThreadContext
allow you to set hardware breakpoints, even on ARM!
CONTEXT ctx;
HANDLE hThread = GetCurrentThread();
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hThread, &ctx);
// ...
ctx.Dr0 = NewDr0;
ctx.Dr1 = NewDr1;
// ...
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
SetThreadContext(hThread, &ctx);
For Linux, it’s a massive pain and very architecture-specific,
but what ends up ultimately happening is that you use ptrace.
However, ptrace can’t be used by a thread in the same
process group, so you have to create a new process.
This is very inconvenient but you can use the clone
syscall with CLONE_VM flag without the CLONE_THREAD
flag to create a new process which shares the same address space
(We love how cursed Linux is…).
In addition, distros have been using yama recently,
so you need to use prctl(PR_SET_PTRACER, pid) on
the child you create in order to allow it to debug you.
int debugger(void *arg)
{
int pid = (long)arg;
// You should error check ptrace calls.
ptrace(PTRACE_ATTACH, mypid);
// POKEUSER is specific to x86 Linux
// ARM, for instance, uses PTRACE_SETHBPREGS
// ...
ptrace(PTRACE_POKEUSER, mypid, offsetof(struct user, u_debugreg) + 0, NewDr0);
ptrace(PTRACE_POKEUSER, mypid, offsetof(struct user, u_debugreg) + sizeof(long), NewDr1);
// ...
ptrace(PTRACE_DETACH, mypid);
return 0;
}
void debugee()
{
int mypid = getpid();
size_t stacksize = 1 << 16;
uint8_t *stack = malloc(stacksize);
int child = clone(debugger, stack + stacksize, (void *)(long)mypid, CLONE_VM | SIGCHLD);
if (child != -1) {
// RACE CONDITION!
// prctl could execute after 'debugger' thread is done.
// You should synchronize with 'debugger'
prctl(PR_SET_PTRACER, child);
// You should error check waitpid
waitpid(child, NULL, 0);
}
}
This is already established in Bag of Tricks 1.
That being said, you still have to be careful about debugging details on each architecture, since the debugger won’t help you there.