
Fabio BaenschWhy I Built a Windows Debugger From Scratch (Because I Wanted to Understand Crashes, Not...
A few months ago I got tired of alt-tabbing between WinDbg, a notepad full of scratch notes, and a Python script that tried (badly) to parse register dumps. I wanted something that just showed me what was happening the moment a target crashed — live, without ceremony. So I built expl0itra, a small Windows binary debugger with a Flutter GUI on top of a C++ debug engine.
To be upfront: this is a small side/fun project, not a production tool. I built it mostly to get my hands dirty with the Windows Debug API while I dig deeper into cybersecurity — specifically software development, driver development, and binary exploitation. If you've built something similar or see an obvious way to make this better, I'd genuinely love to hear it in the comments.
expl0itra wraps the Windows Debug API around any .exe you point it at:
.txt report next to the binary with exception type, register values, memory snapshots, and a timestampThe Flutter frontend never touches the Windows Debug API directly — it just spawns expltr_dbg.exe as a child process and talks to it over stdin/stdout:
expl0itra (Flutter GUI)
│
├── startDebugger() launches expltr_dbg.exe as subprocess
├── stdin ──────────────► file path of target binary
└── stdout ◄────────────── RIP_VAL / RBP_VAL / DBG log lines
parsed by regex, displayed live
expltr_dbg.exe (C++ Debug Engine)
│
├── CreateProcessA() spawns target with DEBUG_ONLY_THIS_PROCESS
├── WaitForDebugEvent() 50ms timeout loop
├── SuspendThread() snapshots thread context between events
├── GetThreadContext() reads RIP, RBP
├── ReadProcessMemory() reads memory at those addresses
├── VirtualQueryEx() validates address before read
└── fstream analytics writes .txt report on second-chance exception
The C++ side does the real work: it attaches with DEBUG_ONLY_THIS_PROCESS, polls debug events, and every time it gets a chance it suspends the thread, grabs RIP/RBP via GetThreadContext, and tries to read whatever memory those addresses point to. If the read fails — which happens a lot once you've smashed the stack — it just falls back to showing the raw register bytes instead of crashing the tool itself.
To sanity-check the tool I threw a classic vulnerable binary at it:
// vuln.c
#include <stdio.h>
#include <string.h>
void vulnerable(char* input) {
char buf[64];
strcpy(buf, input);
}
int main() {
char input[512];
fgets(input, sizeof(input), stdin);
vulnerable(input);
return 0;
}
Compiled without a stack canary (gcc vuln.c -o vuln.exe -fno-stack-protector -m64 -O0) and fed a classic overflow payload, expl0itra shows exactly what you'd expect:
RIP: 0x4343434343434343 (CCCCCCCC)
RBP: 0x4242424242424242 (BBBBBBBB)
Seeing the register values overwrite live, instead of reading them post-mortem from a crash dump, made the whole "why does an overflow actually work" concept click in a way that reading about it never quite did.
expltr_dbg.exe needs admin privileges for OpenProcess with PROCESS_VM_READ
I'm on pwn.college as 0mega_n0d3, and I recently earned the Linux badge there — genuinely one of the things I'm most proud of so far. Right now I'm trying to deepen my binary exploitation skills and connect them to what I already know from reverse engineering, C, and C++. Tools like expl0itra are partly a byproduct of that: building the debugger forces me to actually understand the Windows Debug API instead of just consuming its output through someone else's tool.
Longer term, I want to go further into driver development and lower-level Windows internals — so if you work in that space, or have opinions on how a project like this should evolve (better exception classification? symbol resolution? a proper disassembler view?), I'd really appreciate hearing them.
Repo: expl0itra on GitHub
MIT licensed — do whatever you want with it, just don't hold me liable. 🙂