Detecting Hooked Syscalls
Last updated
Last updated
It's possible to enumerate which Windows API calls are hooked by an EDR using inline patching technique, where a jmp
instruction is inserted at the beginning of the syscall stub to be hooked.
Below shows the stub for for NtReadVirtualMemory
on a system with no EDR present, meaning the syscall NtReadVirtualMemory
is not hooked:
We can see the NtReadVirtualMemory
syscall stub starts with instructions:
The above applies to most routines starting with Zw
, i.e ZwReadVirtualMemory
too.
...which translates to the following 4 opcodes:
4c 8b d1 b8
- are important for this lab - we will come back to this in a moment in a section Checking for Hooks.
Below shows an example of how NtReadVirtualMemory
syscall stub looks like when it's hooked by an EDR:
Note that in this case, the first instruction is a jmp
instruction, redirecting the code execution somewhere else (another module in the process's memory):
...which translates to the following 5 opcodes:
e9
- opcode for near jump
0f64f8c7
- offset, which is relative to the address of the current instruction, where the code will jump to
Knowing that interesting functions/syscalls (that are often used in malware), starting with Nt
| Zw
, before hooking, start with opcodes: 4c 8b d1 b8
, we can determine if a given function is hooked or not by following this process:
Iterate through all the exported functions of the ntdll.dll
Read the first 4 bytes of the the syscall stub and check if they start with 4c 8b d1 b8
If yes, the function is not hooked
If no, the function is most likely hooked (with a couple of exceptions mentioned in the False Positives callout).
Below is a simplified visual example attempting to further explain the above process:
NtReadVirtualMemory
starts with opcodes e9 0f 64 f8
rather than 4c 8b d1 b8
, meaning it's most likely hooked
NtWriteVirtualMemory
starts with opcodes 4c 8b d1 b8
, meaning it has not been hooked
As additional verification for a function really being hooked by a different DLL, we can resolve the jump target and check which module it belongs to using GetMappedFileName.
This can also help detect false-positives. If the jump leads into ntdll.dll itself, it is either supposed to be there, or it could be a more sophisticated hook trying to disguise itself against this technique.
False Positives
****Although highly effective at detecting functions hooked with inline patching, this method returns a few false positives when enumerating hooked functions inside ntdll.dll, such as:
NtGetTickCount
NtQuerySystemTime
NtdllDefWindowProc_A
NtdllDefWindowProc_W
NtdllDialogWndProc_A
NtdllDialogWndProc_W
ZwQuerySystemTime
The above functions are not hooked.
Below is the code that we can compile and run on an endpoint running an AV/EDR to see enumerate APIs that were most likely hooked:
Below is a snippet of the output of the program compiled from the above source code and run on a system with an EDR present. It shows some of the interesting functions (not all displayed) that are most likely hooked, with an exception of NtGetTickCount
, which is a false positive, as mentioned earlier:
After I've posted this note on my twitter, I got a message from someone who is smarter than I am suggesting to check if the syscall
instruction itself is not hooked. The syscall
handler routine (responsible for locating functions in the SSDT based on a syscall number) location can be found by reading the Model Specific Register (MSR) at location 0xc0000082
and confirming that the address stored there points to nt!KiSystemCall64Shadow
.
Below shows how this could be done manually in WinBDG: