A self-contained x64 syscall monitor for Windows, packaged as a single injectable DLL.
VoidSniff hooks low-level ntdll entry points inside the host process and prints a colored, human-readable feed of file, process and section activity to a private console (and optionally to a log file). It is intended as an educational tool for understanding Windows syscall mechanics, observing program behavior, and recognizing common injection / hollowing primitives.
Scope. VoidSniff is a defensive / research utility. It only supports x64 processes.
- Custom 14-byte absolute-JMP hook engine — no MinHook, no Detours, no external hooking library. Each hook is a
FF 25 00 00 00 00+ 8-byte absolute target written directly over the function prologue. - Safe re-entrancy —
CallOriginaldisarms the patch under a per-hook mutex, calls the real function, and rearms on scope exit. Athread_localguard prevents recursion through the detour. - Three high-signal syscalls hooked out of the box:
NtCreateFile— every file open with a decodedACCESS_MASK(READING/WRITING/READ/WRITE/QUERY).NtOpenProcess— flagsPROCESS_VM_WRITE | VM_OPERATION | CREATE_THREADas a likely injection prep.NtMapViewOfSection— distinguishes self-maps from remote maps, highlightsRWX/ executable remote mappings as hollowing candidates.
- Noise filter — read-only opens under
System32,SysWOW64,WinSxS,Fonts,WinSxS,Microsoft.NET,assembly, etc. are suppressed by default. Writes always come through. - Dedicated console + file log —
AllocConsolewindow withSetConsoleTextAttributecolor coding by severity, mirrored to%TEMP%\VoidSniff_<pid>.log. - Hot unload — press F10 in the host process to safely uninstall all hooks and
FreeLibraryAndExitThread. - Zero import deps — all
Nt*functions are resolved dynamically throughGetProcAddress(ntdll, …), so nontdll.lib/ SDK headers are required at link time.
Requirements:
- Windows x64
- Visual Studio 2022 (MSVC v143) with the Desktop development with C++ workload
- CMake 3.20+
From a Developer Command Prompt or a regular shell with CMake in PATH:
build.batThis is just a thin wrapper around:
cmake -S . -B build -A x64
cmake --build build --config ReleaseOutput: build\Release\VoidSniff.dll.
The project is hard-pinned to x64 + MSVC; the CMake config will fail loudly on anything else.
VoidSniff is a plain DLL — load it into a target process with any injector you trust (manual map, CreateRemoteThread + LoadLibrary, a debugger-driven load, etc.). On DLL_PROCESS_ATTACH it spawns a worker thread which:
- Loads
void_sniff.ininext to the DLL (optional — falls back to defaults). - Allocates a console titled "VoidSniff - Syscall Monitor".
- Installs the three hooks and starts streaming events.
- Waits for F10 and then cleanly removes all hooks and unloads itself.
[ INFO ] VoidSniff initializing in host process...
[ INFO ] PID = 12345, Module base = 00007FF8A12B0000
[ INFO ] VoidSniff: 3/3 hooks active.
[SUCCESS ] VoidSniff ready. Press F10 to unload safely.
[SYSCALL ] [FILE] Opened for READING -> C:\Users\me\Documents\notes.txt (GENERIC_READ|SYNCHRONIZE)
[WARNING ] [FILE] Opened for WRITING -> C:\Users\me\AppData\Local\Temp\out.bin (GENERIC_WRITE)
[WARNING ] [PROC] SUSPICIOUS open of pid=4321 -> VM_OPERATION|VM_WRITE|CREATE_THREAD <injection-prep>
[WARNING ] [MEM] Mapped REMOTE section -> EXECUTE_READ <hollowing/injection candidate>
Place next to the DLL:
[Filters]
; Suppress read-only opens of C:\Windows\System32, SysWOW64, WinSxS, Fonts,
; assembly and similar boring system directories. Writes always pass through.
IgnoreSystemDlls=true
[Logging]
; Allocate a console window and stream the activity feed to it.
LogToConsole=true
; Mirror every line to %TEMP%\VoidSniff_<pid>.log via WriteFile.
LogToFile=trueVoidSniff/
├── CMakeLists.txt
├── build.bat
├── void_sniff.ini
├── include/voidsniff/
│ ├── Config.h
│ ├── HookManager.h
│ ├── Hooks.h
│ └── Logger.h
└── src/
├── dllmain.cpp
├── Config.cpp
├── HookManager.cpp
├── Hooks.cpp
└── Logger.cpp
For each target, HookManager::Install:
- Resolves the target address (typically
ntdll!NtXxx). - Snapshots the first 14 bytes (
kPatchSize). - Builds a patch of the form
FF 25 00 00 00 00 <abs64-target>— an absolute indirectJMPwhose 64-bit operand lives inline right after the opcode. - Flips the page to
PAGE_EXECUTE_READWRITE,memcpys the patch in, restores the original protection, and flushes the instruction cache.
Calling the original is done without a trampoline: CallOriginal takes the per-hook mutex, disarms the patch (writes back the original 14 bytes), calls the function, then rearms on scope exit. A thread_local bool g_in_original_call short-circuits recursion if the detour ever re-enters during the unguarded window. This is simpler than a relocation trampoline at the cost of serializing concurrent callers of the same syscall through one mutex — a fine trade-off for a monitoring DLL, not for a production hooking framework.
- x64 only, MSVC only, Windows only. By design.
- The no-trampoline design serializes calls per hooked syscall; not appropriate for high-throughput hooking.
- Patches the very first 14 bytes of the function. Anti-tamper / PatchGuard-protected processes, or anything that already inline-hooks the same prologue, will conflict.
- Only three syscalls are wired up. Adding more is a matter of writing a detour and appending to the
plans[]table inHooks.cpp. - No GUI, no IPC — the activity feed is the console window and the log file.
MIT — see LICENSE.