A transparent debugger that intercepts Windows API calls and reports exact call sites, RVA offsets, decoded parameters, and disassembly context.
For legitimate debugging, reverse engineering, and security research only. Use exclusively on processes you own or have explicit written authorization to analyze.
WinTrace attaches to any Windows process (or launches one under its control) and silently intercepts calls to monitored APIs using software breakpoints (INT3). For every hit it reports:
| Field | Description |
|---|---|
| API | DLL + function name (user32.dll!MessageBoxW) |
| RVA / Offset | Exact offset of the CALL instruction inside the caller module — paste directly into IDA Pro, Ghidra, x64dbg |
| Call site address | Absolute virtual address of the CALL instruction |
| Return address | Instruction that executes after the call returns |
| Parameters | Decoded from registers / stack (strings, handles, integers) |
| Disassembly | ±10 instructions around the call site |
| Stack trace | Return-address chain |
| PID / TID | Process and thread identifiers |
Supports both x86 and x64 targets from a single 64-bit Python host.
- Windows 10 / 11
- Python 3.8+ 64-bit — download
- Administrator privileges for
--attachmode
git clone https://github.com/zorks56/WinTrace.git
cd WinTracepip install -r requirements.txt| Package | Version | Purpose |
|---|---|---|
capstone |
≥ 4.0.2 | x86 / x64 disassembly engine |
pywin32 |
≥ 305 | Windows API types and constants |
pefile |
≥ 2023.2.7 | PE file parsing |
colorama |
≥ 0.4.6 | Colored console output |
pyyaml |
≥ 6.0 | YAML config file support |
python gui.pyThe dark-themed interface provides:
- Target panel — browse for EXE or enter PID to attach
- Config panel — select API watchlist, optional text/JSON log paths
- Filters — restrict by module name or API name
- Options — verbosity, break-on-hit, trace-only mode
- Output Log tab — real-time colored event stream
- API Stats tab — hit frequency table, sorted by count
# Launch a target executable under the debugger
python main.py --launch "C:\path\to\target.exe"
# Attach to an already-running process by PID
python main.py --attach 4321
# Use a custom API config + save output to a text log
python main.py --launch target.exe --config myapis.json --log trace.txt
# Full verbosity: show disassembly and parameter details
python main.py --launch target.exe -vv
# Pause at every API hit (press Enter to continue)
python main.py --launch target.exe --break-on-hit
# Intercept specific APIs only
python main.py --launch target.exe --filter-api MessageBoxW,LoadLibraryA
# Intercept calls from a specific module only
python main.py --launch target.exe --filter-module target.exe
# Stop after the very first hit
python main.py --launch target.exe --first-hit
# Export events as JSON
python main.py --launch target.exe --json trace.json
# List all APIs in the current config and exit
python main.py --list-apis| Flag | Default | Description |
|---|---|---|
--launch EXE |
— | Path to executable to launch under debugger |
--attach PID |
— | PID of running process to attach to |
--args "..." |
"" |
Command-line arguments passed to launched process |
--config FILE |
config/api_watch.json |
API watchlist JSON or YAML file |
--log FILE |
— | Save formatted text log to file |
--json FILE |
— | Save structured JSON log to file |
-v / -vv |
normal | Verbosity: -v shows params, -vv adds disasm + stack |
--quiet |
off | Minimal output (API name + call site only) |
--break-on-hit |
off | Pause at every hit, resume with Enter |
--trace-only |
off | Do not re-arm breakpoints after first hit per API |
--first-hit |
off | Stop the entire trace after the first hit |
--filter-module |
all | Comma-separated list of caller modules to include |
--filter-api |
all | Comma-separated list of API names to include |
--list-apis |
— | Print watchlist from config and exit |
────────────────────────────────────────────────────────────────────────
[14:22:31.447] PID=7312 TID=7316
API : user32.dll!MessageBoxW
API_ADDR : 0x00007ffa8a1c3e40
CALL_SITE : 0x00007ff6ab123456
RET_ADDR : 0x00007ff6ab12345b
MODULE : target.exe
RVA/OFFSET: 0x00023456
ARGS:
hWnd = 0x0
lpText = L"Enter your license key"
lpCaption = L"Activation"
uType = 1
DISASM:
0x00007ff6ab12343a: mov rcx, 0x0
0x00007ff6ab123441: lea rdx, [rip+0x12ef8]
0x00007ff6ab123448: lea r8, [rip+0x12f01]
0x00007ff6ab12344f: mov r9d, 0x1
>>> 0x00007ff6ab123453: call qword ptr [rip+0x4a3b7]
0x00007ff6ab12345b: test eax, eax
0x00007ff6ab12345d: je 0x7ff6ab123471
RVA 0x00023456 → open target.exe in IDA Pro or Ghidra and jump to offset 0x23456 to land exactly on the calling code.
Edit or create a JSON file to control which APIs are monitored and which parameters are decoded:
{
"apis": [
{
"module": "user32.dll",
"name": "MessageBoxW",
"params": [
{"name": "hWnd", "type": "HWND"},
{"name": "lpText", "type": "LPCWSTR"},
{"name": "lpCaption", "type": "LPCWSTR"},
{"name": "uType", "type": "UINT"}
]
},
{
"module": "kernel32.dll",
"name": "CreateFileW",
"params": [
{"name": "lpFileName", "type": "LPCWSTR"},
{"name": "dwDesiredAccess", "type": "DWORD"},
{"name": "dwShareMode", "type": "DWORD"},
{"name": "lpSecurityAttributes", "type": "HANDLE"},
{"name": "dwCreationDisposition", "type": "DWORD"},
{"name": "dwFlagsAndAttributes", "type": "DWORD"},
{"name": "hTemplateFile", "type": "HANDLE"}
]
}
]
}| Type | Display |
|---|---|
LPCWSTR / LPWSTR |
Unicode string read from pointer (L"...") |
LPCSTR / LPSTR |
ANSI string read from pointer |
HANDLE / HWND / HMODULE |
Hex value |
DWORD / UINT / INT |
Decimal value |
Pass the config with --config myapis.json or select it in the GUI.
WinTrace/
├── main.py ← CLI entry point (argparse)
├── gui.py ← Dark-themed tkinter GUI
├── requirements.txt
├── assets/
│ └── banner.png
├── config/
│ └── api_watch.json ← Default API watchlist (25 APIs)
├── debugger/
│ ├── core.py ← WaitForDebugEvent loop, event dispatch
│ ├── breakpoint_manager.py ← INT3 write/restore, single-step re-arm
│ └── context.py ← x86 / x64 / WOW64 CONTEXT structures
├── modules/
│ └── resolver.py ← Module base tracking, RVA calculation
├── api_watch/
│ └── manager.py ← Watchlist loader (JSON / YAML)
├── disasm/
│ └── engine.py ← Capstone wrapper, backward CALL scan
└── logger/
└── log.py ← Console (colored) + text + JSON output
| Component | Responsibility |
|---|---|
debugger/core.py |
WaitForDebugEvent loop; dispatches create/exit/load/exception events |
debugger/breakpoint_manager.py |
Writes INT3, saves original byte, single-steps to restore, re-arms |
debugger/context.py |
Full x64 CONTEXT (1232 bytes, 16-byte aligned), x86 CONTEXT, WOW64 |
modules/resolver.py |
Maintains base→name map; computes call_site − module_base |
api_watch/manager.py |
Loads JSON/YAML watchlist; maps addresses to ApiDef objects |
disasm/engine.py |
Backward scan from return address (Δ 2,3,5,6,7); validates with Capstone |
logger/log.py |
ApiHitEvent dataclass; text + JSON file output; colored console |
gui.py |
tkinter GUI; background debug thread; queue.Queue for thread-safe updates |
At LOAD_DLL_DEBUG_EVENT, for each watched API:
rva_in_our_process = GetProcAddress(dll) - GetModuleHandle(dll)
api_addr_in_target = target_dll_base + rva_in_our_process
WinTrace writes 0xCC (INT3) at api_addr_in_target.
EXCEPTION_BREAKPOINT triggers with ExceptionAddress = api_addr + 1 (CPU steps past INT3).
x64: ret_addr = ReadProcessMemory(RSP, 8 bytes)
x86: ret_addr = ReadProcessMemory(ESP, 4 bytes)
ret_addr is the instruction after the CALL. Candidates:
| Delta | Form |
|---|---|
| −2 | CALL reg (FF D0–FF D7) |
| −3 | CALL [reg+disp8] (FF 50 xx, etc.) |
| −5 | CALL rel32 (E8 xx xx xx xx) |
| −6 | CALL [rip+rel32] (FF 15 xx xx xx xx) |
| −7 | CALL [mem] with REX prefix |
Each candidate is disassembled with Capstone. First valid CALL that ends exactly at ret_addr wins.
module = find_module_containing(call_site)
rva = call_site - module.base_address
Load this RVA directly in IDA Pro / Ghidra / x64dbg.
GetThreadContext requires the CONTEXT buffer to be 16-byte aligned. WinTrace allocates CONTEXT_SIZE + 15 bytes and aligns manually:
aligned_addr = (buf_addr + 15) & ~15
ctx = CONTEXT_x64.from_address(aligned_addr)| Arch | Convention | Argument passing |
|---|---|---|
| x64 | __fastcall |
Args 1–4: RCX, RDX, R8, R9 — further: RSP+0x28, RSP+0x30 … |
| x86 | stdcall |
All args on stack: [ESP+4], [ESP+8], [ESP+12] … |
WinTrace detects WOW64 via IsWow64Process at startup and automatically switches between GetThreadContext / Wow64GetThreadContext.
Note: A 32-bit Python host cannot debug a 64-bit target. Always use 64-bit Python on 64-bit Windows.
| Limitation | Notes |
|---|---|
| DLLs loaded before attach | BPs not placed on APIs in already-loaded DLLs with --attach. Workaround: use --launch. |
| Multi-threaded race | Two threads hitting the same API during BP restore may slip through. Mitigate with --filter-api. |
| Packed / obfuscated targets | INT3 at IAT entries may not fire if the IAT is rebuilt at runtime. |
| PDB symbols | Not implemented — caller shown as module+RVA. Import into IDA/Ghidra for symbol resolution. |
| Kernel-mode APIs | Not supported — user-mode only by design. |
WinTrace uses only documented Windows debugging APIs:
CreateProcesswithDEBUG_ONLY_THIS_PROCESSWaitForDebugEvent/ContinueDebugEventGetThreadContext/SetThreadContextReadProcessMemory/WriteProcessMemoryVirtualProtectEx/FlushInstructionCache
No kernel-mode operations, no AV evasion, no stealth techniques.
Use only on:
- Processes you compiled or own
- Systems you administer
- Processes for which you have explicit written authorization from the software owner
