diff --git a/Dalamud.Bootstrap/BootstrapException.cs b/Dalamud.Bootstrap/BootstrapException.cs index 354ac00a3..dc66ddbf4 100644 --- a/Dalamud.Bootstrap/BootstrapException.cs +++ b/Dalamud.Bootstrap/BootstrapException.cs @@ -2,10 +2,7 @@ using System; namespace Dalamud.Bootstrap { - /// - /// An error that is thrown when there was a problem with bootstraping. - /// - public sealed class BootstrapException : Exception + public class BootstrapException : Exception { internal BootstrapException() : base() { } diff --git a/Dalamud.Bootstrap/BootstraperException.cs b/Dalamud.Bootstrap/BootstraperException.cs new file mode 100644 index 000000000..25984056c --- /dev/null +++ b/Dalamud.Bootstrap/BootstraperException.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Dalamud.Bootstrap +{ + public class BootstraperException : BootstrapException + { + internal BootstraperException() : base() { } + + internal BootstraperException(string message) : base(message) { } + + internal BootstraperException(string message, Exception innerException) : base(message, innerException) { } + } +} diff --git a/Dalamud.Bootstrap/Bootstrapper.cs b/Dalamud.Bootstrap/Bootstrapper.cs index 1d54a85ef..31511d115 100644 --- a/Dalamud.Bootstrap/Bootstrapper.cs +++ b/Dalamud.Bootstrap/Bootstrapper.cs @@ -132,59 +132,7 @@ namespace Dalamud.Bootstrap } } - /// - /// Recovers a key used in encrypting process arguments. - /// - /// A key recovered from the time when the process was created. - /// - /// This is possible because the key to encrypt arguments is just a high nibble value from GetTickCount() at the time when the process was created. - /// (Thanks Wintermute!) - /// - private uint GetArgumentEncryptionKey() - { - var createdTime = m_process.GetCreationTime(); - // Get current tick - var currentDt = DateTime.Now; - var currentTick = Environment.TickCount; - - // We know that GetTickCount() is just a system uptime in milliseconds. - var delta = currentDt - createdTime; - var createdTick = (uint)currentTick - (uint)delta.TotalMilliseconds; - - // only the high nibble is used. - return createdTick & 0xFFFF_0000; - } - - /// - /// Reads command-line arguments from the game and decrypts them if necessary. - /// - /// - /// Command-line arguments that looks like this: - /// /DEV.TestSID =ABCD /UserPath =C:\Examples - /// - public string GetGameArguments() - { - var processArguments = m_process.GetProcessArguments(); - - // arg[0] is a path to exe(normally), arg[1] is actual stuff. - if (processArguments.Length < 2) - { - throw new ProcessException($"Process {m_process.GetProcessId()} only have {processArguments.Length} arguments. It must have atleast 2 arguments."); - } - - // We're interested in argument that contains session id - var argument = processArguments[1]; - - // If it's encrypted, we need to decrypt it first - if (EncryptedArgument.TryParse(argument, out var encryptedArgument)) - { - var key = GetArgumentEncryptionKey(); - argument = encryptedArgument.Decrypt(key); - } - - return argument; - } } internal sealed class PipePlatform : IPipePlatform diff --git a/Dalamud.Bootstrap/GameProcess.Arguments.cs b/Dalamud.Bootstrap/GameProcess.Arguments.cs new file mode 100644 index 000000000..7ed040889 --- /dev/null +++ b/Dalamud.Bootstrap/GameProcess.Arguments.cs @@ -0,0 +1,62 @@ +using Dalamud.Bootstrap.SqexArg; +using System; + +namespace Dalamud.Bootstrap +{ + public sealed partial class GameProcess : IDisposable + { + /// + /// Recovers a key used in encrypting process arguments. + /// + /// A key recovered from the time when the process was created. + /// + /// This is possible because the key to encrypt arguments is just a high nibble value from GetTickCount() at the time when the process was created. + /// (Thanks Wintermute!) + /// + private uint GetArgumentEncryptionKey() + { + var createdTime = GetCreationTime(); + + // Get current tick + var currentDt = DateTime.Now; + var currentTick = Environment.TickCount; + + // We know that GetTickCount() is just a system uptime in milliseconds. + var delta = currentDt - createdTime; + var createdTick = (uint)currentTick - (uint)delta.TotalMilliseconds; + + // only the high nibble is used. + return createdTick & 0xFFFF_0000; + } + + /// + /// Reads command-line arguments from the game and decrypts them if necessary. + /// + /// + /// Command-line arguments that looks like this: + /// /DEV.TestSID =ABCD /UserPath =C:\Examples + /// + public ArgumentBuilder GetGameArguments() + { + var processArguments = GetProcessArguments(); + + // arg[0] is a path to exe(normally), arg[1] is actual stuff. + if (processArguments.Length < 2) + { + throw new ProcessException($"There's only {processArguments.Length} process arguments. It must have at least 2 arguments."); + } + + // We're interested in argument that contains session id + var argument = processArguments[1]; + + // If it's encrypted, we need to decrypt it first + if (EncryptedArgument.TryParse(argument, out var encryptedArgument)) + { + var key = GetArgumentEncryptionKey(); + argument = encryptedArgument.Decrypt(key); + } + + return argument; + } + } +} diff --git a/Dalamud.Bootstrap/GameProcess.Win32.Memory.cs b/Dalamud.Bootstrap/GameProcess.Win32.Memory.cs new file mode 100644 index 000000000..c69ec036a --- /dev/null +++ b/Dalamud.Bootstrap/GameProcess.Win32.Memory.cs @@ -0,0 +1,87 @@ +using Dalamud.Bootstrap.OS.Windows.Raw; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace Dalamud.Bootstrap +{ + public sealed partial class GameProcess : IDisposable + { + /// + /// Reads the process memory. + /// + /// + /// The number of bytes that is actually read. + /// + private int ReadMemory(IntPtr address, Span destination) + { + unsafe + { + fixed (byte* pDest = destination) + { + if (!Kernel32.ReadProcessMemory(m_handle, address, pDest, (IntPtr)destination.Length, out var bytesRead)) + { + ProcessException.ThrowLastOsError(); + } + + // This is okay because destination will never be longer than int.Max + return bytesRead.ToInt32(); + } + } + } + + public void ReadMemoryExact(IntPtr address, Span destination) + { + var totalBytesRead = 0; + + while (totalBytesRead < destination.Length) + { + var bytesRead = ReadMemory(address + totalBytesRead, destination[totalBytesRead..]); + + if (bytesRead == 0) + { + // prolly page fault; there's not much we can do here + var readBeginAddr = address.ToInt64() + totalBytesRead; + var readEndAddr = address.ToInt64() + destination.Length; + + ProcessException.ThrowLastOsError(); + } + + totalBytesRead += bytesRead; + } + } + + private byte[] ReadMemoryExact(IntPtr address, int length) + { + var buffer = new byte[length]; + ReadMemoryExact(address, buffer); + + return buffer; + } + + private void ReadMemoryExact(IntPtr address, ref T value) where T : unmanaged + { + var span = MemoryMarshal.CreateSpan(ref value, 1); // span should never leave this function since it has unbounded lifetime. + var buffer = MemoryMarshal.AsBytes(span); + + ReadMemoryExact(address, buffer); + } + + private IntPtr GetPebAddress() + { + unsafe + { + PROCESS_BASIC_INFORMATION info = default; + + var status = Ntdll.NtQueryInformationProcess(m_handle, PROCESSINFOCLASS.ProcessBasicInformation, &info, sizeof(PROCESS_BASIC_INFORMATION), (IntPtr*)IntPtr.Zero); + + if (!status.Success) + { + throw new ProcessException($"Could not query information on process. (Status: {status})"); + } + + return info.PebBaseAddress; + } + } + } +} diff --git a/Dalamud.Bootstrap/GameProcess.Win32.Process.cs b/Dalamud.Bootstrap/GameProcess.Win32.Process.cs new file mode 100644 index 000000000..dd7ae3f7f --- /dev/null +++ b/Dalamud.Bootstrap/GameProcess.Win32.Process.cs @@ -0,0 +1,94 @@ +using Dalamud.Bootstrap.OS.Windows.Raw; +using System; +using System.Text; + +namespace Dalamud.Bootstrap +{ + public sealed partial class GameProcess : IDisposable + { + /// + /// Reads command-line arguments from the process. + /// + public string[] GetProcessArguments() + { + PEB peb = default; + RTL_USER_PROCESS_PARAMETERS procParam = default; + + // Find where the command line is allocated + var pebAddr = GetPebAddress(); + ReadMemoryExact(pebAddr, ref peb); + ReadMemoryExact(peb.ProcessParameters, ref procParam); + + // Read the command line (which is utf16-like string) + var commandLine = ReadMemoryExact(procParam.CommandLine.Buffer, procParam.CommandLine.Length); + + return ParseCommandLineToArguments(commandLine); + } + + /// + /// Returns a time when the process was started. + /// + private DateTime GetCreationTime() + { + if (!Kernel32.GetProcessTimes(m_handle, out var creationTime, out var _, out var _, out var _)) + { + ProcessException.ThrowLastOsError(); + } + + return creationTime.ToDateTime(); + } + + private string[] ParseCommandLineToArguments(ReadOnlySpan commandLine) + { + unsafe + { + char** argv; + int argc; + + fixed (byte* pCommandLine = commandLine) + { + argv = Shell32.CommandLineToArgvW(pCommandLine, out argc); + } + + if (argv == null) + { + ProcessException.ThrowLastOsError(); + } + + // NOTE: argv must be deallocated via LocalFree when we're done + try + { + var arguments = new string[argc]; + + for (var i = 0; i < argc; i++) + { + arguments[i] = new string(argv[i]); + } + + return arguments; + } + finally + { + Kernel32.LocalFree(argv); + } + } + } + + public string GetImageFilePath() + { + var buffer = new StringBuilder(300); + + // From docs: + // On input, specifies the size of the lpExeName buffer, in characters. + // On success, receives the number of characters written to the buffer, not including the null-terminating character. + var size = buffer.Capacity; + + if (!Kernel32.QueryFullProcessImageNameW(m_handle, 0, buffer, ref size)) + { + ProcessException.ThrowLastOsError(); + } + + return buffer.ToString(); + } + } +} diff --git a/Dalamud.Bootstrap/GameProcess.cs b/Dalamud.Bootstrap/GameProcess.cs index e69de29bb..19fcf4885 100644 --- a/Dalamud.Bootstrap/GameProcess.cs +++ b/Dalamud.Bootstrap/GameProcess.cs @@ -0,0 +1,48 @@ +using Dalamud.Bootstrap.OS; +using Dalamud.Bootstrap.OS.Windows.Raw; +using Microsoft.Win32.SafeHandles; +using System; + +namespace Dalamud.Bootstrap +{ + public sealed partial class GameProcess : IDisposable + { + private SafeProcessHandle m_handle; + + private GameProcess(SafeProcessHandle handle) + { + m_handle = handle; + } + + public static GameProcess Create(GameProcessCreationOptions options) + { + + } + + public static GameProcess Open(uint pid) + { + + var handle = OpenHandle(pid, TODO); + + return new GameProcess(handle); + } + + private static SafeProcessHandle OpenHandle(uint pid, PROCESS_ACCESS_RIGHTS access) + { + var handle = Kernel32.OpenProcess((uint)access, false, pid); + + if (handle.IsInvalid) + { + ProcessException.ThrowLastOsError(); + } + + return handle; + } + + public void Dispose() + { + m_handle?.Dispose(); + m_handle = null!; + } + } +} diff --git a/Dalamud.Bootstrap/OS/Windows/ProcessCreationOptions.cs b/Dalamud.Bootstrap/GameProcessCreationOptions.cs similarity index 56% rename from Dalamud.Bootstrap/OS/Windows/ProcessCreationOptions.cs rename to Dalamud.Bootstrap/GameProcessCreationOptions.cs index 8993dcb87..7cef746d7 100644 --- a/Dalamud.Bootstrap/OS/Windows/ProcessCreationOptions.cs +++ b/Dalamud.Bootstrap/GameProcessCreationOptions.cs @@ -1,14 +1,16 @@ +using System; using System.Collections.Generic; +using System.Text; -namespace Dalamud.Bootstrap.Windows +namespace Dalamud.Bootstrap { - internal sealed class ProcessCreationOptions + public sealed class GameProcessCreationOptions { public string ImagePath { get; set; } = null!; - public string? CommandLine { get; set; } = null; + public IList? Arguments { get; set; } - public IDictionary? Environments { get; set; } = null; + public IDictionary? Environments { get; set; } public bool CreateSuspended { get; set; } } diff --git a/Dalamud.Bootstrap/OS/Windows/Process.cs b/Dalamud.Bootstrap/OS/Windows/Process.cs deleted file mode 100644 index 533d67118..000000000 --- a/Dalamud.Bootstrap/OS/Windows/Process.cs +++ /dev/null @@ -1,233 +0,0 @@ -using Dalamud.Bootstrap.OS.Windows.Raw; -using Dalamud.Bootstrap.Windows; -using Microsoft.Win32.SafeHandles; -using System; -using System.Runtime.InteropServices; -using System.Text; - -namespace Dalamud.Bootstrap.OS -{ - /// - /// A class that provides a wrapper over operations on Win32 process. - /// - internal sealed class Process : IDisposable - { - private SafeProcessHandle m_handle; - - /// - /// Creates a process object that can be used to manipulate process's internal state. - /// - /// A process handle. Note that this functinon will take the ownership of the handle. - public Process(SafeProcessHandle handle) - { - m_handle = handle; - } - - public void Dispose() - { - m_handle?.Dispose(); - m_handle = null!; - } - - public static Process Create(ProcessCreationOptions options) - { - - } - - public static Process Open(uint pid, PROCESS_ACCESS_RIGHTS access) - { - var handle = OpenHandle(pid, access); - - return new Process(handle); - } - - private static SafeProcessHandle OpenHandle(uint pid, PROCESS_ACCESS_RIGHTS access) - { - var handle = Kernel32.OpenProcess((uint)access, false, pid); - - if (handle.IsInvalid) - { - ProcessException.ThrowLastOsError($"Could not open process {pid}"); - } - - return handle; - } - - private static uint GetProcessId(SafeProcessHandle handle) => Kernel32.GetProcessId(handle); - - public uint GetProcessId() => GetProcessId(m_handle); - - public void Terminate(int exitCode = 0) - { - if (!Kernel32.TerminateProcess(m_handle, exitCode)) - { - ProcessException.ThrowLastOsError($"Could not terminate process {GetProcessId()}"); - } - } - - /// - /// Reads the process memory. - /// - /// - /// The number of bytes that is actually read. - /// - public int ReadMemory(IntPtr address, Span destination) - { - unsafe - { - fixed (byte* pDest = destination) - { - if (!Kernel32.ReadProcessMemory(m_handle, address, pDest, (IntPtr)destination.Length, out var bytesRead)) - { - ProcessException.ThrowLastOsError($"Could not read process {GetProcessId()} memory at 0x{address.ToInt64():X8}"); - } - - // This is okay because destination will never be longer than int.Max - return bytesRead.ToInt32(); - } - } - } - - public void ReadMemoryExact(IntPtr address, Span destination) - { - var totalBytesRead = 0; - - while (totalBytesRead < destination.Length) - { - var bytesRead = ReadMemory(address + totalBytesRead, destination[totalBytesRead..]); - - if (bytesRead == 0) - { - // prolly page fault; there's not much we can do here - var readBeginAddr = address.ToInt64() + totalBytesRead; - var readEndAddr = address.ToInt64() + destination.Length; - - ProcessException.ThrowLastOsError($"Could not read process {GetProcessId()} memory at 0x{readBeginAddr:X8} .. 0x{readEndAddr:X8}; This likely means that page fault was hit."); - } - - totalBytesRead += bytesRead; - } - } - - private byte[] ReadMemoryExact(IntPtr address, int length) - { - var buffer = new byte[length]; - ReadMemoryExact(address, buffer); - - return buffer; - } - - private T ReadMemoryValue(IntPtr address) where T : unmanaged - { - unsafe - { - // This assumes that size of T is small enough to be safely allocated on the stack. - Span buffer = stackalloc byte[sizeof(T)]; - ReadMemoryExact(address, buffer); - - // this is still far better than allocating the temporary buffer on the heap when sizeof(T) is small enough. - return MemoryMarshal.Read(buffer); - } - } - - private IntPtr ReadPebAddress() - { - unsafe - { - var info = new PROCESS_BASIC_INFORMATION(); - var status = Ntdll.NtQueryInformationProcess(m_handle, PROCESSINFOCLASS.ProcessBasicInformation, &info, sizeof(PROCESS_BASIC_INFORMATION), (IntPtr*)IntPtr.Zero); - - if (!status.Success) - { - throw new ProcessException($"Could not query information on process {GetProcessId()} (Status: {status})"); - } - - return info.PebBaseAddress; - } - } - - /// - /// Reads command-line arguments from the process. - /// - public string[] GetProcessArguments() - { - unsafe - { - // Find where the command line is allocated - var pebAddr = ReadPebAddress(); - var peb = ReadMemoryValue(pebAddr); - var procParam = ReadMemoryValue(peb.ProcessParameters); - - // Read the command line (which is utf16-like string) - var commandLine = ReadMemoryExact(procParam.CommandLine.Buffer, procParam.CommandLine.Length); - - return ParseCommandLineToArguments(commandLine); - } - } - - /// - /// Returns a time when the process was started. - /// - public DateTime GetCreationTime() - { - if (!Kernel32.GetProcessTimes(m_handle, out var creationTime, out var _, out var _, out var _)) - { - ProcessException.ThrowLastOsError($"Could not read process creation time from process {GetProcessId()}"); - } - - return creationTime.ToDateTime(); - } - - private string[] ParseCommandLineToArguments(ReadOnlySpan commandLine) - { - unsafe - { - char** argv; - int argc; - - fixed (byte* pCommandLine = commandLine) - { - argv = Shell32.CommandLineToArgvW(pCommandLine, out argc); - } - - if (argv == null) - { - ProcessException.ThrowLastOsError($"Could not parse a command-line."); - } - - try - { - var arguments = new string[argc]; - - for (var i = 0; i < argc; i++) - { - arguments[i] = new string(argv[i]); - } - - return arguments; - } - finally - { - Kernel32.LocalFree(argv); - } - } - } - - public string GetImageFilePath() - { - var buffer = new StringBuilder(300); - - // From docs: - // On input, specifies the size of the lpExeName buffer, in characters. - // On success, receives the number of characters written to the buffer, not including the null-terminating character. - var size = buffer.Capacity; - - if (!Kernel32.QueryFullProcessImageNameW(m_handle, 0, buffer, ref size)) - { - ProcessException.ThrowLastOsError($"Could not read image path from process {GetProcessId()}"); - } - - return buffer.ToString(); - } - } -} diff --git a/Dalamud.Bootstrap/OS/Windows/ProcessException.cs b/Dalamud.Bootstrap/ProcessException.cs similarity index 51% rename from Dalamud.Bootstrap/OS/Windows/ProcessException.cs rename to Dalamud.Bootstrap/ProcessException.cs index 87db59560..67ccf0a09 100644 --- a/Dalamud.Bootstrap/OS/Windows/ProcessException.cs +++ b/Dalamud.Bootstrap/ProcessException.cs @@ -1,12 +1,11 @@ using System; +using System.Collections.Generic; using System.ComponentModel; +using System.Text; -namespace Dalamud.Bootstrap.Windows +namespace Dalamud.Bootstrap { - /// - /// An exception that is thrown when there was an error while interacting with the process. - /// - internal sealed class ProcessException : Exception + public class ProcessException : BootstrapException { internal ProcessException() : base() { } @@ -14,10 +13,11 @@ namespace Dalamud.Bootstrap.Windows internal ProcessException(string message, Exception innerException) : base(message, innerException) { } - internal static void ThrowLastOsError(string message) + internal static void ThrowLastOsError() { var inner = new Win32Exception(); - throw new ProcessException(message, inner); + + throw new ProcessException(inner.Message, inner); } } } diff --git a/Dalamud.Bootstrap/SqexArg/ArgumentBuilder.cs b/Dalamud.Bootstrap/SqexArg/ArgumentBuilder.cs index edc84c0b5..443de8032 100644 --- a/Dalamud.Bootstrap/SqexArg/ArgumentBuilder.cs +++ b/Dalamud.Bootstrap/SqexArg/ArgumentBuilder.cs @@ -4,7 +4,7 @@ using System.Text; namespace Dalamud.Bootstrap.SqexArg { - internal sealed class ArgumentBuilder + public sealed class ArgumentBuilder { private readonly Dictionary m_dict;