Tutorial How to Find Hidden Threads - ThreadHideFromDebugger - AntiDebug Trick

Hexui Undetected CSGO Cheats Sinkicheat PUBG Cheat

timb3r

Semi-Retired
Dank Tier VIP
Jul 15, 2018
767
22,668
47

Merry Christmas.

Has this ever happened to you? You’re playing around with some application and and it crashes the moment you attach a debugger? Ever wondered why or how? I do. These types of questions keep me awake at night.

I first became aware of this technique while cruising around some forums on the internet. People typically asking for a bypass or a method to work around it. But I was more interested in HOW this technique works less interested in how to bypass.

During my research phase I noticed that after the application crashed out it would crash out with:

Unhandled exception code 80000003
But that’s an int 3 exception? How is the Debugger not catching that? What the actual hell. Searching around for information I discovered that NtSetInformationThread has a parameter called THREADINFOCLASS. Which contains this interesting snippet:

ThreadHideFromDebugger = 0x11
Why Hans?

You may be wondering why this is even a ‘feature’ of Windows? Wouldn’t malware abuse the hell out of this? Yes, probably. But here’s why it exists: When you attach a debugger to a remote process a new thread is created. If this was just a normal thread the debugger would be caught in an endless loop as it attempted to stop it’s own execution.

So behind the scenes when the debugging thread is created Windows calls NtSetInformationThread with the ThreadHideFromDebugger flag set (1). This way the process can be debugged and a deadlock prevented. Allowing code execution to continue as normal.

However, now that this thread is hidden from the debugger any breakpoints or exceptions that are triggered will cause the process to crash. Due to the fact that the debugger cannot see this thread it’s now unable to trap these events.

So as it turned out some devious individual noticed this odd behaviour and thought: “this would make a really cool anti-debug feature”. Now we’re here with this method widespread enough for me to be aware of it.

Das Kode

So what’s it actually look like in code? I wasn’t able find any live examples so I constructed my own based on how I thought it should work:

C++:
#include <stdio.h>
#include <windows.h>

enum THREADINFOCLASS { ThreadHideFromDebugger = 0x11 };

typedef NTSTATUS (WINAPI *NtQueryInformationThread_t)(HANDLE, THREADINFOCLASS, PVOID, ULONG, PULONG);
typedef NTSTATUS (WINAPI *NtSetInformationThread_t)(HANDLE, THREADINFOCLASS, PVOID, ULONG);

NtQueryInformationThread_t fnNtQueryInformationThread = NULL;
NtSetInformationThread_t fnNtSetInformationThread = NULL;

DWORD WINAPI ThreadMain(LPVOID p) {
        while(1) {
                // This can be any trigger we're using this demo purposes
                if(IsDebuggerPresent())
                        // For MingW replace with __asm { int 3; } on MSVC
                        asm("int3");
                Sleep(500);
        }
        return 0;
}

int main(void)
{
        DWORD dwThreadId = 0;
        HANDLE hThread = CreateThread(NULL, 0, ThreadMain, NULL, 0, &dwThreadId);

        HMODULE hDLL = LoadLibrary("ntdll.dll");
        if(!hDLL) return -1;

        fnNtQueryInformationThread = (NtQueryInformationThread_t)GetProcAddress(hDLL, "NtQueryInformationThread");
        fnNtSetInformationThread = (NtSetInformationThread_t)GetProcAddress(hDLL, "NtSetInformationThread");

        if(!fnNtQueryInformationThread || !fnNtSetInformationThread)
                return -1;

        ULONG lHideThread = 1, lRet = 0;

        fnNtSetInformationThread(hThread, ThreadHideFromDebugger, &lHideThread, sizeof(lHideThread));
        fnNtQueryInformationThread(hThread, ThreadHideFromDebugger, &lHideThread, sizeof(lHideThread), &lRet);

        printf("Thread is hidden: %s\n", val ? "Yes" : "No");
 
        WaitForSingleObject(hThread, INFINITE);
        return 0;
}
Pretty simple yes? Now if you run the program and attempt to attach a debugger you’ll get this interesting crash:

0036:err:seh:raise_exception Unhandled exception code 80000003 flags 0 addr 0x401566
Oh ho ho!

ho-ho-ho.jpg

Poor Tony.

Throwing Hans off Nakatomi

Well now we’ve established how this works we can look at beating it. There’s a number of ways including:
  • Hooking the required Nt Function calls.
  • Replacing the int 3 instruction with a nop.
  • Nopping or hooking the “trigger” function.
I opted for nopping out int 3:

You can use your tool of choice to locate the required int 3 instruction:

2019-01-19-092752_1366x768_scrot.png

You’ll have to search around a bit

2019-01-19-092813_1366x768_scrot.png

Here's the thread with the check.

2019-01-19-092821_1366x768_scrot.png

Just nop that sucka out.


Attaching a debugger and resuming execution will result in everything working as expected.


Bye Hans

die-hard-smoke-204x300.jpg

Ho-ho-ho
 

