Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HookNtSetContextThread race condition and detection vector #44

Open
dauthleikr opened this issue Feb 26, 2020 · 4 comments
Open

HookNtSetContextThread race condition and detection vector #44

dauthleikr opened this issue Feb 26, 2020 · 4 comments

Comments

@dauthleikr
Copy link

dauthleikr commented Feb 26, 2020

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 :)

@dauthleikr
Copy link
Author

dauthleikr commented Feb 27, 2020

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?

@mrexodia
Copy link
Owner

mrexodia commented Feb 27, 2020 via email

dauthleikr added a commit to dauthleikr/TitanHide that referenced this issue Feb 28, 2020
…g registers, fix race condition for stripping ContextFlags
@dauthleikr
Copy link
Author

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

@mrexodia
Copy link
Owner

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

@mrexodia mrexodia reopened this Apr 10, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants