This post is also available in: 日本語 (Japanese)
In May 2019, Microsoft released an out-of-band patch update for remote code execution (RCE) vulnerability CVE-2019-0708, which is also known as “BlueKeep” and resides in code for Remote Desktop Services (RDS). Over the last year, researchers had proved the exploitability of BlueKeep and proposed countermeasures to detect and prevent it. However, RDP is still one of the most popular attack vectors used by attackers today. To make it harder for RDP attacks to succeed and to better protect Windows users and our customers, we will disclose detailed information in this blog about how attackers might exploit BlueKeep on Windows RDP endpoints.
In August 2019, Unit 42 researchers published a blog on CVE-2019-0708, covering how Bitmap Cache protocol data unit (PDU), Refresh Rect PDU and RDPDR Client Name Request PDU can be used to write data into the Windows kernel memory. Additionally, in October 2019, Unit 42 researchers presented three new Windows kernel pool Feng Shui techniques with RDP PDUs and two different exploit techniques of BlueKeep at Microsoft’s BlueHat Seattle 2019 Security Conference. This blog discusses how to combine Refresh Rect PDU and RDPDR Client Name Request PDU to get remote code execution RCE with system privilege.
Palo Alto Networks Next-Generation Firewall customers with the Threat Prevention security subscription are protected from this vulnerability, and Cortex XDR customers can prevent exploitation of this vulnerability on Windows XP, Windows 7 and Windows Server 2003 and 2008.
Our last blog about BlueKeep introduced Refresh Rect PDU and RDPDR Client Name Request PDU in Windows RDP protocols in detail. Here is a quick review of those two PDUs.
Refresh Rect PDU can be used to spray many 0x828-sized kernel pools by sending PDUs multiple times. Those 0x828-sized kernel pools will be aligned by 0x1000 offset, and there are eight bytes of data controlled by RDP Client at offset 0x2c in each 0x828-sized kernel pool. The start address of eight bytes-controlled data looks like 0x8xxxx02c. During our testing, the 0x828-sized kernel pools are groomed consecutively from the address 0x86000000 to 0x8a000000 with the 0x1000-sized kernel pool aligned on our VMware virtual machine. In the exploitation proof of concept (PoC), we used 0x86c1002c as the fixed address to store a function pointer and got a very high success rate when re-exploiting multiple times and when restarting systems. The address 0x88xxx02c and 0x89xxx02c are also good candidates that perform very well in high memory consumption targets, such as RDP servers running a lot of applications at the same time.
RDPDR Client Name Request PDU can be used to reclaim the freed channel object with controlled data by sending PDUs multiple times. The size and data of the kernel pool allocated when the RDP server parses Client Name Request PDU are both controllable. We can make the RDP server allocate the 0xd0-sized kernel pool to claim the freed MS_T120 channel object. By spraying the kernel pool with the specifically crafted data, we can control the reuse route to execute a function call with a controllable function pointer and thus control extended instruction pointer (EIP). We can also use Client Name Request PDU to write the shellcode into the kernel pool since the size and data of Client Name Request PDU are both controllable by the RDP client. All the details about how those two PDUs are used in the exploit will be discussed in the following sections.
To make the exploitation clear, we will briefly introduce the root cause of CVE-2019-0708. CVE-2019-0708 is a Use After Free (UAF) vulnerability related to a dangling object, the MS_T120 virtual channel. The MS_T120 virtual channel is one of two default channels (MS_T120 and CTXTW) used by the RDP server internally that are initialized when the RDP connection is established. However, the RDP client can also create a customized virtual channel named “MS_T120” by adding the “MS_T120” item to channelDefArray in Client MCS Connect Initial PDU, as shown in Figure 1.
The RDP server receives that request and creates a reference for the MS_T120 object in the ChannelPointerTable object. Figure 2 shows that there are two “MS_T120” channel object references in the ChannelPointerTable object. The first one is created for the Client MCS Connect Initial PDU request, and the second one is created for internal use by the RDP server.
After joining the customized MS_T120 channel using MCS Channel Join Request, the MS_T120 channel can be opened successfully by the RDP client. If the RDP client sends crafted data into the MS_T120 channel as shown in Figure 3, the MCSPortData function in rdpwx.dll module will be invoked.
In the function MCSPortData, the data starting from 0x74 offset of the first argument lpAddend are controlled by the RDP client, and MCSChannelClose will be called to free the MS_T120 channel object when the variable “v2” is equal to the value “2”, as shown in Figure 4.
After the MS_T120 channel object is freed, there is still one dangling pointer left pointing to the freed MS_T120 channel object in the slot 0x1f of the ChannelPointerTable object, as shown in Figure 5.
When the RDP client disconnects from the connection, the function RDPWD!SignalBrokenConnection is invoked, and then the function termdd!IcaChannelInput will be called to access the freed MS_T120 object in the slot 0x1f, as shown in Figure 6.
Reuse After Free
We have discussed how the MS_T120 channel object is freed, leaving a dangling pointer to the MS_T120 channel object in the ChannelPointerTable object. Now we will cover how to reuse the freed object. In the function termdd!IcaChannelInput, the function IcaFindChannel will be called to find the channel object. When the RDP client terminates the connection, the second argument slot_base will be 0x05 and the third argument slot_index will be 0x1f, and so the function IcaFindChannel will set the freed MS_T120 channel object as the return value. If the freed MS_T120 channel object is reclaimed with a fake MS_T120 channel object, the following function execution route can be controlled with a function call (call [eax]) in which the function pointer (register eax) is obtained from inside the fake MS_T120 channel object, as shown in Figure 7.
Reclaim the Freed MS_T120 Channel Object With RDPDR Client Name Request PDU and Control EIP
In this section, details about how to reclaim the freed MS_T120 channel object to control EIP will be discussed. The size of the freed MS_T120 Channel is 0xc8 (0xd0 including 8 bytes pool header), as shown in Figure 5. The termdd!IcaChannelInputInternal function will allocate channel_data_size+0x20-sized kernel pool, as shown in Figure 19 of our previous blog. The size of the Client Name Request PDU needs to be set to 0xa8 (0xc8-0x20), and the ComputerNameLen field needs to be set to 0x98 accordingly if we want the function termdd!IcaChannelInputInternal to allocate the 0xc8-sized kernel pool. Considering that there is a memory copy operation after successful pool allocation, by sending those Client Name Request PDUs multiple times, we ensure the freed MS_T120 pool slot has been occupied by the Client Name Request PDU data. Moreover, the first 0x20 bytes of the 0xc8-sized kernel pool is for the internal use of the termdd module, which means the first 0x20 bytes are not controllable, and the following 0x10 bytes that are for the Client Name Request PDU header are also not controllable, so the controllable data size is 0x98 (0xc8-0x20-0x10) in total. Figure 8 shows how the RDP client constructs the RDPDR Client Name Request PDU.
Figure 9 shows the memory dump of the fake MS_T120 channel object created by sending RDPDR Client Name Request PDU. Several important fields in the fake MS_T120 channel object are labeled in different colors. The 4 bytes (DWORD) value of 0x00000000 in green is set to prevent service from crashing in hal!KeAcquireInStackQueuedSpinLockRaiseToSynch, which is called by ExEnterCriticalRegionAndAcquireResourceExclusive. The DWORD value of 0x00000000 in light blue is set to ensure condition check and make the function route arrive at the function call (call [eax]) with the controlled function pointer eax, as shown in Figure 7.
The DWORD value 0x86c10030 in purple is set at the offset 0x8c of the fake MS_T120 channel object, as shown in Figure 9. The debug log in Figure 10 shows how to get the fake object address at the offset 0x8c and make a function call (call [eax]) to control EIP.
Now that we've shown how the EIP is controlled, we will discuss why the eax is set to the address 0x86c10030 and the EIP is set to the address 0x86c1002c. With the technique described in the previous section, we are able to get a stable spray from the address 0x86xxxxxx to 0x8axxxxxx. There are 8 bytes data controllable at the offset 0x002c in each aligned 0x1000 address, as shown in Figure 11.
The code snippet in Figure 12 shows how the RDP client constructs the Refresh Rect PDUs and how many times it needs to send them to the RDP server.
In the 8 bytes of controllable data in each aligned 0x1000 sized kernel pool, 4 bytes at offset 0x0030 (0x86c10030) are set to 0x86c1002c, which is the hard-coded address of stage 0 shellcode. The other 4 bytes at offset 0x002c (0x86c1002c) are used to store the stage 0 shellcode. The next section will introduce more details about the several stages of shellcodes.
Since there are only 4 bytes that can be used for the stage 0 shellcode, we used a trick of 4 bytes shellcode slipper “add bl,al; jmp ebx” instead of “call/jmp ebx+30h” to jump to stage one of shellcode. When termdd!IcaChannelInputInternal executes “call dword ptr [eax]” assembly instruction, the al is 0x30 and ebx points to the fake MS_T120 channel object in which the RDP client can fill the controlled data. Stage one shellcode is put into the kernel pool from the address faked_MS_T120_channel_object+0x30 (0x867b3590). The whole process is shown in Figure 13.
Something worth mentioning: 2 bytes assembly code “add bl, al” is used because only 4 bytes shellcode can be used to implement “jmp ebx+0x30”. This is not perfect, since when “bl” is greater than 0xd0, there is an overflow in “add bl, al”, enabling “jmp ebx” to jump to a wrong address, causing the exploitation to fail. However, this will be good enough, since the success rate will theoretically be 81.25% (0xd/0x10).
Before talking about stage one shellcode, we’d like to discuss stage two, or the final shellcode. We noticed that the RDPDR Client Name Request PDU can also be used to send arbitrary-sized final kernel shellcodes to the kernel pool in the RDP server. As an example, we constructed an RDPDR Client Name Request PDU with the data length of 0x5c8 and the payload embedded, as shown in Figure 14.
When the RDPDR Client Name Request PDU embedded with the shellcode is sent to the RDP server, PDMCS - Hydra MCS Protocol Driver will store the data in the kernel pool. Interestingly, that kernel pool has a reference maintained on the stack, which can be used by stage one shellcode to locate the final shellcode. Specifically, the ECX register here points to the stack, and the address ECX+0x28 stores the kernel pool address. The final shellcode is located at the 0x434 offset of the kernel pool, as shown in Figure 15.
The offset 0x434 may vary in different Windows versions. However, it is easy to craft the stage one shellcode as an egg hunter to make it universal by searching the final shellcode in the kernel pool.
Patch Kernel to Avoid Crash
The following work is now a routine for kernel exploitation. The final shellcode first fixes the return value and patches the kernel to avoid crash after the shellcode finishes, and then it executes the kernel shellcode to insert the APC to lsass.exe or spoolsv.exe and execute user mode shellcode. Figure 16 shows how the final shellcode fixes the ChannelPointerTable object, modifies the return address and emulates the execution of the ExReleaseResourceAndLeaveCriticalRegion function to inc a WORD value in KTHREAD.
After patching the kernel, the functional part of the kernel shellcode is executed. To illustrate the exploitation, we used a kernel shellcode template released by Sleepya for the eternalblue exploit. A userland shellcode of WinExec(‘calc’) for the demo is shown in Figure 17.
Putting It All Together
Now that we have solved all the problems of exploitation, the whole exploitation chain can be described as follows:
- Establish the connection to the victim.
- Spray with Refresh Rect PDU.
- Send the crafted PDU, forcing the MS_T120 channel object to be freed.
- Occupy the freed MS_T120 channel object with multiple RDPDR Client Name Request PDUs.
- Send the final shellcode with RDPDR Client Name Request PDU.
- Terminate the connection to reuse the freed MS_T120 channel object, control EIP and execute several stages of shellcode.
This blog outlines how to get system privilege RCE with Refresh Rect PDU and RDPDR Client Name Request PDU for the Windows RDP vulnerability CVE-2019-0708 (BlueKeep). The 4 bytes shellcode slider in shellcode stage 0 and the shellcode egg hunter in shellcode stage one are general exploit techniques and could be used in all Windows RDP vulnerability exploitations. The deep research on the kernel pool Feng Shui techniques with RDP PDUs and Windows RDP exploit techniques will help defenders to better protect all Windows users from Windows RDP attacks.
Palo Alto Networks customers are protected from BlueKeep:
- Cortex XDR prevents exploitation of this vulnerability on Windows XP, Windows 7 and Windows Server 2003 and 2008.
- Next-Generation Firewall with a Threat Prevention security subscription detects the vulnerability.