In the last post I presented you my first WinDbg extension with a !injectdll command. Theoretically everything was correct, but after some more testing I noticed that the command is not always working as expected. Andrey Bazhan was pretty quick in pointing this out and advised me to use a remote thread, which, as you will see, is a much better approach. But let’s first have a look at the problems in lld 1.0.
What is wrong with my Thread Hijack?
The !injectdll command is working fine when you use it on the initial break in the application. But when you let the application run for a moment, ctrl-break into it and try to inject the dll you will receive:
ntdll!LdrpLoadDependentModule+0x2c6: 77282d06 8906 mov dword ptr [esi],eax ds:002b:00000000=???????? (e40.178): Access violation - code c0000005 (first chance) c0000005 Exception in debugger client IDebugEventCallbacks::Exception callback. PC: 0f9ca400 VA: 067d3270 R/W: 0 Parameter: 00000000 First chance exceptions are reported before any exception handling. This exception may be expected and handled. eax=065bf3c4 ebx=00000000 ecx=00000000 edx=00a84b4c esi=00000000 edi=065bf6ac eip=77282d06 esp=065bf3a4 ebp=065bf688 iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246 ntdll!LdrpLoadDependentModule+0x2c6: 77282d06 8906 mov dword ptr [esi],eax ds:002b:00000000=????????
We see a classic null pointer (esi=0). I thought it must be some kind of a thread-context related issue as the same payload is working fine when the app is starting. I examined executed instructions in a correct and a wrong version (using par
command) and found that they differ in the following part:
WRONG: ntdll!LdrpLoadDependentModule+0x1f0: 77282c33 64a118000000 mov eax,dword ptr fs:[00000018h] fs:0053:00000018=007d3000 77282c39 8bb0a8010000 mov esi,dword ptr [eax+1A8h] ds:002b:007d31a8=00000000 77282c3f 85f6 test esi,esi 77282c41 7404 je ntdll!LdrpLoadDependentModule+0x207 (77282c47) [br=1] CORRECT: 77282c33 64a118000000 mov eax,dword ptr fs:[00000018h] fs:0053:00000018=04f48000 77282c39 8bb0a8010000 mov esi,dword ptr [eax+1A8h] ds:002b:04f481a8=051805a8 77282c3f 85f6 test esi,esi 77282c41 7404 je ntdll!LdrpLoadDependentModule+0x207 (77282c47) [br=0]
So there is a jump in a correct version and no jump in the wrong one. The jump is performed when esi is zero, and esi is zero only in the wrong version. What should be in esi then? When we look at the preceding lines we can see that eax points to TEB and at 0x1A8 offset in TEB there should be the ActivationContextStackPointer
:
0:004> dt /r nt!_TEB ntdll!_TEB +0x000 NtTib : _NT_TIB ... +0x1a4 ExceptionCode : Int4B +0x1a8 ActivationContextStackPointer : Ptr32 _ACTIVATION_CONTEXT_STACK +0x000 ActiveFrame : Ptr32 _RTL_ACTIVATION_CONTEXT_STACK_FRAME +0x000 Previous : Ptr32 _RTL_ACTIVATION_CONTEXT_STACK_FRAME +0x004 ActivationContext : Ptr32 _ACTIVATION_CONTEXT +0x008 Flags : Uint4B +0x004 FrameListCache : _LIST_ENTRY +0x000 Flink : Ptr32 _LIST_ENTRY +0x004 Blink : Ptr32 _LIST_ENTRY +0x00c Flags : Uint4B +0x010 NextCookieSequenceNumber : Uint4B +0x014 StackId : Uint4B
In my broken injection scenario this context is null and from this MSDN note on Activation Contexts we might guess it may have something to do with the DLL loading process. In my hijacking I am leaving the original thread context untouched except for ecx and eip registers. Apprently this approach is wrong in some situations. I didn’t go deeper into this so the question remains: how to perform a valid thread hijack on Windows? If you know the answer or have a guess please leave a comment 🙂
Remote thread approach
As Andrey pointed more stable solution is to inject a remote thread into a process and make it execute the LoadLibrary
function. And this is what I’m doing in version 1.1 of the lld extension. After reserving some space for the DLL name in the process memory I’m creating a remote thread in it:
size_t dllNameLength = strlen(dllName) + 1; SIZE_T n; // allocate injection buffer PBYTE injectionBuffer = (PBYTE)VirtualAllocEx((HANDLE)hProcess, nullptr, dllNameLength, MEM_COMMIT, PAGE_EXECUTE_READWRITE); if (!injectionBuffer) { throw ::Exception(HRESULT_FROM_WIN32(GetLastError())); } if (!WriteProcessMemory((HANDLE)hProcess, injectionBuffer, dllName, dllNameLength, &n)) { throw ::Exception(HRESULT_FROM_WIN32(GetLastError())); } HANDLE hThread; hThread = CreateRemoteThread((HANDLE)hProcess, nullptr, 0, (LPTHREAD_START_ROUTINE)offset, injectionBuffer, 0, &m_remoteThreadId); CloseHandle(hThread);
Of course my thread won’t execute when we stay in the debugger. That’s why I need to allow the process to run for a moment and listen for the thread exit event when my thread finishes:
void InjectionControl::Inject(PCSTR dllName) { ... CheckHResult(m_pDebugClient->SetEventCallbacks(this)); // run the only thread - should break after the injection CheckHResult(m_pDebugControl->Execute(DEBUG_OUTPUT_NORMAL, "g", DEBUG_EXECUTE_NOT_LOGGED)); } STDMETHODIMP InjectionControl::ExitThread(ULONG ExitCode) { DWORD threadId; CheckHResult(m_pDebugSystemObjects->GetCurrentThreadSystemId(&threadId)); if (threadId == m_remoteThreadId) { m_pDebugClient->SetEventCallbacks(nullptr); delete this; return DEBUG_STATUS_BREAK; } return DEBUG_STATUS_GO; }
This causes an unpleasant side effect that after calling the !injectdll you will land in a different place than you were before. And you most probably will be on a dead thread (the one that was injected) 🙂 One might say that I could freeze all threads except the injected one and this problem would not appear. Unfortunately this approach won’t work on the initial process break as there is a loader lock kept by the main thread and no other thread may load DLLs at this time. The good news is that the new version of the command is much more reliable so don’t wait and grab the binaries from the release page.
Hi great reading youur post