Close additional anti-debug side channels

This commit is contained in:
Duncan Ogilvie 2026-07-18 13:11:04 +02:00
parent 7b7b3a00f5
commit b13a71ca7a
3 changed files with 337 additions and 22 deletions

View file

@ -22,10 +22,10 @@ jobs:
msbuild.exe ${{ github.event.repository.name }}.sln /m /verbosity:minimal "/t:TitanHideGUI:Rebuild;TitanHideTest:Rebuild;TitanHide_x64dbg:Rebuild;TitanHide_TitanEngine:Rebuild" /p:Configuration=Release /p:Platform=x64
msbuild.exe ${{ github.event.repository.name }}.sln /m /verbosity:minimal "/t:TitanHideGUI:Rebuild;TitanHideTest:Rebuild;TitanHide_x64dbg:Rebuild;TitanHide_OllyDbg:Rebuild;TitanHide_TitanEngine:Rebuild" /p:Configuration=Release /p:Platform=Win32
- name: Verify native ProcessDebugObjectHandle contract
- name: Verify native anti-debug contracts
run: |
.\Release\TitanHideTest.exe --process-debug-object-contract
.\x64\Release\TitanHideTest.exe --process-debug-object-contract
.\Release\TitanHideTest.exe --native-contracts
.\x64\Release\TitanHideTest.exe --native-contracts
- name: Install Visual Studio 2019 Build Tools
shell: pwsh

View file

