Implementing Windows Anti-Debugging Techniques

Background

  • Implemented for CTF Challenges
    • Give participants experience with reversing hardened Windows binaries
    • Many CTFs focus heavily on Unix based Reverse Engineering

Focused on three anti-debugging techniques

Heap Flag Checks

  • Debuggers can modify flags in heap
    • Flag that tracks state of heap (almost always growable) 
    • The Force Flags (which should be not set by a program alone). 
  • Check whether these flags have unexpected values
    • Need to implement for both 32 and 64 bit 

Windows NT, 2000 and XP 32 bit Heap Flags

Windows XP 64bit and Up ​

Force Flags

/* Gets the Heap Flag Offset */
int GetHeapFlagsOffset(bool x64)
{
    int retVal = 0;
    if (x64) {
        if (IsVistaOrHigher()) {
            retVal = 0x70;
        }
        else {
            retVal = 0x14;
        }
    }
    else {
        if (IsVistaOrHigher()) {
            retVal = 0x40;
        }
        else {
            retVal = 0x0C;
        }
    }
    return retVal;
}

/* Returns the Heap Force Flags offset */
int GetHeapForceFlagsOffset(bool x64)
{
    int retVal = 0;
    if (x64) {
        if (IsVistaOrHigher()) {
            retVal = 0x74;
        }
        else {
            retVal = 0x18;
        }
    }
    else {
        if (IsVistaOrHigher()) {
            retVal = 0x44;
        }
        else {
            retVal = 0x10;
        }
    }
    return retVal;
}
int CheckHeap()
{
	/* Initializes variables and retrieves the process environment blocks */
    PVOID pPeb = GetPEB();
    PVOID pPeb64 = GetPEB64();
    PVOID heap = 0;
    DWORD offsetProcessHeap = 0;
    PDWORD heapFlagsPtr = 0, heapForceFlagsPtr = 0;
    BOOL x64 = FALSE;
#ifdef _WIN64
    x64 = TRUE;
    offsetProcessHeap = 0x30;
#else
    offsetProcessHeap = 0x18;
#endif
	/* Get the heap and the flags associated with it */
    heap = (PVOID) * (PDWORD_PTR)((PBYTE)pPeb + offsetProcessHeap);
    heapFlagsPtr = (PDWORD)((PBYTE)heap + GetHeapFlagsOffset(x64));
    heapForceFlagsPtr = (PDWORD)((PBYTE)heap + GetHeapForceFlagsOffset(x64));
    if (*heapFlagsPtr & ~HEAP_GROWABLE || *heapForceFlagsPtr != 0)
    {
    	/* First instance of catching modified heap flags */
        printf("That is not allowed!\n");
        return -1;
    }
    /* For the case where we are using Process Environment Block 64 */
    if (pPeb64)
    {
    	/* Get the heap and the flags associated with it */
        heap = (PVOID) * (PDWORD_PTR)((PBYTE)pPeb64 + 0x30);
        heapFlagsPtr = (PDWORD)((PBYTE)heap + GetHeapFlagsOffset(true));
        heapForceFlagsPtr = (PDWORD)((PBYTE)heap + GetHeapForceFlagsOffset(true));
        if (*heapFlagsPtr & ~HEAP_GROWABLE || *heapForceFlagsPtr != 0)
        {
        	/* Second instance of catching modified heap flags */
            printf("That is not allowed!\n");
            return -1;
        }
    }
    return 0;
}

Heap Flag Check Bypass

  • Override their value when being debugged 
  • Set the HEAP_GROWABLE flag for the Heap Flags
  • Set the Force Flags to 0

It is possible by using a different debugger, you can bypass this.

Checksum to Detect Software Breakpoints

  • Performs checksum of functions. 
    • Checks for injected instructions (like int 3)
    • Need to calculate checksum beforehand 
  • If checksums don't match, then it is likely a debugger is being used. 
DWORD g_origCrc = value; //Initialize with Crc value
DWORD CalcFuncCrc(PUCHAR funcBegin, PUCHAR funcEnd)
{
    DWORD crc = 0;
    for (; funcBegin < funcEnd; ++funcBegin)
    {
        crc += *funcBegin;
    }
    return crc;
}

VOID BeginSimulation(){
	//Code Goes here
}

VOID EndSimulation() {
	//Can be empty or do nothing
}

int main(){
    DWORD crc = CalcFuncCrc((PUCHAR)BeginSimulation, (PUCHAR)EndSimulation);
    if (g_origCrc != crc)
    {                                      
        printf("Stop debugging program!");
        exit(-1);
    }
}

DWORD g_origCrc = value; //Initialize with Crc value
DWORD CalcFuncCrc(PUCHAR funcBegin, PUCHAR funcEnd)
{
    DWORD crc = 0;
    for (; funcBegin < funcEnd; ++funcBegin)
    {
        crc += *funcBegin;
    }
    return crc;
}

VOID BeginSimulation(){
	//Code Goes here
}

VOID EndSimulation() {
	//Can be empty or do nothing
}

