Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
HallWatch — Usermode detector that catches indirect syscalls. Traps Hell's Hall, Tartarus' Gate, RecycledGate, and VEH syscalls & Many more. | Kitploit
Tools/GitHubGitHub/zypherion-technologies/hallwatch
Defensive ToolsMalware AnalysisBinary AnalysisThreat IntelligenceIncident Response
GitHubzypherion-technologies/hallwatch

HallWatch

Usermode detector that catches indirect syscalls. Traps Hell's Hall, Tartarus' Gate, RecycledGate, and VEH syscalls & Many more.

View Repository
8710103 months agoReviewed by Kitploit

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

HallWatch

License: GPL v3 Website Discord Telegram X

Copyright (C) 2026 Adam Zypherion <[email protected]> — licensed under GPL-3.0.


Indirect syscalls are simple once you have seen one. The loader walks ntdll, reads the SSN out of Nt* prologue, finds the 0F 05 two bytes further in, sets r10/rax/rdx/r8/r9 itself, and jmps straight at those two bytes. The syscall happens from inside ntdll. Your hook on the kernel32 export is never touched. Your hook on the ntdll export is never touched. [RSP] at the moment of the SYSCALL points back into the loaders RWX page, but nothing about the call looks unusual from outside.

HellHall proc
    mov  r10, rcx
    mov  eax, dwSSN
    jmp  qword ptr [qAddr]
    ret
HellHall endp

The variants have names. Tartarus' Gate and RecycledGate use the syscall instruction belonging to one stub but the SSN of a different one, so even if you log the stub name your hook reports, it is a lie. VEH syscall triggers an access violation intentionally and uses its own VEH to rewrite the context so RIP lands on ntdll's syscall instruction with the registers already prepped. Hell's Gate does not use ntdll at all. The loader writes 0F 05 into its own RWX page and runs from there.

The kernel mode (KM) answer is a driver, i might actually get to this overtime and release an project but not happening anytime soon :D


PAGE_GUARD looks like it should work. Mark the page that holds the syscall bytes with PAGE_GUARD, catch the STATUS_GUARD_PAGE_VIOLATION in a VEH, inspect, redirect to a private trampoline, set the trap flag, single-step out, arm the page back. One trap per Nt call, no matter how the loader got there. It works against everything except Hell's Gate.

The problem is the OS does not cooperate. PAGE_GUARD is a one shot what does that mean? every time it fires the bit is cleared and you have to put it back. Your handler does syscalls (NtProtect to put the guard back, NtContinue to resume) and those syscalls have stubs and those stubs are on the page you just guarded. i worked around most of that with private syscall stubs built on a separate page we owned (allocate RWX, write mov r10,rcx; mov eax,SSN; syscall; ret, lock RX, never touch ntdll's), but it was fragile. Every Windows minor version changed the timing. Every thread the sample spawned was another race against the integrity worker that was rebuilding the guard. Demo success was around 30 to 50 percent across ten runs on the same machine .

Eventually I stopped trying to convince Windows that PAGE_GUARD should behave the way I wanted, and tried overwriting the byte instead.


If u have issues understanding, watch this video :)

https://github.com/user-attachments/assets/0cb670fd-e51b-413e-bf00-08f9297888ed


The byte at the syscall instruction is 0F 05. The first byte alone (0F) is the prefix for a family of two byte opcodes including SYSCALL, CPUID, and RDTSC. By itself it is not executable; the CPU needs the second byte to decode. If you replace the first byte with 0xCC (INT3), the pair becomes CC 05, which the CPU decodes as INT3 followed by a stray byte it never reaches. Any code path that lands on that address raises EXCEPTION_BREAKPOINT.

That is the whole mechanism. Our VEH catches the breakpoint, looks up which stub the address belongs to (we build the map at init time by enumerating ntdll's exports), runs three checks on whoever turned up, and sets Context->Rip to a private trampoline that does the real syscall and returns. The byte stays CC. The next caller hits it the same way so no page protection flipping back and forth.

To be clear: yes, this is hooking by byte overwrite. it is just not at the byte people usually mean by "ntdll hook". the classic EDR hook overwrites the first bytes of the stub with a JMP <my_func>:

ntdll!NtAllocateVirtualMemory:
  E9 ?? ?? ?? ??       jmp my_hook     ; overwrites "mov r10, rcx"
  ...
  0F 05                syscall
  C3                   ret

that is exactly what indirect syscalls walk around. the loader reads the SSN itself and jumps straight at the 0F 05, the prologue (and your jmp) never runs, the hook never fires. what we do is overwrite the syscall byte itself:

ntdll!NtAllocateVirtualMemory:
  4C 8B D1             mov r10, rcx     ; untouched
  B8 18 00 00 00       mov eax, 18h     ; untouched
  CC 05                int3 / 05        ; was 0F 05, we wrote CC over the 0F
  C3                   ret

now it does not matter how you got to that address. through the prologue, through an indirect jump that skips the prologue, through a VEH-syscall context rewrite that drops RIP there, whatever. if the CPU executes that byte, it traps. same family of technique as the classic hook, different location, completely different coverage.

The trampoline looks like this:

F3 0F 1E FA              endbr64
49 89 CA                 mov r10, rcx
B8 <stub SSN>            mov eax, ssn
0F 05                    syscall
C3                       ret

The three checks are unchanged from the PAGE_GUARD version because they were the right three checks; they just needed somewhere reliable to live.

Return address. [RSP] is where the syscall would have returned to. For a real call it is inside ntdll, kernel32, kernelbase, or one of the related runtime DLLs. For Hell's Hall it is inside whatever RWX page the loader is running from. We have a short list of trusted return targets built at init by calling GetModuleHandle for those names and reading the .text ranges out of their PE headers.

SSN. The stub's prologue has already run before it reaches the syscall byte, so eax holds whatever value was loaded into it. If the loader did a Tartarus swap, that value will not match the SSN we read out of this same stub at enumeration. We log the mismatch, and the trampoline writes the correct SSN before its own syscall, so the kernel function that runs is the one that belongs to the byte rather than the one the loader wanted. The technique gets logged and neutralised in the same step.

Stack walk. RtlVirtualUnwind from the current context, five frames up. Every frame's RIP should be inside a known module and should have a RUNTIME_FUNCTION entry. Shellcode and ROP gadgets fail this even when [RSP] itself looks trusted, which a loader can fake (it can predict roughly where its caller will be in memory and forge a believable return address there).

If any check fails, we push a small struct into a lock free ring and the drain thread prints it the next time it wakes

[!! hallwatch !!] indirect syscall (untrusted caller, wrong ssn for this stub)
    syscall      : NtAllocateVirtualMemory
    syscall rip  : 0x00007FF827660372
    return addr  : 0x00007FF7EED719D6
    rax (ssn)    : 0x0000000F (stub encodes 0x00000018)
    thread       : 26388

Hell's Gate is where INT3 stops helping. We never patched the loader's RWX page because we never knew it existed.

Download Tool