NtClose Invalid Handle
Malware closes a deliberately invalid handle; under a debugger the kernel raises STATUS_INVALID_HANDLE, so catching that exception reveals an attached debugger.
Closing an invalid handle behaves differently depending on whether a debugger is attached. When a process runs without a debugger, CloseHandle/NtClose on a bogus handle simply returns an error. But when a debugger is present, the kernel raises a STATUS_INVALID_HANDLE (0xC0000008) exception. Malware wraps the close in a try/except block: if the exception fires, it concludes a debugger is attached.
This is a pure side-effect check — there is no debugger-detection API involved, which makes it harder to neutralise with API hooks alone.
How it works
The code passes a garbage handle value to NtClose (or CloseHandle) inside an exception handler. The handler running is the signal.
#include <windows.h>
typedef NTSTATUS (NTAPI *pfnNtClose)(HANDLE);
BOOL DebuggerByInvalidHandle(void)
{
HMODULE hNtdll = GetModuleHandleW(L"ntdll.dll");
pfnNtClose NtClose = (pfnNtClose)GetProcAddress(hNtdll, "NtClose");
__try {
NtClose((HANDLE)0xDEADBEEF); // invalid handle
}
__except (GetExceptionCode() == 0xC0000008 /* STATUS_INVALID_HANDLE */
? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {
return TRUE; // debugger present
}
return FALSE;
}Any obviously invalid value works (0xDEADBEEF, an odd number, a closed handle). The distinguishing behaviour is that the exception is only raised when the process is being debugged.
Detection & bypass
- Static — Grep in IDA/Ghidra for
NtClose/CloseHandlecalls with a constant, non-handle argument (e.g.0xDEADBEEF) inside a structured-exception (__try/__except) frame. The comparisonGetExceptionCode() == 0xC0000008(constant0xC0000008) in an exception filter is the giveaway. - Dynamic — In x64dbg, breakpoint on
ntdll!NtCloseand inspect the handle argument; a junk value confirms the trick. Watch for the0xC0000008first-chance exception — configure the debugger to pass it to the application so the handler does not interpret it as "debugger present". - Patch / bypass — Let the debugger forward the exception to the app (do not break on it), or patch the call: NOP the
NtClosecall, or force the__exceptfilter / result so the "debugger present" branch is never taken. You can also hookNtCloseto return cleanly for invalid handles. - Tools — ScyllaHide's "NtClose" option intercepts the call and suppresses the spurious exception; x64dbg exception settings (pass
0xC0000008to the application); TitanHide at the kernel level.