The story of a privileged handle...
As virtualization technology continues to become the corporate standard, the popularity of Virtual Desktop Infrastructure (VDI) in large enterprises has been increasing. These automated environments can provision desktops and applications from the internal and external network on top of virtualization technology without an IT administrator’s input. There are many components involved in a VDI infrastructure, but one specifically caught our attention on a customer mandate back in September 2017: the Windows "vmwagent.exe".
On this particular mandate, we had to escape the VDI environment with developer access and without local administrative access. The customer had done a great job at image hardening; services, applications and operating systems were well configured and patched, with up-to-date antivirus software, behavior monitoring, and strong passwords. Faced with this situation, we decided to perform a quick look around with the popular Process Explorer from the SysInternals Suite. One of the many notable features we like about this tool is the ability to display opened handles per process.
We were shocked to find a privileged process handle in an unprivileged process! We had stumbled onto a potential vulnerability in the VMWare Horizon solution. Ultimately, we were able to exploit this to grant us local administrative privileges on all Windows desktops in the VDI. These results were then reported to VMWare.
Our goal therefore for this blog post is to deliver some background as well as technical details on the CVE-2017-4946 vulnerability. Hopefully we will be able to draw the attention of as many VMWare customers as possible and better explain the urgency of applying the new update, as stated in the VMSA-2018-0003 advisory.
We also want to urge as many security professionals to not overlook such vulnerabilities. They are easy to identify.
Handles in Windows
Each process in Windows has its own memory space in which various code, data, and metadata sections are mapped. A process cannot directly access resources outside of its memory space boundary. It needs a handle authorized by the kernel first.
The handle allocation protocol is simple, you declare what resource you want and what you intend to do with it. The kernel will then decide, based on the originating security token and other parameters, if it is authorized to allocate a handle. Once authorized, the process needs to use the handle for each action on the associated resource. The kernel will only allow actions that were previously authorized when the handle was given. Basically, this works like a session token in the web application world.
Here is a list of objects from which we can retrieve a handle:
|Access token||Change notification|
|Console input||Console screen buffer|
Identifying the vulnerability
In a VDI session, using Process Explorer, it took no time to realize that the "vmwagent.exe" process (6912) had an opened process handle to its parent process "v4pa_agent.exe" (1540). The lower pane of Process Explorer window, as displayed below, reveals the opened handle. The upper pane of the window shows that we were not authorized to access the "v4pa_agent.exe" process since it was running under the NT AUTHORITY\SYSTEM privileged account.
Our plan became obvious, if we could use that handle from our user-owned "vmwagent.exe" process and interact with the privileged parent process "v4pa_agent.exe", we could leverage code execution under the security context of the system.
Image 1 - Process Explorer capture showing the privileged handle in an unprivileged process.
The only missing element was to confirm the handle's granted access rights. Since we were the owner of the "vmwagent.exe" process which holds the handle we wanted, we could dump the process memory using the minidump feature of Process Explorer and analyze it with the Windows Debugger as shown below.
Image 2 - WinDBG capture showing the privileged handle granted access rights.
At this point, we confirmed that the vulnerability was exploitable. Due to time constraints, we decided to develop the exploit directly on the vulnerable system. Visual Studio was available, we were given developer access, remember?!
Exploiting a privileged process handle
There are multiple ways to exploit a privileged process handle, but first we needed to acquire the handle! We could either inject a library in the "vmwagent.exe" process itself and use the handle from there or use the DuplicateHandle call. Once acquired, we could use the privileged process handle to hijack the execution flow of the parent process: allocating memory and altering protections was possible with VirtualAllocEx and VirtualProtectEx calls. This done, we examined possible exploits.
Below is a partial list of our options:
- Perform a DLL injection technique targeting the parent process with CreateRemoteThread call.
- Overwrite a callback in the parent process. (Note that this technique could fail under the Control Flow Guard (CFG) security mechanism. Unless you attack CFG directly.)
- Hook a common library function with raw instruction patching.
In this environment, CreateRemoteThread was not possible and a callback overwrite would be too risky, so we chose to go with a good old hook.
The function we decided to hook was RtlInitUnicodeString in NtDll.dll library. This function initializes UNICODE_STRING structure from a wide char string (PCWSTR). It is often called and was our best bet on hijacking execution flow. We could find its address easily in our own process with the GetProcAddress function. NtDll.dll shares the same base address across all processes until reboot.
Image 3 - Attack scenario representation. Processes from left to right are vmwagent.exe, exploit.exe and v4pa_agent.exe.
Finally, the exploit shellcode used for the payload was straightforward. We only wanted to allow a simple LoadLibraryA call to load a malicious library into the process. At this point, we could have used any type of shellcode, but we found it useful to have a "static" shellcode. It would allow us to only have to rebuild a library to execute our desired code. Time not being on our side, we quickly allocated a full memory page for the shellcode and used the following offsets for dynamic values:
- +0x0 => shellcode base
- +0xF00 => Name of the library to load
- +0xFF0 => Toggle bit (used to avoid calling LoadLibraryA on every call)
- +0xFF8 => Address of LoadLibrary
;; (Poorly written) Exploit shellcode to call a function with 1 argument within process memory. ;; by Martin Lemay, GoSecure Inc. 2017 push rcx push rdx push rax push rbx call bridge and rbx, 0xfffffffffffff000 mov rcx, qword ptr [rsp+0x20] add rcx, 8 mov qword ptr [rsp+0x20], rcx mov rax, qword ptr [rbx+0xff0] cmp al, 1 je repair mov qword ptr [rbx+0xff0], 1 mov rcx, rbx add rcx, 0xf00 call qword ptr [rbx+0xff8] repair: pop rbx pop rax pop rdx pop rcx xor eax,eax mov QWORD PTR [rcx+0x8],rdx mov WORD PTR [rcx+0x2],ax mov WORD PTR [rcx],ax jmp qword ptr [rsp] bridge: pop rbx push rbx ret
After execution, we recovered the execution state and returned to the original RtlInitUnicodeString function. Improvements could be made at this point for a more stable and re-usable exploit across various Windows flavors and architectures, but in our case, it was more than enough for our needs: we had successfully exploited the vulnerability.
In the past months, we've encountered multiple instances of bad handle usage that went undetected by vendors. Note that we are not only seeing bad process handle usage, but also bad device handle usage that led to kernel privilege escalations.
Interestingly, this old attack vector is still not well understood in the industry. We hope this blog serves as an incentive for security professionals to not overlook this crucial attack vector on engagements and for developers to properly manage handles on Windows applications.