Rake

Cesspool Admin
Administrator
Jan 21, 2014
11,573
78,998
2,316
Perfect I will add this to our anticheat guide
 

RyccoSN

Dank Tier Donator
Sep 14, 2019
30
2,218
0
Great explanation! Just a quick note, I spent two hours fighting NtQueryInformationThread, it was returning 0xC0000004 (STATUS_INFO_LENGTH_MISMATCH). Only when I changed the lHideThread param to unsigned char I was able to get it working.

Here's a small snippet of a dll that loops thru the process threads and check if it has the ThreadHideFromDebugger set:

C++:
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include <iostream>
#include <iomanip>
#include <windows.h>
#include <tlhelp32.h>
#include <tchar.h>
#include <winternl.h>

std::string GetLastErrorAsString()
{
    //Get the error message, if any.
    DWORD errorMessageID = ::GetLastError();
    if (errorMessageID == 0)
        return std::string(); //No error message has been recorded

    LPSTR messageBuffer = nullptr;
    size_t size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL, errorMessageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, NULL);

    std::string message(messageBuffer, size);

    //Free the buffer.
    LocalFree(messageBuffer);

    return message;
};

//enum THREADINFOCLASS { ThreadHideFromDebugger = 0x11 };
// typedef bitset<8> BYTE;

typedef NTSTATUS(WINAPI* NtQueryInformationThread_t)(HANDLE, THREADINFOCLASS, PVOID, ULONG, LPVOID );
typedef NTSTATUS(WINAPI* NtSetInformationThread_t)(HANDLE, THREADINFOCLASS, PVOID, ULONG);

NtQueryInformationThread_t fnNtQueryInformationThread = NULL;
NtSetInformationThread_t fnNtSetInformationThread = NULL;

DWORD WINAPI Bypassa(HMODULE hModule)
{

    AllocConsole();
    FILE* f;
    freopen_s(&f, "CONOUT$", "w", stdout);

    HMODULE hDLL = LoadLibraryA("ntdll.dll");
    if (!hDLL)
        return -1;

    fnNtQueryInformationThread = (NtQueryInformationThread_t)GetProcAddress(hDLL, "NtQueryInformationThread");
    // fnNtSetInformationThread = (NtSetInformationThread_t)GetProcAddress(hDLL, "NtSetInformationThread");

    HANDLE h = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
    if (h != INVALID_HANDLE_VALUE) {
        THREADENTRY32 te;
        te.dwSize = sizeof(te);
        unsigned char lHideThread = 0;
        ULONG lRet = 0;

        if (Thread32First(h, &te)) {
            do {
                if (te.dwSize >= FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) + sizeof(te.th32OwnerProcessID)) {
                    if (GetCurrentProcessId() == te.th32OwnerProcessID) {
                        printf("Process 0x%04x Thread 0x%04x\n",
                            te.th32OwnerProcessID, te.th32ThreadID);

                        HANDLE hThread = OpenThread(
                            THREAD_QUERY_INFORMATION,
                            false,
                            te.th32ThreadID);

                        if (hThread == NULL) {
                            std::cout << " - Error :" << GetLastErrorAsString();
                        } else {
                            NTSTATUS errorCode = fnNtQueryInformationThread(hThread, (THREADINFOCLASS)0x11, &lHideThread, sizeof(lHideThread), &lRet);
                            std::cout << "errorCode: " << std::hex << errorCode << std::endl;
                            std::cout << "lRet: " << std::hex << lRet << std::endl;
                            std::cout << "sizeof(NTSTATUS): " << std::hex << sizeof(NTSTATUS ) << std::endl;
                            printf("Thread is hidden: %s\n", lHideThread ? "Yes" : "No");
                            WaitForSingleObject(hThread, INFINITE);
                        }

                        
                    }
                }
                te.dwSize = sizeof(te);
            } while (Thread32Next(h, &te));
        }
        CloseHandle(h);
    }

    Sleep(30 * 1000);

    fclose(f);
    FreeConsole();
    FreeLibraryAndExitThread(hModule, 0);
    return 0;
}

BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD ul_reason_for_call,
    LPVOID lpReserved)
{
    switch (ul_reason_for_call) {
    case DLL_PROCESS_ATTACH:
        CloseHandle(CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)Bypassa, hModule, 0, nullptr));
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}
 
  • Like
Reactions: Rake
Attention! Before you post:

Read the How to Ask Questions Guide
99% of questions are answered in the Beginner's Guide, do it before asking a question.

No Hack Requests. Post in the correct section.  Search the forum first. Read the rules.

How to make a good post:

  • Fill out the form correctly
  • Tell us the game name & coding language
  • Post everything we need to know to help you
  • Ask specific questions, be descriptive
  • Post errors, line numbers & screenshots
  • Post code snippets using code tags
  • If it's a large project, zip it up and attach it

If you do not comply, your post may be deleted.  We want to help, please make a good post and we will do our best to help you.

Community Mods League of Legends Accounts