diff --git a/Brovan/Core/Emulation/BinaryEmulator.cs b/Brovan/Core/Emulation/BinaryEmulator.cs index 574605c..029de1c 100644 --- a/Brovan/Core/Emulation/BinaryEmulator.cs +++ b/Brovan/Core/Emulation/BinaryEmulator.cs @@ -257,6 +257,8 @@ public struct BinaryEmulatorSettings /// public string RawProgramArguments; + public string WorkingDirectory; + /// /// Parsed arguments passed to the emulated process, excluding argv[0]. /// @@ -370,6 +372,7 @@ public int Compare(MemoryRegion x, MemoryRegion y) public bool Debug { get; set; } public string RawProgramArguments { get; } + public string WorkingDirectory { get; } public string[] ProgramArguments { get; } public int IPRegister { get; private set; } @@ -528,6 +531,7 @@ public BinaryEmulator(BinaryFile Binary, BinaryEmulatorSettings Settings) this.Settings = Settings; Debug = Settings.Debug; RawProgramArguments = Settings.RawProgramArguments ?? string.Empty; + WorkingDirectory = Settings.WorkingDirectory; ProgramArguments = Settings.ProgramArguments?.ToArray() ?? Array.Empty(); if (_binary.Architecture == BinaryArchitecture.x64) @@ -567,6 +571,7 @@ public BinaryEmulator(IGuestEnvironment Guest, BinaryEmulatorSettings Settings, this.Settings = Settings; Debug = Settings.Debug; RawProgramArguments = Settings.RawProgramArguments ?? string.Empty; + WorkingDirectory = Settings.WorkingDirectory; ProgramArguments = Settings.ProgramArguments?.ToArray() ?? Array.Empty(); if (Guest is GenericGuest Generic) @@ -2396,6 +2401,9 @@ public bool RunMlfqScheduler(uint BaseQuantumInstructions = 200000, int Levels = { SchedulerTick++; + if (WinHelper != null) + OS.Windows.RemoteProcessRequests.Drain(this); + bool ThreadOrderChanged = ThreadOrder.Count != KnownThreadOrderCount; bool AgingDue = AgingThresholdSlices > 0 && SchedulerTick % AgingThresholdSlices == 0; if (AgingDue) diff --git a/Brovan/Core/Emulation/Guests/WindowsGuest.cs b/Brovan/Core/Emulation/Guests/WindowsGuest.cs index e5d50f1..ea90ba2 100644 --- a/Brovan/Core/Emulation/Guests/WindowsGuest.cs +++ b/Brovan/Core/Emulation/Guests/WindowsGuest.cs @@ -7,6 +7,7 @@ using System.Runtime.InteropServices; using System.Text; using Brovan.Core.Emulation.OS.Windows; +using Brovan.Core.Emulation.OS.Windows.Win32k; using Brovan.Core.Helpers; using static Brovan.Core.Emulation.OS.Windows.WinSysHelper; using static Brovan.Core.Helpers.BinaryHelpers; @@ -829,6 +830,7 @@ public ulong AllocateAndInitializeTEB(BinaryEmulator Instance, EmulatedThread Th Instance._emulator.WriteMemory(Teb + Wow64InfoOffset + 0x0C, 0u); Instance._emulator.WriteMemory(Teb + Wow64TlsWow64InfoOffset, (uint)(Teb + Wow64InfoOffset)); + Win32kDpi.ApplyThreadContext(Instance, Teb); return Teb; } @@ -859,6 +861,8 @@ public ulong AllocateAndInitializeTEB(BinaryEmulator Instance, EmulatedThread Th SameTebFlags |= TEB_SAME_TEB_FLAG_SKIP_LOADER_INIT; Instance._emulator.WriteMemory(Teb + TebSameTebFlagsOffset64, SameTebFlags, 2); Instance._emulator.WriteMemory(Teb + 0x180C, (uint)0u); + + Win32kDpi.ApplyThreadContext(Instance, Teb); return Teb; } @@ -1157,6 +1161,36 @@ private void EnsureDirectBlobStandardHandles() } } + private static void JoinGuestSession(BinaryEmulator Instance, WinModule MainModule) + { + string ImageName = MainModule?.Name; + if (string.IsNullOrEmpty(ImageName)) + ImageName = Path.GetFileName(Instance._binary?.Location ?? string.Empty); + + GuestSessionRegistry.Join( + Instance.WinHelper.PID, + (uint)Instance._binary.Architecture, + ImageName, + ExitCode => + { + Instance.TriggerEventMessage($"[!] Another process in the session asked this one to stop with exit code 0x{ExitCode:X}.", LogFlags.Important); + Instance.WinHelper.HideDesktopWindow(); + Instance.StopEmulation(); + }); + } + + private static string ResolveStartupDirectory(BinaryEmulator Instance, string ImagePath) + { + string Directory = Instance.WorkingDirectory; + if (string.IsNullOrWhiteSpace(Directory)) + Directory = Path.GetDirectoryName(ImagePath); + + if (string.IsNullOrWhiteSpace(Directory)) + Directory = "C:\\"; + + return Directory.EndsWith("\\", StringComparison.Ordinal) ? Directory : Directory + "\\"; + } + public void PrepareWinEnvironment(BinaryEmulator Instance, WinModule MainModule) { Instance.EnsureInstructionHook(); @@ -1171,6 +1205,8 @@ public void PrepareWinEnvironment(BinaryEmulator Instance, WinModule MainModule) EnsureDirectBlobStandardHandles(); WinSyscallTable = HelperFunctions.BuildWinSyscallTable(Instance._binary.Architecture); + Win32kDpi.SeedFromImage(Instance, MainModule); + JoinGuestSession(Instance, MainModule); ulong PageSize = 0x2000; PEB = Instance.MapUniqueAddress(PageSize, MemoryProtection.ReadWrite); @@ -1215,9 +1251,7 @@ public void PrepareWinEnvironment(BinaryEmulator Instance, WinModule MainModule) string ImagePath = IsPeImage ? Instance._binary.Location : (!string.IsNullOrWhiteSpace(Instance._binary.Location) ? Instance._binary.Location : (!string.IsNullOrWhiteSpace(MainModule.Path) ? MainModule.Path : (!string.IsNullOrWhiteSpace(MainModule.Name) ? MainModule.Name : "blob.bin"))); - string CurrentDir = Path.GetDirectoryName(ImagePath) ?? "C:\\"; - if (string.IsNullOrWhiteSpace(CurrentDir)) CurrentDir = "C:\\"; - if (!CurrentDir.EndsWith("\\")) CurrentDir += "\\"; + string CurrentDir = ResolveStartupDirectory(Instance, ImagePath); string DesktopInfo = "Winsta0\\Default"; string WindowTitle = ImagePath; string CommandLine = GeneralHelper.QuoteCommandLineArg(ImagePath); @@ -1335,9 +1369,7 @@ private ulong BuildProcessParameters32(BinaryEmulator Instance, WinModule MainMo string ImagePath = Instance._binary.Location; if (string.IsNullOrWhiteSpace(ImagePath)) ImagePath = !string.IsNullOrWhiteSpace(MainModule.Path) ? MainModule.Path : (!string.IsNullOrWhiteSpace(MainModule.Name) ? MainModule.Name : "app.exe"); - string CurrentDir = Path.GetDirectoryName(ImagePath) ?? "C:\\"; - if (string.IsNullOrWhiteSpace(CurrentDir)) CurrentDir = "C:\\"; - if (!CurrentDir.EndsWith("\\")) CurrentDir += "\\"; + string CurrentDir = ResolveStartupDirectory(Instance, ImagePath); string DesktopInfo = "Winsta0\\Default"; string WindowTitle = ImagePath; string CommandLine = GeneralHelper.QuoteCommandLineArg(ImagePath); diff --git a/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/LinuxWinManager.cs b/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/LinuxWinManager.cs index e8a5ecd..6c94cd4 100644 --- a/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/LinuxWinManager.cs +++ b/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/LinuxWinManager.cs @@ -32,6 +32,9 @@ public static partial class X11 [LibraryImport("libX11.so.6")] public static partial int XDisplayHeight(IntPtr display, int screen); + [LibraryImport("libX11.so.6")] + public static partial IntPtr XResourceManagerString(IntPtr display); + [LibraryImport("libX11.so.6")] public static partial nuint XWhitePixel(IntPtr display, int screen); diff --git a/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/WindowManager.cs b/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/WindowManager.cs index 743b4e5..758778f 100644 --- a/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/WindowManager.cs +++ b/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/WindowManager.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using System.Threading; namespace Brovan.Core.Emulation.OS.SharedHelpers @@ -23,6 +24,7 @@ private struct HostEvent private static int _pendingRepaint; private static int _closeRequested; + private static int _pendingDpi; public static void RequestClose() { @@ -36,6 +38,7 @@ public static void Reset() { Interlocked.Exchange(ref _closeRequested, 0); Interlocked.Exchange(ref _pendingRepaint, 0); + Interlocked.Exchange(ref _pendingDpi, 0); lock (InputSync) { @@ -44,6 +47,16 @@ public static void Reset() } } + public static void MarkDpiChanged(uint dpi) + { + Interlocked.Exchange(ref _pendingDpi, (int)dpi); + } + + public static uint ConsumeDpiChange() + { + return (uint)Interlocked.Exchange(ref _pendingDpi, 0); + } + public static void MarkRepaint() { Interlocked.Exchange(ref _pendingRepaint, 1); @@ -130,6 +143,211 @@ public enum WindowState Fullscreen } + public enum DpiAwareness + { + Unaware, + System, + PerMonitor + } + + internal static class HostDisplayMetrics + { + internal const uint DefaultDpi = 96; + + private const int MonitorDefaultToPrimary = 0x0001; + private const int MdtEffectiveDpi = 0; + private const int MdtRawDpi = 2; + private const int SmCxScreen = 0; + private const int SmCyScreen = 1; + private const int FallbackScreenWidth = 1920; + private const int FallbackScreenHeight = 1080; + private const uint MinimumDpi = 48; + private const uint MaximumDpi = 480; + + private static readonly object Sync = new(); + private static readonly IntPtr PerMonitorAwareV2 = new(-4); + + private static uint _systemDpi; + private static uint _rawDpi; + private static int _screenWidth; + private static int _screenHeight; + + public static bool VirtualizesUnawareWindows => OperatingSystem.IsWindows(); + + public static uint SystemDpi + { + get + { + Ensure(); + return _systemDpi; + } + } + + public static uint RawDpi + { + get + { + Ensure(); + return _rawDpi; + } + } + + public static int ScreenWidth + { + get + { + Ensure(); + return _screenWidth; + } + } + + public static int ScreenHeight + { + get + { + Ensure(); + return _screenHeight; + } + } + + public static void Invalidate() + { + lock (Sync) + _systemDpi = 0; + } + + private static void Ensure() + { + lock (Sync) + { + if (_systemDpi != 0) + return; + + _systemDpi = DefaultDpi; + _rawDpi = DefaultDpi; + _screenWidth = FallbackScreenWidth; + _screenHeight = FallbackScreenHeight; + + if (OperatingSystem.IsLinux()) + { + EnsureFromX11(); + return; + } + + if (!OperatingSystem.IsWindows()) + return; + + IntPtr previous = IntPtr.Zero; + try + { + previous = NativeWinImports.SetThreadDpiAwarenessContext(PerMonitorAwareV2); + + IntPtr monitor = NativeWinImports.MonitorFromWindow(IntPtr.Zero, MonitorDefaultToPrimary); + if (monitor != IntPtr.Zero) + { + if (NativeWinImports.GetDpiForMonitor(monitor, MdtEffectiveDpi, out uint effectiveDpi, out _) == 0 && effectiveDpi != 0) + _systemDpi = effectiveDpi; + + if (NativeWinImports.GetDpiForMonitor(monitor, MdtRawDpi, out uint rawDpi, out _) == 0 && rawDpi != 0) + _rawDpi = rawDpi; + else + _rawDpi = _systemDpi; + } + + int width = NativeWinImports.GetSystemMetrics(SmCxScreen); + int height = NativeWinImports.GetSystemMetrics(SmCyScreen); + if (width > 0 && height > 0) + { + _screenWidth = width; + _screenHeight = height; + } + } + catch + { + } + finally + { + if (previous != IntPtr.Zero) + { + try + { + NativeWinImports.SetThreadDpiAwarenessContext(previous); + } + catch + { + } + } + } + } + } + + private static void EnsureFromX11() + { + IntPtr display = IntPtr.Zero; + try + { + display = X11.XOpenDisplay(IntPtr.Zero); + if (display == IntPtr.Zero) + return; + + int screen = X11.XDefaultScreen(display); + int width = X11.XDisplayWidth(display, screen); + int height = X11.XDisplayHeight(display, screen); + if (width > 0 && height > 0) + { + _screenWidth = width; + _screenHeight = height; + } + + if (TryReadXftDpi(X11.XResourceManagerString(display), out uint dpi)) + { + _systemDpi = dpi; + _rawDpi = dpi; + } + } + catch (DllNotFoundException) + { + } + catch (EntryPointNotFoundException) + { + } + finally + { + if (display != IntPtr.Zero) + X11.XCloseDisplay(display); + } + } + + private static bool TryReadXftDpi(IntPtr resourceString, out uint dpi) + { + dpi = 0; + if (resourceString == IntPtr.Zero) + return false; + + string resources = Marshal.PtrToStringUTF8(resourceString); + if (string.IsNullOrEmpty(resources)) + return false; + + foreach (string line in resources.Split('\n')) + { + int separator = line.IndexOf(':'); + if (separator < 0 || !line.AsSpan(0, separator).TrimEnd().SequenceEqual("Xft.dpi")) + continue; + + if (!uint.TryParse(line.AsSpan(separator + 1).Trim(), out uint parsed)) + return false; + + if (parsed < MinimumDpi || parsed > MaximumDpi) + return false; + + dpi = parsed; + return true; + } + + return false; + } + } + public sealed record WindowOptions { public string Title { get; init; } = string.Empty; @@ -143,6 +361,7 @@ public sealed record WindowOptions public bool Decorated { get; init; } = true; public bool Center { get; init; } = false; public WindowState State { get; init; } = WindowState.Normal; + public DpiAwareness DpiAwareness { get; init; } = DpiAwareness.Unaware; } public struct WindowData diff --git a/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/WindowsWinManager.cs b/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/WindowsWinManager.cs index 27d9799..40e190e 100644 --- a/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/WindowsWinManager.cs +++ b/Brovan/Core/Emulation/OS/SharedHelpers/WindowManager/WindowsWinManager.cs @@ -150,6 +150,15 @@ private static void DisposeCachedPensAndBrushes() private const uint SWP_NOACTIVATE = 0x0010; private const uint SWP_FRAMECHANGED = 0x0020; + private const uint WM_DPICHANGED = 0x02E0; + + private static readonly IntPtr DpiContextUnaware = new(-1); + private static readonly IntPtr DpiContextSystemAware = new(-2); + private static readonly IntPtr DpiContextPerMonitorAwareV2 = new(-4); + + private DpiAwareness _dpiAwareness = DpiAwareness.Unaware; + private bool _dpiAwarenessApplied; + public WindowsWinManager() { if (!GeneralHelper.IsWindows) @@ -170,6 +179,7 @@ public IWindow CreateWindow(WindowOptions options) throw new ObjectDisposedException(nameof(WindowsWinManager)); options = Normalize(options ?? new WindowOptions()); + ApplyDpiAwareness(options.DpiAwareness); uint style = options.Decorated ? WS_OVERLAPPEDWINDOW : WS_POPUP; if (options.Visible) @@ -180,7 +190,7 @@ public IWindow CreateWindow(WindowOptions options) int requestedClientWidth = Math.Max(options.Width, 1); int requestedClientHeight = Math.Max(options.Height, 1); - ResolveOuterFromClient(style, 0, requestedClientWidth, requestedClientHeight, out int outerWidth, out int outerHeight); + ResolveOuterFromClient(style, 0, requestedClientWidth, requestedClientHeight, FrameDpi(IntPtr.Zero), out int outerWidth, out int outerHeight); IntPtr hwnd = CreateWindowExW( 0, @@ -371,7 +381,7 @@ internal void UpdateWindowSize(IntPtr hwnd, uint style, int width, int height) if (!GetWindowRect(hwnd, out rect)) return; - ResolveOuterFromClient(style, 0, Math.Max(width, 1), Math.Max(height, 1), out int outerWidth, out int outerHeight); + ResolveOuterFromClient(style, 0, Math.Max(width, 1), Math.Max(height, 1), FrameDpi(hwnd), out int outerWidth, out int outerHeight); if (rect.Right - rect.Left == outerWidth && rect.Bottom - rect.Top == outerHeight) return; @@ -379,10 +389,51 @@ internal void UpdateWindowSize(IntPtr hwnd, uint style, int width, int height) SetWindowPos(hwnd, IntPtr.Zero, rect.Left, rect.Top, outerWidth, outerHeight, SWP_NOZORDER | SWP_NOACTIVATE); } - private static void ResolveOuterFromClient(uint style, uint exStyle, int clientWidth, int clientHeight, out int outerWidth, out int outerHeight) + private void ApplyDpiAwareness(DpiAwareness awareness) + { + if (_dpiAwarenessApplied && _dpiAwareness == awareness) + return; + + IntPtr context = awareness switch + { + DpiAwareness.PerMonitor => DpiContextPerMonitorAwareV2, + DpiAwareness.System => DpiContextSystemAware, + _ => DpiContextUnaware, + }; + + try + { + if (SetThreadDpiAwarenessContext(context) == IntPtr.Zero) + return; + } + catch (EntryPointNotFoundException) + { + return; + } + + _dpiAwareness = awareness; + _dpiAwarenessApplied = true; + } + + private uint FrameDpi(IntPtr hwnd) + { + if (_dpiAwareness == DpiAwareness.Unaware) + return 0; + + if (hwnd != IntPtr.Zero) + { + uint windowDpi = GetDpiForWindow(hwnd); + if (windowDpi != 0) + return windowDpi; + } + + return HostDisplayMetrics.SystemDpi; + } + + private static void ResolveOuterFromClient(uint style, uint exStyle, int clientWidth, int clientHeight, uint dpi, out int outerWidth, out int outerHeight) { RECT rect = new RECT { Left = 0, Top = 0, Right = clientWidth, Bottom = clientHeight }; - if (AdjustWindowRectEx(ref rect, style, false, exStyle)) + if (AdjustFrameRect(ref rect, style, exStyle, dpi)) { outerWidth = Math.Max(rect.Right - rect.Left, 1); outerHeight = Math.Max(rect.Bottom - rect.Top, 1); @@ -393,6 +444,23 @@ private static void ResolveOuterFromClient(uint style, uint exStyle, int clientW outerHeight = Math.Max(clientHeight, 1); } + private static bool AdjustFrameRect(ref RECT rect, uint style, uint exStyle, uint dpi) + { + if (dpi != 0) + { + try + { + if (AdjustWindowRectExForDpi(ref rect, style, false, exStyle, dpi)) + return true; + } + catch (EntryPointNotFoundException) + { + } + } + + return AdjustWindowRectEx(ref rect, style, false, exStyle); + } + internal void UpdateWindowVisibility(IntPtr hwnd, bool visible) { ShowWindow(hwnd, visible ? SW_SHOW : SW_HIDE); @@ -849,6 +917,12 @@ internal IntPtr HandleMessage(uint msg, IntPtr wParam, IntPtr lParam) HostEventQueue.MarkRepaint(); break; + case WM_DPICHANGED: + HostDisplayMetrics.Invalidate(); + HostEventQueue.MarkDpiChanged(unchecked((uint)(long)wParam) & 0xFFFF); + HostEventQueue.MarkRepaint(); + return IntPtr.Zero; + case WM_CLOSE: HostEventQueue.RequestClose(); return IntPtr.Zero; @@ -957,6 +1031,16 @@ private static extern IntPtr CreateWindowExW( [return: MarshalAs(UnmanagedType.Bool)] private static extern bool AdjustWindowRectEx(ref RECT lpRect, uint dwStyle, [MarshalAs(UnmanagedType.Bool)] bool bMenu, uint dwExStyle); + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool AdjustWindowRectExForDpi(ref RECT lpRect, uint dwStyle, [MarshalAs(UnmanagedType.Bool)] bool bMenu, uint dwExStyle, uint dpi); + + [DllImport("user32.dll")] + private static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext); + + [DllImport("user32.dll")] + private static extern uint GetDpiForWindow(IntPtr hWnd); + [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr DefWindowProcW(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); diff --git a/Brovan/Core/Emulation/OS/Windows/BinaryEmulator.WindowsBridge.cs b/Brovan/Core/Emulation/OS/Windows/BinaryEmulator.WindowsBridge.cs index 8dbc3e3..c22721e 100644 --- a/Brovan/Core/Emulation/OS/Windows/BinaryEmulator.WindowsBridge.cs +++ b/Brovan/Core/Emulation/OS/Windows/BinaryEmulator.WindowsBridge.cs @@ -139,6 +139,12 @@ internal bool CanSatisfyWaitHandle(ulong Handle, EmulatedThread AcquiringThread) if (Obj is EmulatedThread Thread) return Thread.State == EmulatedThreadState.Terminated; + if (Obj is WinProcess Process) + return Process.SpawnedHasExited; + + if (Obj is WinSpawnedThread SpawnedThread) + return SpawnedThread.Process == null || SpawnedThread.Process.SpawnedHasExited; + if (Obj is WinTimer Timer) { RefreshTimerState(Timer); diff --git a/Brovan/Core/Emulation/OS/Windows/Process/GuestProcessLauncher.cs b/Brovan/Core/Emulation/OS/Windows/Process/GuestProcessLauncher.cs new file mode 100644 index 0000000..6c0741e --- /dev/null +++ b/Brovan/Core/Emulation/OS/Windows/Process/GuestProcessLauncher.cs @@ -0,0 +1,313 @@ +using System.Diagnostics; +using System.Text; +using Brovan.Core.Helpers; +using static Brovan.Core.Helpers.BinaryHelpers; + +namespace Brovan.Core.Emulation.OS.Windows +{ + internal static class GuestProcessLauncher + { + private const string SpawnDepthVariable = "BROVAN_GUEST_SPAWN_DEPTH"; + private const int MaxSpawnDepth = 8; + + private const int MaxSessionProcesses = 6; + + private const int MaxCommandLineChars = 8000; + + private const ulong ParamsCurrentDirectory64 = 0x38; + private const ulong ParamsImagePathName64 = 0x60; + private const ulong ParamsCommandLine64 = 0x70; + + private const ulong ParamsCurrentDirectory32 = 0x24; + private const ulong ParamsImagePathName32 = 0x38; + private const ulong ParamsCommandLine32 = 0x40; + + private const int MaxStringBytes = 0x8000; + + internal static bool TryLaunch(BinaryEmulator Instance, ulong ProcessParameters, string ImageNameHint, out WinProcess Process, out NTSTATUS Status) + { + Process = null; + + bool Is64 = Instance._binary.Architecture == BinaryArchitecture.x64; + string ImagePath = ReadUnicodeString(Instance, ProcessParameters + (Is64 ? ParamsImagePathName64 : ParamsImagePathName32), Is64); + string CommandLine = ReadUnicodeString(Instance, ProcessParameters + (Is64 ? ParamsCommandLine64 : ParamsCommandLine32), Is64); + string CurrentDirectory = ReadUnicodeString(Instance, ProcessParameters + (Is64 ? ParamsCurrentDirectory64 : ParamsCurrentDirectory32), Is64); + + if (string.IsNullOrWhiteSpace(ImagePath)) + ImagePath = ImageNameHint; + + if (string.IsNullOrWhiteSpace(ImagePath)) + { + Status = NTSTATUS.STATUS_INVALID_PARAMETER; + return false; + } + + if (!IsAcceptableImagePath(ImagePath)) + { + Utils.LogError($"[GuestProcessLauncher] Refusing to launch {ImagePath}: unsupported path form."); + Status = NTSTATUS.STATUS_OBJECT_PATH_SYNTAX_BAD; + return false; + } + + string HostImage = GeneralHelper.IO.ResolveHostPath(StripNtPrefix(ImagePath), BinaryFormat.PE); + if (string.IsNullOrEmpty(HostImage) || !File.Exists(HostImage)) + { + Status = NTSTATUS.STATUS_OBJECT_NAME_NOT_FOUND; + return false; + } + + if (!IsPortableExecutable(HostImage)) + { + Utils.LogError($"[GuestProcessLauncher] Refusing to launch {HostImage}: not a PE image."); + Status = NTSTATUS.STATUS_INVALID_IMAGE_FORMAT; + return false; + } + + if ((CommandLine?.Length ?? 0) > MaxCommandLineChars || (CurrentDirectory?.Length ?? 0) > MaxCommandLineChars) + { + Status = NTSTATUS.STATUS_INVALID_PARAMETER; + return false; + } + + if (!TryReserveLaunchSlot(out Status)) + return false; + + string HostExecutable = Environment.ProcessPath; + if (string.IsNullOrEmpty(HostExecutable)) + { + Status = NTSTATUS.STATUS_NOT_SUPPORTED; + return false; + } + + int Depth = GetSpawnDepth(); + if (Depth >= MaxSpawnDepth) + { + Utils.LogError($"[GuestProcessLauncher] Refusing to launch {ImagePath}: spawn depth {Depth} reached."); + Status = NTSTATUS.STATUS_INSUFFICIENT_RESOURCES; + return false; + } + + ProcessStartInfo StartInfo = new ProcessStartInfo + { + FileName = HostExecutable, + UseShellExecute = false, + WorkingDirectory = ResolveWorkingDirectory(CurrentDirectory, HostImage), + }; + + AppendEmulatorOptions(Instance, StartInfo.ArgumentList); + + StartInfo.CreateNoWindow = Utils.SilentMode; + + string GuestArguments = StripArgv0(CommandLine); + if (!string.IsNullOrEmpty(GuestArguments)) + { + StartInfo.ArgumentList.Add("--guest-cmdline"); + StartInfo.ArgumentList.Add(Encode(GuestArguments)); + } + + if (!string.IsNullOrWhiteSpace(CurrentDirectory)) + { + StartInfo.ArgumentList.Add("--cwd"); + StartInfo.ArgumentList.Add(Encode(StripNtPrefix(CurrentDirectory))); + } + + StartInfo.ArgumentList.Add(HostImage); + StartInfo.Environment[SpawnDepthVariable] = (Depth + 1).ToString(); + StartInfo.Environment["BROVAN_SESSION_ID"] = GuestSessionRegistry.SessionId; + + Process HostProcess; + try + { + HostProcess = System.Diagnostics.Process.Start(StartInfo); + } + catch (Exception Ex) + { + Utils.LogError($"[GuestProcessLauncher] Failed to launch {HostImage}: {Ex.Message}"); + Status = NTSTATUS.STATUS_NOT_SUPPORTED; + return false; + } + + if (HostProcess == null) + { + Status = NTSTATUS.STATUS_NOT_SUPPORTED; + return false; + } + + Process = new WinProcess + { + PID = unchecked((uint)HostProcess.Id), + PPID = Instance.WinHelper.PID, + Name = Path.GetFileName(HostImage), + Path = ImagePath, + Arch = Instance._binary.Architecture, + CreationTime = DateTime.UtcNow.ToFileTimeUtc(), + SpawnedHost = HostProcess, + }; + + Instance.TriggerEventMessage($"[GuestProcessLauncher] Launched {Process.Name} as host process {Process.PID} (depth {Depth + 1}).", LogFlags.Syscall); + + Status = NTSTATUS.STATUS_SUCCESS; + return true; + } + + private static string ResolveWorkingDirectory(string RequestedDirectory, string HostImage) + { + if (!string.IsNullOrWhiteSpace(RequestedDirectory)) + { + string Resolved = GeneralHelper.IO.ResolveHostPath(StripNtPrefix(RequestedDirectory), BinaryFormat.PE); + if (!string.IsNullOrEmpty(Resolved) && Directory.Exists(Resolved)) + return Resolved; + } + + return Path.GetDirectoryName(HostImage) ?? Environment.CurrentDirectory; + } + + private static void AppendEmulatorOptions(BinaryEmulator Instance, System.Collections.ObjectModel.Collection Arguments) + { + string Backend = Instance.Settings.BackendKind switch + { + EmulationBackendKind.Whp => "whp", + EmulationBackendKind.Kvm => "kvm", + _ => "unicorn", + }; + + Arguments.Add($"--backend={Backend}"); + + if (Utils.SilentMode) + Arguments.Add("--silent"); + + if (Instance.Settings.NoHooks) + Arguments.Add("--no-hooks"); + + ForwardHostOptions(Arguments); + + Arguments.Add("-c"); + Arguments.Add("start;exit"); + } + + private static void ForwardHostOptions(System.Collections.ObjectModel.Collection Arguments) + { + string[] HostArguments = Environment.GetCommandLineArgs(); + + for (int i = 1; i < HostArguments.Length; i++) + { + string Argument = HostArguments[i]; + + if (Argument.StartsWith("--net=", StringComparison.OrdinalIgnoreCase) || + Argument.StartsWith("--net-allow=", StringComparison.OrdinalIgnoreCase) || + Argument.Equals("-q", StringComparison.OrdinalIgnoreCase) || + Argument.Equals("--quick", StringComparison.OrdinalIgnoreCase)) + { + Arguments.Add(Argument); + continue; + } + + if ((Argument.Equals("--net", StringComparison.OrdinalIgnoreCase) || + Argument.Equals("--net-allow", StringComparison.OrdinalIgnoreCase)) && i + 1 < HostArguments.Length) + { + Arguments.Add(Argument); + Arguments.Add(HostArguments[++i]); + } + } + } + + private static int GetSpawnDepth() + { + return int.TryParse(Environment.GetEnvironmentVariable(SpawnDepthVariable), out int Depth) && Depth > 0 ? Depth : 0; + } + + private static bool TryReserveLaunchSlot(out NTSTATUS Status) + { + int SessionProcesses = GuestSessionRegistry.CountLive(); + if (SessionProcesses >= MaxSessionProcesses) + { + Utils.LogError($"[GuestProcessLauncher] Refusing to launch: the session already has {SessionProcesses} guest processes."); + Status = NTSTATUS.STATUS_INSUFFICIENT_RESOURCES; + return false; + } + + Status = NTSTATUS.STATUS_SUCCESS; + return true; + } + + private static bool IsAcceptableImagePath(string ImagePath) + { + string Path = StripNtPrefix(ImagePath).Replace('/', '\\'); + + if (Path.StartsWith("\\\\", StringComparison.Ordinal)) + return false; + + if (Path.StartsWith("\\Device\\", StringComparison.OrdinalIgnoreCase)) + return false; + + return Path.Length >= 2 && Path[1] == ':' && char.IsAsciiLetter(Path[0]); + } + + private static bool IsPortableExecutable(string HostImage) + { + try + { + using FileStream Stream = File.OpenRead(HostImage); + Span Header = stackalloc byte[2]; + return Stream.Read(Header) == 2 && Header[0] == (byte)'M' && Header[1] == (byte)'Z'; + } + catch (IOException) + { + return false; + } + catch (UnauthorizedAccessException) + { + return false; + } + } + + private static string StripNtPrefix(string Path) + { + if (Path.StartsWith("\\??\\", StringComparison.Ordinal)) + return Path.Substring(4); + + if (Path.StartsWith("\\\\?\\", StringComparison.Ordinal)) + return Path.Substring(4); + + return Path; + } + + private static string ReadUnicodeString(BinaryEmulator Instance, ulong Address, bool Is64) + { + if (Address == 0 || !Instance.IsRegionMapped(Address, Is64 ? 16UL : 8UL)) + return null; + + ushort Length = Instance._emulator.ReadMemoryUShort(Address); + ulong Buffer = Is64 ? Instance.ReadMemoryULong(Address + 8) : Instance.ReadMemoryUInt(Address + 4); + if (Length == 0 || Length > MaxStringBytes || Buffer == 0 || !Instance.IsRegionMapped(Buffer, Length)) + return null; + + return Instance._emulator.ReadMemoryString(Buffer, Length, Encoding.Unicode)?.TrimEnd('\0'); + } + + private static string Encode(string Value) + { + return "base64:" + Convert.ToBase64String(Encoding.UTF8.GetBytes(Value)); + } + + private static string StripArgv0(string CommandLine) + { + if (string.IsNullOrWhiteSpace(CommandLine)) + return null; + + int Index = 0; + if (CommandLine[0] == '"') + { + Index = CommandLine.IndexOf('"', 1); + Index = Index < 0 ? CommandLine.Length : Index + 1; + } + else + { + while (Index < CommandLine.Length && CommandLine[Index] != ' ' && CommandLine[Index] != '\t') + Index++; + } + + return Index >= CommandLine.Length ? null : CommandLine.Substring(Index).TrimStart(' ', '\t'); + } + } +} diff --git a/Brovan/Core/Emulation/OS/Windows/Process/GuestSessionRegistry.cs b/Brovan/Core/Emulation/OS/Windows/Process/GuestSessionRegistry.cs new file mode 100644 index 0000000..df6bfe4 --- /dev/null +++ b/Brovan/Core/Emulation/OS/Windows/Process/GuestSessionRegistry.cs @@ -0,0 +1,649 @@ +using System.Buffers.Binary; +using System.IO.MemoryMappedFiles; +using System.Text; +using System.Threading; +using Brovan.Core.Helpers; + +namespace Brovan.Core.Emulation.OS.Windows +{ + internal static class GuestSessionRegistry + { + internal const uint ControlNone = 0; + internal const uint ControlTerminate = 1; + + private const string SessionVariable = "BROVAN_SESSION_ID"; + private const uint Magic = 0x5652424E; + private const uint Version = 1; + private const int SlotCount = 64; + private const int SlotSize = 512; + private const int HeaderSize = 64; + private const int MapSize = HeaderSize + (SlotCount * SlotSize); + private const int MaxImageBytes = SlotSize - SlotImageOffset - 2; + private const int WatcherIntervalMilliseconds = 150; + + internal const uint OpcodeReadMemory = 1; + internal const uint OpcodeWriteMemory = 2; + internal const uint OpcodeQueryPeb = 3; + internal const uint OpcodeCreateThread = 4; + + private const int MailboxSize = 0x10000; + private const int MailboxPayloadOffset = 0x40; + internal const int MaxPayloadBytes = MailboxSize - MailboxPayloadOffset; + private const int RequestTimeoutMilliseconds = 5000; + + private const int MailboxRequestSeqOffset = 0x00; + private const int MailboxResponseSeqOffset = 0x04; + private const int MailboxOpcodeOffset = 0x08; + private const int MailboxStatusOffset = 0x0C; + private const int MailboxAddressOffset = 0x10; + private const int MailboxLengthOffset = 0x18; + private const int MailboxResultLengthOffset = 0x1C; + private const int MailboxExtraOffset = 0x20; + private const int MailboxResultOffset = 0x28; + + private const int SlotStateOffset = 0x00; + private const int SlotHostPidOffset = 0x04; + private const int SlotGuestPidOffset = 0x08; + private const int SlotArchOffset = 0x0C; + private const int SlotStartTimeOffset = 0x10; + private const int SlotControlOffset = 0x18; + private const int SlotControlExitOffset = 0x1C; + private const int SlotImageLengthOffset = 0x20; + private const int SlotImageOffset = 0x28; + + private const uint SlotFree = 0; + private const uint SlotLive = 1; + + private static readonly object Sync = new(); + + private static FileStream _stream; + private static MemoryMappedFile _map; + private static MemoryMappedViewAccessor _view; + private static FileStream _mailboxStream; + private static MemoryMappedFile _mailboxMap; + private static MemoryMappedViewAccessor _mailboxView; + private static uint _lastHandledSequence; + private static Mutex _mutex; + private static Thread _watcher; + private static int _ownSlot = -1; + private static bool _initialiseFailed; + private static Action _terminateCallback; + + internal static string SessionId + { + get + { + string Existing = Environment.GetEnvironmentVariable(SessionVariable); + if (!string.IsNullOrWhiteSpace(Existing)) + return Sanitize(Existing); + + string Created = Environment.ProcessId.ToString("X") + "-" + DateTime.UtcNow.Ticks.ToString("X"); + Environment.SetEnvironmentVariable(SessionVariable, Created); + return Created; + } + } + + internal static void Join(uint GuestProcessId, uint Architecture, string ImageName, Action OnTerminateRequested) + { + lock (Sync) + { + if (_ownSlot >= 0 || _initialiseFailed) + return; + + if (!TryOpen()) + { + _initialiseFailed = true; + return; + } + + _terminateCallback = OnTerminateRequested; + + if (!TryAcquire()) + return; + + try + { + for (int i = 0; i < SlotCount; i++) + { + int Offset = HeaderSize + (i * SlotSize); + uint State = _view.ReadUInt32(Offset + SlotStateOffset); + if (State == SlotLive && IsHostProcessAlive(_view.ReadUInt32(Offset + SlotHostPidOffset))) + continue; + + ClearMailbox(i); + WriteSlot(Offset, GuestProcessId, Architecture, ImageName); + _ownSlot = i; + break; + } + } + finally + { + Release(); + } + + if (_ownSlot < 0) + return; + + AppDomain.CurrentDomain.ProcessExit += static (_, _) => Leave(); + StartWatcher(); + } + } + + internal static void Leave() + { + lock (Sync) + { + if (_ownSlot < 0 || _view == null) + return; + + if (TryAcquire()) + { + try + { + _view.Write(HeaderSize + (_ownSlot * SlotSize) + SlotStateOffset, SlotFree); + _view.Flush(); + } + finally + { + Release(); + } + } + + _ownSlot = -1; + } + } + + internal static int CountLive() + { + int Count = 0; + + lock (Sync) + { + if (!TryOpen() || !TryAcquire()) + return 0; + + try + { + for (int i = 0; i < SlotCount; i++) + { + int Offset = HeaderSize + (i * SlotSize); + if (_view.ReadUInt32(Offset + SlotStateOffset) != SlotLive) + continue; + + if (IsHostProcessAlive(_view.ReadUInt32(Offset + SlotHostPidOffset))) + Count++; + else + _view.Write(Offset + SlotStateOffset, SlotFree); + } + } + finally + { + Release(); + } + } + + return Count; + } + + internal static bool RequestTerminate(uint GuestProcessId, uint ExitCode) + { + lock (Sync) + { + if (!TryOpen() || !TryAcquire()) + return false; + + try + { + for (int i = 0; i < SlotCount; i++) + { + int Offset = HeaderSize + (i * SlotSize); + if (_view.ReadUInt32(Offset + SlotStateOffset) != SlotLive) + continue; + + if (_view.ReadUInt32(Offset + SlotGuestPidOffset) != GuestProcessId) + continue; + + if (!IsHostProcessAlive(_view.ReadUInt32(Offset + SlotHostPidOffset))) + { + _view.Write(Offset + SlotStateOffset, SlotFree); + return false; + } + + _view.Write(Offset + SlotControlExitOffset, ExitCode); + _view.Write(Offset + SlotControlOffset, ControlTerminate); + _view.Flush(); + return true; + } + } + finally + { + Release(); + } + } + + return false; + } + + internal static NTSTATUS SendRequest( + uint GuestProcessId, + uint Opcode, + ulong Address, + ulong Extra, + ReadOnlySpan Input, + Span Output, + out int OutputLength, + out ulong Result) + { + OutputLength = 0; + Result = 0; + + if (Input.Length > MaxPayloadBytes || Output.Length > MaxPayloadBytes) + return NTSTATUS.STATUS_INVALID_PARAMETER; + + int Slot; + uint Sequence; + + lock (Sync) + { + if (!TryOpen() || _mailboxView == null || !TryAcquire()) + return NTSTATUS.STATUS_NOT_SUPPORTED; + + try + { + Slot = FindLiveSlot(GuestProcessId); + if (Slot < 0) + return NTSTATUS.STATUS_INVALID_CID; + + long Base = (long)Slot * MailboxSize; + uint Pending = _mailboxView.ReadUInt32(Base + MailboxRequestSeqOffset); + if (Pending != _mailboxView.ReadUInt32(Base + MailboxResponseSeqOffset)) + return NTSTATUS.STATUS_INSUFFICIENT_RESOURCES; + + _mailboxView.Write(Base + MailboxOpcodeOffset, Opcode); + _mailboxView.Write(Base + MailboxAddressOffset, Address); + _mailboxView.Write(Base + MailboxExtraOffset, Extra); + _mailboxView.Write(Base + MailboxLengthOffset, (uint)(Opcode == OpcodeReadMemory ? Output.Length : Input.Length)); + _mailboxView.Write(Base + MailboxResultLengthOffset, 0u); + _mailboxView.Write(Base + MailboxStatusOffset, 0u); + + if (Input.Length > 0) + { + byte[] Buffer = Input.ToArray(); + _mailboxView.WriteArray(Base + MailboxPayloadOffset, Buffer, 0, Buffer.Length); + } + + Sequence = Pending + 1; + _mailboxView.Write(Base + MailboxRequestSeqOffset, Sequence); + _mailboxView.Flush(); + } + finally + { + Release(); + } + } + + long Deadline = Environment.TickCount64 + RequestTimeoutMilliseconds; + while (Environment.TickCount64 < Deadline) + { + Thread.Sleep(1); + + lock (Sync) + { + if (_mailboxView == null || !TryAcquire()) + continue; + + try + { + long Base = (long)Slot * MailboxSize; + if (_mailboxView.ReadUInt32(Base + MailboxResponseSeqOffset) != Sequence) + continue; + + NTSTATUS Status = (NTSTATUS)_mailboxView.ReadUInt32(Base + MailboxStatusOffset); + Result = _mailboxView.ReadUInt64(Base + MailboxResultOffset); + int Length = (int)Math.Min(_mailboxView.ReadUInt32(Base + MailboxResultLengthOffset), (uint)Output.Length); + + if (Length > 0) + { + byte[] Buffer = new byte[Length]; + _mailboxView.ReadArray(Base + MailboxPayloadOffset, Buffer, 0, Length); + Buffer.AsSpan().CopyTo(Output); + } + + OutputLength = Length; + return Status; + } + finally + { + Release(); + } + } + } + + return NTSTATUS.STATUS_TIMEOUT; + } + + internal static bool TryTakeRequest(out uint Opcode, out ulong Address, out ulong Extra, out int Length, out byte[] Input) + { + Opcode = 0; + Address = 0; + Extra = 0; + Length = 0; + Input = null; + + lock (Sync) + { + if (_ownSlot < 0 || _mailboxView == null) + return false; + + long Base = (long)_ownSlot * MailboxSize; + if (_mailboxView.ReadUInt32(Base + MailboxRequestSeqOffset) == _lastHandledSequence) + return false; + + if (!TryAcquire()) + return false; + + try + { + uint Sequence = _mailboxView.ReadUInt32(Base + MailboxRequestSeqOffset); + if (Sequence == _lastHandledSequence) + return false; + + Opcode = _mailboxView.ReadUInt32(Base + MailboxOpcodeOffset); + Address = _mailboxView.ReadUInt64(Base + MailboxAddressOffset); + Extra = _mailboxView.ReadUInt64(Base + MailboxExtraOffset); + Length = (int)Math.Min(_mailboxView.ReadUInt32(Base + MailboxLengthOffset), (uint)MaxPayloadBytes); + + if (Opcode == OpcodeWriteMemory && Length > 0) + { + Input = new byte[Length]; + _mailboxView.ReadArray(Base + MailboxPayloadOffset, Input, 0, Length); + } + + _lastHandledSequence = Sequence; + return true; + } + finally + { + Release(); + } + } + } + + internal static void CompleteRequest(uint Status, ulong Result, ReadOnlySpan Output) + { + lock (Sync) + { + if (_ownSlot < 0 || _mailboxView == null || !TryAcquire()) + return; + + try + { + long Base = (long)_ownSlot * MailboxSize; + int Length = Math.Min(Output.Length, MaxPayloadBytes); + + if (Length > 0) + { + byte[] Buffer = Output.Slice(0, Length).ToArray(); + _mailboxView.WriteArray(Base + MailboxPayloadOffset, Buffer, 0, Length); + } + + _mailboxView.Write(Base + MailboxResultLengthOffset, (uint)Length); + _mailboxView.Write(Base + MailboxResultOffset, Result); + _mailboxView.Write(Base + MailboxStatusOffset, Status); + _mailboxView.Write(Base + MailboxResponseSeqOffset, _lastHandledSequence); + _mailboxView.Flush(); + } + finally + { + Release(); + } + } + } + + private static int FindLiveSlot(uint GuestProcessId) + { + for (int i = 0; i < SlotCount; i++) + { + int Offset = HeaderSize + (i * SlotSize); + if (_view.ReadUInt32(Offset + SlotStateOffset) != SlotLive) + continue; + + if (_view.ReadUInt32(Offset + SlotGuestPidOffset) != GuestProcessId) + continue; + + return IsHostProcessAlive(_view.ReadUInt32(Offset + SlotHostPidOffset)) ? i : -1; + } + + return -1; + } + + private static void StartWatcher() + { + _watcher = new Thread(WatchForRequests) + { + IsBackground = true, + Name = "BrovanSessionWatcher", + }; + + _watcher.Start(); + } + + private static void WatchForRequests() + { + while (true) + { + Thread.Sleep(WatcherIntervalMilliseconds); + + uint Request; + uint ExitCode; + + lock (Sync) + { + if (_ownSlot < 0 || _view == null) + return; + + if (!TryAcquire()) + continue; + + try + { + int Offset = HeaderSize + (_ownSlot * SlotSize); + Request = _view.ReadUInt32(Offset + SlotControlOffset); + ExitCode = _view.ReadUInt32(Offset + SlotControlExitOffset); + if (Request != ControlNone) + _view.Write(Offset + SlotControlOffset, ControlNone); + } + finally + { + Release(); + } + } + + if (Request == ControlTerminate) + { + try + { + _terminateCallback?.Invoke(ExitCode); + } + catch (Exception Ex) + { + Utils.LogError($"[GuestSessionRegistry] Terminate request failed: {Ex.Message}"); + } + + return; + } + } + } + + private static bool TryOpen() + { + if (_view != null) + return true; + + if (_initialiseFailed) + return false; + + try + { + string Directory = Path.Combine(Path.GetTempPath(), "brovan-session-" + SessionId); + System.IO.Directory.CreateDirectory(Directory); + + _mutex = new Mutex(false, "BrovanSession_" + SessionId); + + _stream = new FileStream( + Path.Combine(Directory, "processes.bin"), + FileMode.OpenOrCreate, + FileAccess.ReadWrite, + FileShare.ReadWrite | FileShare.Delete); + + if (_stream.Length < MapSize) + _stream.SetLength(MapSize); + + _map = MemoryMappedFile.CreateFromFile(_stream, null, MapSize, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, true); + _view = _map.CreateViewAccessor(0, MapSize); + + long MailboxTotal = (long)SlotCount * MailboxSize; + _mailboxStream = new FileStream( + Path.Combine(Directory, "mailboxes.bin"), + FileMode.OpenOrCreate, + FileAccess.ReadWrite, + FileShare.ReadWrite | FileShare.Delete); + + if (_mailboxStream.Length < MailboxTotal) + _mailboxStream.SetLength(MailboxTotal); + + _mailboxMap = MemoryMappedFile.CreateFromFile(_mailboxStream, null, MailboxTotal, MemoryMappedFileAccess.ReadWrite, HandleInheritability.None, true); + _mailboxView = _mailboxMap.CreateViewAccessor(0, MailboxTotal); + + if (TryAcquire()) + { + try + { + if (_view.ReadUInt32(0) != Magic) + { + _view.Write(0, Magic); + _view.Write(4, Version); + _view.Write(8, (uint)SlotCount); + } + } + finally + { + Release(); + } + } + + return true; + } + catch (Exception Ex) + { + Utils.LogError($"[GuestSessionRegistry] Unavailable: {Ex.Message}"); + _initialiseFailed = true; + _view = null; + return false; + } + } + + private static bool TryAcquire() + { + if (_mutex == null) + return false; + + try + { + return _mutex.WaitOne(2000); + } + catch (AbandonedMutexException) + { + return true; + } + catch (Exception) + { + return false; + } + } + + private static void Release() + { + try + { + _mutex?.ReleaseMutex(); + } + catch (Exception) + { + } + } + + /// + /// A reused slot still holds the sequence numbers of its previous owner. This process starts counting + /// from zero, so without this it would read that stale request as a new one and run it. + /// + private static void ClearMailbox(int Slot) + { + if (_mailboxView == null) + return; + + long Base = (long)Slot * MailboxSize; + for (int i = 0; i < MailboxPayloadOffset; i += 8) + _mailboxView.Write(Base + i, 0UL); + + _mailboxView.Flush(); + _lastHandledSequence = 0; + } + + private static void WriteSlot(int Offset, uint GuestProcessId, uint Architecture, string ImageName) + { + for (int i = 0; i < SlotSize; i += 8) + _view.Write(Offset + i, 0UL); + + byte[] Name = Encoding.Unicode.GetBytes(ImageName ?? string.Empty); + int NameLength = Math.Min(Name.Length, MaxImageBytes); + + _view.Write(Offset + SlotHostPidOffset, (uint)Environment.ProcessId); + _view.Write(Offset + SlotGuestPidOffset, GuestProcessId); + _view.Write(Offset + SlotArchOffset, Architecture); + _view.Write(Offset + SlotStartTimeOffset, DateTime.UtcNow.Ticks); + _view.Write(Offset + SlotImageLengthOffset, (uint)NameLength); + + if (NameLength > 0) + _view.WriteArray(Offset + SlotImageOffset, Name, 0, NameLength); + + _view.Write(Offset + SlotStateOffset, SlotLive); + _view.Flush(); + } + + private static bool IsHostProcessAlive(uint HostProcessId) + { + if (HostProcessId == 0) + return false; + + if (HostProcessId == (uint)Environment.ProcessId) + return true; + + try + { + using System.Diagnostics.Process Existing = System.Diagnostics.Process.GetProcessById((int)HostProcessId); + return !Existing.HasExited; + } + catch (ArgumentException) + { + return false; + } + catch (InvalidOperationException) + { + return false; + } + } + + private static string Sanitize(string Value) + { + StringBuilder Builder = new StringBuilder(Value.Length); + foreach (char Character in Value) + { + if (char.IsAsciiLetterOrDigit(Character) || Character == '-' || Character == '_') + Builder.Append(Character); + } + + return Builder.Length == 0 ? "default" : Builder.ToString(); + } + } +} diff --git a/Brovan/Core/Emulation/OS/Windows/Process/NtCreateThreadEx.cs b/Brovan/Core/Emulation/OS/Windows/Process/NtCreateThreadEx.cs index 88123c6..86652b4 100644 --- a/Brovan/Core/Emulation/OS/Windows/Process/NtCreateThreadEx.cs +++ b/Brovan/Core/Emulation/OS/Windows/Process/NtCreateThreadEx.cs @@ -72,15 +72,44 @@ public NTSTATUS Handle(BinaryEmulator Instance) if (StartRoutine == 0) return NTSTATUS.STATUS_INVALID_PARAMETER; - // Only current-process thread creation is modeled. if (!HandleManager.IsCurrentProcessPseudoHandle(ProcessHandle)) { if (!Instance.WinHelper.ValidProcessHandle(ProcessHandle)) return NTSTATUS.STATUS_INVALID_HANDLE; WinProcess Target = Instance.WinHelper.GetProcessByHandle(ProcessHandle, AccessMask.ProcessCreateThread); - if (Target == null || Target.PID != Instance.WinHelper.PID) - return NTSTATUS.STATUS_NOT_SUPPORTED; + if (Target == null) + return NTSTATUS.STATUS_ACCESS_DENIED; + + if (Target.PID != Instance.WinHelper.PID) + { + NTSTATUS RemoteStatus = GuestSessionRegistry.SendRequest( + Target.PID, + GuestSessionRegistry.OpcodeCreateThread, + StartRoutine, + Argument, + ReadOnlySpan.Empty, + Span.Empty, + out _, + out ulong RemoteThreadId); + + if (RemoteStatus != NTSTATUS.STATUS_SUCCESS) + return RemoteStatus; + + WinSpawnedThread Remote = new WinSpawnedThread + { + Process = Target, + ThreadId = (uint)RemoteThreadId, + }; + + WinHandle RemoteHandle = Instance.WinHelper.HandleManager.AddHandle(Remote, (AccessMask)(uint)DesiredAccess); + Instance.WinHelper.AddWinHandle(RemoteHandle); + + if (!Instance.WinHelper.WritePointer(ThreadHandlePtr, RemoteHandle.Handle)) + return NTSTATUS.STATUS_ACCESS_VIOLATION; + + return NTSTATUS.STATUS_SUCCESS; + } } ulong? StackOverride = null; diff --git a/Brovan/Core/Emulation/OS/Windows/Process/NtCreateUserProcess.cs b/Brovan/Core/Emulation/OS/Windows/Process/NtCreateUserProcess.cs new file mode 100644 index 0000000..d63146b --- /dev/null +++ b/Brovan/Core/Emulation/OS/Windows/Process/NtCreateUserProcess.cs @@ -0,0 +1,132 @@ +using System.Text; +using static Brovan.Core.Helpers.BinaryHelpers; + +namespace Brovan.Core.Emulation.OS.Windows +{ + internal class NtCreateUserProcess : IWinSyscall + { + private const uint PsCreateSuccess = 4; + + private const ulong PsAttributeClientId = 3 | 0x10000; + private const ulong PsAttributeImageName = 5 | 0x20000; + + private const int CreateInfoStateOffset = 0x08; + private const int CreateInfoSuccessFileHandleOffset = 0x18; + private const int CreateInfoSuccessSectionHandleOffset = 0x20; + private const int MaxAttributes = 32; + + public NTSTATUS Handle(BinaryEmulator Instance) + { + ulong ProcessHandlePtr = Instance.WinHelper.GetArg(0); + ulong ThreadHandlePtr = Instance.WinHelper.GetArg(1); + ulong ProcessParameters = Instance.WinHelper.GetArg(8); + ulong CreateInfo = Instance.WinHelper.GetArg(9); + ulong AttributeList = Instance.WinHelper.GetArg(10); + + bool Is64 = Instance._binary.Architecture == BinaryArchitecture.x64; + int PointerSize = Is64 ? 8 : 4; + + if (ProcessParameters == 0 || !Instance.IsRegionMapped(ProcessParameters, 0x40)) + return NTSTATUS.STATUS_INVALID_PARAMETER; + + string ImageNameHint = ReadImageNameAttribute(Instance, AttributeList, Is64); + + if (!GuestProcessLauncher.TryLaunch(Instance, ProcessParameters, ImageNameHint, out WinProcess Process, out NTSTATUS Status)) + return Status; + + WinSpawnedThread Thread = new WinSpawnedThread + { + Process = Process, + ThreadId = Process.PID, + }; + + Instance.WinHelper.WinProcesses.Add(Process); + + ulong ProcessHandle = Instance.WinHelper.HandleManager.AddHandle(Process, AccessMask.GenericAll).Handle; + ulong ThreadHandle = Instance.WinHelper.HandleManager.AddHandle(Thread, AccessMask.GenericAll).Handle; + + if (ProcessHandlePtr != 0 && Instance.IsRegionMapped(ProcessHandlePtr, (ulong)PointerSize)) + Instance._emulator.WriteMemory(ProcessHandlePtr, ProcessHandle, (uint)PointerSize); + + if (ThreadHandlePtr != 0 && Instance.IsRegionMapped(ThreadHandlePtr, (ulong)PointerSize)) + Instance._emulator.WriteMemory(ThreadHandlePtr, ThreadHandle, (uint)PointerSize); + + WriteCreateInfoSuccess(Instance, CreateInfo, Is64); + WriteClientIdAttribute(Instance, AttributeList, Is64, Process.PID, Thread.ThreadId); + + return NTSTATUS.STATUS_SUCCESS; + } + + private static void WriteCreateInfoSuccess(BinaryEmulator Instance, ulong CreateInfo, bool Is64) + { + if (CreateInfo == 0 || !Instance.IsRegionMapped(CreateInfo, 0x58)) + return; + + Instance._emulator.WriteMemory(CreateInfo + CreateInfoStateOffset, PsCreateSuccess, 4); + + if (!Is64) + return; + + Instance._emulator.WriteMemory(CreateInfo + CreateInfoSuccessFileHandleOffset, 0UL, 8); + Instance._emulator.WriteMemory(CreateInfo + CreateInfoSuccessSectionHandleOffset, 0UL, 8); + } + + private static void WriteClientIdAttribute(BinaryEmulator Instance, ulong AttributeList, bool Is64, uint ProcessId, uint ThreadId) + { + if (!TryFindAttribute(Instance, AttributeList, Is64, PsAttributeClientId, out ulong ValuePointer, out ulong Size)) + return; + + int PointerSize = Is64 ? 8 : 4; + if (ValuePointer == 0 || Size < (ulong)(PointerSize * 2) || !Instance.IsRegionMapped(ValuePointer, Size)) + return; + + Instance._emulator.WriteMemory(ValuePointer, ProcessId, (uint)PointerSize); + Instance._emulator.WriteMemory(ValuePointer + (ulong)PointerSize, ThreadId, (uint)PointerSize); + } + + private static string ReadImageNameAttribute(BinaryEmulator Instance, ulong AttributeList, bool Is64) + { + if (!TryFindAttribute(Instance, AttributeList, Is64, PsAttributeImageName, out ulong ValuePointer, out ulong Size)) + return null; + + if (ValuePointer == 0 || Size == 0 || Size > 0x8000 || !Instance.IsRegionMapped(ValuePointer, Size)) + return null; + + return Instance._emulator.ReadMemoryString(ValuePointer, (int)Size, Encoding.Unicode)?.TrimEnd('\0'); + } + + private static bool TryFindAttribute(BinaryEmulator Instance, ulong AttributeList, bool Is64, ulong Attribute, out ulong ValuePointer, out ulong Size) + { + ValuePointer = 0; + Size = 0; + + int PointerSize = Is64 ? 8 : 4; + int EntrySize = PointerSize * 4; + + if (AttributeList == 0 || !Instance.IsRegionMapped(AttributeList, (ulong)PointerSize)) + return false; + + ulong TotalLength = Is64 ? Instance.ReadMemoryULong(AttributeList) : Instance.ReadMemoryUInt(AttributeList); + if (TotalLength <= (uint)PointerSize) + return false; + + ulong Count = (TotalLength - (ulong)PointerSize) / (ulong)EntrySize; + if (Count == 0 || Count > MaxAttributes || !Instance.IsRegionMapped(AttributeList, TotalLength)) + return false; + + for (ulong i = 0; i < Count; i++) + { + ulong Entry = AttributeList + (ulong)PointerSize + i * (ulong)EntrySize; + ulong EntryAttribute = Is64 ? Instance.ReadMemoryULong(Entry) : Instance.ReadMemoryUInt(Entry); + if (EntryAttribute != Attribute) + continue; + + Size = Is64 ? Instance.ReadMemoryULong(Entry + (ulong)PointerSize) : Instance.ReadMemoryUInt(Entry + (ulong)PointerSize); + ValuePointer = Is64 ? Instance.ReadMemoryULong(Entry + (ulong)(PointerSize * 2)) : Instance.ReadMemoryUInt(Entry + (ulong)(PointerSize * 2)); + return true; + } + + return false; + } + } +} diff --git a/Brovan/Core/Emulation/OS/Windows/Process/NtQueryInformationProcess.cs b/Brovan/Core/Emulation/OS/Windows/Process/NtQueryInformationProcess.cs index 1243574..43cc76e 100644 --- a/Brovan/Core/Emulation/OS/Windows/Process/NtQueryInformationProcess.cs +++ b/Brovan/Core/Emulation/OS/Windows/Process/NtQueryInformationProcess.cs @@ -57,7 +57,14 @@ void SetReturnLength(uint Len) return NTSTATUS.STATUS_ACCESS_DENIED; } - if (!WriteProcessBasicInformation(Instance, OutBufferPtr, Instance.PEB, 0x8UL, Process.PID, Process.PPID)) + ulong Peb = Instance.PEB; + if (Process.PID != Instance.WinHelper.PID && + GuestSessionRegistry.SendRequest(Process.PID, GuestSessionRegistry.OpcodeQueryPeb, 0, 0, ReadOnlySpan.Empty, Span.Empty, out _, out ulong RemotePeb) == NTSTATUS.STATUS_SUCCESS) + { + Peb = RemotePeb; + } + + if (!WriteProcessBasicInformation(Instance, OutBufferPtr, Peb, 0x8UL, Process.PID, Process.PPID)) return NTSTATUS.STATUS_ACCESS_VIOLATION; SetReturnLength(PbiSize); if ((Instance.Settings.Flags & LogFlags.Syscall) != 0) diff --git a/Brovan/Core/Emulation/OS/Windows/Process/NtReadVirtualMemory.cs b/Brovan/Core/Emulation/OS/Windows/Process/NtReadVirtualMemory.cs index 59fabf7..df6d808 100644 --- a/Brovan/Core/Emulation/OS/Windows/Process/NtReadVirtualMemory.cs +++ b/Brovan/Core/Emulation/OS/Windows/Process/NtReadVirtualMemory.cs @@ -81,23 +81,45 @@ public NTSTATUS Handle(BinaryEmulator Instance) goto current_process; // jump to the current process handling } - if (BaseAddressPtr == 0) - return NTSTATUS.STATUS_INVALID_PARAMETER; - - if (!Instance.IsRegionMapped(BaseAddressPtr, sizeof(ulong))) - return NTSTATUS.STATUS_MEMORY_NOT_ALLOCATED; + ulong RemoteAddress = BaseAddressPtr; + ulong LocalBuffer = BufferPtr; + ulong BytesReadPtr = Instance.WinHelper.GetArg(4); - if (BufferPtr == 0) + if (RemoteAddress == 0 || LocalBuffer == 0 || NumberOfBytesToRead == 0) return NTSTATUS.STATUS_INVALID_PARAMETER; - if(NumberOfBytesToRead == 0) - return NTSTATUS.STATUS_INVALID_PARAMETER; + if (NumberOfBytesToRead > GuestSessionRegistry.MaxPayloadBytes) + NumberOfBytesToRead = GuestSessionRegistry.MaxPayloadBytes; - if (!Instance.WriteMemory(BufferPtr, Instance.WinHelper.GenerateRandomData((int)NumberOfBytesToRead))) // generate random data? + if (!Instance.IsRegionMapped(LocalBuffer, NumberOfBytesToRead)) + return NTSTATUS.STATUS_MEMORY_NOT_ALLOCATED; + + byte[] Remote = new byte[NumberOfBytesToRead]; + NTSTATUS RemoteStatus = GuestSessionRegistry.SendRequest( + Process.PID, + GuestSessionRegistry.OpcodeReadMemory, + RemoteAddress, + 0, + ReadOnlySpan.Empty, + Remote, + out int RemoteLength, + out _); + + if (RemoteStatus != NTSTATUS.STATUS_SUCCESS) + return RemoteStatus; + + if (!Instance._emulator.WriteMemory(LocalBuffer, Remote, 0, RemoteLength)) return NTSTATUS.STATUS_ACCESS_VIOLATION; + if (BytesReadPtr != 0) + { + uint PointerSize = (uint)Instance.WinHelper.PointerSize; + if (Instance.IsRegionMapped(BytesReadPtr, PointerSize)) + Instance._emulator.WriteMemory(BytesReadPtr, (ulong)RemoteLength, PointerSize); + } + if ((Instance.Settings.Flags & LogFlags.Syscall) != 0) - Instance.TriggerEventMessage($"[+] The emulated process tried to read the memory of process \"{Process.Name}\", random data was generated for it.", LogFlags.Syscall); + Instance.TriggerEventMessage($"[+] Read 0x{RemoteLength:X} bytes from process \"{Process.Name}\" at 0x{RemoteAddress:X}.", LogFlags.Syscall); return NTSTATUS.STATUS_SUCCESS; } } diff --git a/Brovan/Core/Emulation/OS/Windows/Process/NtTerminateProcess.cs b/Brovan/Core/Emulation/OS/Windows/Process/NtTerminateProcess.cs index 0ae196f..5691272 100644 --- a/Brovan/Core/Emulation/OS/Windows/Process/NtTerminateProcess.cs +++ b/Brovan/Core/Emulation/OS/Windows/Process/NtTerminateProcess.cs @@ -1,3 +1,4 @@ +using Brovan.Core.Helpers; using static Brovan.Core.Helpers.BinaryHelpers; namespace Brovan.Core.Emulation.OS.Windows @@ -68,6 +69,23 @@ public NTSTATUS Handle(BinaryEmulator Instance) Instance.StopEmulation(); return NTSTATUS.STATUS_SUCCESS; } + + if (GuestSessionRegistry.RequestTerminate(Process.PID, (uint)ExitCode)) + return NTSTATUS.STATUS_SUCCESS; + + if (Process.SpawnedHost != null && !Process.SpawnedHasExited) + { + try + { + Process.SpawnedHost.Kill(); + return NTSTATUS.STATUS_SUCCESS; + } + catch (Exception Ex) + { + Utils.LogError($"[NtTerminateProcess] Failed to stop host process {Process.PID}: {Ex.Message}"); + return NTSTATUS.STATUS_ACCESS_DENIED; + } + } } } return Instance.WinUnimplemented; diff --git a/Brovan/Core/Emulation/OS/Windows/Process/NtWriteVirtualMemory.cs b/Brovan/Core/Emulation/OS/Windows/Process/NtWriteVirtualMemory.cs new file mode 100644 index 0000000..8393780 --- /dev/null +++ b/Brovan/Core/Emulation/OS/Windows/Process/NtWriteVirtualMemory.cs @@ -0,0 +1,77 @@ +using static Brovan.Core.Helpers.BinaryHelpers; + +namespace Brovan.Core.Emulation.OS.Windows +{ + internal class NtWriteVirtualMemory : IWinSyscall + { + public NTSTATUS Handle(BinaryEmulator Instance) + { + ulong ProcessHandle = Instance.WinHelper.GetArg(0); + ulong BaseAddress = Instance.WinHelper.GetArg(1); + ulong Buffer = Instance.WinHelper.GetArg(2); + ulong NumberOfBytesToWrite = Instance.WinHelper.GetArg(3); + ulong BytesWrittenPtr = Instance.WinHelper.GetArg(4); + + if (BaseAddress == 0 || Buffer == 0 || NumberOfBytesToWrite == 0) + return NTSTATUS.STATUS_INVALID_PARAMETER; + + if (NumberOfBytesToWrite > GuestSessionRegistry.MaxPayloadBytes) + NumberOfBytesToWrite = GuestSessionRegistry.MaxPayloadBytes; + + if (!Instance.IsRegionMapped(Buffer, NumberOfBytesToWrite)) + return NTSTATUS.STATUS_MEMORY_NOT_ALLOCATED; + + byte[] Payload = Instance.ReadMemory(Buffer, (uint)NumberOfBytesToWrite); + if (Payload.Length == 0) + return NTSTATUS.STATUS_ACCESS_VIOLATION; + + if (!HandleManager.IsCurrentProcessPseudoHandle(ProcessHandle)) + { + if (!Instance.WinHelper.HandleExists(ProcessHandle)) + return NTSTATUS.STATUS_INVALID_HANDLE; + + WinProcess Process = Instance.WinHelper.GetProcessByHandle(ProcessHandle, AccessMask.ProcessVMOperation | AccessMask.ProcessVMWrite); + if (Process == null) + return NTSTATUS.STATUS_ACCESS_DENIED; + + if (Process.PID != Instance.WinHelper.PID) + { + NTSTATUS RemoteStatus = GuestSessionRegistry.SendRequest( + Process.PID, + GuestSessionRegistry.OpcodeWriteMemory, + BaseAddress, + 0, + Payload, + Span.Empty, + out _, + out ulong Written); + + if (RemoteStatus != NTSTATUS.STATUS_SUCCESS) + return RemoteStatus; + + WriteCount(Instance, BytesWrittenPtr, Written); + return NTSTATUS.STATUS_SUCCESS; + } + } + + if (!Instance.IsRegionMapped(BaseAddress, NumberOfBytesToWrite)) + return NTSTATUS.STATUS_MEMORY_NOT_ALLOCATED; + + if (!Instance._emulator.WriteMemory(BaseAddress, Payload)) + return NTSTATUS.STATUS_ACCESS_VIOLATION; + + WriteCount(Instance, BytesWrittenPtr, (ulong)Payload.Length); + return NTSTATUS.STATUS_SUCCESS; + } + + private static void WriteCount(BinaryEmulator Instance, ulong BytesWrittenPtr, ulong Count) + { + if (BytesWrittenPtr == 0) + return; + + int PointerSize = Instance._binary.Architecture == BinaryArchitecture.x64 ? 8 : 4; + if (Instance.IsRegionMapped(BytesWrittenPtr, (ulong)PointerSize)) + Instance._emulator.WriteMemory(BytesWrittenPtr, Count, (uint)PointerSize); + } + } +} diff --git a/Brovan/Core/Emulation/OS/Windows/Process/RemoteProcessRequests.cs b/Brovan/Core/Emulation/OS/Windows/Process/RemoteProcessRequests.cs new file mode 100644 index 0000000..72c94b6 --- /dev/null +++ b/Brovan/Core/Emulation/OS/Windows/Process/RemoteProcessRequests.cs @@ -0,0 +1,88 @@ +using Brovan.Core.Emulation.Guests; +using Brovan.Core.Helpers; +using static Brovan.Core.Helpers.BinaryHelpers; + +namespace Brovan.Core.Emulation.OS.Windows +{ + internal static class RemoteProcessRequests + { + internal static void Drain(BinaryEmulator Instance) + { + while (GuestSessionRegistry.TryTakeRequest(out uint Opcode, out ulong Address, out ulong Extra, out int Length, out byte[] Input)) + { + uint Status; + ulong Result = 0; + byte[] Output = null; + + try + { + Status = Execute(Instance, Opcode, Address, Extra, Length, Input, out Result, out Output); + } + catch (Exception Ex) + { + Utils.LogError($"[RemoteProcessRequests] Opcode {Opcode} failed: {Ex.Message}"); + Status = (uint)NTSTATUS.STATUS_UNSUCCESSFUL; + } + + GuestSessionRegistry.CompleteRequest(Status, Result, Output ?? Array.Empty()); + } + } + + private static uint Execute(BinaryEmulator Instance, uint Opcode, ulong Address, ulong Extra, int Length, byte[] Input, out ulong Result, out byte[] Output) + { + Result = 0; + Output = null; + + switch (Opcode) + { + case GuestSessionRegistry.OpcodeQueryPeb: + Result = Instance.PEB; + return (uint)NTSTATUS.STATUS_SUCCESS; + + case GuestSessionRegistry.OpcodeReadMemory: + { + if (Length <= 0 || !Instance.IsRegionMapped(Address, (ulong)Length)) + return (uint)NTSTATUS.STATUS_ACCESS_VIOLATION; + + byte[] Buffer = new byte[Length]; + if (!Instance.ReadMemory(Address, Buffer, (uint)Length)) + return (uint)NTSTATUS.STATUS_ACCESS_VIOLATION; + + Output = Buffer; + Result = (ulong)Length; + return (uint)NTSTATUS.STATUS_SUCCESS; + } + + case GuestSessionRegistry.OpcodeWriteMemory: + { + if (Input == null || Input.Length == 0 || !Instance.IsRegionMapped(Address, (ulong)Input.Length)) + return (uint)NTSTATUS.STATUS_ACCESS_VIOLATION; + + if (!Instance._emulator.WriteMemory(Address, Input)) + return (uint)NTSTATUS.STATUS_ACCESS_VIOLATION; + + Result = (ulong)Input.Length; + return (uint)NTSTATUS.STATUS_SUCCESS; + } + + case GuestSessionRegistry.OpcodeCreateThread: + { + if (Address == 0 || !Instance.IsRegionMapped(Address, 1)) + return (uint)NTSTATUS.STATUS_INVALID_PARAMETER; + + EmulatedThread Thread = Instance.Guest is WindowsGuest Guest + ? Guest.CreateEmulatedThread(Instance, Address, null, Extra, null, 8, 0, false) + : Instance.CreateEmulatedThread(Address, null, Extra, null); + + if (Thread == null) + return (uint)NTSTATUS.STATUS_NO_MEMORY; + + Result = Thread.ThreadId; + return (uint)NTSTATUS.STATUS_SUCCESS; + } + } + + return (uint)NTSTATUS.STATUS_NOT_SUPPORTED; + } + } +} diff --git a/Brovan/Core/Emulation/OS/Windows/Win32k/NtGdiGetDeviceCaps.cs b/Brovan/Core/Emulation/OS/Windows/Win32k/NtGdiGetDeviceCaps.cs index adaa2b2..b50f86d 100644 --- a/Brovan/Core/Emulation/OS/Windows/Win32k/NtGdiGetDeviceCaps.cs +++ b/Brovan/Core/Emulation/OS/Windows/Win32k/NtGdiGetDeviceCaps.cs @@ -2,27 +2,47 @@ namespace Brovan.Core.Emulation.OS.Windows.Win32k { internal class NtGdiGetDeviceCaps : IWinSyscall { + private const int HORZSIZE = 4; + private const int VERTSIZE = 6; + private const int HORZRES = 8; + private const int VERTRES = 10; + private const int LOGPIXELSX = 88; + private const int LOGPIXELSY = 90; + + private const int TenthsOfMillimetrePerInch = 254; + public NTSTATUS Handle(BinaryEmulator Instance) { int Index = unchecked((int)Instance.WinHelper.GetArg(1)); - Instance.SetRawSyscallReturn(unchecked((ulong)(uint)GetDeviceCapability(Index))); + Instance.SetRawSyscallReturn(unchecked((ulong)(uint)GetDeviceCapability(Instance, Index))); return NTSTATUS.STATUS_SUCCESS; } - internal static int GetDeviceCapability(int Index) + internal static int GetDeviceCapability(BinaryEmulator Instance, int Index) { + int Dpi = (int)Win32kDpi.GetEffectiveDpi(Instance); + + switch (Index) + { + case HORZSIZE: + return Win32kDpi.GetScreenWidth(Instance) * TenthsOfMillimetrePerInch / (Dpi * 10); + case VERTSIZE: + return Win32kDpi.GetScreenHeight(Instance) * TenthsOfMillimetrePerInch / (Dpi * 10); + case HORZRES: + return Win32kDpi.GetScreenWidth(Instance); + case VERTRES: + return Win32kDpi.GetScreenHeight(Instance); + case LOGPIXELSX: + case LOGPIXELSY: + return Dpi; + } + return Index switch { 2 => 1, // TECHNOLOGY: DT_RASDISPLAY - 4 => 320, // HORZSIZE - 6 => 180, // VERTSIZE - 8 => 1920, // HORZRES - 10 => 1080, // VERTRES 12 => 32, // BITSPIXEL 14 => 1, // PLANES 24 => -1, // NUMCOLORS - 88 => 96, // LOGPIXELSX - 90 => 96, // LOGPIXELSY 116 => 60, // VREFRESH 121 => 0x00000003, // COLORMGMTCAPS: CM_DEVICE_ICM | CM_GAMMA_RAMP _ => 0, diff --git a/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserEnableNonClientDpiScaling.cs b/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserEnableNonClientDpiScaling.cs new file mode 100644 index 0000000..2e3d8db --- /dev/null +++ b/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserEnableNonClientDpiScaling.cs @@ -0,0 +1,22 @@ +using static Brovan.Core.Helpers.BinaryHelpers; + +namespace Brovan.Core.Emulation.OS.Windows.Win32k +{ + internal class NtUserEnableNonClientDpiScaling : IWinSyscall + { + public NTSTATUS Handle(BinaryEmulator Instance) + { + ulong Hwnd = Instance.WinHelper.GetArg(0); + + if (Hwnd == 0 || Instance.WinHelper.GetWindow(Hwnd) == null) + { + Instance.SetLastWinError(Win32kHelper.ERROR_INVALID_WINDOW_HANDLE); + Instance.SetRawSyscallReturn(0); + return NTSTATUS.STATUS_SUCCESS; + } + + Instance.SetRawSyscallReturn(1); + return NTSTATUS.STATUS_SUCCESS; + } + } +} diff --git a/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserGetDpiForCurrentProcess.cs b/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserGetDpiForCurrentProcess.cs index fc32d84..a5fdc50 100644 --- a/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserGetDpiForCurrentProcess.cs +++ b/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserGetDpiForCurrentProcess.cs @@ -1,3 +1,4 @@ +using Brovan.Core.Emulation.OS.SharedHelpers; using static Brovan.Core.Helpers.BinaryHelpers; namespace Brovan.Core.Emulation.OS.Windows.Win32k @@ -6,9 +7,8 @@ internal class NtUserGetDpiForCurrentProcess : IWinSyscall { public NTSTATUS Handle(BinaryEmulator Instance) { - - Instance.SetRawSyscallReturn(Win32kHelper.DEFAULT_SCREEN_DPI); + Instance.SetRawSyscallReturn(HostDisplayMetrics.SystemDpi); return NTSTATUS.STATUS_SUCCESS; } } -} \ No newline at end of file +} diff --git a/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserGetDpiForMonitor.cs b/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserGetDpiForMonitor.cs index d01f64a..97a65b1 100644 --- a/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserGetDpiForMonitor.cs +++ b/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserGetDpiForMonitor.cs @@ -6,16 +6,27 @@ internal class NtUserGetDpiForMonitor : IWinSyscall { public NTSTATUS Handle(BinaryEmulator Instance) { + ulong Monitor = Instance.WinHelper.GetArg(0); + uint DpiType = (uint)Instance.WinHelper.GetArg(1); ulong DpiXPtr = Instance.WinHelper.GetArg(2); ulong DpiYPtr = Instance.WinHelper.GetArg(3); + if (Monitor == 0) + { + Instance.SetLastWinError(Win32kHelper.ERROR_INVALID_PARAMETER); + Instance.SetRawSyscallReturn(0); + return NTSTATUS.STATUS_SUCCESS; + } + + uint Dpi = Win32kDpi.GetMonitorDpi(Instance, DpiType); + if (DpiXPtr != 0 && Instance.IsRegionMapped(DpiXPtr, 4)) - Instance._emulator.WriteMemory(DpiXPtr, Win32kHelper.DEFAULT_SCREEN_DPI, 4); + Instance._emulator.WriteMemory(DpiXPtr, Dpi, 4); if (DpiYPtr != 0 && Instance.IsRegionMapped(DpiYPtr, 4)) - Instance._emulator.WriteMemory(DpiYPtr, Win32kHelper.DEFAULT_SCREEN_DPI, 4); + Instance._emulator.WriteMemory(DpiYPtr, Dpi, 4); - Instance.SetRawSyscallReturn(0); + Instance.SetRawSyscallReturn(1); return NTSTATUS.STATUS_SUCCESS; } } diff --git a/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserGetProcessDpiAwarenessContext.cs b/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserGetProcessDpiAwarenessContext.cs new file mode 100644 index 0000000..c6bd5c8 --- /dev/null +++ b/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserGetProcessDpiAwarenessContext.cs @@ -0,0 +1,13 @@ +using static Brovan.Core.Helpers.BinaryHelpers; + +namespace Brovan.Core.Emulation.OS.Windows.Win32k +{ + internal class NtUserGetProcessDpiAwarenessContext : IWinSyscall + { + public NTSTATUS Handle(BinaryEmulator Instance) + { + Instance.SetRawSyscallReturn(Win32kDpi.GetProcessContext(Instance)); + return NTSTATUS.STATUS_SUCCESS; + } + } +} diff --git a/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserGetSystemDpiForProcess.cs b/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserGetSystemDpiForProcess.cs new file mode 100644 index 0000000..5bf03be --- /dev/null +++ b/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserGetSystemDpiForProcess.cs @@ -0,0 +1,13 @@ +using static Brovan.Core.Helpers.BinaryHelpers; + +namespace Brovan.Core.Emulation.OS.Windows.Win32k +{ + internal class NtUserGetSystemDpiForProcess : IWinSyscall + { + public NTSTATUS Handle(BinaryEmulator Instance) + { + Instance.SetRawSyscallReturn(Win32kDpi.GetEffectiveDpi(Instance)); + return NTSTATUS.STATUS_SUCCESS; + } + } +} diff --git a/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserSetProcessDpiAwarenessContext.cs b/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserSetProcessDpiAwarenessContext.cs new file mode 100644 index 0000000..15511bc --- /dev/null +++ b/Brovan/Core/Emulation/OS/Windows/Win32k/NtUserSetProcessDpiAwarenessContext.cs @@ -0,0 +1,15 @@ +using static Brovan.Core.Helpers.BinaryHelpers; + +namespace Brovan.Core.Emulation.OS.Windows.Win32k +{ + internal class NtUserSetProcessDpiAwarenessContext : IWinSyscall + { + public NTSTATUS Handle(BinaryEmulator Instance) + { + uint Context = (uint)Instance.WinHelper.GetArg(0); + + Instance.SetBooleanSyscallReturn(Win32kDpi.TrySetProcessContext(Instance, Context)); + return NTSTATUS.STATUS_SUCCESS; + } + } +} diff --git a/Brovan/Core/Emulation/OS/Windows/Win32k/Win32kDpi.cs b/Brovan/Core/Emulation/OS/Windows/Win32k/Win32kDpi.cs new file mode 100644 index 0000000..02933eb --- /dev/null +++ b/Brovan/Core/Emulation/OS/Windows/Win32k/Win32kDpi.cs @@ -0,0 +1,427 @@ +using System.Runtime.CompilerServices; +using System.Xml; +using Brovan.Core.Emulation.OS.SharedHelpers; +using static Brovan.Core.Helpers.BinaryHelpers; + +namespace Brovan.Core.Emulation.OS.Windows.Win32k +{ + internal static class Win32kDpi + { + internal const uint ContextUnaware = 0x00006010; + internal const uint ContextUnawareGdiScaled = 0x40006010; + internal const uint ContextPerMonitorV2 = 0x00000022; + + internal const uint AwarenessUnaware = 0; + internal const uint AwarenessSystem = 1; + internal const uint AwarenessPerMonitor = 2; + + internal const uint MonitorDpiTypeEffective = 0; + + private const uint SystemAwareFlags = 0x11; + private const uint AwarenessMask = 0xF; + private const uint DpiFieldMask = 0x1FF; + private const int DpiFieldShift = 8; + + private const ulong TebContextOffset = 0x8E8; + + private const ulong WindowContextOffset = 0x120; + private const ulong WindowDpiOffset = 0x11C; + private const ulong WindowDpiAlternateOffset = 0x11E; + + private const int ResourceDataDirectoryIndex = 2; + private const uint ResourceTypeManifest = 24; + private const uint ResourceIdCreateProcessManifest = 1; + private const int ResourceDirectoryHeaderSize = 0x10; + private const int ResourceDirectoryEntrySize = 8; + private const int ResourceDataEntrySize = 0x10; + private const int MaxResourceDirectoryEntries = 0x1000; + private const int MaxManifestBytes = 0x100000; + + private static readonly ConditionalWeakTable States = new(); + + private sealed class DpiState + { + public uint ProcessContext = ContextUnaware; + public bool Locked; + } + + private static DpiState GetState(BinaryEmulator Instance) + { + return States.GetValue(Instance, static _ => new DpiState()); + } + + internal static uint GetProcessContext(BinaryEmulator Instance) + { + return GetState(Instance).ProcessContext; + } + + internal static uint GetAwareness(BinaryEmulator Instance) + { + return GetState(Instance).ProcessContext & AwarenessMask; + } + + internal static DpiAwareness GetHostAwareness(BinaryEmulator Instance) + { + return GetAwareness(Instance) switch + { + AwarenessPerMonitor => DpiAwareness.PerMonitor, + AwarenessSystem => DpiAwareness.System, + _ => DpiAwareness.Unaware, + }; + } + + internal static uint GetEffectiveDpi(BinaryEmulator Instance) + { + return GetAwareness(Instance) == AwarenessUnaware ? HostDisplayMetrics.DefaultDpi : HostDisplayMetrics.SystemDpi; + } + + internal static uint GetMonitorDpi(BinaryEmulator Instance, uint DpiType) + { + return DpiType == MonitorDpiTypeEffective ? GetEffectiveDpi(Instance) : HostDisplayMetrics.RawDpi; + } + + internal static int GetScreenWidth(BinaryEmulator Instance) + { + return ScaleToProcess(Instance, HostDisplayMetrics.ScreenWidth); + } + + internal static int GetScreenHeight(BinaryEmulator Instance) + { + return ScaleToProcess(Instance, HostDisplayMetrics.ScreenHeight); + } + + private static int ScaleToProcess(BinaryEmulator Instance, int PhysicalPixels) + { + if (!HostDisplayMetrics.VirtualizesUnawareWindows) + return PhysicalPixels; + + uint Dpi = GetEffectiveDpi(Instance); + uint SystemDpi = HostDisplayMetrics.SystemDpi; + if (Dpi == SystemDpi || SystemDpi == 0) + return PhysicalPixels; + + return Math.Max((int)((long)PhysicalPixels * Dpi / SystemDpi), 1); + } + + internal static uint BuildContext(uint Awareness, bool GdiScaled = false) + { + return Awareness switch + { + AwarenessSystem => ((HostDisplayMetrics.SystemDpi & DpiFieldMask) << DpiFieldShift) | SystemAwareFlags, + AwarenessPerMonitor => ContextPerMonitorV2, + _ => GdiScaled ? ContextUnawareGdiScaled : ContextUnaware, + }; + } + + internal static bool IsValidContext(uint Context) + { + return (Context & AwarenessMask) <= AwarenessPerMonitor; + } + + internal static bool TrySetProcessContext(BinaryEmulator Instance, uint Context) + { + if (!IsValidContext(Context)) + return false; + + DpiState State = GetState(Instance); + if (State.Locked) + return false; + + State.ProcessContext = (Context & AwarenessMask) == AwarenessSystem + ? BuildContext(AwarenessSystem) + : Context; + + State.Locked = true; + PublishContext(Instance); + return true; + } + + private static void PublishContext(BinaryEmulator Instance) + { + foreach (EmulatedThread Thread in Instance.GetThreadsSnapshot()) + { + WindowsThreadState ThreadState = WinEmulatedThread.TryGetState(Thread); + if (ThreadState != null) + ApplyThreadContext(Instance, ThreadState.Teb); + } + + Instance.WinHelper?.RefreshDisplayDependentState(); + } + + internal static void ApplyThreadContext(BinaryEmulator Instance, ulong Teb) + { + if (Teb == 0) + return; + + Instance._emulator.WriteMemory(Teb + TebContextOffset, GetProcessContext(Instance), 4); + } + + internal static void ApplyWindowContext(BinaryEmulator Instance, ulong ClientWindowAddress) + { + if (ClientWindowAddress == 0) + return; + + uint Context = GetProcessContext(Instance); + Instance._emulator.WriteMemory(ClientWindowAddress + WindowContextOffset, Context, 4); + Instance._emulator.WriteMemory(ClientWindowAddress + WindowDpiOffset, (ushort)GetEffectiveDpi(Instance), 2); + Instance._emulator.WriteMemory(ClientWindowAddress + WindowDpiAlternateOffset, (ushort)0, 2); + } + + internal static void DrainHostDpiChange(BinaryEmulator Instance) + { + uint NewDpi = HostEventQueue.ConsumeDpiChange(); + if (NewDpi == 0) + return; + + Instance.WinHelper?.RefreshDisplayDependentState(); + + if (GetAwareness(Instance) != AwarenessPerMonitor) + return; + + ulong Hwnd = Instance.WinHelper?.GetForegroundWindow() ?? 0; + WinWindow Window = Hwnd == 0 ? null : Instance.WinHelper.GetWindow(Hwnd); + if (Window == null) + return; + + ulong Rect = Instance.WinHelper.EnsureDpiChangeRect(); + if (Rect == 0) + return; + + int Left = Window.X == unchecked((int)0x80000000) ? 0 : Window.X; + int Top = Window.Y == unchecked((int)0x80000000) ? 0 : Window.Y; + Instance._emulator.WriteMemory(Rect + 0x00, (uint)Left, 4); + Instance._emulator.WriteMemory(Rect + 0x04, (uint)Top, 4); + Instance._emulator.WriteMemory(Rect + 0x08, (uint)(Left + (int)Window.Width), 4); + Instance._emulator.WriteMemory(Rect + 0x0C, (uint)(Top + (int)Window.Height), 4); + + Win32kHelper.PostMessage(Instance, Hwnd, Win32kHelper.WM_DPICHANGED, (NewDpi << 16) | NewDpi, Rect); + } + + internal static void SeedFromImage(BinaryEmulator Instance, WinModule Module) + { + if (Module == null || Instance._binary?.FileFormat != BinaryFormat.PE) + return; + + byte[] Manifest = ReadExternalManifest(Module) ?? ReadEmbeddedManifest(Instance, Module); + if (Manifest == null) + return; + + try + { + if (TryParseManifestAwareness(Manifest, out uint Awareness, out bool GdiScaled)) + { + DpiState State = GetState(Instance); + State.ProcessContext = BuildContext(Awareness, GdiScaled); + State.Locked = true; + } + } + catch (XmlException) + { + } + catch (InvalidOperationException) + { + } + } + + private static byte[] ReadExternalManifest(WinModule Module) + { + if (string.IsNullOrEmpty(Module.Path)) + return null; + + try + { + string Path = Module.Path + ".manifest"; + if (!File.Exists(Path)) + return null; + + using FileStream Stream = File.OpenRead(Path); + if (Stream.Length == 0 || Stream.Length > MaxManifestBytes) + return null; + + byte[] Buffer = new byte[Stream.Length]; + return Stream.ReadAtLeast(Buffer, Buffer.Length, false) == Buffer.Length ? Buffer : null; + } + catch (IOException) + { + return null; + } + catch (UnauthorizedAccessException) + { + return null; + } + } + + private static byte[] ReadEmbeddedManifest(BinaryEmulator Instance, WinModule Module) + { + if (!TryFindManifestResource(Instance, Module, out ulong Address, out uint Size)) + return null; + + byte[] Buffer = new byte[Size]; + return Instance.ReadMemory(Address, Buffer, Size) ? Buffer : null; + } + + private static bool TryFindManifestResource(BinaryEmulator Instance, WinModule Module, out ulong Address, out uint Size) + { + Address = 0; + Size = 0; + + uint DirectoryRva = Instance._binary.Architecture == BinaryArchitecture.x64 + ? Instance._binary.PE.OptionalHeader64.DataDirectory[ResourceDataDirectoryIndex].VirtualAddress + : Instance._binary.PE.OptionalHeader32.DataDirectory[ResourceDataDirectoryIndex].VirtualAddress; + + if (DirectoryRva == 0) + return false; + + ulong Root = Module.MappedBase + DirectoryRva; + if (!TryFindDirectoryEntry(Instance, Root, Root, ResourceTypeManifest, out ulong TypeDirectory, out bool IsDirectory) || !IsDirectory) + return false; + + if (!TryFindDirectoryEntry(Instance, Root, TypeDirectory, ResourceIdCreateProcessManifest, out ulong NameDirectory, out IsDirectory) || !IsDirectory) + return false; + + if (!TryFindDirectoryEntry(Instance, Root, NameDirectory, uint.MaxValue, out ulong DataEntry, out IsDirectory) || IsDirectory) + return false; + + if (!Instance.IsRegionMapped(DataEntry, ResourceDataEntrySize)) + return false; + + uint DataRva = Instance.ReadMemoryUInt(DataEntry + 0x00); + uint DataSize = Instance.ReadMemoryUInt(DataEntry + 0x04); + if (DataRva == 0 || DataSize == 0 || DataSize > MaxManifestBytes) + return false; + + Address = Module.MappedBase + DataRva; + Size = DataSize; + return Instance.IsRegionMapped(Address, DataSize); + } + + private static bool TryFindDirectoryEntry(BinaryEmulator Instance, ulong Root, ulong Directory, uint Id, out ulong Target, out bool IsDirectory) + { + Target = 0; + IsDirectory = false; + + if (!Instance.IsRegionMapped(Directory, ResourceDirectoryHeaderSize)) + return false; + + int NamedEntries = Instance._emulator.ReadMemoryUShort(Directory + 0x0C); + int IdEntries = Instance._emulator.ReadMemoryUShort(Directory + 0x0E); + int TotalEntries = NamedEntries + IdEntries; + if (TotalEntries <= 0 || TotalEntries > MaxResourceDirectoryEntries) + return false; + + ulong EntryBase = Directory + ResourceDirectoryHeaderSize; + if (!Instance.IsRegionMapped(EntryBase, (ulong)(TotalEntries * ResourceDirectoryEntrySize))) + return false; + + for (int i = NamedEntries; i < TotalEntries; i++) + { + ulong Entry = EntryBase + (ulong)(i * ResourceDirectoryEntrySize); + uint EntryId = Instance.ReadMemoryUInt(Entry + 0x00); + if (Id != uint.MaxValue && EntryId != Id) + continue; + + uint OffsetToData = Instance.ReadMemoryUInt(Entry + 0x04); + IsDirectory = (OffsetToData & 0x80000000u) != 0; + Target = Root + (OffsetToData & 0x7FFFFFFFu); + return true; + } + + return false; + } + + private static bool TryParseManifestAwareness(byte[] Manifest, out uint Awareness, out bool GdiScaled) + { + Awareness = AwarenessUnaware; + GdiScaled = false; + + string DpiAwarenessValue = null; + string DpiAwareValue = null; + string GdiScalingValue = null; + + XmlReaderSettings Settings = new XmlReaderSettings + { + DtdProcessing = DtdProcessing.Prohibit, + IgnoreComments = true, + IgnoreWhitespace = true, + IgnoreProcessingInstructions = true, + XmlResolver = null, + CloseInput = true, + }; + + using MemoryStream Stream = new MemoryStream(Manifest, 0, Manifest.Length, false); + using XmlReader Reader = XmlReader.Create(Stream, Settings); + + while (!Reader.EOF) + { + if (Reader.NodeType != XmlNodeType.Element) + { + Reader.Read(); + continue; + } + + string Name = Reader.LocalName; + if (Name != "dpiAwareness" && Name != "dpiAware" && Name != "gdiScaling") + { + Reader.Read(); + continue; + } + + string Value = Reader.ReadElementContentAsString(); + switch (Name) + { + case "dpiAwareness": + DpiAwarenessValue ??= Value; + break; + case "dpiAware": + DpiAwareValue ??= Value; + break; + default: + GdiScalingValue ??= Value; + break; + } + } + + GdiScaled = string.Equals(GdiScalingValue?.Trim(), "true", StringComparison.OrdinalIgnoreCase); + + if (DpiAwarenessValue != null) + { + foreach (string Token in DpiAwarenessValue.Split(',')) + { + switch (Token.Trim().ToLowerInvariant()) + { + case "permonitorv2": + case "permonitor": + Awareness = AwarenessPerMonitor; + return true; + case "system": + Awareness = AwarenessSystem; + return true; + case "unaware": + Awareness = AwarenessUnaware; + return true; + } + } + } + + if (DpiAwareValue == null) + return GdiScaled; + + switch (DpiAwareValue.Trim().ToLowerInvariant()) + { + case "true/pm": + case "per monitor": + case "permonitor": + Awareness = AwarenessPerMonitor; + return true; + case "true": + Awareness = AwarenessSystem; + return true; + case "false": + Awareness = AwarenessUnaware; + return true; + default: + return GdiScaled; + } + } + } +} diff --git a/Brovan/Core/Emulation/OS/Windows/Win32k/Win32kHelper.cs b/Brovan/Core/Emulation/OS/Windows/Win32k/Win32kHelper.cs index c70d72e..e500c3c 100644 --- a/Brovan/Core/Emulation/OS/Windows/Win32k/Win32kHelper.cs +++ b/Brovan/Core/Emulation/OS/Windows/Win32k/Win32kHelper.cs @@ -40,7 +40,6 @@ internal static class Win32kHelper internal const uint ERROR_INVALID_PARAMETER = 87; internal const uint ERROR_CALL_NOT_IMPLEMENTED = 120; internal const uint ERROR_INVALID_WINDOW_HANDLE = 1400; - internal const uint DEFAULT_SCREEN_DPI = 96; internal const byte PenHandleType = 0x30; internal const byte BrushHandleType = 0x10; @@ -63,6 +62,7 @@ internal static class Win32kHelper internal const uint WM_SYSKEYDOWN = 0x0104; internal const uint WM_SYSKEYUP = 0x0105; internal const uint WM_SYSCHAR = 0x0106; + internal const uint WM_DPICHANGED = 0x02E0; internal const uint WM_MOUSEMOVE = 0x0200; internal const uint WM_LBUTTONDOWN = 0x0201; internal const uint WM_LBUTTONUP = 0x0202; @@ -454,6 +454,8 @@ private static void DrainHostEvents(BinaryEmulator Instance) if (Foreground == 0) return; + Win32kDpi.DrainHostDpiChange(Instance); + if (HostEventQueue.ConsumeRepaint()) InvalidateWindow(Instance, Foreground); diff --git a/Brovan/Core/Emulation/OS/Windows/WinHelperConstants.cs b/Brovan/Core/Emulation/OS/Windows/WinHelperConstants.cs index b504c83..3a83836 100644 --- a/Brovan/Core/Emulation/OS/Windows/WinHelperConstants.cs +++ b/Brovan/Core/Emulation/OS/Windows/WinHelperConstants.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -26,6 +26,9 @@ public struct UserSharedDisplayInfo [FieldOffset(0x08)] public ulong PrimaryMonitor; + + [FieldOffset(0x40)] + public uint CompositionFlags; } [StructLayout(LayoutKind.Explicit, Pack = 1)] @@ -1091,10 +1094,36 @@ public class WinProcess : IHandleObject public ulong InstrumentationCallback; public ulong JobObjectHandle; + public System.Diagnostics.Process SpawnedHost; + + public bool SpawnedHasExited + { + get + { + try + { + return SpawnedHost != null && SpawnedHost.HasExited; + } + catch (InvalidOperationException) + { + return true; + } + } + } + public string ObjectId => PID.ToString(); public HandleType ObjectType => HandleType.ProcessHandle; } + public sealed class WinSpawnedThread : IHandleObject + { + public WinProcess Process; + public uint ThreadId; + + public string ObjectId => $"SPAWNEDTHREAD_{ThreadId}"; + public HandleType ObjectType => HandleType.ThreadHandle; + } + public sealed class WinDirectoryEntry { public string Name; diff --git a/Brovan/Core/Emulation/OS/Windows/WinSyscallsHelper.cs b/Brovan/Core/Emulation/OS/Windows/WinSyscallsHelper.cs index 9d224a1..86bc990 100644 --- a/Brovan/Core/Emulation/OS/Windows/WinSyscallsHelper.cs +++ b/Brovan/Core/Emulation/OS/Windows/WinSyscallsHelper.cs @@ -957,6 +957,15 @@ public ulong GetArg(int Index) private uint SequentialIdCursor = 30000; private uint AnonymousObjectCursor; + private uint AdoptHostProcessId() + { + uint HostId = (uint)Environment.ProcessId; + if (HostId != 0 && PIDs.Add(HostId)) + return HostId; + + return GenerateRandomPID(); + } + public uint GenerateRandomPID() { for (int Attempt = 0; Attempt < 64; Attempt++) @@ -1212,8 +1221,6 @@ internal void RemoveWinHandle(ulong Handle) private readonly Stack FreeGdiHandleIndices = new(); private readonly byte[] GdiHandleUniq = new byte[GdiHandleEntryCount]; private readonly ulong[] GdiHandleAttributes = new ulong[GdiHandleEntryCount]; - private const int SmCxScreen = 0; - private const int SmCyScreen = 1; private const int EnumCurrentSettings = -1; private const ushort DevModeSize = 0xDC; private const int DevModeOffsetSize = 0x44; @@ -1230,6 +1237,8 @@ internal void RemoveWinHandle(ulong Handle) private ulong UserDesktopInfoAddress; private ulong UserDesktopOwnerAddress; private ulong UserPrimaryMonitorAddress; + private const uint DpiChangeRectSize = 0x10; + private ulong DpiChangeRectAddress; private ulong UserSharedInfoMirrorAddress; private ulong UserSharedDelta; public ulong ActiveWindow; @@ -1248,7 +1257,7 @@ public WinSysHelper(BinaryEmulator Emulator) SyntheticVolumeWin32GuidPath = $"\\\\?\\Volume{{{SyntheticVolumeGuid}}}\\"; SyntheticMountDevUniqueId = Guid.Parse(SyntheticVolumeGuid).ToByteArray(); KuserSharedData = new KuserSharedDataManager(Emulator); - PID = GenerateRandomPID(); + PID = AdoptHostProcessId(); PPID = GenerateRandomPID(); string FileName = null; BinaryFile Binary = Emulator._binary; @@ -3560,6 +3569,8 @@ private void WriteWin32ClientInfoSlot(ulong Teb, int Slot, ulong Value) } Emulator._emulator.WriteMemory(Teb + Win32ClientInfoX86Base + ((ulong)Slot * 4UL), (uint)Value, 4); + + Emulator._emulator.WriteMemory(Teb + Win32ClientInfoX64Base + ((ulong)Slot * 8UL), Value, 8); } private ulong ComputeUserSharedDelta(ulong ServerInfo, ulong HandleTable, ulong DisplayInfo) @@ -3596,7 +3607,18 @@ private ulong EnsureUserPrimaryMonitor() if (!WriteZeroMemory(Monitor, (uint)UserPrimaryMonitorSize)) return 0; - (int Width, int Height) = GetHostPrimaryMonitorSize(); + if (!WriteUserPrimaryMonitorInfo(Monitor)) + return 0; + + UserPrimaryMonitorAddress = Monitor; + return UserPrimaryMonitorAddress; + } + + private bool WriteUserPrimaryMonitorInfo(ulong Monitor) + { + int Width = Win32kDpi.GetScreenWidth(Emulator); + int Height = Win32kDpi.GetScreenHeight(Emulator); + ushort Dpi = (ushort)Win32kDpi.GetEffectiveDpi(Emulator); UserPrimaryMonitorInfo MonitorInfo = new UserPrimaryMonitorInfo { @@ -3609,36 +3631,37 @@ private ulong EnsureUserPrimaryMonitor() WorkTop = 0, WorkRight = Width, WorkBottom = Height, - DpiX = 96, - DpiY = 96 + DpiX = Dpi, + DpiY = Dpi }; Span Buffer = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref MonitorInfo, 1)); - if (!Emulator._emulator.WriteMemory(Monitor, Buffer)) - return 0; - - UserPrimaryMonitorAddress = Monitor; - return UserPrimaryMonitorAddress; + return Emulator._emulator.WriteMemory(Monitor, Buffer); } - private static (int Width, int Height) GetHostPrimaryMonitorSize() + public void RefreshDisplayDependentState() { - try - { - if (OperatingSystem.IsWindows()) - { - int Width = NativeWinImports.GetSystemMetrics(SmCxScreen); - int Height = NativeWinImports.GetSystemMetrics(SmCyScreen); + if (UserPrimaryMonitorAddress != 0 && Emulator.IsRegionMapped(UserPrimaryMonitorAddress, UserPrimaryMonitorSize)) + WriteUserPrimaryMonitorInfo(UserPrimaryMonitorAddress); - if (Width > 0 && Height > 0) - return (Width, Height); - } - } - catch + foreach (WinWindow Window in WinWindows.Values) { + if (Window.ClientWindowAddress != 0 && Emulator.IsRegionMapped(Window.ClientWindowAddress, UserWindowObjectSize)) + Win32kDpi.ApplyWindowContext(Emulator, Window.ClientWindowAddress); } + } + + public ulong EnsureDpiChangeRect() + { + if (DpiChangeRectAddress != 0 && Emulator.IsRegionMapped(DpiChangeRectAddress, DpiChangeRectSize)) + return DpiChangeRectAddress; - return (1920, 1080); + ulong Address = Emulator.MapUniqueAddress(DpiChangeRectSize, MemoryProtection.ReadWrite); + if (Address == 0 || !WriteZeroMemory(Address, DpiChangeRectSize)) + return 0; + + DpiChangeRectAddress = Address; + return DpiChangeRectAddress; } public unsafe uint GetPrimaryDisplayFrequency() @@ -3702,7 +3725,8 @@ public ulong EnsureUserDesktopInfo() UserSharedDisplayInfo DisplayInfo = new UserSharedDisplayInfo { Type = 1u, - PrimaryMonitor = PrimaryMonitor + PrimaryMonitor = PrimaryMonitor, + CompositionFlags = 1u }; Span Buffer = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref DisplayInfo, 1)); @@ -3873,6 +3897,8 @@ private void RefreshUserWindowObject(WinWindow Window) Emulator._emulator.WriteMemory(Window.ClientWindowAddress + 0xB8, Window.ClientTextBytes, 4); Emulator._emulator.WriteMemory(Window.ClientWindowAddress + 0xC0, TextObject, 8); Emulator._emulator.WriteMemory(Window.ClientWindowAddress + 0xE0, 0UL, 8); + + Win32kDpi.ApplyWindowContext(Emulator, Window.ClientWindowAddress); } private ulong EnsureUserClassObject(WinWindow Window) @@ -4265,6 +4291,7 @@ private void EnsureDesktopWindow() Decorated = true, Center = true, State = WindowState.Normal, + DpiAwareness = Win32kDpi.GetHostAwareness(Emulator), }); } diff --git a/Brovan/EmulationMenu/EmulationMenu.cs b/Brovan/EmulationMenu/EmulationMenu.cs index 97e759b..7f910b8 100644 --- a/Brovan/EmulationMenu/EmulationMenu.cs +++ b/Brovan/EmulationMenu/EmulationMenu.cs @@ -2341,7 +2341,7 @@ private static void ShowData(bool Quick) } } - public static void RunEmulator(string FilePath, bool Quick, bool Silent, [AllowNull] string Command, [AllowNull] string RawProgramArguments, string[] ProgramArguments, NetworkAccessPolicy NetworkPolicyValue, bool NoHooks, EmulationBackendKind BackendKind) + public static void RunEmulator(string FilePath, bool Quick, bool Silent, [AllowNull] string Command, [AllowNull] string RawProgramArguments, string[] ProgramArguments, NetworkAccessPolicy NetworkPolicyValue, bool NoHooks, EmulationBackendKind BackendKind, [AllowNull] string WorkingDirectory = null) { SilentMode = Silent; @@ -2406,6 +2406,7 @@ public static void RunEmulator(string FilePath, bool Quick, bool Silent, [AllowN EmulateNetworking = NetworkPolicyValue.HasAnyAccess(), NetworkPolicy = NetworkPolicyValue, RawProgramArguments = RawProgramArguments, + WorkingDirectory = WorkingDirectory, ProgramArguments = ProgramArguments ?? Array.Empty(), NoHooks = NoHooks, BackendKind = BackendKind diff --git a/Brovan/GeneralHelper.cs b/Brovan/GeneralHelper.cs index edf1a50..f32f4d7 100644 --- a/Brovan/GeneralHelper.cs +++ b/Brovan/GeneralHelper.cs @@ -143,6 +143,15 @@ internal class NativeWinImports [DllImport("user32.dll")] public static extern int GetSystemMetricsForDpi(int nIndex, uint dpi); + + [DllImport("user32.dll")] + public static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext); + + [DllImport("user32.dll")] + public static extern IntPtr MonitorFromWindow(IntPtr hWnd, int dwFlags); + + [DllImport("shcore.dll")] + public static extern int GetDpiForMonitor(IntPtr hMonitor, int dpiType, out uint dpiX, out uint dpiY); } internal class NativeUnixImports diff --git a/Brovan/Program.cs b/Brovan/Program.cs index 63de98f..5e6244b 100644 --- a/Brovan/Program.cs +++ b/Brovan/Program.cs @@ -128,6 +128,11 @@ static void ShowHelp() Console.WriteLine(" --net-allow= Allow a specific IPv4 or IPv6 address in addition to the selected policy."); Console.WriteLine(" --no-hooks Run the emulator with no hooks. useful when you want maximum performance and want to see some program output."); Console.WriteLine(" --backend= Choose the emulation backend: unicorn (default), kvm (Linux), or whp (Windows Hypervisor Platform)."); + Console.WriteLine(" --cwd Directory the emulated program starts in. Defaults to the directory of the binary."); + Console.WriteLine(" --guest-cmdline "); + Console.WriteLine(" Command line for the emulated program, excluding argv[0], used instead of"); + Console.WriteLine(" trailing arguments. Both options also accept \"base64:\" so that"); + Console.WriteLine(" quotes and spaces survive intact."); Console.WriteLine(); Console.WriteLine("Notes:"); Console.WriteLine(" All Brovan flags must be passed before the program path."); @@ -185,6 +190,97 @@ private static bool TryAddAllowedNetworkAddress(NetworkAccessPolicy Policy, stri return true; } + private const string Base64ArgumentPrefix = "base64:"; + + private static string DecodeArgumentValue(string Value) + { + if (string.IsNullOrEmpty(Value) || !Value.StartsWith(Base64ArgumentPrefix, StringComparison.Ordinal)) + return Value; + + try + { + return Encoding.UTF8.GetString(Convert.FromBase64String(Value.Substring(Base64ArgumentPrefix.Length))); + } + catch (FormatException) + { + PrintHighlight("[-] Invalid base64 argument value.", true); + return null; + } + } + + private static string[] SplitCommandLine(string CommandLine) + { + List Arguments = new List(); + if (string.IsNullOrWhiteSpace(CommandLine)) + return Arguments.ToArray(); + + StringBuilder Current = new StringBuilder(); + bool Quoted = false; + bool HasArgument = false; + int Backslashes = 0; + + void Flush(bool BeforeQuote, out bool EscapedQuote) + { + EscapedQuote = false; + if (BeforeQuote) + { + Current.Append('\\', Backslashes / 2); + EscapedQuote = (Backslashes % 2) != 0; + } + else + { + Current.Append('\\', Backslashes); + } + + Backslashes = 0; + } + + foreach (char Character in CommandLine) + { + if (Character == '\\') + { + Backslashes++; + HasArgument = true; + continue; + } + + if (Character == '"') + { + Flush(true, out bool EscapedQuote); + if (EscapedQuote) + Current.Append('"'); + else + Quoted = !Quoted; + + HasArgument = true; + continue; + } + + Flush(false, out _); + + if (!Quoted && (Character == ' ' || Character == '\t')) + { + if (HasArgument) + { + Arguments.Add(Current.ToString()); + Current.Clear(); + HasArgument = false; + } + + continue; + } + + Current.Append(Character); + HasArgument = true; + } + + Flush(false, out _); + if (HasArgument) + Arguments.Add(Current.ToString()); + + return Arguments.ToArray(); + } + private static bool TryParseBackendKind(string Value, out EmulationBackendKind Kind) { switch ((Value ?? string.Empty).Trim().ToLowerInvariant()) @@ -330,6 +426,8 @@ static void Main(string[] args) bool Silent = false; string Command = null; string FilePath = null; + string WorkingDirectory = null; + string GuestCommandLine = null; EmulationBackendKind BackendKind = EmulationBackendKind.Unicorn; NetworkAccessPolicy NetworkPolicy = new NetworkAccessPolicy(NetworkAccessMode.Loopback); List ProgramArgumentsList = new List(); @@ -390,6 +488,20 @@ static void Main(string[] args) case "--no-hooks": NoHooks = true; continue; + case "--cwd": + if (i + 1 >= args.Length) + continue; + + WorkingDirectory = DecodeArgumentValue(args[i + 1]); + i++; + continue; + case "--guest-cmdline": + if (i + 1 >= args.Length) + continue; + + GuestCommandLine = DecodeArgumentValue(args[i + 1]); + i++; + continue; case "--backend": if (i + 1 >= args.Length || !TryParseBackendKind(args[i + 1], out EmulationBackendKind ArgumentBackendKind)) { @@ -479,11 +591,13 @@ static void Main(string[] args) } string[] ProgramArguments = ProgramArgumentsList.ToArray(); - string RawProgramArguments = BuildRawProgramArguments(ProgramArguments); + string RawProgramArguments = GuestCommandLine ?? BuildRawProgramArguments(ProgramArguments); + if (GuestCommandLine != null) + ProgramArguments = SplitCommandLine(GuestCommandLine); // Set the dll import resolver based on the platform NativeLibraryResolver.Register(); - EmulationMenu.EmulationMenu.RunEmulator(FilePath, Quick, Silent, Command, RawProgramArguments, ProgramArguments, NetworkPolicy, NoHooks, BackendKind); + EmulationMenu.EmulationMenu.RunEmulator(FilePath, Quick, Silent, Command, RawProgramArguments, ProgramArguments, NetworkPolicy, NoHooks, BackendKind, WorkingDirectory); } } } \ No newline at end of file