This is the fifth 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 fix the audio latency in mstsc.exe. Something that people really ask about on the Internet and there is no definite solution. I’ll show how to hack mstsc.exe to fix the latency. First, I’ll explain why existing solutions do not work and what needs to be done, and at the end of this post I provide an automated PowerShell script that does the magic.
Table of Contents
What is the issue
First, a disclaimer. I’ll describe what I suspect is happening. I’m not an expert of the RDP and I didn’t see the source code of mstsc.exe. I’m just guessing what happens based on what I see.
RDP supports multiple channels. There is a separate channel for video and another one for audio. Unfortunately, these two channels are not synchronized which means that they are synchronized on the client on a best effort basis (or rather sheer luck). To understand why it’s hard to synchronize two independent streams, we need to understand how much different they are.
Video is something that doesn’t need to be presented “for some time”. We can simply take the last video frame and show it to the user. If frames are buffered, we just process them as fast as possible to present the last state. You can see that mstsc.exe does exactly that by connecting to a remote host, playing some video, and then suspending the mstsc.exe process with ProcessExplorer. When you resume it after few seconds, you’ll see the video moving much faster until the client catches up with the latest state.
When it comes to audio, things are much different. You can’t just jump to the latest audio because you’d loose the content as it would be unintelligible. Each audio piece has its desired length. When you get delayed, you could just skip the packets (and lose some audio), play it faster (which would make it less intelligible for some time), or just play it as it goes. However, to decide what to do, you would need to understand whether the piece you want to play is delayed or not. You can’t tell that without timestamps or time markers, and I believe RDP doesn’t send those.
As long as you’re getting audio packets “on time”, there is no issue. You just play them. The first part is if you get them “in time”. This depends on your network quality and on the server that sends you the sound. From my experience, Windows Server is better when it comes to speed of sending updates. I can see my mouse moving faster and audio delayed less often when I connect to the Windows Server than Windows 10 (or other client edition). Therefore, use Windows Server where possible. Just keep in mind that you’ll need CAL licenses to send microphone and camera to the server which is a bummer (client edition lets you do that for free). Making the packets to be sent as fast as possible is the problem number one.
However, there is another part to that. If your client gets delayed for whatever reason (CPU spike, overload, or preemption), your sound will effectively slow down. You will just play it “later” even though you received it “on time”. Unfortunately, RDP cannot detect whether this happened because there are no timestamps in the stream. As far as I can tell, this is the true reason why your sound is often delayed. You can get “no latency audio” over the Internet and I had it many times. However, the longer you run the client, the higher the chance that you’ll go out of sync. This is the problem number two.
Therefore, we need to “resync” the client. Let’s see how to do it.
Why existing solutions don’t work and what fix we need
First, let me explain why existing solutions won’t work. Many articles on the Internet tell you to change the audio quality to High in Group Policy and enforce the quality in your rdp.file by setting audioqualitymode:i:2
. This fixes the problem number one (although I don’t see much difference to be honest), but it doesn’t address the problem number two.
Some other articles suggest other fixes on the remote side. All these fixes have one thing in common – they don’t fix the client. If the mstsc.exe client cannot catch up when it gets delayed, then the only thing you can do is to reset the audio stream. Actually, this is how you can fix the delay easily – just restart the audio service:
1 |
net stop audiosrv & timeout 3 & net start audiosrv |
I add the timeout to give some time to clear the buffer on the client. Once the buffer is empty, we restart the service and then the audio should be in sync. Try this to verify if it’s physically possible to deliver the audio “on time” in your networking conditions.
Unfortunately, restarting the audio service have many issues. First, it resets the devices on the remote end, so your audio streams may break and you’ll need to restart them. Second, this simply takes time. You probably don’t want to lose a couple of seconds of audio and microphone when you’re presenting (and unfortunately, you’ll get delayed exactly during that time).
What we need is to fix the client to catch up when there is a delay. However, how can we do that when we don’t have any timestamps? Well, the solution is simple – just drop some audio packages (like 10%) periodically to sync over time. This will decrease the audio quality to some extent and won’t fix the delay immediately, but after few seconds we’ll get back on track. Obviously, you could implement some better heuristics and solutions. There is one problem, though – we need to do that in the mstsc.exe itself. And here comes the low level magic. Let’s implement the solution that drops the audio frames to effectively “resync” the audio.
How to identify
We need to figure out how the sound is played. Let’s take ApiMonitor to trace the application. Luckily enough, it seems that the waveOutPrepareHeader is used, as we can see in the screenshot:
Let’s break there win WinDBG:
1 |
bu 0x00007ffb897d3019 |
1 2 3 4 5 6 7 8 9 |
kb # RetAddr : Args to Child : Call Site 00 00007ffb`897d1064 : 00000252`6d68beb8 00000252`6d68beb8 00000000`000014ac 00000252`6d68be00 : mstscax!CRdpWinAudioWaveoutPlayback::vcwaveOutWrite+0x6d 01 00007ffb`897e7ee8 : 00000000`00000000 00000073`bfc7fcb9 00000252`6d68be00 00000252`7e69df80 : mstscax!CRdpWinAudioWaveoutPlayback::vcwaveWritePCM+0xec 02 00007ffb`898e73bf : 00000000`00000001 00000000`00000003 00000073`bfc7d055 00000073`00001000 : mstscax!CRdpWinAudioWaveoutPlayback::RenderThreadProc+0x2c8 03 00007ffc`02e57344 : 00000000`000000ac 00000252`6d68be00 00000000`00000000 00000000`00000000 : mstscax!CRdpWinAudioWaveoutPlayback::STATIC_ThreadProc+0xdf 04 00007ffc`046826b1 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x14 05 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21 |
We can see a method named mstscax!CRdpWinAudioWaveoutPlayback::vcwaveOutWrite
. Let’s see it:
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 |
u mstscax!CRdpWinAudioWaveoutPlayback::vcwaveOutWrite mstscax!CRdpWinAudioWaveoutPlayback::vcwaveOutWrite+0x6d mstscax!CRdpWinAudioWaveoutPlayback::vcwaveOutWrite: 00007ffb`897d2fac 48895c2410 mov qword ptr [rsp+10h],rbx 00007ffb`897d2fb1 55 push rbp 00007ffb`897d2fb2 56 push rsi 00007ffb`897d2fb3 57 push rdi 00007ffb`897d2fb4 4883ec40 sub rsp,40h 00007ffb`897d2fb8 488bf2 mov rsi,rdx 00007ffb`897d2fbb 488bd9 mov rbx,rcx 00007ffb`897d2fbe 488b0543287500 mov rax,qword ptr [mstscax!WPP_GLOBAL_Control (00007ffb`89f25808)] 00007ffb`897d2fc5 488d2d3c287500 lea rbp,[mstscax!WPP_GLOBAL_Control (00007ffb`89f25808)] 00007ffb`897d2fcc 483bc5 cmp rax,rbp 00007ffb`897d2fcf 740a je mstscax!CRdpWinAudioWaveoutPlayback::vcwaveOutWrite+0x2f (00007ffb`897d2fdb) 00007ffb`897d2fd1 f6401c01 test byte ptr [rax+1Ch],1 00007ffb`897d2fd5 0f85e8000000 jne mstscax!CRdpWinAudioWaveoutPlayback::vcwaveOutWrite+0x117 (00007ffb`897d30c3) 00007ffb`897d2fdb 83bbc000000000 cmp dword ptr [rbx+0C0h],0 00007ffb`897d2fe2 7418 je mstscax!CRdpWinAudioWaveoutPlayback::vcwaveOutWrite+0x50 (00007ffb`897d2ffc) 00007ffb`897d2fe4 488b8bb8000000 mov rcx,qword ptr [rbx+0B8h] 00007ffb`897d2feb 4885c9 test rcx,rcx 00007ffb`897d2fee 740c je mstscax!CRdpWinAudioWaveoutPlayback::vcwaveOutWrite+0x50 (00007ffb`897d2ffc) 00007ffb`897d2ff0 48ff15a9295c00 call qword ptr [mstscax!_imp_EnterCriticalSection (00007ffb`89d959a0)] 00007ffb`897d2ff7 0f1f440000 nop dword ptr [rax+rax] 00007ffb`897d2ffc 488b4b70 mov rcx,qword ptr [rbx+70h] 00007ffb`897d3000 4885c9 test rcx,rcx 00007ffb`897d3003 0f84f2000000 je mstscax!CRdpWinAudioWaveoutPlayback::vcwaveOutWrite+0x14f (00007ffb`897d30fb) 00007ffb`897d3009 41b830000000 mov r8d,30h 00007ffb`897d300f 488bd6 mov rdx,rsi 00007ffb`897d3012 48ff15e7a87900 call qword ptr [mstscax!_imp_waveOutPrepareHeader (00007ffb`89f6d900)] 00007ffb`897d3019 0f1f440000 nop dword ptr [rax+rax] |
Okay, we can see that this method passes the audio packet to the API. When we look at WAVEHDR structure, we can see that it has the following fields:
1 2 3 4 5 6 7 8 9 10 |
typedef struct wavehdr_tag { LPSTR lpData; DWORD dwBufferLength; DWORD dwBytesRecorded; DWORD_PTR dwUser; DWORD dwFlags; DWORD dwLoops; struct wavehdr_tag *lpNext; DWORD_PTR reserved; } WAVEHDR, *LPWAVEHDR; |
This is exactly what we see in ApiMonitor. Seems like the dwBufferLength
is what we might want to change. When we shorten this buffer, we’ll effectively make the audio last shorter. We can do that for some of the packets to not break the quality much, and then all should be good.
We can verify that this works with this breakpoint:
1 |
bp 00007ffb`897d3012 "r @$t0 = poi(rdx + 8); r @$t1 = @$t0 / 2; ed rdx+8 @$t1; g" |
Unfortunately, this makes the client terribly slow. We need to patch the code in place. Effectively, we need to inject a shellcode.
First, we need to allocate some meory with VirtualAllocEx via .dvalloc.
1 |
.dvalloc 1024 |
The debugger allocates the memory. In my case the address is 25fb8960000
.
The address of the WinAPI function is in the memory, so we need to remember to extract the pointer from the address:
1 |
00007ffb`897d3012 48ff15e7a87900 call qword ptr [mstscax!_imp_waveOutPrepareHeader (00007ffb`89f6d900)] |
Now we need to do two things: first, we need to patch the call site to call our shellcode instead of [mstscax!_imp_waveOutPrepareHeader (00007ffb89f6d900)]
. Second, we need to construct a shell code that fixes the audio packet for some of the packets, and then calls [mstscax!_imp_waveOutPrepareHeader (00007ffb89f6d900)]
correctly.
To do the first thing, we can do the absolute jump trick. We put the address in the rax
register, push it on the stack, and then return. This is the code:
1 2 3 4 5 6 7 |
mov rax, 0x25fb8960000 ;Move the address to the register push rax ;Push the address on the stack ret ;Return. This takes the address from the stuck and jumps nop ;Nops are just to not break the following instructions when you disassemble with u nop nop nop |
We can compile the code with Online assembler and we should get a shell code. We can then put it in place with this line:
1 |
e mstscax!CRdpWinAudioWaveoutPlayback::vcwaveOutWrite+5d 0x48 0xB8 0x00 0x00 0x96 0xb8 0x5f 0x02 0x00 0x00 0x50 0xC3 0x90 0x90 0x90 0x90 |
Unfortunately, this patch is long. We need to break few lines and then restore them in the shellcode. So our shell code starts with the lines that we broke:
1 2 |
mov r8d, 0x30 ;Preserved code mov rdx,rsi ;Preserved code |
Next, we need to preserve our working registers:
1 2 |
push rbx push rdx |
Okay, now we can do the logic. We want to modify the buffer length. However, we don’t want to do it for all of the packets. We need some source of randomness, like time, random values, or something else. Fortunately, the WAVEHDR
structure has the field dwUser
which may be “random enough” for our needs. Let’s take that value modulo some constant, and then change the packet length only for some cases.
First, let’s preserve the buffer length for the sake of what we do later:
1 2 |
mov rax, [rdx + 8] ;Load buffer length (that's the second field of the structure) push rax ;Store buffer length on the stack |
Now, let’s load dwUser
and divide it by some constant like 23:
1 2 3 4 |
mov rax, [rdx + 16] ;Load dwUser which is the fourth field mov rbx, 23 ;Move the constant to the register xor rdx, rdx ;Clear upper divisor part div rbx ;Divide |
Now, we can restore the buffer length to rax
:
1 |
pop rax ;Restore buffer length |
At this point we have rax
with the buffer length, and rdx
with the remainder. We can now compare the reminder and skip the code modifying the pucket length if needed:
1 2 |
cmp rdx, 20 ;Compare with 20 jbe 0x17 ;Skip the branch |
We can see that we avoid the buffer length modification if the remainder is at most 20. Effectively, we have 20/22 = 0.909% chance that we won’t modify the package. This means that we modify something like 9% of the packages, assuming the dwUser
has a good distribution. The code is written in this way so you can tune the odds of changing the packet.
Now, let’s modify the package. We want to divide the buffer length by 2, however, we want to keep it generic to be able to experiment with other values:
1 2 3 4 5 6 |
mov rbx, 1 ;Move 1 to rbx to multiply by 1/2 xor rdx, rdx ;Clear remainder mul rbx ;Multiply mov rbx, 2 ;Store 2 to rbx to multiply by 1/2 xor rdx, rdx ;Clear remainder div rbx ;Divide |
You can play with other values, obviously. From my experiments, halving the value works the best.
Now it’s rather easy. rax
has the new buffer length or the original one if we decided not to modify it. Let’s restore other registers:
1 2 |
pop rdx pop rbx |
Let’s update the buffer length:
1 |
mov [rdx + 8], rax |
Now, we need to prepare the jump addresses. First, we want to put the original return address of the method mstscax!CRdpWinAudioWaveoutPlayback::vcwaveOutWrite
which is mstscax!CRdpWinAudioWaveoutPlayback::vcwaveOutWrite+0x6d
:
1 2 |
mov rax, 0x00007ffb897d3019 push rax |
Now, we can jump to the WinAPI method:
1 2 3 4 5 6 |
push rbx mov rbx, 0x00007ffbe6c6a860 mov rax, [rbx] pop rbx push rax ret |
That’s it. The final shellcode looks like this:
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 |
mov r8d, 0x30 mov rdx,rsi push rbx push rdx mov rax, [rdx + 8] push rax mov rax, [rdx + 16] mov rbx, 23 xor rdx, rdx div rbx pop rax cmp rdx, 20 jbe 0x17 mov rbx, 1 xor rdx, rdx mul rbx mov rbx, 2 xor rdx, rdx div rbx pop rdx pop rbx mov [rdx + 8], rax mov rax, 0x00007ffb897d3019 push rax push rbx mov rbx, 0x00007ffbe6c6a860 mov rax, [rbx] pop rbx push rax ret |
We can implant it with this:
1 |
e 25f`b8960000 0x41 0xB8 0x30 0x00 0x00 0x00 0x48 0x89 0xF2 0x53 0x52 0x48 0x8B 0x42 0x08 0x50 0x48 0x8B 0x42 0x10 0x48 0xC7 0xc3 0x17 0x00 0x00 0x00 0x48 0x31 0xD2 0x48 0xF7 0xF3 0x58 0x48 0x83 0xFA 0x14 0x76 0x1A 0x48 0xC7 0xC3 0x01 0x00 0x00 0x00 0x48 0x31 0xD2 0x48 0xF7 0xE3 0x48 0xC7 0xC3 0x02 0x00 0x00 0x00 0x48 0x31 0xD2 0x48 0xF7 0xF3 0x5A 0x5B 0x48 0x89 0x42 0x08 0x48 0xB8 0x19 0x30 0x7D 0x89 0xFB 0x7F 0x00 0x00 0x50 0x48 0xBB 0x60 0xA8 0xC6 0xE6 0xFB 0x7F 0x00 0x00 0x50 0xC3 |
Automated fix
We can now fix the code automatically. We need to do the following:
- Start mstsc.exe
- Find it’s process ID
- Attach the debugger and find all the addresses: free memory, mstsc.exe method, WinAPI method
- Construct the shellcode
- Attach the debugger and patch the code
- Detach the debugger
We can do all of that with PowerShell. Here is the code:
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
Function Run-Mstsc($rdpPath, $cdbPath, $numerator, $denominator){ $id = get-random $code = @" using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; namespace MstscPatcher { public class Env$id { public static void Start() { Process[] processes = Process.GetProcessesByName("mstsc"); RunProcess("mstsc.exe", "$rdpPath"); Thread.Sleep(3000); Process[] processes2 = Process.GetProcessesByName("mstsc"); var idToPatch = processes2.Select(p => p.Id).OrderBy(i => i).Except(processes.Select(p => p.Id).OrderBy(i => i)).First(); Patch(idToPatch); } public static void Patch(int id){ Console.WriteLine(id); var addresses = RunCbd(id, @" .sympath srv*C:\tmp*http://msdl.microsoft.com/download/symbols .reload !address u mstscax!CRdpWinAudioWaveoutPlayback::vcwaveOutWrite mstscax!CRdpWinAudioWaveoutPlayback::vcwaveOutWrite+100 .dvalloc 1024 qd "); string freeMemoryAddress = addresses.Where(o => o.Contains("Allocated 2000 bytes starting at")).First().Split(' ').Last().Trim(); Console.WriteLine("Free memory: " + freeMemoryAddress); var patchAddress = addresses.SkipWhile(o => !o.StartsWith("mstscax!CRdpWinAudioWaveoutPlayback::vcwaveOutWrite:")) .SkipWhile(o => !o.Contains("r8d,30h")) .First().Split(' ').First().Trim(); Console.WriteLine("Patch address: " + patchAddress); var returnAddress = (Convert.ToUInt64(patchAddress.Replace(((char)96).ToString(),""), 16) + 0x10).ToString("X").Replace("0x", ""); Console.WriteLine("Return address: " + returnAddress); var winApiAddress = addresses.SkipWhile(o => !o.Contains("[mstscax!_imp_waveOutPrepareHeader")) .First().Split('(')[1].Split(')')[0].Trim(); Console.WriteLine("WinAPI address: " + winApiAddress); 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 finalScript = @" .sympath srv*C:\tmp*http://msdl.microsoft.com/download/symbols .reload e " + patchAddress + @" 0x48 0xB8 " + translateToBytes(freeMemoryAddress) + @" 0x50 0xC3 0x90 0x90 0x90 0x90 e " + freeMemoryAddress + @" 0x41 0xB8 0x30 0x00 0x00 0x00 0x48 0x89 0xF2 0x53 0x52 0x48 0x8B 0x42 0x08 0x50 0x48 0x8B 0x42 0x10 0x48 0xC7 0xc3 $denominator 0x00 0x00 0x00 0x48 0x31 0xD2 0x48 0xF7 0xF3 0x58 0x48 0x83 0xFA $numerator 0x76 0x1A 0x48 0xC7 0xC3 0x01 0x00 0x00 0x00 0x48 0x31 0xD2 0x48 0xF7 0xE3 0x48 0xC7 0xC3 0x02 0x00 0x00 0x00 0x48 0x31 0xD2 0x48 0xF7 0xF3 0x5A 0x5B 0x48 0x89 0x42 0x08 0x48 0xB8 " + translateToBytes(returnAddress) + @" 0x50 0x53 0x48 0xBB " + translateToBytes(winApiAddress) + @" 0x48 0x8B 0x03 0x5B 0x50 0xC3 qd "; Console.WriteLine(finalScript); RunCbd(id, finalScript); } public static string[] RunCbd(int id, string script) { Console.WriteLine(script); File.WriteAllText("mstsc.txt", script); string output = ""; Process process = RunProcess("$cdbPath", "-p " + id + " -cf mstsc.txt", text => output += text + "\n"); process.WaitForExit(); File.Delete("mstsc.txt"); return output.Split('\n'); } public static Process RunProcess(string fileName, string arguments, Action<string> outputReader = null){ ProcessStartInfo startInfo = new ProcessStartInfo { FileName = fileName, Arguments = arguments, UseShellExecute = outputReader == null, RedirectStandardOutput = outputReader != null, RedirectStandardError = outputReader != null }; if(outputReader != null){ var process = new Process{ StartInfo = startInfo }; process.OutputDataReceived += (sender, args) => outputReader(args.Data); process.ErrorDataReceived += (sender, args) => outputReader(args.Data); process.Start(); process.BeginOutputReadLine(); process.BeginErrorReadLine(); return process; }else { return Process.Start(startInfo); } } } } "@.Replace('$id', $id) $assemblies = ("System.Core","System.Xml.Linq","System.Data","System.Xml", "System.Data.DataSetExtensions", "Microsoft.CSharp") Add-Type -referencedAssemblies $assemblies -TypeDefinition $code -Language CSharp iex "[MstscPatcher.Env$id]::Start()" } Run-Mstsc "my_rdp_settings.rdp".Replace("\", "\\") "cdb.exe".Replace("\", "\\") "0x14" "0x17" |
We compile some C# code on the fly. First, we find existing mstsc.exe instances (line 16), then run the new instance (line 17), wait a bit for the mstsc.exe to spawn a child process, and then find the id (lines 19-20). We can then patch the existing id.
First, we look for addresses. We do all the manual steps we did above to find the memory address, and two function addresses. The script is in lines 27-32. Notice that I load symbols as we need them and CDB may not have them configured on the box.
We can now parse the output. We first extract the allocated memory in lines 35-36.
Next, we look for the call site. We dump the whole method, and then find the first occurrence of mov 8d,30h
. That’s our call site. This is in lines 38-41.
Next, we calculate the return address which is 16 bytes further. This is in lines 42-43.
Finally, I calculate the WinAPI method address. I extract the location of the pointer for the method (lines 45-47).
Next, we need to construct the shell code. This is exactly what we did above. We just need to format addresses properly (this is in helper methods in lines 49-50), and then build the script (lines 52-558). We can run it and that’s it. The last thing is customization of the values. You can see in line 108 that I made two parameters to change numerator and denominator for the odds of modifying the package. This way you can easily change how many packets are broken. The more you break, the faster you resynchronize, however, the worse the sound is.
That’s it. I verified that on Windows 10 22H2 x64, Windows 11 22H2 x64, Windows Server 2016 1607 x64, and Windows Server 2019 1809 x64, and it worked well. Your mileage may vary, however, the approach should work anywhere. Generally, to make this script work somewhere else, you just need to adjust how we find the call site, the return address, and the address of the WinAPI function. Assuming that the WinAPI is still called via the pointer stored in memory, then you won’t need to touch the machine code payload.
Below is the script for x86 bit (worked on Windows 10 10240 x86). Main differences are in how we access the data structure as the pointer is on the stack (and not in the register).
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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
Function Run-Mstsc($rdpPath, $cdbPath, $numerator, $denominator){ $id = get-random $code = @" using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; namespace MstscPatcher { public class Env$id { public static void Start() { Process[] processes = Process.GetProcessesByName("mstsc"); RunProcess("mstsc.exe", "$rdpPath"); Thread.Sleep(3000); Process[] processes2 = Process.GetProcessesByName("mstsc"); var idToPatch = processes2.Select(p => p.Id).OrderBy(i => i).Except(processes.Select(p => p.Id).OrderBy(i => i)).First(); Patch(idToPatch); } public static void Patch(int id){ Console.WriteLine(id); var addresses = RunCbd(id, @" .sympath srv*C:\tmp*http://msdl.microsoft.com/download/symbols .reload !address u mstscax!CRdpWinAudioWaveoutPlayback::vcwaveOutWrite mstscax!CRdpWinAudioWaveoutPlayback::vcwaveOutWrite+100 .dvalloc 1024 qd "); string freeMemoryAddress = addresses.Where(o => o.Contains("Allocated 2000 bytes starting at")).First().Split(' ').Last().Trim(); Console.WriteLine("Free memory: " + freeMemoryAddress); var patchAddress = addresses.SkipWhile(o => !o.StartsWith("mstscax!CRdpWinAudioWaveoutPlayback::vcwaveOutWrite:")) .SkipWhile(o => !(o.Contains("dword ptr [ebx+3Ch]") && o.Contains("push"))) .First().Split(' ').First().Trim(); Console.WriteLine("Patch address: " + patchAddress); var returnAddress = (Convert.ToUInt64(patchAddress.Replace(((char)96).ToString(),""), 16) + 0x9).ToString("X").Replace("0x", ""); Console.WriteLine("Return address: " + returnAddress); var winApiAddress = addresses.SkipWhile(o => !o.Contains("[mstscax!_imp__waveOutPrepareHeader")) .First().Split('(')[1].Split(')')[0].Trim(); Console.WriteLine("WinAPI address: " + winApiAddress); 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(8, '0')).Reverse().Select(p => "0x" + p)); var finalScript = @" .sympath srv*C:\tmp*http://msdl.microsoft.com/download/symbols .reload e " + patchAddress + @" 0xB8 " + translateToBytes(freeMemoryAddress) + @" 0x50 0xC3 0x90 0x90 e " + freeMemoryAddress + @" 0xFF 0x73 0x3C 0x53 0x52 0x8B 0x53 0x3C 0x52 0x8B 0x42 0x04 0x50 0x8B 0x42 0x0C 0xBB $denominator 0x00 0x00 0x00 0x31 0xD2 0xF7 0xF3 0x58 0x83 0xFA $numerator 0x76 0x12 0xBB 0x01 0x00 0x00 0x00 0x31 0xD2 0xF7 0xE3 0xBB 0x02 0x00 0x00 0x00 0x31 0xD2 0xF7 0xF3 0x5A 0x89 0x42 0x04 0x5A 0x5B 0xB8 " + translateToBytes(returnAddress) + @" 0x50 0x53 0xBB " + translateToBytes(winApiAddress) + @" 0x8B 0x03 0x5B 0x50 0xC3 qd "; Console.WriteLine(finalScript); RunCbd(id, finalScript); } public static string[] RunCbd(int id, string script) { Console.WriteLine(script); File.WriteAllText("mstsc.txt", script); string output = ""; Process process = RunProcess("$cdbPath", "-p " + id + " -cf mstsc.txt", text => output += text + "\n"); process.WaitForExit(); File.Delete("mstsc.txt"); return output.Split('\n'); } public static Process RunProcess(string fileName, string arguments, Action<string> outputReader = null){ ProcessStartInfo startInfo = new ProcessStartInfo { FileName = fileName, Arguments = arguments, UseShellExecute = outputReader == null, RedirectStandardOutput = outputReader != null, RedirectStandardError = outputReader != null }; if(outputReader != null){ var process = new Process{ StartInfo = startInfo }; process.OutputDataReceived += (sender, args) => outputReader(args.Data); process.ErrorDataReceived += (sender, args) => outputReader(args.Data); process.Start(); process.BeginOutputReadLine(); process.BeginErrorReadLine(); return process; }else { return Process.Start(startInfo); } } } } "@.Replace('$id', $id) $assemblies = ("System.Core","System.Xml.Linq","System.Data","System.Xml", "System.Data.DataSetExtensions", "Microsoft.CSharp") Add-Type -referencedAssemblies $assemblies -TypeDefinition $code -Language CSharp iex "[MstscPatcher.Env$id]::Start()" } Run-Mstsc "my_rdp_settings.rdp".Replace("\", "\\") "cdb.exe".Replace("\", "\\") "0x14" "0x17" |
Some keyword to make this easier to find on the Internet
Her some keywords to make this article easier to find on the Internet.
audio latency in mstsc.exe
audio latency in rdp
audio delay in mstsc.exe
audio delay in rdp
laggy sound in rdp
sound desynchronized in rdp
sound latency in rdp
slow audio
how to fix audio in rdp
Enjoy!