int main(){
    DWORD crc = CalcFuncCrc((PUCHAR)BeginSimulation, (PUCHAR)EndSimulation);
    if (g_origCrc != crc)
    {                                      
        printf("Stop debugging program!");
        exit(-1);
    }
}

The address to Begin Simulation and End Simulation are used to calculate funcBegin and FuncEnd

Checksum Bypass

  • No universal way of bypassing
    • Implementation dependent
  • Override the value being checked with the value of the original checksum
  • Change the output of calculating checksum with a constant

Hardware Breakpoint Check

  • Uses Windows API CONTEXT_DEBUG_REGISTERS to determine whether debug registers are set.
  • Dr0 - Dr3 are for hardware breakpoints (Dr4-Dr7 are used for other purposes).
int main()
{
    CONTEXT ctx = {};
    ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
    if (GetThreadContext(GetCurrentThread(), &ctx))
    {
        if (ctx.Dr0 != 0 || ctx.Dr1 != 0 || ctx.Dr2 != 0 || ctx.Dr3 != 0)
        {
            printf("Stop debugging program!");
            exit(-1);
        }
    }
}

Hardware Breakpoint Check Bypass

  • Based on the Windows API We know that GetThreadContext calls NtGetContextThread
  • After Retrieving thread, reset the CONTEXT_DEBUG_REGISTERS
  • Sets Dr0 - Dr7 to all be 0
  • Can be implemented through NtSetContextThread 

0

0

0

0

0

0

0

0

/* From https://www.apriorit.com/dev-blog/367-anti-reverse-engineering-protection-techniques-to-use-before-releasing-software */
typedef NTSTATUS(NTAPI *pfnNtGetContextThread)(
    _In_  HANDLE             ThreadHandle,
    _Out_ PCONTEXT           pContext
    );
typedef NTSTATUS(NTAPI *pfnNtSetContextThread)(
    _In_ HANDLE              ThreadHandle,
    _In_ PCONTEXT            pContext
    );
pfnNtGetContextThread g_origNtGetContextThread = NULL;
pfnNtSetContextThread g_origNtSetContextThread = NULL;
NTSTATUS NTAPI HookNtGetContextThread(
    _In_  HANDLE              ThreadHandle,
    _Out_ PCONTEXT            pContext)
{
    DWORD backupContextFlags = pContext->ContextFlags;
    pContext->ContextFlags &= ~CONTEXT_DEBUG_REGISTERS;
    NTSTATUS status = g_origNtGetContextThread(ThreadHandle, pContext);
    pContext->ContextFlags = backupContextFlags;
    return status;
}
NTSTATUS NTAPI HookNtSetContextThread(
    _In_ HANDLE              ThreadHandle,
    _In_ PCONTEXT            pContext)
{
    DWORD backupContextFlags = pContext->ContextFlags;
    pContext->ContextFlags &= ~CONTEXT_DEBUG_REGISTERS;
    NTSTATUS status = g_origNtSetContextThread(ThreadHandle, pContext);   
    pContext->ContextFlags = backupContextFlags;
    return status;
}
void HookThreadContext()
{
  HMODULE hNtDll = LoadLibrary(TEXT("ntdll.dll"));
  g_origNtGetContextThread = (pfnNtGetContextThread)GetProcAddress(hNtDll, "NtGetContextThread");
  g_origNtSetContextThread = (pfnNtSetContextThread)GetProcAddress(hNtDll, "NtSetContextThread");
  Mhook_SetHook((PVOID*)&g_origNtGetContextThread, HookNtGetContextThread);
}

Other Considerations

  • Handle Tracing
    • Checking the handle error code when calling Close Handle
  • Debug messages 
    • Windows 10 - Raise Exception prints directly to a debugger (if fails then no debugger detected).

EXCEPTION_DISPOSITION ExceptionRoutine(
    PEXCEPTION_RECORD ExceptionRecord,
    PVOID             EstablisherFrame,
    PCONTEXT          ContextRecord,
    PVOID             DispatcherContext)
{
    if (EXCEPTION_INVALID_HANDLE == ExceptionRecord->ExceptionCode)
    {
        std::cout << "Stop debugging program!" << std::endl;
        exit(-1);
    }
    return ExceptionContinueExecution;
}
int main()
{
    __asm
    {
        // set SEH handler
        push ExceptionRoutine
        push dword ptr fs : [0]
        mov  dword ptr fs : [0], esp
    }
    CloseHandle((HANDLE)0xBAAD);
    __asm
    {
        // return original SEH handler
        mov  eax, [esp]
        mov  dword ptr fs : [0], eax
        add  esp, 8
    }
    return 0
}
#define DBG_PRINTEXCEPTION_WIDE_C 0x4001000A
WCHAR * outputString = L"Any text";
ULONG_PTR args[4] = {0};
args[0] = (ULONG_PTR)wcslen(outputString) + 1;
args[1] = (ULONG_PTR)outputString;
__try
{
    RaiseException(DBG_PRINTEXCEPTION_WIDE_C, 0, 4, args);
    printf("Debugger detected");
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
    printf("Debugger NOT detected");
}

Learn More

Thank You

Windows Anti-Debugging Techniques

By Ragnar Security

Windows Anti-Debugging Techniques

  • 221