Go back

Contents

Misc


Misc


1. FindWindow()

This technique includes the simple enumeration of window classes in the system and comparing them with known windows classes of debuggers.

The following functions can be used:

  • user32!FindWindowW()
  • user32!FindWindowA()
  • user32!FindWindowExW()
  • user32!FindWindowExA()

C/C++ Code

const std::vector<std::string> vWindowClasses = {
    "antidbg",
    "ID",               // Immunity Debugger
    "ntdll.dll",        // peculiar name for a window class
    "ObsidianGUI",
    "OLLYDBG",
    "Rock Debugger",
    "SunAwtFrame",
    "Qt5QWindowIcon"
    "WinDbgFrameClass", // WinDbg
    "Zeta Debugger",
};

bool IsDebugged()
{
    for (auto &sWndClass : vWindowClasses)
    {
        if (NULL != FindWindowA(sWndClass.c_str(), NULL))
            return true;
    }
    return false;
}


2. Parent Process Check

Normally, a user-mode process is executed by double-clicking on a file icon. If the process is executed this way, its parent process will be the shell process (“explorer.exe”).

The main idea of the two following methods is to compare the PID of the parent process with the PID of “explorer.exe”.


2.1. NtQueryInformationProcess()

This method includes obtaining the shell process window handle using user32!GetShellWindow() and obtaining its process ID by calling user32!GetWindowThreadProcessId().

Then, the parent process ID can be obtained from the PROCESS_BASIC_INFORMATION structure by calling ntdll!NtQueryInformationProcess() with the ProcessBasicInformation class.


C/C++ Code

bool IsDebugged()
{
    HWND hExplorerWnd = GetShellWindow();
    if (!hExplorerWnd)
        return false;

    DWORD dwExplorerProcessId;
    GetWindowThreadProcessId(hExplorerWnd, &dwExplorerProcessId);

    ntdll::PROCESS_BASIC_INFORMATION ProcessInfo;
    NTSTATUS status = ntdll::NtQueryInformationProcess(
        GetCurrentProcess(),
        ntdll::PROCESS_INFORMATION_CLASS::ProcessBasicInformation,
        &ProcessInfo,
        sizeof(ProcessInfo),
        NULL);
    if (!NT_SUCCESS(status))
        return false;

    return (DWORD)ProcessInfo.InheritedFromUniqueProcessId != dwExplorerProcessId;
}


2.2. CreateToolhelp32Snapshot()

The parent process ID and the parent process name can be obtained using the kernel32!CreateToolhelp32Snapshot() and kernel32!Process32Next() functions.


C/C++ Code

DWORD GetParentProcessId(DWORD dwCurrentProcessId)
{
    DWORD dwParentProcessId = -1;
    PROCESSENTRY32W ProcessEntry = { 0 };
    ProcessEntry.dwSize = sizeof(PROCESSENTRY32W);

    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if(Process32FirstW(hSnapshot, &ProcessEntry))
    {
        do
        {
            if (ProcessEntry.th32ProcessID == dwCurrentProcessId)
            {
                dwParentProcessId = ProcessEntry.th32ParentProcessID;
                break;
            }
        } while(Process32NextW(hSnapshot, &ProcessEntry));
    }

    CloseHandle(hSnapshot);
    return dwParentProcessId;
}

bool IsDebugged()
{
    bool bDebugged = false;
    DWORD dwParentProcessId = GetParentProcessId(GetCurrentProcessId());

    PROCESSENTRY32 ProcessEntry = { 0 };
    ProcessEntry.dwSize = sizeof(PROCESSENTRY32W);

    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if(Process32First(hSnapshot, &ProcessEntry))
    {
        do
        {
            if ((ProcessEntry.th32ProcessID == dwParentProcessId) &&
                (strcmp(ProcessEntry.szExeFile, "explorer.exe")))
            {
                bDebugged = true;
                break;
            }
        } while(Process32Next(hSnapshot, &ProcessEntry));
    }

    CloseHandle(hSnapshot);
    return bDebugged;
}


3. Selectors

