!injectdll – a remote thread approach


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.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s