Anti-Debug: Direct debugger interaction
Contents
- 1. Self-Debugging
- 2. GenerateConsoleCtrlEvent()
- 3. BlockInput()
- 4. NtSetInformationThread()
- 5. EnumWindows() and SuspendThread()
- 6. SwitchDesktop()
- 7. OutputDebugString()
- 8. Process Suspension Detection
- Mitigations
Direct debugger interaction
The following techniques let the running process manage a user interface or engage with its parent process to discover inconsistencies that are inherent for a debugged process.
1. Self-Debugging
There are at least three functions that can be used to attach as a debugger to a running process:
- kernel32!DebugActiveProcess()
- ntdll!DbgUiDebugActiveProcess()
- ntdll!NtDebugActiveProcess()
As only one debugger can be attached to a process at a time, a failure to attach to the process might indicate the presence of another debugger.
In the example below, we run the second instance of our process which tries to attach a debugger to its parent (the first instance of the process). If kenel32!DebugActiveProcess() finishes unsuccessfully, we set the named event which was created by the first instance. If the event is set, the first instance understands that a debugger is present.
C/C++ Code
2. GenerateConsoleCtrlEvent()
When a user presses Ctrl+C or Ctrl+Break and a console window is in the focus, Windows checks if there is a handler for this event. All console processes have a default handler function that calls the kernel32!ExitProcess() function. However, we can register a custom handler for these events which neglects the Ctrl+C or Ctrl+Break signals.
However, if a console process is being debugged and CTRL+C signals have not been disabled, the system generates a DBG_CONTROL_C exception. Usually this exception is intercepted by a debugger, but if we register an exception handler, we will be able to check whether DBG_CONTROL_C is raised. If we intercepted the DBG_CONTROL_C exception in our own exception handler, it may indicate that the process is being debugged.
C/C++ Code
3. BlockInput()
The function user32!BlockInput() can block all mouse and keyboard events, which is quite an effective way to disable a debugger. On Windows Vista and higher versions, this call requires administrator privileges.
We can also detect whether a tool that hooks the user32!BlockInput() and other anti-debug calls is present. The function allows the input to be blocked only once. The second call will return FALSE. If the function returns TRUE regardless of the input, it may indicate that some hooking solution is present.
C/C++ Code
4. NtSetInformationThread()
The function ntdll!NtSetInformationThread() can be used to hide a thread from a debugger. It is possible with a help of the undocumented value THREAD_INFORMATION_CLASS::ThreadHideFromDebugger (0x11). This is intended to be used by an external process, but any thread can use it on itself.
After the thread is hidden from the debugger, it will continue running but the debugger won’t receive events related to this thread. This thread can perform anti-debugging checks such as code checksum, debug flags verification, etc.
However, if there is a breakpoint in the hidden thread or if we hide the main thread from the debugger, the process will crash and the debugger will be stuck.
In the example, we hide the current thread from the debugger. This means that if we trace this code in the debugger or put a breakpoint to any instruction of this thread, the debugging will be stuck once ntdll!NtSetInformationThread() is called.
C/C++ Code
5. EnumWindows() and SuspendThread()
The idea of this technique is to suspend the owning thread of the parent process.
First, we need to verify whether the parent process is a debugger. This can be achieved by enumerating all top-level windows on the screen (using user32!EnumWindows() or user32!EnumThreadWindows()), searching the window for which process ID is the ID of the parent process (using user32!GetWindowThreadProcessId()), and checking the title of this window (by user32!GetWindowTextW()). If the window title of the parent process looks like a debugger title, we can suspend the owning thread using kernel32!SuspendThread() or ntdll!NtSuspendThread().
C/C++ Code
6. SwitchDesktop()
Windows supports multiple desktops per session. It is possible to select a different active desktop, which has the effect of hiding the windows of the previously active desktop, and with no obvious way to switch back to the old desktop.
Further, the mouse and keyboard events from the debugged process desktop will no longer be delivered to the debugger, because their source is no longer shared. This obviously makes debugging impossible.
C/C++ Code
7. OutputDebugString()
This technique is deprecated as it works only for Windows versions earlier than Vista. However, this technique is too well known to not mention here.
The idea is simple. If a debugger is not present and kernel32!OutputDebugString is called, then an error will occur.
C/C++ Code (variant1)
C/C++ Code (variant2)
8. Process Suspension Detection
This evasion depends on having the thread creation flag THREAD_CREATE_FLAGS_BYPASS_PROCESS_FREEZE that Microsoft added into Windows 10, version 1903 (19H1). This flag makes the thread ignore any PsSuspendProcess API being called.
Then an attacker can create two threads with this flag, one of which keeps suspending the other one until the suspend counter limit which is 127 is reached (suspend count is a signed 8-bit value).
When you get to the limit, every call for PsSuspendProcess doesn’t increment the suspend counter and returns STATUS_SUSPEND_COUNT_EXCEEDED. But what happens if someone calls NtResumeProcess? It decrements the suspend count! So when someone decides to suspend and resume the thread, they’ll leave the count in a state it wasn’t previously in.
C/C++ Code
Mitigations
During debugging, it is better to skip suspicious function calls (e.g. fill them with NOPs).
If you write an anti-anti-debug solution, all the following functions can be hooked:
- kernel32!DebugActiveProcess
- ntdll!DbgUiDebugActiveProcess
- ntdll!NtDebugActiveProcess
- kernel32!GenerateConsoleCtrlEvent()
- user32!NtUserBlockInput
- ntdll!NtSetInformationThread
- user32!NtUserBuildHwndList (for filtering EnumWindows output)
- kernel32!SuspendThread
- user32!SwitchDesktop
- kernel32!OutputDebugStringW
Hooked functions can check input arguments and modify the original function behavior. To circumvent the Process Suspension detection evasion, you can hook NtCreateThread to omit the THREAD_CREATE_FLAGS_BYPASS_PROCESS_FREEZE flag.