Selector values might appear to be stable, but they are actually volatile in certain circumstances, and also depending on the version of Windows. For example, a selector value can be set within a thread, but it might not hold that value for very long. Certain events might cause the selector value to be changed back to its default value. One such event is an exception. In the context of a debugger, the single-step exception is still an exception, which can cause some unexpected behavior.

x86 Assembly

    xor  eax, eax 
    push fs 
    pop  ds 
l1: xchg [eax], cl 
    xchg [eax], cl

On the 64-bit versions of Windows, single-stepping through this code will cause an access violation exception at l1 because the DS selector will be restored to its default value even before l1 is reached. On the 32-bit versions of Windows, the DS selector will not have its value restored, unless a non-debugging exception occurs. The version-specific difference in behaviors expands even further if the SS selector is used. On the 64-bit versions of Windows, the SS selector will be restored to its default value, as in the DS selector case. However, on the 32-bit versions of Windows, the SS selector value will not be restored, even if an exception occurs.

x86-64 Assembly

    xor  eax, eax 
    push offset l2 
    push d fs:[eax] 
    mov  fs:[eax], esp 
    push fs 
    pop  ss 
    xchg [eax], cl 
    xchg [eax], cl 
l1: int  3 ;force exception to occur 
l2: ;looks like it would be reached 
    ;if an exception occurs 
    ...

then when the “int 3” instruction is reached at l1 and the breakpoint exception occurs, the exception handler at l2 is not called as expected. Instead, the process is simply terminated.

A variation of this technique detects the single-step event by simply checking if the assignment was successful.

push 3 
pop  gs 
mov  ax, gs 
cmp  al, 3 
jne  being_debugged

The FS and GS selectors are special cases. For certain values, they will be affected by the single-step event, even on the 32-bit versions of Windows. However, in the case of the FS selector (and, technically, the GS selector), it will be not restored to its default value on the 32-bit versions of Windows, if it was set to a value from zero to three. Instead, it will be set to zero (the GS selector is affected in the same way, but the default value for the GS selector is zero). On the 64-bit versions of Windows, it (they) will be restored to its (their) default value.

This code is also vulnerable to a race condition caused by a thread-switch event. When a thread-switch event occurs, it behaves like an exception, and will cause the selector values to be altered, which, in the case of the FS selector, means that it will be set to zero.

A variation of this technique solves that problem by waiting intentionally for a thread-switch event to occur.

    push 3 
    pop  gs 
l1: mov  ax, gs 
    cmp  al, 3 
    je   l1

However, this code is vulnerable to the problem that it was trying to detect in the first place, because it does not check if the original assignment was successful. Of course, the two code snippets can be combined to produce the desired effect, by waiting until the thread-switch event occurs, and then performing the assignment within the window of time that should exist until the next one occurs. [Ferrie]


C/C++ Code

bool IsTraced()
{
    __asm
    {
        push 3
        pop  gs

    __asm SeclectorsLbl:
        mov  ax, gs
        cmp  al, 3
        je   SeclectorsLbl

        push 3
        pop  gs
        mov  ax, gs
        cmp  al, 3
        jne  Selectors_Debugged
    }

    return false;

Selectors_Debugged:
    return true;
}


4. DbgPrint()

The debug functions such as ntdll!DbgPrint() and kernel32!OutputDebugStringW() cause the exception DBG_PRINTEXCEPTION_C (0x40010006). If a program is executed with an attached debugger, then the debugger will handle this exception. But if no debugger is present, and an exception handler is registered, this exception will be caught by the exception handler.


C/C++ Code

bool IsDebugged()
{
    __try
    {
        RaiseException(DBG_PRINTEXCEPTION_C, 0, 0, 0);
    }
    __except(GetExceptionCode() == DBG_PRINTEXCEPTION_C)
    {
        return false;
    }

    return true;
}


5. DbgSetDebugFilterState()

The functions ntdll!DbgSetDebugFilterState() and ntdll!NtSetDebugFilterState() only set a flag which will be checked be a kernel-mode debugger if it is present. Therefore, if a kernel debugger is attached to the system, these functions will succeed. However, the functions can also succeed because of side-effects caused by some user-mode debuggers. These functions require administrator privileges.


C/C++ Code

bool IsDebugged()
{
    return NT_SUCCESS(ntdll::NtSetDebugFilterState(0, 0, TRUE));
}


