This is the seventh part of the Bit Twiddling series. For your convenience you can find other parts in the table of contents in Par 1 — Modifying Android application on a binary level
Today we’re going to tackle the problem of keeping windows in the same place when connecting over RDP. In the other blog post I said, that it’s not possible to control the order of the screens enumerated by mstsc /l
programmatically, and that we have to move the screens by plugging them differently. Let’s actually change that with some low-level hacks.
Table of Contents
What is the problem again
Let’s say, that I have the following monitors reported by mstsc /l
:
We can see that I have 4 monitors:
- Monitor 1 is in the center. It’s reported as id
0
in the MSTSC Setup - Monitor 2 is on the left. It’s reported as id
3
in the MSTSC Setup - Monitor 3 is on the right. It’s reported as id
4
in the MSTSC Setup - Monitor 4 is at the bottom. It’s reported as id
30
in the MSTSC Setup
Once I log in to the remote machine, this is what the Display in RDP shows:
Looks good. Most importantly, if I open a window on the Monitor 1 (the one in the center), then it is shown like this:
Important part in this picture is that the notepad is in the center. You don’t need to zoom in to see any other details.
Now, let’s say that I connect another display and make it duplicate the Monitor 1. Windows may decide that the order of the devices changed, and now mstsc /l
shows this:
Notice that the Monitor 1 in the center changed its id to 5
.
Let’s connect to the RDP server again. This is what Display in RDP shows:
Notice that the monitors changed their numbering. First monitor is now on the left. What’s worse, the windows have moved:
You can see that the notepad moved to the left. It didn’t move actually, it is still on the same first monitor. It’s that the monitor changed its position.
What’s worse, even if I add selectedmonitors:s:5,3,4,30
to the .rdp
file, the problem is still there.
Why does it happen? You can read more in this article where I describe it in details. But now, let’s try to fix it.
How it works
We can use API Monitor or other strace-like application to figure out what happens. mstsc
uses EnumDisplayDevicesW function to find all the devices.
The API accepts a parameter with the device id (that is the second parameter), and the pointer to the structure where the details will be stored (the third parameter). Most importantly, it returns true or false indicating whether a monitor with given device id exists.
mstsc
simply runs in a loop like this:
1 2 3 4 5 |
for(int i=0;;++i){ if(!EnumDisplayDevicesW(null, i, pointer, flags)){ break; } } |
mstsc
iterates over the devices starting from zero until there are no more devices reported by the API. This explains why numbering in mstsc /l
is not continuous and why the numbers may change any time.
How can we fix that? We need to hijack the method, check the second parameter, and override it accordingly. With the screenshots above, the loop runs like this:
1 2 3 4 5 6 7 8 9 10 |
id = 0 => some virtual screen and returns true id = 1 => some virtual screen and returns true id = 2 => some virtual screen and returns true id = 3 => Monitor 2 and returns true id = 4 => Monitor 3 and returns true id = 5 => Monitor 1 and returns true ... id = 30 => Monitor 4 and returns true ... id = something big => returns false |
We would like it to effectively do something like this:
1 2 3 4 5 |
id = 0 => changes id to 5 => Monitor 1 and returns true id = 1 => changes id to 3 => Monitor 2 and returns true id = 2 => changes id to 4 => Monitor 3 and returns true id = 3 => changes id to 30 => Monitor 4 and returns true id = 4 => returns false |
Let’s do it.
Implementation
As with other dirty hacks, we are going to use the debugger to inject the payload and do some memory operations.
The plan is as follows:
- We allocate some memory on the side
- We add a payload
call_and_return_false
that calls thejust_call
payload and returnsfalse
afterwards - We add a payload
call_and_return_true
that calls thejust_call
payload and returnstrue
afterwards - We add a payload
just_call
that restores proper registers and then just calls the originalEnumDisplayDevicesW
- We find the regular
EnumDisplayDevicesW
implementation and do the long jump to our main payload - The main payload checks the second parameter (which is in the
rdx
register). If the parameter should be changed, it is modified and then we jump to thecall_and_return_true
orcall_and_return_false
payload accordingly. Otherwise, it jumps to thejust_call
payload.
Let’s see the payloads in action. First, let’s examine the original EnumDisplayDevicesW
method:
1 2 3 4 5 6 7 8 9 10 11 |
u USER32!EnumDisplayDevicesW 0:000> u USER32!EnumDisplayDevicesW USER32!EnumDisplayDevicesW: 00007ffa`0cd01240 4053 push rbx 00007ffa`0cd01242 55 push rbp 00007ffa`0cd01243 56 push rsi 00007ffa`0cd01244 57 push rdi 00007ffa`0cd01245 4156 push r14 00007ffa`0cd01247 4157 push r15 00007ffa`0cd01249 4881ec98030000 sub rsp,398h 00007ffa`0cd01250 488b0529e60700 mov rax,qword ptr [USER32!_security_cookie (00007ffa`0cd7f880)] |
Nothing special here. It simply preserves the registers. We’ll need to override this with a long jump to our payload, like this:
1 2 3 4 5 6 7 |
mov rax, payload_address push rax ret nop nop nop nop |
This way, we override the first 16 bytes.
Okay, let’s now see the main payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
cmp rdx, monitor_id_1 jne 0x13 mov rdx, overriden_id_1 mov rax, call_and_return_false or call_and_return_true push rax ret cmp rdx, monitor_id_2 jne 0x13 mov rdx, overriden_id_2 mov rax, call_and_return_false or call_and_return_true push rax ret ... cmp rdx, monitor_id_n jne 0x13 mov rdx, overriden_id_n mov rax, call_and_return_false or call_and_return_true push rax ret mov rax, just_call push rax ret |
We have a series of instructions like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
if(second_parameter == monitor_id_1){ second_parameter = overriden_id_1 jump call_and_return_false or call_and_return_true } if(second_parameter == monitor_id_2){ second_parameter = overriden_id_2 jump call_and_return_false or call_and_return_true } ... if(second_parameter == monitor_id_n){ second_parameter = overriden_id_n jump call_and_return_false or call_and_return_true } jump just_call |
We compare each monitor in a sequence, override the parameter if needed, and jump accordingly.
Now, let’s see the payload for call_and_return_false
1 2 3 4 5 6 7 |
movabs rax, call_and_return_false+0x13 push rax movabs rax, just_call push rax ret mov rax, 0 ret |
This conceptually looks like this:
1 2 3 4 5 |
put after_call address on the stack jump just_call :after_call return 0 |
We do the same for call_and_return_true
and just return different value.
The payload for just_call
looks like this:
1 2 3 4 5 6 7 8 9 10 |
push rbx push rbp push rsi push rdi push r14 push r15 sub rsp,398h movabs rax, EnumDisplayDevicesW+0x16 push rax ret |
We simply run the preamble of the WinAPI function (which we scratched by introducing long jump over there), and then we jump th the WinAPI function in the correct place.
Automation
Let’s now see some sample C# code that does all of that automatically:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
public static void Patch(int id){ Console.WriteLine(id); var addresses = RunCbd(id, @" .sympath srv*C:\tmp*http://msdl.microsoft.com/download/symbols .reload .dvalloc 3000 .dvalloc 4000 .dvalloc 5000 .dvalloc 6000 u USER32!EnumDisplayDevicesW qd "); Func<string, IEnumerable<string>> splitInPairs = address => address.Where((c, i) => i % 2 == 0).Zip(address.Where((c, i) => i % 2 == 1), (first, second) => first.ToString() + second.ToString()); Func<string, string> translateToBytes = address => string.Join(" ", splitInPairs(address.Replace(((char)96).ToString(), "").PadLeft(16, '0')).Reverse().Select(p => "0x" + p)); var pattern = "0,0,1-1,3,1-2,5,1-3,30,1-4,0,0"; var freeMemoryForEnumDisplayReturnFalse = addresses.Where(o => o.Contains("Allocated 3000 bytes starting at")).First().Split(' ').Last().Trim(); var freeMemoryForEnumDisplayReturnTrue = addresses.Where(o => o.Contains("Allocated 4000 bytes starting at")).First().Split(' ').Last().Trim(); var freeMemoryForEnumDisplayJustCall = addresses.Where(o => o.Contains("Allocated 5000 bytes starting at")).First().Split(' ').Last().Trim(); var freeMemoryForEnumDisplay = addresses.Where(o => o.Contains("Allocated 6000 bytes starting at")).First().Split(' ').Last().Trim(); var enumDisplayAddress = addresses.SkipWhile(o => !o.StartsWith("USER32!EnumDisplayDevicesW:")) .Skip(1) .First().Split(' ').First().Trim(); var enumAfterPayloadReturnAddress = (Convert.ToUInt64(enumDisplayAddress.Replace(((char)96).ToString(),""), 16) + 0x10).ToString("X").Replace("0x", ""); var freeMemoryForEnumDisplayReturnFalseAfterCall = (Convert.ToUInt64(freeMemoryForEnumDisplayReturnFalse.Replace(((char)96).ToString(),""), 16) + 23).ToString("X").Replace("0x", ""); var freeMemoryForEnumDisplayReturnTrueAfterCall = (Convert.ToUInt64(freeMemoryForEnumDisplayReturnTrue.Replace(((char)96).ToString(),""), 16) + 23).ToString("X").Replace("0x", ""); var patternInstruction = ""; if(pattern != ""){ foreach(var part in pattern.Split('-')){ var sourceMonitor = int.Parse(part.Split(',')[0]).ToString("X"); var destinationMonitor = int.Parse(part.Split(',')[1]).ToString("X"); var returnValue = part.Split(',')[2]; patternInstruction += @" 0x48 0x83 0xFA " + sourceMonitor + @" "; patternInstruction += @" 0x75 13 "; // jump short 13 bytes patternInstruction += @" 0x48 0xC7 0xC2 " + destinationMonitor + @" 0x00 0x00 0x00 "; patternInstruction += @" 0x48 0xB8 " + translateToBytes(returnValue == "1" ? freeMemoryForEnumDisplayReturnTrue : freeMemoryForEnumDisplayReturnFalse) + @" 0x50 0xC3 "; } } var patchEnumDisplayScript = @" .sympath srv*C:\tmp*http://msdl.microsoft.com/download/symbols .reload e " + enumDisplayAddress + @" 0x48 0xB8 " + translateToBytes(freeMemoryForEnumDisplay) + @" 0x50 0xC3 0x90 0x90 0x90 0x90 e " + freeMemoryForEnumDisplayReturnFalse + @" 0x48 0xB8 " + translateToBytes(freeMemoryForEnumDisplayReturnFalseAfterCall) + @" 0x50 0x48 0xB8 " + translateToBytes(freeMemoryForEnumDisplayJustCall) + @" 0x50 0xC3 0x48 0xC7 0xC0 0x00 0x00 0x00 0x00 0xC3 e " + freeMemoryForEnumDisplayReturnTrue + @" 0x48 0xB8 " + translateToBytes(freeMemoryForEnumDisplayReturnTrueAfterCall) + @" 0x50 0x48 0xB8 " + translateToBytes(freeMemoryForEnumDisplayJustCall) + @" 0x50 0xC3 0x48 0xC7 0xC0 0x01 0x00 0x00 0x00 0xC3 e " + freeMemoryForEnumDisplayJustCall + @" 0x53 0x55 0x56 0x57 0x41 0x56 0x41 0x57 0x48 0x81 0xEC 0x98 0x03 0x00 0x00 0x48 0xB8 " + translateToBytes(enumAfterPayloadReturnAddress) + @" 0x50 0xC3 e " + freeMemoryForEnumDisplay + @" " + patternInstruction + @" 0x48 0xB8 " + translateToBytes(freeMemoryForEnumDisplayJustCall) + @" 0x50 0xC3 qd "; RunCbd(id, patchEnumDisplayScript); } |
Most of that should be rather straightforward, but let’s go through it line by line.
We start by allocating some memory in the process and dumping the machine code of the EnumDisplayDevicesW
function (lines 3-12).
We then parse the output (lines 14-15 and 19-25). We then calculate some dependent labels (lines 27-29).
Important part is the pattern in line 17. We encode it in the following way (spaces added for clarity):
1 |
monitor_id_1,override_id_1,return_true_or_false - monitor_id_2,override_id_2,return_true_or_false - ... |
We then parse this pattern and use it in lines 31-44.
Finally, we concatenate all of that together and create the instructions for the debugger (lines 46-57).