From 180dfb80f65121c3991f935c48ee4477b1bb8a5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20R=C3=B8nne=20Petersen?= Date: Fri, 15 Jul 2022 05:54:25 +0200 Subject: [PATCH] Switch most interop code in Vezel.Ruptura.Injection to Vezel.Ruptura.System APIs. Part of #30. --- src/injection/AssemblyInjector.cs | 107 ++++++++--------- src/injection/IO/ProcessMemoryStream.cs | 42 +++---- src/injection/NativeMethods.txt | 11 -- src/injection/TargetProcess.cs | 149 +++++++++--------------- src/injection/injection.csproj | 3 + 5 files changed, 137 insertions(+), 175 deletions(-) diff --git a/src/injection/AssemblyInjector.cs b/src/injection/AssemblyInjector.cs index 24049e4..58cfc7b 100644 --- a/src/injection/AssemblyInjector.cs +++ b/src/injection/AssemblyInjector.cs @@ -1,9 +1,6 @@ using Vezel.Ruptura.Injection.IO; using Vezel.Ruptura.Injection.Threading; -using Windows.Win32.Foundation; -using Windows.Win32.System.Memory; using static Iced.Intel.AssemblerRegisters; -using Win32 = Windows.Win32.WindowsPInvoke; namespace Vezel.Ruptura.Injection; @@ -31,13 +28,15 @@ unsafe struct RupturaParameters bool _injecting; + bool _waiting; + nuint _loadLibraryW; nuint _getProcAddress; nuint _getLastError; - SafeHandle? _threadHandle; + ThreadObject? _thread; public AssemblyInjector(TargetProcess process, AssemblyInjectorOptions options) { @@ -63,7 +62,7 @@ public void Dispose() void DisposeCore() { - _threadHandle?.Dispose(); + _thread?.Dispose(); } string GetModulePath() @@ -74,9 +73,9 @@ string GetModulePath() return File.Exists(path) ? path : throw new InjectionException("Could not locate the Ruptura native module."); } - void PopulateMemoryArea(nuint area, nuint length, Action action) + void PopulateMemoryArea(nuint area, nint length, Action action) { - using var stream = new ProcessMemoryStream(_process, area, length); + using var stream = new ProcessMemoryStream(_process.Object, area, length); using var writer = new InjectionBinaryWriter(stream, true); action(stream, writer); @@ -102,23 +101,20 @@ void ForceLoaderInitialization() { // Spawning a live thread in a process that was created suspended forces the Windows image loader to finish // loading the image so that, among other things, we will be able to resolve kernel32.dll exports. - using var threadHandle = _process.CreateThread(initializeShell, 0); + using var thread = _process.CreateThread(initializeShell, 0); - switch ((WIN32_ERROR)Win32.WaitForSingleObjectEx( - threadHandle, (uint)(long)_options.InjectionTimeout.TotalMilliseconds, false)) + // TODO: Consider making this async with ThreadPool.UnsafeRegisterWaitForSingleObject(). + switch (thread.Wait(_options.InjectionTimeout, false)) { - case WIN32_ERROR.WAIT_OBJECT_0: + case WaitResult.Signaled: break; - case WIN32_ERROR.WAIT_TIMEOUT: + case WaitResult.TimedOut: throw new TimeoutException(); default: - throw new Win32Exception(); + throw new UnreachableException(); } - if (!Win32.GetExitCodeThread(threadHandle, out var code)) - throw new Win32Exception(); - - if (code != 0) + if (thread.GetExitCode() is var code and not 0) throw new InjectionException($"Failed to initialize the target process: 0x{code:x}"); } finally @@ -132,7 +128,7 @@ void RetrieveKernel32Exports() if (_process.GetModule("kernel32.dll") is not (var k32Addr, var k32Size)) throw new InjectionException("Could not locate 'kernel32.dll' in the target process."); - using var stream = new ProcessMemoryStream(_process, k32Addr, k32Size); + using var stream = new ProcessMemoryStream(_process.Object, k32Addr, k32Size); var exports = new PeFile(stream).ExportedFunctions; @@ -148,7 +144,7 @@ nuint GetExport(string name) _getLastError = GetExport("GetLastError"); } - unsafe (nuint Address, nuint Length) CreateParameterArea() + unsafe (nuint Address, nint Length) CreateParameterArea() { // Keep in sync with src/module/main.h. @@ -159,12 +155,10 @@ nuint GetExport(string name) foreach (var arg in _options.Arguments.Prepend(_options.FileName)) length += Encoding.Unicode.GetByteCount(arg) + sizeof(char); - var len = (nuint)length; - - return (_process.AllocMemory((nuint)length, PAGE_PROTECTION_FLAGS.PAGE_READWRITE), len); + return (_process.AllocateMemory(length, MemoryAccess.ReadWrite), length); } - unsafe void PopulateParameterArea(nuint address, nuint length) + unsafe void PopulateParameterArea(nuint address, nint length) { // Keep in sync with src/module/main.h. @@ -213,14 +207,14 @@ async Task InjectModuleAsync(string modulePath, nuint parameterArea, MemoryMappe { var nameAreaLength = Encoding.Unicode.GetByteCount(modulePath) + sizeof(char) + Encoding.ASCII.GetByteCount(NativeEntryPoint) + sizeof(byte); - var nameArea = _process.AllocMemory((uint)nameAreaLength, PAGE_PROTECTION_FLAGS.PAGE_READWRITE); + var nameArea = _process.AllocateMemory(nameAreaLength, MemoryAccess.ReadWrite); try { nuint modulePathPtr = 0; nuint entryPointPtr = 0; - PopulateMemoryArea(nameArea, (uint)nameAreaLength, (stream, writer) => + PopulateMemoryArea(nameArea, nameAreaLength, (stream, writer) => { modulePathPtr = nameArea + (nuint)stream.Position; @@ -300,7 +294,7 @@ async Task InjectModuleAsync(string modulePath, nuint parameterArea, MemoryMappe try { - var threadHandle = _process.CreateThread(injectShell, parameterArea); + var thread = _process.CreateThread(injectShell, parameterArea); try { @@ -312,24 +306,22 @@ async Task InjectModuleAsync(string modulePath, nuint parameterArea, MemoryMappe // Did injection complete successfully? if (accessor.ReadBoolean(0)) { - _threadHandle = threadHandle; + _thread = thread; break; } // Did the thread exit with an error? - switch ((WIN32_ERROR)Win32.WaitForSingleObjectEx(threadHandle, 0, false)) + switch (thread.Wait(TimeSpan.Zero, false)) { - case WIN32_ERROR.WAIT_OBJECT_0: - if (!Win32.GetExitCodeThread(threadHandle, out var code)) - throw new Win32Exception(); - + case WaitResult.Signaled: throw new InjectionException( - $"Failed to inject the native module into the target process: 0x{code:x}"); - case WIN32_ERROR.WAIT_TIMEOUT: + $"Failed to inject the native module into the target process: " + + $"0x{thread.GetExitCode():x}"); + case WaitResult.TimedOut: break; default: - throw new Win32Exception(); + throw new UnreachableException(); } await Task.Delay(100); @@ -340,12 +332,12 @@ async Task InjectModuleAsync(string modulePath, nuint parameterArea, MemoryMappe } catch (Exception) { - threadHandle.Dispose(); + thread.Dispose(); throw; } - _threadHandle = threadHandle; + _thread = thread; } finally { @@ -399,32 +391,41 @@ public Task InjectAssemblyAsync() public Task WaitForCompletionAsync() { - _ = _threadHandle is not null and { IsInvalid: false } ? true : throw new InvalidOperationException(); + _ = _thread != null && !_waiting ? true : throw new InvalidOperationException(); + + _waiting = true; return Task.Run(async () => { - using var waitHandle = new ThreadWaitHandle(new(_threadHandle.DangerousGetHandle(), true)); - - // Transfer ownership of the native handle from _threadHandle to waitHandle so that it stays alive until the - // injected assembly returns, allowing us to retrieve the exit code. - _threadHandle.SetHandleAsInvalid(); + // This is safe because the lambda below captures the thread object and keeps it alive. + using var waitHandle = new ThreadWaitHandle(new(_thread.SafeHandle.DangerousGetHandle(), false)); var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var registration = ThreadPool.UnsafeRegisterWaitForSingleObject( waitHandle, (_, timeout) => { - var ex = default(Exception); - if (timeout) - ex = new TimeoutException(); - else if (!Win32.GetExitCodeThread(waitHandle.SafeWaitHandle, out var code)) - ex = new Win32Exception(); - else - tcs.SetResult((int)code); - - if (ex != null) - tcs.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(ex)); + { + tcs.SetException(ExceptionDispatchInfo.SetCurrentStackTrace(new TimeoutException())); + + return; + } + + int code; + + try + { + code = _thread.GetExitCode(); + } + catch (Win32Exception ex) + { + tcs.SetException(ex); + + return; + } + + tcs.SetResult(code); }, null, _options.CompletionTimeout, diff --git a/src/injection/IO/ProcessMemoryStream.cs b/src/injection/IO/ProcessMemoryStream.cs index 8c6a0e2..0cc1975 100644 --- a/src/injection/IO/ProcessMemoryStream.cs +++ b/src/injection/IO/ProcessMemoryStream.cs @@ -1,6 +1,6 @@ namespace Vezel.Ruptura.Injection.IO; -sealed class ProcessMemoryStream : Stream +sealed unsafe class ProcessMemoryStream : Stream { // TODO: Review some of the casts here. @@ -10,39 +10,39 @@ sealed class ProcessMemoryStream : Stream public override bool CanSeek => true; - public override long Length => (nint)_length; + public override long Length => _length; public override long Position { - get => (nint)_position; + get => _position; set { _ = value >= 0 ? true : throw new ArgumentOutOfRangeException(nameof(value)); - _position = (nuint)value; + _position = (nint)value; } } - readonly TargetProcess _process; + readonly ProcessObject _process; - readonly nuint _address; + readonly void* _address; - readonly nuint _length; + readonly nint _length; - nuint _position; + nint _position; bool _wrote; - public ProcessMemoryStream(TargetProcess process, nuint address, nuint length) + public ProcessMemoryStream(ProcessObject process, nuint address, nint length) { _process = process; - _address = address; + _address = (void*)address; _length = length; } public override long Seek(long offset, SeekOrigin origin) { - var off = (nuint)offset; + var off = (nint)offset; switch (origin) { @@ -68,13 +68,13 @@ public override long Seek(long offset, SeekOrigin origin) throw new ArgumentOutOfRangeException(nameof(origin)); } - return (nint)_position; + return _position; } public override void Flush() { if (_wrote) - _process.FlushCache(_address, _length); + _process.FlushInstructionCache(_address, _length); } public override void SetLength(long value) @@ -102,21 +102,22 @@ public override Task ReadAsync( public override int Read(Span buffer) { - var len = (int)nuint.Min(_length - _position, (uint)buffer.Length); + var len = (int)nint.Min(_length - _position, buffer.Length); if (len <= 0) return 0; try { - _process.ReadMemory(_address + _position, buffer[..len]); + fixed (byte* p = buffer) + _process.ReadMemory((byte*)_address + (nuint)_position, p, len); } catch (Win32Exception ex) { throw new IOException(null, ex); } - _position += (uint)len; + _position += len; return len; } @@ -126,7 +127,7 @@ public override ValueTask ReadAsync(Memory buffer, CancellationToken return ValueTask.FromResult(Read(buffer.Span)); } - public override unsafe int ReadByte() + public override int ReadByte() { byte value; @@ -151,20 +152,21 @@ public override Task WriteAsync( public override void Write(ReadOnlySpan buffer) { - _ = _position + (uint)buffer.Length <= _length ? true : throw new NotSupportedException(); + _ = _position + buffer.Length <= _length ? true : throw new NotSupportedException(); _wrote = true; try { - _process.WriteMemory(_address + _position, buffer); + fixed (byte* p = buffer) + _process.WriteMemory((byte*)_address + (nuint)_position, p, buffer.Length); } catch (Win32Exception ex) { throw new IOException(null, ex); } - _position += (uint)buffer.Length; + _position += buffer.Length; } public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) diff --git a/src/injection/NativeMethods.txt b/src/injection/NativeMethods.txt index 9b11a5a..8d62f8e 100644 --- a/src/injection/NativeMethods.txt +++ b/src/injection/NativeMethods.txt @@ -1,19 +1,8 @@ CreateProcessW CreateRemoteThreadEx -CreateToolhelp32Snapshot -FlushInstructionCache -GetExitCodeThread IsWow64Process2 K32GetModuleBaseNameW -Module32FirstW -Module32NextW OpenProcess -ReadProcessMemory -VirtualAlloc2 -VirtualFreeEx -VirtualProtectEx -WaitForSingleObjectEx -WriteProcessMemory WIN32_ERROR diff --git a/src/injection/TargetProcess.cs b/src/injection/TargetProcess.cs index cc90196..1cf8269 100644 --- a/src/injection/TargetProcess.cs +++ b/src/injection/TargetProcess.cs @@ -1,7 +1,5 @@ using Vezel.Ruptura.Injection.IO; using Windows.Win32.Foundation; -using Windows.Win32.System.Diagnostics.ToolHelp; -using Windows.Win32.System.Memory; using Windows.Win32.System.SystemInformation; using Windows.Win32.System.Threading; using Win32 = Windows.Win32.WindowsPInvoke; @@ -10,9 +8,11 @@ namespace Vezel.Ruptura.Injection; public sealed unsafe class TargetProcess : IDisposable { + // TODO: Move more of the code here to Vezel.Ruptura.System. + public int Id { get; } - public SafeHandle Handle => !_handle.IsClosed ? _handle : throw new ObjectDisposedException(GetType().Name); + public ProcessObject Object => !_object.IsDisposed ? _object : throw new ObjectDisposedException(GetType().Name); public Architecture Architecture { get; } @@ -20,17 +20,17 @@ public sealed unsafe class TargetProcess : IDisposable internal bool IsSupported => Architecture == Architecture.X64; - readonly SafeHandle _handle; + readonly ProcessObject _object; - TargetProcess(int id, SafeHandle handle, int? mainThreadId) + TargetProcess(int id, ProcessObject @object, int? mainThreadId) { Id = id; - _handle = handle; + _object = @object; MainThreadId = mainThreadId; IMAGE_FILE_MACHINE os; - if (!Win32.IsWow64Process2(handle, out var proc, &os)) + if (!Win32.IsWow64Process2(@object.SafeHandle, out var proc, &os)) throw new Win32Exception(); Architecture = (os, proc) switch @@ -87,7 +87,7 @@ public static TargetProcess Create(string fileName, string arguments, string? wo { return new( (int)info.dwProcessId, - new SafeFileHandle(info.hProcess, true), + ProcessObject.OpenHandle(info.hProcess), suspended ? (int)info.dwThreadId : null); } catch (Exception) @@ -109,14 +109,19 @@ public static TargetProcess Open(int id) // I am not sure why we can get away with not using PROCESS_CREATE_THREAD (CreateRemoteThread) and // PROCESS_QUERY_LIMITED_INFORMATION (IsWow64Process2), but apparently we can. The below rights are the absolute // minimum needed for successful injection (tested on Windows 11 22H2). - var handle = Win32.OpenProcess_SafeHandle( + using var handle = Win32.OpenProcess_SafeHandle( PROCESS_ACCESS_RIGHTS.PROCESS_VM_OPERATION | PROCESS_ACCESS_RIGHTS.PROCESS_VM_READ | PROCESS_ACCESS_RIGHTS.PROCESS_VM_WRITE, false, (uint)id); - return !handle.IsInvalid ? new(id, handle, null) : throw new Win32Exception(); + var obj = ProcessObject.OpenHandle(handle.DangerousGetHandle()); + + // Transfer handle ownership to the process object. + handle.SetHandleAsInvalid(); + + return new(id, obj, null); } public void Dispose() @@ -128,110 +133,64 @@ public void Dispose() void DisposeCore() { - _handle.Dispose(); + _object.Dispose(); } - internal (nuint Address, nuint Length)? GetModule(string name) + internal (nuint Address, nint Length)? GetModule(string name) { - SafeFileHandle snap; + SnapshotObject snapshot; - while ((snap = Win32.CreateToolhelp32Snapshot_SafeHandle( - CREATE_TOOLHELP_SNAPSHOT_FLAGS.TH32CS_SNAPMODULE, (uint)Id)).IsInvalid) + while (true) { - // We may get ERROR_BAD_LENGTH for processes that have not finished initializing or if the process loads - // or unloads a module while we are capturing the snapshot. For a process that was created suspended, this - // could get us into an infinite loop, but we handle that with CreateRemoteThread before accessing modules. - if (Marshal.GetLastPInvokeError() != (int)WIN32_ERROR.ERROR_BAD_LENGTH) - throw new Win32Exception(); - } - - using (snap) - { - var entry = new MODULEENTRY32W + try { - dwSize = (uint)sizeof(MODULEENTRY32W), - }; - - var result = Win32.Module32FirstW(snap, ref entry); - - while (true) + snapshot = SnapshotObject.Create(SnapshotFlags.Modules, Id); + } + catch (Win32Exception ex) when (ex.ErrorCode == (int)WIN32_ERROR.ERROR_BAD_LENGTH) { - if (!result) - { - if (Marshal.GetLastPInvokeError() != (int)WIN32_ERROR.ERROR_NO_MORE_FILES) - throw new Win32Exception(); - - break; - } + // We may get ERROR_BAD_LENGTH for processes that have not finished initializing or if the process loads + // or unloads a module while we are capturing the snapshot. For a process that was created suspended, + // this could get us into an infinite loop, but we handle that with CreateRemoteThread before accessing + // modules. + continue; + } - if (entry.dwSize != Unsafe.SizeOf()) - continue; + break; + } - using var modHandle = new SafeFileHandle(entry.hModule, false); + using (snapshot) + { + foreach (var mod in snapshot.EnumerateModules()) + { + using var handle = new SafeFileHandle(mod.Handle, false); var arr = new char[Win32.MAX_PATH]; uint len; fixed (char* p = arr) - while ((len = Win32.K32GetModuleBaseNameW(_handle, modHandle, p, (uint)arr.Length)) >= arr.Length) + while ((len = Win32.K32GetModuleBaseNameW( + _object.SafeHandle, handle, p, (uint)arr.Length)) >= arr.Length) Array.Resize(ref arr, (int)len); var baseName = arr.AsSpan(0, (int)len).ToString(); if (baseName.Equals(name, StringComparison.OrdinalIgnoreCase)) - return ((nuint)entry.modBaseAddr, entry.modBaseSize); - - result = Win32.Module32NextW(snap, ref entry); + return ((nuint)mod.Address, mod.Length); } } return null; } - internal nuint AllocMemory(nuint length, PAGE_PROTECTION_FLAGS flags) + internal nuint AllocateMemory(nint length, MemoryAccess access) { - return Win32.VirtualAlloc2( - _handle, - null, - length, - VIRTUAL_ALLOCATION_TYPE.MEM_COMMIT | VIRTUAL_ALLOCATION_TYPE.MEM_RESERVE, - (uint)flags, - Span.Empty) is var ptr and not null - ? (nuint)ptr - : throw new Win32Exception(); + return (nuint)_object.AllocateMemory(null, length, access); } internal void FreeMemory(nuint address) { - if (!Win32.VirtualFreeEx(_handle, (void*)address, 0, VIRTUAL_FREE_TYPE.MEM_RELEASE)) - throw new Win32Exception(); - } - - internal void ProtectMemory(nuint address, nuint length, PAGE_PROTECTION_FLAGS flags) - { - if (!Win32.VirtualProtectEx(_handle, (void*)address, length, flags, out _)) - throw new Win32Exception(); - } - - internal void ReadMemory(nuint address, Span buffer) - { - fixed (byte* p = buffer) - if (!Win32.ReadProcessMemory(_handle, (void*)address, p, (nuint)buffer.Length, null)) - throw new Win32Exception(); - } - - internal void WriteMemory(nuint address, ReadOnlySpan buffer) - { - fixed (byte* p = buffer) - if (!Win32.WriteProcessMemory(_handle, (void*)address, p, (nuint)buffer.Length, null)) - throw new Win32Exception(); - } - - internal void FlushCache(nuint address, nuint length) - { - if (!Win32.FlushInstructionCache(_handle, (void*)address, length)) - throw new Win32Exception(); + _object.FreeMemory((void*)address); } internal nuint CreateFunction(Action action) @@ -250,17 +209,17 @@ internal nuint CreateFunction(Action action) // Do an initial assembly pass to estimate how much memory we will need. _ = asm.Assemble(new StreamCodeWriter(tempStream), 0); - var len = (nuint)tempStream.Length * 2; // Usually way too much, but safe. - var code = AllocMemory(len, PAGE_PROTECTION_FLAGS.PAGE_READWRITE); + var len = (nint)tempStream.Length * 2; // Usually way too much, but safe. + var code = AllocateMemory(len, MemoryAccess.ReadWrite); try { - using var stream = new ProcessMemoryStream(this, code, len); + using var stream = new ProcessMemoryStream(_object, code, len); // Now assemble into the process for real with a known RIP value. _ = asm.Assemble(new StreamCodeWriter(stream), code); - ProtectMemory(code, len, PAGE_PROTECTION_FLAGS.PAGE_EXECUTE_READ); + _ = _object.ProtectMemory((void*)code, len, MemoryAccess.ExecuteRead); } catch (Exception) { @@ -272,10 +231,10 @@ internal nuint CreateFunction(Action action) return code; } - internal SafeHandle CreateThread(nuint address, nuint parameter) + internal ThreadObject CreateThread(nuint address, nuint parameter) { - var handle = Win32.CreateRemoteThreadEx( - _handle, + using var handle = Win32.CreateRemoteThreadEx( + _object.SafeHandle, null, 0, (delegate* unmanaged[Stdcall])address, @@ -284,6 +243,14 @@ internal SafeHandle CreateThread(nuint address, nuint parameter) (LPPROC_THREAD_ATTRIBUTE_LIST)null, null); - return !handle.IsInvalid ? handle : throw new Win32Exception(); + if (handle.IsInvalid) + throw new Win32Exception(); + + var obj = ThreadObject.OpenHandle(handle.DangerousGetHandle()); + + // Transfer handle ownership to the thread object. + handle.SetHandleAsInvalid(); + + return obj; } } diff --git a/src/injection/injection.csproj b/src/injection/injection.csproj index f3935e6..f06fb3c 100644 --- a/src/injection/injection.csproj +++ b/src/injection/injection.csproj @@ -25,6 +25,7 @@ assemblies into processes. + @@ -69,4 +70,6 @@ assemblies into processes. ItemName="Content" /> + +