6. NtYieldExecution() / SwitchToThread()

This method is not really reliable because it only shows if there a high priority thread in the current process. However, it could be used as an anti-tracing technique.

When an application is traced in a debugger and a single-step is executed, the context can’t be switched to other thread. This means that ntdll!NtYieldExecution() returns STATUS_NO_YIELD_PERFORMED (0x40000024), which leads to kernel32!SwitchToThread() returning zero.

The strategy of using this technique is that there is a loop which modifies some counter if kernel32!SwitchToThread() returns zero, or ntdll!NtYieldExecution() returns STATUS_NO_YIELD_PERFORMED. This can be a loop which decrypts strings or some other loop which is supposed to be analyzed manually in a debugger. If the counter has the expected value (expected i.e. the value if all kernel32!SwitchToThread() returned zero) after leaving the loop, we consider that the debugger is present.

In the example below, we define a one-byte counter (initialized with 0) which shifts one bit to the left if kernel32!SwitchToThread returns zero. If it shifts 8 times, then the value of the counter will become 0 and the debugger is considered to be present.


C/C++ Code

bool IsDebugged()
{
    BYTE ucCounter = 1;
    for (int i = 0; i < 8; i++)
    {
        Sleep(0x0F);
        ucCounter <<= (1 - SwitchToThread());
    }

    return ucCounter == 0;
}


7. VirtualAlloc() / GetWriteWatch()

This technique was described as a suggestion for a famous al-khaser solution, a tool for testing VMs, debuggers, sandboxes, AV, etc. against many malware-like defences.

The idea is drawn from the documentation for GetWriteWatch function where the following is stated in a “Remarks” section:

“When you call the VirtualAlloc function to reserve or commit memory, you can specify MEM_WRITE_WATCH. This value causes the system to keep track of the pages that are written to in the committed memory region. You can call the GetWriteWatch function to retrieve the addresses of the pages that have been written to since the region has been allocated or the write-tracking state has been reset”.

This feature can be used to track debuggers that may modify memory pages outside the expected pattern.


C/C++ Code (variant 1)