@ -18,6 +18,19 @@ static HOOK hNtSystemDebugControl = 0;
static HOOK hNtCreateThreadEx = 0;
static KMUTEX gDebugPortMutex;
struct VIRTUAL_THREAD_HIDE_ENTRY
{
HANDLE ProcessId;
HANDLE ThreadId;
PETHREAD Thread;
};
#define MAX_VIRTUAL_THREAD_HIDE_ENTRIES 4096
static VIRTUAL_THREAD_HIDE_ENTRY gVirtualThreadHideEntries[MAX_VIRTUAL_THREAD_HIDE_ENTRIES];
static ULONG gVirtualThreadHideEntryCount = 0;
static KSPIN_LOCK gVirtualThreadHideLock;
static bool gThreadNotifyRegistered = false;
//https://forum.tuts4you.com/topic/40011-debugme-vmprotect-312-build-886-anti-debug-method-improved/#comment-192824
//https://github.com/x64dbg/ScyllaHide/issues/47
//https://github.com/mrexodia/TitanHide/issues/27
@ -55,6 +68,85 @@ static bool IsDebugObjectTypeInformation(
return RtlCompareMemory(InlineTypeName, DebugObject->Buffer, DebugObject->Length) == DebugObject->Length;
}
static void RemoveVirtualThreadHide(HANDLE ProcessId, HANDLE ThreadId)
{
PETHREAD Thread = nullptr;
KIRQL Irql;
KeAcquireSpinLock(&gVirtualThreadHideLock, &Irql);
for(ULONG i = 0; i < gVirtualThreadHideEntryCount; i++)
{
if(gVirtualThreadHideEntries[i].ProcessId == ProcessId &&
gVirtualThreadHideEntries[i].ThreadId == ThreadId)
{
Thread = gVirtualThreadHideEntries[i].Thread;
gVirtualThreadHideEntries[i] =
gVirtualThreadHideEntries[--gVirtualThreadHideEntryCount];
break;
}
}
KeReleaseSpinLock(&gVirtualThreadHideLock, Irql);
if(Thread != nullptr)
ObDereferenceObject(Thread);
}
static bool RegisterVirtualThreadHide(PETHREAD Thread)
{
VIRTUAL_THREAD_HIDE_ENTRY Entry;
Entry.ProcessId = PsGetProcessId(PsGetThreadProcess(Thread));
Entry.ThreadId = PsGetThreadId(Thread);
Entry.Thread = Thread;
KIRQL Irql;
KeAcquireSpinLock(&gVirtualThreadHideLock, &Irql);
for(ULONG i = 0; i < gVirtualThreadHideEntryCount; i++)
{
if(gVirtualThreadHideEntries[i].Thread == Thread)
{
KeReleaseSpinLock(&gVirtualThreadHideLock, Irql);
return true;
}
}
const bool Registered = gVirtualThreadHideEntryCount < MAX_VIRTUAL_THREAD_HIDE_ENTRIES;
if(Registered)
{
ObReferenceObject(Thread);
gVirtualThreadHideEntries[gVirtualThreadHideEntryCount++] = Entry;
}
KeReleaseSpinLock(&gVirtualThreadHideLock, Irql);
// If exit notification raced ahead of registration, remove the entry now.
if(Registered && PsIsThreadTerminating(Thread))
{
RemoveVirtualThreadHide(Entry.ProcessId, Entry.ThreadId);
return false;
}
return Registered;
}
static bool HasVirtualThreadHide(PETHREAD Thread)
{
bool Found = false;
KIRQL Irql;
KeAcquireSpinLock(&gVirtualThreadHideLock, &Irql);
for(ULONG i = 0; i < gVirtualThreadHideEntryCount; i++)
{
if(gVirtualThreadHideEntries[i].Thread == Thread)
{
Found = true;
break;
}
}
KeReleaseSpinLock(&gVirtualThreadHideLock, Irql);
return Found;
}
static void ThreadNotifyRoutine(HANDLE ProcessId, HANDLE ThreadId, BOOLEAN Create)
{
if(!Create)
RemoveVirtualThreadHide(ProcessId, ThreadId);
}
static NTSTATUS NTAPI HookNtQueryInformationThread(
IN HANDLE ThreadHandle,
IN THREADINFOCLASS ThreadInformationClass,
@ -98,8 +190,8 @@ static NTSTATUS NTAPI HookNtQueryInformationThread(
ProbeForWrite(&Wow64Context->ContextFlags, sizeof(ULONG), 1);
Wow64Context->ContextFlags = OriginalContextFlags;
// If debug registers were requested, zero user input
if(DebugRegistersRequested)
// If debug registers were requested successfully, zero user input.
if(NT_SUCCESS(Status) && DebugRegistersRequested)
{
Wow64Context->Dr0 = 0;
Wow64Context->Dr1 = 0;
@ -123,7 +215,8 @@ static NTSTATUS NTAPI HookNtQueryInformationThread(
if(NT_SUCCESS(Status) && ThreadInformationClass == ThreadHideFromDebugger)
{
if(Hider::IsHidden(pid, HideThreadHideFromDebugger) &&
if(gThreadNotifyRegistered &&
Hider::IsHidden(pid, HideThreadHideFromDebugger) &&
Hider::IsHidden(targetPid, HideThreadHideFromDebugger))
{
Log("[TITANHIDE] NtQueryInformationThread(ThreadHideFromDebugger) by %d\r\n", pid);
@ -132,8 +225,20 @@ static NTSTATUS NTAPI HookNtQueryInformationThread(
{
BACKUP_RETURNLENGTH();
// Since they're asking, assume they're expecting "yes"
*(BOOLEAN*)ThreadInformation = TRUE;
PETHREAD Thread = nullptr;
NTSTATUS ReferenceStatus = ObReferenceObjectByHandle(
ThreadHandle,
0,
*PsThreadType,
ExGetPreviousMode(),
(PVOID*)&Thread,
nullptr);
if(NT_SUCCESS(ReferenceStatus))
{
if(HasVirtualThreadHide(Thread))
*(BOOLEAN*)ThreadInformation = TRUE;
ObDereferenceObject(Thread);
}
RESTORE_RETURNLENGTH();
}
@ -158,7 +263,7 @@ static NTSTATUS NTAPI HookNtSetInformationThread(
//Bug found by Aguila, thanks!
if(ThreadInformationClass == ThreadHideFromDebugger && !ThreadInformationLength)
{
if(Hider::IsHidden(pid, HideThreadHideFromDebugger))
if(gThreadNotifyRegistered && Hider::IsHidden(pid, HideThreadHideFromDebugger))
{
Log("[TITANHIDE] NtSetInformationThread(ThreadHideFromDebugger) by %d\r\n", pid);
PETHREAD Thread;
@ -169,7 +274,16 @@ static NTSTATUS NTAPI HookNtSetInformationThread(
(PVOID*)&Thread,
NULL);
if(NT_SUCCESS(status))
{
const bool Registered = RegisterVirtualThreadHide(Thread);
ObDereferenceObject(Thread);
if(!Registered)
return Undocumented::NtSetInformationThread(
ThreadHandle,
ThreadInformationClass,
ThreadInformation,
ThreadInformationLength);
}
return status;
}
}
@ -500,25 +614,49 @@ static NTSTATUS NTAPI HookNtQueryObject(
OBJECT_ALL_INFORMATION* pObjectAllInfo = (OBJECT_ALL_INFORMATION*)ObjectInformation;
unsigned char* pObjInfoLocation = (unsigned char*)pObjectAllInfo->ObjectTypeInformation;
unsigned char* BufferEnd = (unsigned char*)ObjectInformation + ObjectInformationLength;
if(BufferEnd < (unsigned char*)ObjectInformation)
{
RESTORE_RETURNLENGTH();
return ret;
}
unsigned int TotalObjects = pObjectAllInfo->NumberOfObjects;
for(unsigned int i = 0; i < TotalObjects; i++)
{
if(pObjInfoLocation > BufferEnd ||
(SIZE_T)(BufferEnd - pObjInfoLocation) < sizeof(OBJECT_TYPE_INFORMATION))
{
break;
}
OBJECT_TYPE_INFORMATION* pObjectTypeInfo = (OBJECT_TYPE_INFORMATION*)pObjInfoLocation;
ProbeForRead(pObjectTypeInfo, 1, 1);
ProbeForRead(pObjectTypeInfo->TypeName.Buffer, 1, 1);
if(RtlEqualUnicodeString(&pObjectTypeInfo->TypeName, &DebugObject, FALSE)) //DebugObject
ProbeForRead(pObjectTypeInfo, sizeof(OBJECT_TYPE_INFORMATION), 1);
// The name is inline after the fixed structure. TypeName.Buffer
// may have been overwritten by an overlapping ReturnLength.
unsigned char* InlineTypeName = (unsigned char*)(pObjectTypeInfo + 1);
if(InlineTypeName > BufferEnd ||
pObjectTypeInfo->TypeName.MaximumLength > (SIZE_T)(BufferEnd - InlineTypeName))
{
break;
}
if(pObjectTypeInfo->TypeName.Length == DebugObject.Length &&
pObjectTypeInfo->TypeName.Length <= pObjectTypeInfo->TypeName.MaximumLength &&
RtlCompareMemory(InlineTypeName, DebugObject.Buffer, DebugObject.Length) == DebugObject.Length)
{
Log("[TITANHIDE] DebugObject by %d\r\n", pid);
DEBUG_OBJECT_CONTRIBUTION Contribution;
if(QueryDebugObjectContribution(&Contribution))
RemoveDebugObjectContribution(pObjectTypeInfo, &Contribution);
}
pObjInfoLocation = (unsigned char*)pObjectTypeInfo->TypeName.Buffer;
pObjInfoLocation += pObjectTypeInfo->TypeName.MaximumLength;
ULONG_PTR tmp = ((ULONG_PTR)pObjInfoLocation) & -(LONG_PTR)sizeof(void*);
if((ULONG_PTR)tmp != (ULONG_PTR)pObjInfoLocation)
tmp += sizeof(void*);
pObjInfoLocation = ((unsigned char*)tmp);
ULONG_PTR Next = (ULONG_PTR)InlineTypeName + pObjectTypeInfo->TypeName.MaximumLength;
Next = (Next + sizeof(void*) - 1) & -(LONG_PTR)sizeof(void*);
if(Next <= (ULONG_PTR)pObjInfoLocation || Next > (ULONG_PTR)BufferEnd)
break;
pObjInfoLocation = (unsigned char*)Next;
}
RESTORE_RETURNLENGTH();
@ -675,8 +813,8 @@ static NTSTATUS NTAPI HookNtGetContextThread(
ProbeForWrite(&Context->ContextFlags, sizeof(ULONG), 1);
Context->ContextFlags = OriginalContextFlags;
// If debug registers were requested, zero user input
if(DebugRegistersRequested)
// Failed queries must leave the caller's output untouched.
if(NT_SUCCESS(ret) && DebugRegistersRequested)
{
Context->Dr0 = 0;
Context->Dr1 = 0;
@ -784,6 +922,10 @@ static NTSTATUS NTAPI HookNtCreateThreadEx(
int Hooks::Initialize()
{
KeInitializeMutex(&gDebugPortMutex, 0);
KeInitializeSpinLock(&gVirtualThreadHideLock);
gVirtualThreadHideEntryCount = 0;
gThreadNotifyRegistered = NT_SUCCESS(PsSetCreateThreadNotifyRoutine(ThreadNotifyRoutine));
int hook_count = 0;
hNtQueryInformationProcess = SSDT::Hook("NtQueryInformationProcess", (void*)HookNtQueryInformationProcess);
if(hNtQueryInformationProcess)
@ -840,4 +982,16 @@ void Hooks::Deinitialize()
{
SSDT::Unhook(hNtCreateThreadEx, true);
}
if(gThreadNotifyRegistered)
{
PsRemoveCreateThreadNotifyRoutine(ThreadNotifyRoutine);
gThreadNotifyRegistered = false;
}
while(gVirtualThreadHideEntryCount != 0)
{
PETHREAD Thread = gVirtualThreadHideEntries[--gVirtualThreadHideEntryCount].Thread;
ObDereferenceObject(Thread);
}
}

View file

@ -247,6 +247,94 @@ bool HideFromDebugger()
0));
}
static DWORD WINAPI ContractThreadProc(PVOID)
{
return 0;
}
bool CheckThreadHideFromDebuggerContract()
{
typedef NTSTATUS(NTAPI * NT_QUERY_INFORMATION_THREAD)(
HANDLE, ULONG, PVOID, ULONG, PULONG);
typedef NTSTATUS(NTAPI * NT_SET_INFORMATION_THREAD)(
HANDLE, ULONG, PVOID, ULONG);
NT_QUERY_INFORMATION_THREAD NtQIT = (NT_QUERY_INFORMATION_THREAD)
GetProcAddress(GetModuleHandle(TEXT("ntdll.dll")), "NtQueryInformationThread");
NT_SET_INFORMATION_THREAD NtSIT = (NT_SET_INFORMATION_THREAD)
GetProcAddress(GetModuleHandle(TEXT("ntdll.dll")), "NtSetInformationThread");
if(NtQIT == nullptr || NtSIT == nullptr)
return false;
HANDLE Thread = CreateThread(nullptr, 0, ContractThreadProc, nullptr, CREATE_SUSPENDED, nullptr);
if(Thread == nullptr)
return false;
bool Detected = false;
BOOLEAN Hidden = TRUE;
ULONG ReturnLength = 0;
NTSTATUS Status = NtQIT(Thread, 0x11, &Hidden, sizeof(Hidden), &ReturnLength);
if(!NT_SUCCESS(Status) || Hidden != FALSE || ReturnLength != sizeof(Hidden))
{
printf("ThreadHideFromDebugger initial-state mismatch: %08X, %u, %u\n",
Status, Hidden, ReturnLength);
Detected = true;
}
Status = NtSIT(Thread, 0x11, nullptr, 0);
if(!NT_SUCCESS(Status))
{
printf("ThreadHideFromDebugger set failed: %08X\n", Status);
Detected = true;
}
Hidden = FALSE;
ReturnLength = 0;
Status = NtQIT(Thread, 0x11, &Hidden, sizeof(Hidden), &ReturnLength);
if(!NT_SUCCESS(Status) || Hidden != TRUE || ReturnLength != sizeof(Hidden))
{
printf("ThreadHideFromDebugger virtual-state mismatch: %08X, %u, %u\n",
Status, Hidden, ReturnLength);
Detected = true;
}
TerminateThread(Thread, 0);
CloseHandle(Thread);
return Detected;
}
bool CheckGetContextFailureContract()
{
typedef NTSTATUS(NTAPI * NT_GET_CONTEXT_THREAD)(HANDLE, PCONTEXT);
NT_GET_CONTEXT_THREAD NtGCT = (NT_GET_CONTEXT_THREAD)
GetProcAddress(GetModuleHandle(TEXT("ntdll.dll")), "NtGetContextThread");
if(NtGCT == nullptr)
return false;
HANDLE LimitedThread = OpenThread(THREAD_QUERY_LIMITED_INFORMATION,
FALSE,
GetCurrentThreadId());
if(LimitedThread == nullptr)
return false;
__declspec(align(16)) CONTEXT Context;
memset(&Context, 0xA5, sizeof(Context));
Context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
CONTEXT OriginalContext;
memcpy(&OriginalContext, &Context, sizeof(Context));
NTSTATUS Status = NtGCT(LimitedThread, &Context);
CloseHandle(LimitedThread);
const NTSTATUS StatusAccessDenied = (NTSTATUS)0xC0000022L;
if(Status != StatusAccessDenied || memcmp(&Context, &OriginalContext, sizeof(Context)) != 0)
{
printf("NtGetContextThread failure contract mismatch: %08X\n", Status);
return true;
}
return false;
}
typedef struct _OBJECT_TYPE_INFORMATION
{
UNICODE_STRING TypeName;
@ -469,6 +557,71 @@ bool CheckObjectList()
}
}
bool CheckObjectTypesInformationOverlapContract()
{
typedef NTSTATUS(NTAPI * pNtQueryObject)(
HANDLE, OBJECT_INFORMATION_CLASS, PVOID, ULONG, PULONG);
pNtQueryObject NtQO = (pNtQueryObject)GetProcAddress(
GetModuleHandle(TEXT("ntdll.dll")),
"NtQueryObject");
if(NtQO == nullptr)
return false;
ULONG Size = 0;
NTSTATUS Status = NtQO(nullptr, ObjectTypesInformation, nullptr, 0, &Size);
if(Status != (NTSTATUS)0xC0000004L || Size == 0)
return false;
const ULONG BufferSize = Size + 0x10000;
unsigned char* Buffer = (unsigned char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, BufferSize);
if(Buffer == nullptr)
return false;
ULONG ActualLength = 0;
Status = NtQO(nullptr, ObjectTypesInformation, Buffer, BufferSize, &ActualLength);
if(!NT_SUCCESS(Status))
{
printf("ObjectTypesInformation baseline query failed: %08X\n", Status);
HeapFree(GetProcessHeap(), 0, Buffer);
return true;
}
OBJECT_ALL_INFORMATION* All = (OBJECT_ALL_INFORMATION*)Buffer;
unsigned char* Location = (unsigned char*)All->ObjectTypeInformation;
PULONG Overlap = nullptr;
const wchar_t DebugObject[] = L"DebugObject";
const USHORT DebugObjectLength = sizeof(DebugObject) - sizeof(wchar_t);
for(ULONG i = 0; i < All->NumberOfObjects; i++)
{
OBJECT_TYPE_INFORMATION* Type = (OBJECT_TYPE_INFORMATION*)Location;
if(Type->TypeName.Length == DebugObjectLength &&
memcmp(Type->TypeName.Buffer, DebugObject, DebugObjectLength) == 0)
{
Overlap = (PULONG)&Type->TypeName.Buffer;
break;
}
Location = (unsigned char*)Type->TypeName.Buffer + Type->TypeName.MaximumLength;
Location = (unsigned char*)(((ULONG_PTR)Location + sizeof(void*) - 1) &
-(LONG_PTR)sizeof(void*));
}
bool Detected = Overlap == nullptr;
if(Overlap == nullptr)
puts("ObjectTypesInformation did not contain DebugObject");
if(Overlap != nullptr)
{
Status = NtQO(nullptr, ObjectTypesInformation, Buffer, BufferSize, Overlap);
if(!NT_SUCCESS(Status) || *Overlap != ActualLength)
{
printf("ObjectTypesInformation overlap contract mismatch: %08X\n", Status);
Detected = true;
}
}
HeapFree(GetProcessHeap(), 0, Buffer);
return Detected;
}
enum PROCESSINFOCLASS
{
ProcessBasicInformation = 0, // 0, q: PROCESS_BASIC_INFORMATION, PROCESS_EXTENDED_BASIC_INFORMATION
@ -605,8 +758,16 @@ bool CheckNtClose()
int main(int argc, char* argv[])
{
if(argc == 2 && strcmp(argv[1], "--process-debug-object-contract") == 0)
return CheckProcessDebugObjectHandle() ? 1 : 0;
if(argc == 2 && strcmp(argv[1], "--native-contracts") == 0)
{
const bool ProcessDebugObject = CheckProcessDebugObjectHandle();
const bool ThreadHide = CheckThreadHideFromDebuggerContract();
const bool GetContextFailure = CheckGetContextFailureContract();
const bool ObjectTypesOverlap = CheckObjectTypesInformationOverlapContract();
printf("Native contracts: ProcessDebugObject=%d ThreadHide=%d GetContextFailure=%d ObjectTypesOverlap=%d\n",
ProcessDebugObject, ThreadHide, GetContextFailure, ObjectTypesOverlap);
return ProcessDebugObject || ThreadHide || GetContextFailure || ObjectTypesOverlap ? 1 : 0;
}
char title[256] = "";
sprintf_s(title, "pid: %d", (int)GetCurrentProcessId());