mrexodia / TitanHide

Hiding kernel-driver for x86/x64.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

HookNtSetContextThread race condition and detection vector

dauthleikr opened this issue · comments

I am aware that detection vectors and race conditions have low priority due to nobody abusing them, but this one may be in actual use (not 100% sure yet, as understanding obfuscated code without breakpoints is not fun).

Race condition

TitanHide strips the ContextFlags in HookNtSetContextThread so that the debug registers will not be overwritten:

 Context->ContextFlags = OriginalContextFlags & ~0x10; //CONTEXT_DEBUG_REGISTERS ^ CONTEXT_AMD64/CONTEXT_i386

However TitanHide assumes that the ContextFlags cannot be set after the strip, and just passes the Context pointer to the actual function:

NTSTATUS ret = Undocumented::NtSetContextThread(ThreadHandle, Context);

This gives the caller plenty of time to try to un-strip the ContextFlags - consider the following proof of concept code:

void RaceFunction(CONTEXT* ptr, bool* run)
{
	while (*run)
	{
		ptr->ContextFlags = 0x10;
	}
}

void TryStealHardwareBreakpoints()
{
	CONTEXT ctx = {};
	ctx.ContextFlags = 0x10;
	if (!GetThreadContext(GetCurrentThread(), &ctx))
		return;
	// Dr0, ... are all 0 if this is run through TitanHide :)

	auto run = true;
	std::thread race(RaceFunction, &ctx, &run);
	SetThreadContext(GetCurrentThread(), &ctx);
	
	run = false;
	race.join();
}

Detection vector

An additional detection vector that I believe is being used in my target program is that GetThreadContext does not return a previously set hardware breakpoint when running under TitanHide. Proof of concept:

void DummyFunction() {}

bool HasTitanHide()
{
	const auto dummy_breakpoint = reinterpret_cast<DWORD64>(&DummyFunction);
	
	CONTEXT ctx = {};
	ctx.Dr0 = dummy_breakpoint;
	ctx.Dr7 = 1;
	ctx.ContextFlags = 0x10;

	if (!SetThreadContext(GetCurrentThread(), &ctx))
		return false;

	if (!GetThreadContext(GetCurrentThread(), &ctx))
		return false;

	if (ctx.Dr0 == dummy_breakpoint)
		return false;
	
	return true;
}

Possible fix

I understand that you guys are not trying to be super stealthy, but at least copying the Context before checking it and passing it on, to avoid the race condition, would be nice :)

I found another issue with the current HookNtSetContextThread:
It does not check if the target of the SetThreadContext call is hidden, instead it checks if the process that called the function is hidden. This usually the same process, but it's possible that another process protects the debugged process by constantly removing it's hardware breakpoints.

Here's a proof of concept code that erases all hardware breakpoints from another process, even with TitanHide enabled:

DWORD pidToStealFrom = /* put some pid here */;
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (snapshot != INVALID_HANDLE_VALUE)
{
    THREADENTRY32 te;
    te.dwSize = sizeof(te);
    if (Thread32First(snapshot, &te))
    {
	    do
	    {
		    if (te.dwSize >= FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) +
			    sizeof(te.th32OwnerProcessID) && te.th32OwnerProcessID == pidToStealFrom)
		    {
			    HANDLE threadHandle = OpenThread(THREAD_ALL_ACCESS, false, te.th32ThreadID);
			    if(threadHandle == NULL)
					continue;

				printf("Stealing hw bp's from Process 0x%04x Thread 0x%04x\n", te.th32OwnerProcessID, te.th32ThreadID);
				SuspendThread(threadHandle); // probably not needed
			    
				CONTEXT threadContext = {};
				threadContext.ContextFlags = 0x10;
				SetThreadContext(threadHandle, &threadContext);
			    
				ResumeThread(threadHandle); // probably not needed
		    }
		    te.dwSize = sizeof(te);
	    }
	    while (Thread32Next(snapshot, &te));
    }
    CloseHandle(snapshot);
}

The tricky thing here is that if you patch this issue, maybe debuggers such as x64dbg won't work either?

Closing this for now, a proper implementation is not trivial, and I doubt anybody really abuses this specific vector

I think the vector is still there so I'll keep the issue open :)