bool Generic::CheckWrittenPages1() const {
    const int SIZE_TO_CHECK = 4096;

    PVOID* addresses = static_cast<PVOID*>(VirtualAlloc(NULL, SIZE_TO_CHECK * sizeof(PVOID), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
    if (addresses == NULL)
    {
        return true;
    }

    int* buffer = static_cast<int*>(VirtualAlloc(NULL, SIZE_TO_CHECK * SIZE_TO_CHECK, MEM_RESERVE | MEM_COMMIT | MEM_WRITE_WATCH, PAGE_READWRITE));
    if (buffer == NULL)
    {
        VirtualFree(addresses, 0, MEM_RELEASE);
        return true;
    }

    // Read the buffer once
    buffer[0] = 1234;

    ULONG_PTR hits = SIZE_TO_CHECK;
    DWORD granularity;
    if (GetWriteWatch(0, buffer, SIZE_TO_CHECK, addresses, &hits, &granularity) != 0)
    {
        return true;
    }
    else
    {
        VirtualFree(addresses, 0, MEM_RELEASE);
        VirtualFree(buffer, 0, MEM_RELEASE);

        return (hits == 1) ? false : true;
    }
}

C/C++ Code (variant 2)

bool Generic::CheckWrittenPages2() const {
    BOOL result = FALSE, error = FALSE;

    const int SIZE_TO_CHECK = 4096;

    PVOID* addresses = static_cast<PVOID*>(VirtualAlloc(NULL, SIZE_TO_CHECK * sizeof(PVOID), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
    if (addresses == NULL)
    {
        return true;
    }

    int* buffer = static_cast<int*>(VirtualAlloc(NULL, SIZE_TO_CHECK * SIZE_TO_CHECK, MEM_RESERVE | MEM_COMMIT | MEM_WRITE_WATCH, PAGE_READWRITE));
    if (buffer == NULL)
    {
        VirtualFree(addresses, 0, MEM_RELEASE);
        return true;
    }

    // Make some calls where a buffer *can* be written to, but isn't actually edited because we pass invalid parameters    
    if (GlobalGetAtomName(INVALID_ATOM, (LPTSTR)buffer, 1) != FALSE
        || GetEnvironmentVariable("This variable does not exist", (LPSTR)buffer, 4096 * 4096) != FALSE
        || GetBinaryType("This name does not exist", (LPDWORD)buffer) != FALSE
        || HeapQueryInformation(0, (HEAP_INFORMATION_CLASS)69, buffer, 4096, NULL) != FALSE
        || ReadProcessMemory(INVALID_HANDLE_VALUE, (LPCVOID)0x69696969, buffer, 4096, NULL) != FALSE
        || GetThreadContext(INVALID_HANDLE_VALUE, (LPCONTEXT)buffer) != FALSE
        || GetWriteWatch(0, &result, 0, NULL, NULL, (PULONG)buffer) == 0)
    {
        result = false;
        error = true;
    }

    if (error == FALSE)
    {
        // A this point all calls failed as they're supposed to
        ULONG_PTR hits = SIZE_TO_CHECK;
        DWORD granularity;
        if (GetWriteWatch(0, buffer, SIZE_TO_CHECK, addresses, &hits, &granularity) != 0)
        {
            result = FALSE;
        }
        else
        {
            // Should have zero reads here because GlobalGetAtomName doesn't probe the buffer until other checks have succeeded
            // If there's an API hook or debugger in here it'll probably try to probe the buffer, which will be caught here
            result = hits != 0;
        }
    }

    VirtualFree(addresses, 0, MEM_RELEASE);
    VirtualFree(buffer, 0, MEM_RELEASE);

    return result;
}


8. IFEO removal

This technique involves modifying the Image File Execution Options (IFEO) registry key, which is used by the Windows operating system to set debugging options for executable files. When an executable file is launched, the operating system checks the corresponding IFEO registry key for any specified debugging options. If the key exists, the operating system launches the specified debugger instead of the executable file. Removing these entries further complicates analysis efforts by eliminating one potential avenue for researchers to attach debuggers to the malware process.

Check if the following process names are being removed (also check if the current process name is being removed)
Path value
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options rundll32.exe
regsvr32.exe
dllhost.exe
msiexec.exe
explorer.exe
odbcconf.exe

C/C++ Code

int main() {
    // Define the registry key path for explorer.exe under IFEO
    LPCWSTR keyPath = L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Image File Execution Options\\explorer.exe";

    // Attempt to open the registry key
    LONG result = RegDeleteKey(HKEY_LOCAL_MACHINE, keyPath);

    if (result == ERROR_SUCCESS) {
        std::cout << "IFEO entry for explorer.exe successfully deleted." << std::endl;
    } else {
        std::cerr << "Failed to delete IFEO entry for explorer.exe. Error code: " << result << std::endl;
    }

    return 0;
}


Mitigations

During debugging: Fill anti-debug pr anti-traced checks with NOPs.

For anti-anti-debug tool development:

  1. For FindWindow(): Hook user32!NtUserFindWindowEx(). In the hook, call the original user32!NtUserFindWindowEx() function. If it is called from the debugged process and the parent process looks suspicious, then return unsuccessfully from the hook.

  2. For Parent Process Checks: Hook ntdll!NtQuerySystemInformation(). If SystemInformationClass is one of the following values:
    • SystemProcessInformation
    • SystemSessionProcessInformation
    • SystemExtendedProcessInformation

    and the process name looks suspicious, then the hook must modify the process name.

  3. For Selectors: No mitigations.

  4. For DbgPrint: you have to implement a plugin for a specific debugger and change the behavior of event handler which is triggered after the DBG_PRINTEXCEPTION_C exception has arrived.

  5. For DbgSetDebugFilterState(): Hook ntdll!NtSetDebugFilterState(). If the process is running with debug privileges, return unsuccessfully from the hook.

  6. For SwitchToThread: Hook ntdll!NtYieldExecution() and return an unsuccessful status from the hook.

  7. For GetWriteWatch: Hook VirtualAlloc() and GetWriteWatch() to track if VirtualAlloc() is called with MEM_WRITE_WATCH flag. If it is the case, check what is the region to track and return the expected value in GetWriteWatch().

Go back