Skip to content
Anti-Analysisintermediate

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.

c
#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/CloseHandle calls with a constant, non-handle argument (e.g. 0xDEADBEEF) inside a structured-exception (__try/__except) frame. The comparison GetExceptionCode() == 0xC0000008 (constant 0xC0000008) in an exception filter is the giveaway.
  • Dynamic — In x64dbg, breakpoint on ntdll!NtClose and inspect the handle argument; a junk value confirms the trick. Watch for the 0xC0000008 first-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 NtClose call, or force the __except filter / result so the "debugger present" branch is never taken. You can also hook NtClose to return cleanly for invalid handles.
  • Tools — ScyllaHide's "NtClose" option intercepts the call and suppresses the spurious exception; x64dbg exception settings (pass 0xC0000008 to the application); TitanHide at the kernel level.
Votes

Comments(0)