A Debugging Primer with CVE-2019–0708
By: @straight_blast ; straightblast426@gmail.com
The purpose of this post is to share how one would use a debugger to identify the relevant code path that can trigger the crash. I hope this post will be educational to people that are excited to learning how to use debugger for vulnerability analysis.
This post will not visit details on RDP communication basics and MS_T120. Interested readers should refer to the following blogs that sum up the need to know basis:
Furthermore, no PoC code will be provided in this post, as the purpose is to show vulnerability analysis with a debugger.
The target machine (debuggee) will be a Windows 7 x64 and the debugger machine will be a Windows 10 x64. Both the debugger and debuggee will run within VirtualBox.
Setting up the kernel debugging environment with VirtualBox
- On the target machine, run cmd.exe with administrative privilege. Use the bcdedit command to enable kernel debugging.
bcdedit /set {current} debug yes
bcdedit /set {current} debugtype serial
bcdedit /set {current} debugport 1
bcdedit /set {current} baudrate 115200
bcdedit /set {current} description "Windows 7 with kernel debug via COM"
When you type bcdedit again, something similar to the following screenshot should display:
2. Shutdown the target machine (debuggee) and right click on the target image in the VirtualBox Manager. Select “Settings” and then “Serial Ports”. Copy the settings as illustrated in the following image and click “OK”:
3. Right click on the image that will host the debugger, and go to the “Serial Ports” setting and copy the settings as shown and click “OK”:
4. Keep the debuggee VM shutdown, and boot up the debugger VM. On the debugger VM, download and install WinDBG. I will be using the WinDBG Preview edition.
5. Once the debugger is installed, select “Attach to kernel”, set the “Baud Rate” to “115200" and “Port” to “com1”. Click on the “initial break” as well.
Click “OK” and the debugger is now ready to attach to the debuggee.
6. Fire up the target “debuggee” machine, and the following prompt will be displayed. Select the one with “debugger enabled” and proceed.
On the debugger end, the WinDBG will have established a connection with the debuggee. It is going to require a few manual enter of “g” into the “debugger command prompt” to have the debuggee completely loaded up. Also, because the debugging action is handled through “com”, the initial start up will take a bit of time.
7. Once the debuggee is loaded, fire up “cmd.exe” and type “netstat -ano”. Locate the PID that runs port 3389, as following:
8. Go back to the debugger and click on “Home” -> “Break” to enable the debugger command prompt and type:
!process 0 0 svchost.exe
This will list a bunch of process that is associated with svchost.exe. We’re interested in the process that has PID 1216 (0x4C0).
9. We will now switch into the context of svchost.exe that runs RDP. In the debugger command prompt, type:
.process /i /p fffffa80082b72a0
After the context switched, pause the debugger and run the command “.reload” to reload all the symbols that the process will use.
Identifying the relevant code path
Without repeating too much of the public information, the patched vulnerability have code changed in the IcaBindVirtualChannels. We know that if IcaFindChannelByName finds the string “MS_T120”, it calls IcaBindchannel such as:
_IcaBindChannel(ChannelControlStructure*, 5, index, dontcare)
The following screenshots depicts the relevant unpatched code in IcaBindVirtualChannels:
We’re going to set two breakpoints.
One will be on _IcaBindChannel where the channel control structure is stored into the channel pointer table. The index of where the channel control structure is stored is based on the index of where the Virtual Channel name is declared within the clientNetworkData of the MCS Initial Connect and GCC Create packet.
and the other one on the “call _IcaBindChannel” within the IcaBindVirtualChannels.
The purpose of these breakpoints areto observe the creation of virtual channels and the orders these channels are created.
bp termdd!IcaBindChannel+0x55 ".printf \"rsi=%d and rbp=%d\\n\", rsi, rbp;dd rdi;.echo"bp termdd!IcaBindVirtualChannels+0x19e ".printf \"We got a MS_T120, r8=%d\\n\",r8;dd rcx;r $t0=rcx;.echo"
The breakpoint first hits the following, with an index value of “31”:
Listing the call stack with “kb” shows the following:
We can see the IcaBindChannel is called from a IcaCreateChannel, which can be traced all the way to the rdpwsx!MSCreateDomain. If we take a look at that function under a disassembler, we noticed it is creating the MS_T120 channel:
Also, but looking at the patched termdd.sys, we know that the patched code enforces the index for MS_T120 virtual channel to be 31, this first breakpoint indicates the first channel that gets created is the MS_T120 channel.
The next breakpoint hit is the 2nd breakpoint (within the IcaBindVirtualChannel), followed by the 1st breakpoint (within IcaBindChannel) again:
This gets hit as it observed the MS_T120 value from the clientNetworkData. If we compared the address and content displayed in above image with the one way, way above, we can see they’re identical. This means both are referring to the same channel control structure. However, the reference to this structure is being stored at two different locations:
rsi = 31, rbp = 5;
[rax + (31 + 5) * 8 + 0xe0] = MST_120_structurersi = 1, rbp = 5;
[rax + (1 + 5) * 8 + 0xe0] = MS_T120_structure
In another words, there are two entries in the channel pointer table that have references to the MS_T120 structure.
Afterwards, a few more channels are created which we don’t care about:
The next step into finding other relevant code to look at will be to set a break read/write on the MS_T120 structure. It is with certain the MS_T120 structure will be ‘touch’ in the future.
I set the break read/write breakpoint on the data within the red box, as shown in the following:
As we proceed with the execution, we get calls to IcaDereferenceChannel, which we’re not interested in. Then, we hit termdd!IcaFindChannel, with some more information to look into from the call stack:
The termdd!IcaChannelInput and termdd!IcaChannelInputInternal sounds like something that might process data sent to the virtual channel.
A pro tip is to set breakpoint before a function call, to see if the registers or stacks (depending how data are passed to a function) could contain recognizable or readable data.
I will set a breakpoint on the call to IcaChannelInputInternal, within the IcaChannelInput function:
bp termdd!IcaChannelInput+0xd8
We’re interested in calls to the IcaChannelInput breakpoint after IcaBindVirtualChannels has been called. From the above image, just right before the call to IcaChannelInputInternal, the rax register holds an address that references to the “A”s I passed over as data through the virtual channel.
I will now set another set of break on read/write on the “A”s to see what code will ‘touch’ them.
ba r8 rax+0xa
The reason I had to add 0xA to the rax register is because the break on read/write requires an align address (ends in0x0 or 0x8 for x64 env)
So the “A”s are now being worked in a “memmove” function. Looking at the call stack, the “memmove” is called from the “IcaCopyDataToUserBuffer”.
Lets step out (gu) of the “memmove” to see where is the destination address that the “A”s are moving to.
Which is here looking at it from the disassembler:
The values for “Src”, “Dst” and “Size” are as follow:
So the “memmove” copy “A”s from an kernel’s address space into a user’s address space.
We will now set another groups of break on read/write on the user’s address space to see how these values are ‘touched’
ba r8 00000000`030ec590
ba r8 00000000`030ec598
ba r8 00000000`030ec5a0
ba r8 00000000`030ec5a8
(side note: If you get a message “Too many data breakpoints for processor 0…”, remove some of the older breakpoints you set then enter “g” again)
We then get a hit on rdpwsx!IoThreadFunc:
The breakpoint touched the memory section in the highlighted red box:
The rdpwsx!IoThreadFunc appears to be the code that parses and handle the MS_T120 data content.
Using a disassembler will provide a greater view:
We will now use “p” command to step over each instruction.
It looks like because I supplied ‘AAAA’, it took a different path.
According to the blog post from ZDI, we need to send crafted data to the MS_T120 channel (over our selected index), so it will terminate the channel (free the MS_T120 channel control structure), such that when the RDPWD!SignalBrokenConnection tries to reach out to the MS_T120 channel again over index 31 from the channel pointer structure, it will Use a Freed MS_T120 channel control structure, leading to the crash.
Based on the rdpwsx!IoThreadFunc, it appears to make sense to create crafted data that will hit the IcaChannelClose function.
When the crafted data is correct, it will hit the rdpwsx!IcaChannelClose
Before stepping through the IcaChannelClose, lets set a breakpoint on the MS_T120 control channel structure to see how does it get affected
The following picture shows the call stack when the breakpoint read is hit. A call is made to ExFreePoolWithTag, which frees the MS_T120 channel control structure.
We can proceed with “g” until we hit the breakpoint in termdd!IcaChannelInput:
Taking a look at the address that holds the MS_T120 channel control structure, the content looks pretty different.
Furthermore, the call stack shows the call to IcaChannelInput comes from RDPWD!SignalBrokenConnection. The ZDI blog noted this function gets called when the connection terminates.
We will use “t” command to step into the IcaChannelInputInternal function. Once we’re inside the function, we will set a new breakpoint:
bp termdd!IcaFindChannel
Once we’re inside the IcaFindChannel function, use “gu” to step out of it to return back to the IcaChannelInputInternal function:
The rax registers holds the reference to the freed MS_T120 control channel structure.
As we continue to step through the code, the address at MS_T120+0x18 is being used as an parameter (rcx) to the ExEnterCriticalRegionAndAcquireResourceExclusive function.
Lets take a look at rcx:
And there we go, if we dereference rcx, it is nothing! So lets step over ExEnterCriticalRegionAndAcquireResourceExclusive and see the result: