This is the fifteenth part of the .NET Inside Out series. For your convenience you can find other parts in the table of contents in Part 1 – Virtual and non-virtual calls in C#
Windows supports multiple desktops for a long time. We may want to run an application on different one than the current but it isn’t easy in C#. There is an lpDesktop property but it isn’t directly exposed to the C# wrapper. Let’s see what we can do about that.
First, let’s see if it works. I’m using Sysinternals Desktops application to create new desktop. It is named “Sysinternals Desktop 1”. To start a process there I can use the following C++ 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 |
#include "stdafx.h" #include <windows.h> #include <stdio.h> #include <tchar.h> void _tmain(int argc, TCHAR *argv[]) { STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); ZeroMemory(&pi, sizeof(pi)); wchar_t* name = L"Sysinternals Desktop 1"; si.lpDesktop = name; if (argc != 2) { printf("Usage: %s [cmdline]\n", argv[0]); return; } // Start the child process. if (!CreateProcess(NULL, // No module name (use command line) argv[1], // Command line NULL, // Process handle not inheritable NULL, // Thread handle not inheritable FALSE, // Set handle inheritance to FALSE 0, // No creation flags NULL, // Use parent's environment block NULL, // Use parent's starting directory &si, // Pointer to STARTUPINFO structure &pi) // Pointer to PROCESS_INFORMATION structure ) { printf("CreateProcess failed (%d).\n", GetLastError()); return; } // Wait until child process exits. WaitForSingleObject(pi.hProcess, INFINITE); // Close process and thread handles. CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } |
Now, we somehow need to get an access to STARTUPINFO
record in C#. Let’s take this code:
1 2 3 4 5 6 7 8 9 10 11 12 |
var process = new Process(); var startInfo = new ProcessStartInfo { WindowStyle = ProcessWindowStyle.Hidden, FileName = "notepad.exe", Arguments = "", RedirectStandardOutput = true, UseShellExecute = false, StandardOutputEncoding = Encoding.UTF8 }; process.StartInfo = startInfo; process.Start(); |
When we debug Start
method internals we get to the following:
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 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 |
private bool StartWithCreateProcess(ProcessStartInfo startInfo) { if (startInfo.StandardOutputEncoding != null && !startInfo.RedirectStandardOutput) { throw new InvalidOperationException(SR.GetString("StandardOutputEncodingNotAllowed")); } if (startInfo.StandardErrorEncoding != null && !startInfo.RedirectStandardError) { throw new InvalidOperationException(SR.GetString("StandardErrorEncodingNotAllowed")); } if (this.disposed) { throw new ObjectDisposedException(base.GetType().Name); } StringBuilder stringBuilder = Process.BuildCommandLine(startInfo.FileName, startInfo.Arguments); NativeMethods.STARTUPINFO startupinfo = new NativeMethods.STARTUPINFO(); SafeNativeMethods.PROCESS_INFORMATION process_INFORMATION = new SafeNativeMethods.PROCESS_INFORMATION(); SafeProcessHandle safeProcessHandle = new SafeProcessHandle(); SafeThreadHandle safeThreadHandle = new SafeThreadHandle(); int num = 0; SafeFileHandle handle = null; SafeFileHandle handle2 = null; SafeFileHandle handle3 = null; GCHandle gchandle = default(GCHandle); object obj = Process.s_CreateProcessLock; lock (obj) { try { if (startInfo.RedirectStandardInput || startInfo.RedirectStandardOutput || startInfo.RedirectStandardError) { if (startInfo.RedirectStandardInput) { this.CreatePipe(out handle, out startupinfo.hStdInput, true); } else { startupinfo.hStdInput = new SafeFileHandle(NativeMethods.GetStdHandle(-10), false); } if (startInfo.RedirectStandardOutput) { this.CreatePipe(out handle2, out startupinfo.hStdOutput, false); } else { startupinfo.hStdOutput = new SafeFileHandle(NativeMethods.GetStdHandle(-11), false); } if (startInfo.RedirectStandardError) { this.CreatePipe(out handle3, out startupinfo.hStdError, false); } else { startupinfo.hStdError = new SafeFileHandle(NativeMethods.GetStdHandle(-12), false); } startupinfo.dwFlags = 256; } int num2 = 0; if (startInfo.CreateNoWindow) { num2 |= 134217728; } IntPtr intPtr = (IntPtr)0; if (startInfo.environmentVariables != null) { bool unicode = false; if (ProcessManager.IsNt) { num2 |= 1024; unicode = true; } byte[] value = EnvironmentBlock.ToByteArray(startInfo.environmentVariables, unicode); gchandle = GCHandle.Alloc(value, GCHandleType.Pinned); intPtr = gchandle.AddrOfPinnedObject(); } string text = startInfo.WorkingDirectory; if (text == string.Empty) { text = Environment.CurrentDirectory; } bool flag2; if (startInfo.UserName.Length != 0) { if (startInfo.Password != null && startInfo.PasswordInClearText != null) { throw new ArgumentException(SR.GetString("CantSetDuplicatePassword")); } NativeMethods.LogonFlags logonFlags = (NativeMethods.LogonFlags)0; if (startInfo.LoadUserProfile) { logonFlags = NativeMethods.LogonFlags.LOGON_WITH_PROFILE; } IntPtr intPtr2 = IntPtr.Zero; try { if (startInfo.Password != null) { intPtr2 = Marshal.SecureStringToCoTaskMemUnicode(startInfo.Password); } else if (startInfo.PasswordInClearText != null) { intPtr2 = Marshal.StringToCoTaskMemUni(startInfo.PasswordInClearText); } else { intPtr2 = Marshal.StringToCoTaskMemUni(string.Empty); } RuntimeHelpers.PrepareConstrainedRegions(); try { } finally { flag2 = NativeMethods.CreateProcessWithLogonW(startInfo.UserName, startInfo.Domain, intPtr2, logonFlags, null, stringBuilder, num2, intPtr, text, startupinfo, process_INFORMATION); if (!flag2) { num = Marshal.GetLastWin32Error(); } if (process_INFORMATION.hProcess != (IntPtr)0 && process_INFORMATION.hProcess != NativeMethods.INVALID_HANDLE_VALUE) { safeProcessHandle.InitialSetHandle(process_INFORMATION.hProcess); } if (process_INFORMATION.hThread != (IntPtr)0 && process_INFORMATION.hThread != NativeMethods.INVALID_HANDLE_VALUE) { safeThreadHandle.InitialSetHandle(process_INFORMATION.hThread); } } if (flag2) { goto IL_416; } if (num == 193 || num == 216) { throw new Win32Exception(num, SR.GetString("InvalidApplication")); } throw new Win32Exception(num); } finally { if (intPtr2 != IntPtr.Zero) { Marshal.ZeroFreeCoTaskMemUnicode(intPtr2); } } } RuntimeHelpers.PrepareConstrainedRegions(); try { } finally { flag2 = NativeMethods.CreateProcess(null, stringBuilder, null, null, true, num2, intPtr, text, startupinfo, process_INFORMATION); if (!flag2) { num = Marshal.GetLastWin32Error(); } if (process_INFORMATION.hProcess != (IntPtr)0 && process_INFORMATION.hProcess != NativeMethods.INVALID_HANDLE_VALUE) { safeProcessHandle.InitialSetHandle(process_INFORMATION.hProcess); } if (process_INFORMATION.hThread != (IntPtr)0 && process_INFORMATION.hThread != NativeMethods.INVALID_HANDLE_VALUE) { safeThreadHandle.InitialSetHandle(process_INFORMATION.hThread); } } if (!flag2) { if (num == 193 || num == 216) { throw new Win32Exception(num, SR.GetString("InvalidApplication")); } throw new Win32Exception(num); } } finally { if (gchandle.IsAllocated) { gchandle.Free(); } startupinfo.Dispose(); } } IL_416: if (startInfo.RedirectStandardInput) { this.standardInput = new StreamWriter(new FileStream(handle, FileAccess.Write, 4096, false), Console.InputEncoding, 4096); this.standardInput.AutoFlush = true; } if (startInfo.RedirectStandardOutput) { Encoding encoding = (startInfo.StandardOutputEncoding != null) ? startInfo.StandardOutputEncoding : Console.OutputEncoding; this.standardOutput = new StreamReader(new FileStream(handle2, FileAccess.Read, 4096, false), encoding, true, 4096); } if (startInfo.RedirectStandardError) { Encoding encoding2 = (startInfo.StandardErrorEncoding != null) ? startInfo.StandardErrorEncoding : Console.OutputEncoding; this.standardError = new StreamReader(new FileStream(handle3, FileAccess.Read, 4096, false), encoding2, true, 4096); } bool result = false; if (!safeProcessHandle.IsInvalid) { this.SetProcessHandle(safeProcessHandle); this.SetProcessId(process_INFORMATION.dwProcessId); safeThreadHandle.Close(); result = true; } return result; } |
It’s quite a lot of code which we don’t want to duplicate. Crucial line is:
1 |
NativeMethods.STARTUPINFO startupinfo = new NativeMethods.STARTUPINFO(); |
Let’s go deeper:
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 |
// Token: 0x020006AD RID: 1709 [StructLayout(LayoutKind.Sequential)] internal class STARTUPINFO { // Token: 0x06003FC2 RID: 16322 RVA: 0x0010B934 File Offset: 0x00109B34 public STARTUPINFO() { this.cb = Marshal.SizeOf(this); } // Token: 0x06003FC3 RID: 16323 RVA: 0x0010B9B4 File Offset: 0x00109BB4 public void Dispose() { if (this.hStdInput != null && !this.hStdInput.IsInvalid) { this.hStdInput.Close(); this.hStdInput = null; } if (this.hStdOutput != null && !this.hStdOutput.IsInvalid) { this.hStdOutput.Close(); this.hStdOutput = null; } if (this.hStdError != null && !this.hStdError.IsInvalid) { this.hStdError.Close(); this.hStdError = null; } } // Token: 0x04002EB6 RID: 11958 public int cb; // Token: 0x04002EB7 RID: 11959 public IntPtr lpReserved = IntPtr.Zero; // Token: 0x04002EB8 RID: 11960 public IntPtr lpDesktop = IntPtr.Zero; // Token: 0x04002EB9 RID: 11961 public IntPtr lpTitle = IntPtr.Zero; // Token: 0x04002EBA RID: 11962 public int dwX; // Token: 0x04002EBB RID: 11963 public int dwY; // Token: 0x04002EBC RID: 11964 public int dwXSize; // Token: 0x04002EBD RID: 11965 public int dwYSize; // Token: 0x04002EBE RID: 11966 public int dwXCountChars; // Token: 0x04002EBF RID: 11967 public int dwYCountChars; // Token: 0x04002EC0 RID: 11968 public int dwFillAttribute; // Token: 0x04002EC1 RID: 11969 public int dwFlags; // Token: 0x04002EC2 RID: 11970 public short wShowWindow; // Token: 0x04002EC3 RID: 11971 public short cbReserved2; // Token: 0x04002EC4 RID: 11972 public IntPtr lpReserved2 = IntPtr.Zero; // Token: 0x04002EC5 RID: 11973 public SafeFileHandle hStdInput = new SafeFileHandle(IntPtr.Zero, false); // Token: 0x04002EC6 RID: 11974 public SafeFileHandle hStdOutput = new SafeFileHandle(IntPtr.Zero, false); // Token: 0x04002EC7 RID: 11975 public SafeFileHandle hStdError = new SafeFileHandle(IntPtr.Zero, false); } |
Okay, so we have a class with two interesting fields. One of them is the size of the object — standard WinAPI approach. The other one is lpDesktop
which is initialized to null pointer and cannot be directly modified. What can we do?
Well, as usual — let’s hijack the constructor and do some magic.
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 |
using System; using System.Linq; using System.Text; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace DesktopHack { unsafe class Program { public static byte[] tempDesktopName = Encoding.UTF8.GetBytes("Sysinternals Desktop 1"); public static byte[] desktopName = tempDesktopName.SelectMany(b => new byte[] { b, 0 }).Concat(new byte[] { 0, 0 }).ToArray(); public static GCHandle handle = GCHandle.Alloc(desktopName, GCHandleType.Pinned); static void Main(string[] args) { var matchingType = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes()).First(t => t.Name.Contains("STARTUPINFO")); var constructor = matchingType.GetConstructor(new Type[0]); var newConstructor = typeof(Program).GetMethod(nameof(NewConstructor), BindingFlags.Static | BindingFlags.Public); HijackMethod(constructor, newConstructor); var process = new Process(); var startInfo = new ProcessStartInfo { WindowStyle = ProcessWindowStyle.Hidden, FileName = "notepad.exe", Arguments = "", RedirectStandardOutput = true, UseShellExecute = false, StandardOutputEncoding = Encoding.UTF8 }; process.StartInfo = startInfo; process.Start(); } public static void NewConstructor(object startupInfo) { startupInfo.GetType().GetField("cb", BindingFlags.Instance | BindingFlags.Public).SetValue(startupInfo, Marshal.SizeOf(startupInfo)); startupInfo.GetType().GetField("lpDesktop", BindingFlags.Instance | BindingFlags.Public).SetValue(startupInfo, handle.AddrOfPinnedObject()); } public enum Protection { PAGE_NOACCESS = 0x01, PAGE_READONLY = 0x02, PAGE_READWRITE = 0x04, PAGE_WRITECOPY = 0x08, PAGE_EXECUTE = 0x10, PAGE_EXECUTE_READ = 0x20, PAGE_EXECUTE_READWRITE = 0x40, PAGE_EXECUTE_WRITECOPY = 0x80, PAGE_GUARD = 0x100, PAGE_NOCACHE = 0x200, PAGE_WRITECOMBINE = 0x400 } [DllImport("kernel32.dll", SetLastError = true)] static extern bool VirtualProtect(IntPtr lpAddress, uint dwSize, uint flNewProtect, out uint lpflOldProtect); private static void UnlockPage(int address) { uint old; VirtualProtect((IntPtr)address, 4096, (uint)Protection.PAGE_EXECUTE_READWRITE, out old); } public static void HijackMethod(ConstructorInfo source, MethodInfo target) { RuntimeHelpers.PrepareMethod(source.MethodHandle); RuntimeHelpers.PrepareMethod(target.MethodHandle); var sourceAddress = source.MethodHandle.GetFunctionPointer(); var targetAddress = (long)target.MethodHandle.GetFunctionPointer(); UnlockPage((int)sourceAddress); UnlockPage((int)targetAddress); int offset = (int)(targetAddress - (long)sourceAddress - 4 - 1); // four bytes for relative address and one byte for opcode byte[] instruction = { 0xE9, // Long jump relative instruction (byte)(offset & 0xFF), (byte)((offset >> 8) & 0xFF), (byte)((offset >> 16) & 0xFF), (byte)((offset >> 24) & 0xFF) }; Marshal.Copy(instruction, 0, sourceAddress, instruction.Length); } } } |
We start with creating a string for desktop name (lines 13-15). We need to pin it to make sure it is not moved by the GC. Next, we get the type used by .NET (line 19) and its constructor (line 20). Finally, we have a method for new constructor (39-43) which we get through reflection in line 21.
So what do we do? We use reflection to get method handles for old constructor and a new dummy method which we want to inject in the old place. We then do the trick with jump to hijack the constructor and execute NewConstructor
method in place of the old one.
Now, in NewConstructor
we use reflection to modify both fields. First, we set the size in line 41. Finally, we just get the pointer to the byte array and put it in the field (line 42).
Rest of the code should be pretty familiar, we just unlock the page, calculate relative address and do the jump. Nothing special here.
One important thing to note is line 14: we get normal bytes of the desktop name and then encode it in UTF 16 because application runs as a Unicode. So if we have string like abcde
we need to introduce zeroes after each letter to get something like a 0 b 0 c 0 d 0 e 0
. Obviously, letters must be replaced with ASCII codes. At the very end we need to add two more zeroes to represent the zero terminating the string (to get the null-terminated string).
That’s it. Changes for x64 or Ansii application should be pretty obvious now.