From 4d7b3bca9ce336fd9ee669bf5bc5d4f7a47965ad Mon Sep 17 00:00:00 2001 From: kizer Date: Sat, 23 Apr 2022 23:46:58 +0900 Subject: [PATCH] Turn Dalamud Injector into a console app (#811) --- .../Dalamud.Injector.Boot.vcxproj | 2 +- Dalamud.Injector.Boot/main.cpp | 21 +- Dalamud.Injector/Dalamud.Injector.csproj | 1 + Dalamud.Injector/EntryPoint.cs | 641 ++++++++++++++---- Dalamud.Injector/NativeAclFix.cs | 544 +++++++++++++++ Dalamud.Injector/NativeFunctions.cs | 77 +++ 6 files changed, 1125 insertions(+), 161 deletions(-) create mode 100644 Dalamud.Injector/NativeAclFix.cs diff --git a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj index 66b55feb2..93dfa1cfa 100644 --- a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj +++ b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj @@ -45,7 +45,7 @@ CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) - Windows + Console true false ..\lib\CoreCLR;%(AdditionalLibraryDirectories) diff --git a/Dalamud.Injector.Boot/main.cpp b/Dalamud.Injector.Boot/main.cpp index d3deb051b..f3f6980e6 100644 --- a/Dalamud.Injector.Boot/main.cpp +++ b/Dalamud.Injector.Boot/main.cpp @@ -6,16 +6,8 @@ #include "..\lib\CoreCLR\CoreCLR.h" #include "..\lib\CoreCLR\boot.h" -int wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nShowCmd) +int wmain(int argc, wchar_t** argv) { - #ifndef NDEBUG - ConsoleSetup(L"Dalamud.Injector"); - #endif - - int argc = 0; - LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc); - //ShowWindow(GetConsoleWindow(), SW_HIDE); - printf("Dalamud.Injector, (c) 2021 XIVLauncher Contributors\nBuilt at: %s@%s\n\n", __DATE__, __TIME__); wchar_t _module_path[MAX_PATH]; @@ -42,18 +34,9 @@ int wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LP typedef void (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(int, wchar_t**); custom_component_entry_point_fn entrypoint_fn = reinterpret_cast(entrypoint_vfn); - printf("Running Dalamud Injector... "); + printf("Running Dalamud Injector...\n"); entrypoint_fn(argc, argv); printf("Done!\n"); - // =========================================================================== // - - #ifndef NDEBUG - fclose(stdin); - fclose(stdout); - fclose(stderr); - FreeConsole(); - #endif - return 0; } diff --git a/Dalamud.Injector/Dalamud.Injector.csproj b/Dalamud.Injector/Dalamud.Injector.csproj index dbe4c94da..8da174533 100644 --- a/Dalamud.Injector/Dalamud.Injector.csproj +++ b/Dalamud.Injector/Dalamud.Injector.csproj @@ -67,6 +67,7 @@ + all diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index 2462176a8..9c2945284 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; @@ -37,45 +38,78 @@ namespace Dalamud.Injector /// byte** string arguments. public static void Main(int argc, IntPtr argvPtr) { - InitUnhandledException(); - InitLogging(); - - var args = new string[argc]; + Init(); + List args = new(argc); unsafe { var argv = (IntPtr*)argvPtr; for (var i = 0; i < argc; i++) + args.Add(Marshal.PtrToStringUni(argv[i])); + } + + if (args[1].ToLowerInvariant() == "launch-test") + { + Environment.Exit(ProcessLaunchTestCommand(args)); + return; + } + + DalamudStartInfo startInfo = null; + if (args.Count == 1) + { + // No command defaults to inject + args.Add("inject"); + args.Add("--all"); + args.Add("--warn"); + } + else if (int.TryParse(args[1], out var _)) + { + // Assume that PID has been passed. + args.Insert(1, "inject"); + + // If originally second parameter exists, then assume that it's a base64 encoded start info. + // Dalamud.Injector.exe inject [pid] [base64] + if (args.Count == 4) { - args[i] = Marshal.PtrToStringUni(argv[i]); + startInfo = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(args[2]))); + args.RemoveAt(3); } } + startInfo = ExtractAndInitializeStartInfoFromArguments(startInfo, args); + + var mainCommand = args[1].ToLowerInvariant(); + if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "inject"[..mainCommand.Length] == mainCommand) + { + Environment.Exit(ProcessInjectCommand(args, startInfo)); + } + else if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "launch"[..mainCommand.Length] == mainCommand) + { + Environment.Exit(ProcessLaunchCommand(args, startInfo)); + } + else if (mainCommand.Length > 0 && mainCommand.Length <= 4 && "help"[..mainCommand.Length] == mainCommand) + { + Environment.Exit(ProcessHelpCommand(args, args.Count >= 3 ? args[2] : null)); + } + else + { + Console.WriteLine("Invalid command: {0}", mainCommand); + ProcessHelpCommand(args); + Environment.Exit(-1); + } + } + + private static void Init() + { + InitUnhandledException(); + InitLogging(); + var cwd = new FileInfo(Assembly.GetExecutingAssembly().Location).Directory; if (cwd.FullName != Directory.GetCurrentDirectory()) { Log.Debug($"Changing cwd to {cwd}"); Directory.SetCurrentDirectory(cwd.FullName); } - - var process = GetProcess(args.ElementAtOrDefault(1)); - if (process == null) - { - Log.Error("Could not open process"); - return; - } - - var startInfo = GetStartInfo(args.ElementAtOrDefault(2), process); - - // TODO: XL does not set this!!! we need to keep this line here for now, otherwise we crash in the Dalamud entrypoint - startInfo.WorkingDirectory = Directory.GetCurrentDirectory(); - - // This seems to help with the STATUS_INTERNAL_ERROR condition - Thread.Sleep(1000); - - Inject(process, startInfo); - - Thread.Sleep(1000); } private static void InitUnhandledException() @@ -138,6 +172,7 @@ namespace Dalamud.Injector CullLogFile(logPath, 1 * 1024 * 1024); Log.Logger = new LoggerConfiguration() + .WriteTo.Console(standardErrorFromLevel: LogEventLevel.Verbose) .WriteTo.Async(a => a.File(logPath)) .MinimumLevel.ControlledBy(levelSwitch) .CreateLogger(); @@ -189,156 +224,412 @@ namespace Dalamud.Injector } } - private static Process GetProcessFromExecuting(string gamePath) + private static DalamudStartInfo ExtractAndInitializeStartInfoFromArguments(DalamudStartInfo? startInfo, List args) { - var gameVersion = File.ReadAllText(Path.Combine(Directory.GetParent(gamePath).FullName, "ffxivgame.ver")); - var sqpackPath = Path.Combine(Directory.GetParent(gamePath).FullName, "sqpack"); - var maxEntitledExpansionId = 0; - while (File.Exists(Path.Combine(sqpackPath, $"ex{maxEntitledExpansionId + 1}", $"ex{maxEntitledExpansionId + 1}.ver"))) - maxEntitledExpansionId++; + if (startInfo == null) + startInfo = new(); - var process = Process.Start(gamePath, new string[] + var workingDirectory = startInfo.WorkingDirectory; + var configurationPath = startInfo.ConfigurationPath; + var pluginDirectory = startInfo.PluginDirectory; + var defaultPluginDirectory = startInfo.DefaultPluginDirectory; + var assetDirectory = startInfo.AssetDirectory; + var delayInitializeMs = startInfo.DelayInitializeMs; + + for (var i = 2; i < args.Count; i++) { - "DEV.TestSID=0", - "DEV.UseSqPack=1", - "DEV.DataPathType=1", - "DEV.LobbyHost01=127.0.0.1", - "DEV.LobbyPort01=54994", - "DEV.LobbyHost02=127.0.0.2", - "DEV.LobbyPort02=54994", - "DEV.LobbyHost03=127.0.0.3", - "DEV.LobbyPort03=54994", - "DEV.LobbyHost04=127.0.0.4", - "DEV.LobbyPort04=54994", - "DEV.LobbyHost05=127.0.0.5", - "DEV.LobbyPort05=54994", - "DEV.LobbyHost06=127.0.0.6", - "DEV.LobbyPort06=54994", - "DEV.LobbyHost07=127.0.0.7", - "DEV.LobbyPort07=54994", - "DEV.LobbyHost08=127.0.0.8", - "DEV.LobbyPort08=54994", - "DEV.LobbyHost09=127.0.0.9", - "DEV.LobbyPort09=54994", - "SYS.Region=0", - "language=1", - $"ver={gameVersion}", - $"DEV.MaxEntitledExpansionID={maxEntitledExpansionId}", - "DEV.GMServerHost=127.0.0.100", - "DEV.GameQuitMessageBox=0", - }); + string key; + if (args[i].StartsWith(key = "--dalamud-working-directory=")) + workingDirectory = args[i][key.Length..]; + else if (args[i].StartsWith(key = "--dalamud-configuration-path=")) + configurationPath = args[i][key.Length..]; + else if (args[i].StartsWith(key = "--dalamud-plugin-directory=")) + pluginDirectory = args[i][key.Length..]; + else if (args[i].StartsWith(key = "--dalamud-dev-plugin-directory=")) + defaultPluginDirectory = args[i][key.Length..]; + else if (args[i].StartsWith(key = "--dalamud-asset-directory=")) + assetDirectory = args[i][key.Length..]; + else if (args[i].StartsWith(key = "--dalamud-delay-initialize=")) + delayInitializeMs = int.Parse(args[i][key.Length..]); + else + continue; - Thread.Sleep(1000); - - return process; - } - - private static Process? GetProcess(string? arg) - { - Process process = null; - - var pid = -1; - if (arg != default) - { - try - { - pid = int.Parse(arg); - } - catch (FormatException) - { - if (File.Exists(arg)) - return GetProcessFromExecuting(arg); - } + args.RemoveAt(i); + i--; } - switch (pid) + var appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var xivlauncherDir = Path.Combine(appDataDir, "XIVLauncher"); + + workingDirectory ??= Directory.GetCurrentDirectory(); + configurationPath ??= Path.Combine(xivlauncherDir, "dalamudConfig.json"); + pluginDirectory ??= Path.Combine(xivlauncherDir, "installedPlugins"); + defaultPluginDirectory ??= Path.Combine(xivlauncherDir, "devPlugins"); + assetDirectory ??= Path.Combine(xivlauncherDir, "dalamudAssets", "dev"); + + return new() { - case -1: - process = Process.GetProcessesByName("ffxiv_dx11").FirstOrDefault(); + WorkingDirectory = workingDirectory, + ConfigurationPath = configurationPath, + PluginDirectory = pluginDirectory, + DefaultPluginDirectory = defaultPluginDirectory, + AssetDirectory = assetDirectory, + Language = ClientLanguage.English, + GameVersion = null, + DelayInitializeMs = delayInitializeMs, + }; + } - if (process == default) - { - throw new Exception("Could not find process"); - } + private static int ProcessHelpCommand(List args, string? particularCommand = default) + { + var exeName = Path.GetFileName(args[0]); - #if !DEBUG - var result = MessageBoxW(IntPtr.Zero, $"Take care: you are manually injecting Dalamud into FFXIV({process.Id}).\n\nIf you are doing this to use plugins before they are officially whitelisted on patch days, things may go wrong and you may get into trouble.\nWe discourage you from doing this and you won't be warned again in-game.", "Dalamud", MessageBoxType.IconWarning | MessageBoxType.OkCancel); + var exeSpaces = string.Empty; + for (var i = exeName.Length; i > 0; i--) + exeSpaces += " "; - // IDCANCEL - if (result == 2) - { - Log.Information("User cancelled injection"); - return null; - } - #endif + if (particularCommand is null or "help") + Console.WriteLine("{0} help [command]", exeName); - break; + if (particularCommand is null or "inject") + Console.WriteLine("{0} inject [-h/--help] [-a/--all] [--warn] [pid1] [pid2] [pid3] ...", exeName); - case -2: - return GetProcessFromExecuting("C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn\\game\\ffxiv_dx11.exe"); + if (particularCommand is null or "launch") + { + Console.WriteLine("{0} launch [-h/--help] [-f/--fake-arguments]", exeName); + Console.WriteLine("{0} [-g path/to/ffxiv_dx11.exe] [--game=path/to/ffxiv_dx11.exe]", exeSpaces); + Console.WriteLine("{0} [-m entrypoint|inject] [--mode=entrypoint|inject]", exeSpaces); + Console.WriteLine("{0} [--handle-owner=inherited-handle-value]", exeSpaces); + Console.WriteLine("{0} [-- game_arg1=value1 game_arg2=value2 ...]", exeSpaces); + } - default: + Console.WriteLine("Specifying dalamud start info: [--dalamud-working-directory path] [--dalamud-configuration-path path]"); + Console.WriteLine(" [--dalamud-plugin-directory path] [--dalamud-dev-plugin-directory path]"); + Console.WriteLine(" [--dalamud-asset-directory path] [--dalamud-delay-initialize 0(ms)]"); + + return 0; + } + + private static int ProcessInjectCommand(List args, DalamudStartInfo dalamudStartInfo) + { + List processes = new(); + + var targetProcessSpecified = false; + var warnManualInjection = false; + var showHelp = args.Count <= 2; + + for (var i = 2; i < args.Count; i++) + { + if (int.TryParse(args[i], out int pid)) + { + targetProcessSpecified = true; try { - process = Process.GetProcessById(pid); + processes.Add(Process.GetProcessById(pid)); } catch (ArgumentException) { Log.Error("Could not find process with PID: {Pid}", pid); } - break; + continue; + } + + if (args[i] == "-h" || args[i] == "--help") + { + showHelp = true; + } + else if (args[i] == "-a" || args[i] == "--all") + { + targetProcessSpecified = true; + processes.AddRange(Process.GetProcessesByName("ffxiv_dx11")); + } + else if (args[i] == "--warn") + { + warnManualInjection = true; + } + else + { + Log.Error("\"{0}\" is not a valid argument.", args[i]); + return -1; + } } - return process; + if (showHelp) + { + ProcessHelpCommand(args, "inject"); + return args.Count <= 2 ? -1 : 0; + } + + if (!targetProcessSpecified) + { + Log.Error("No target process has been specified."); + return -1; + } + else if (!processes.Any()) + { + Log.Error("No suitable target process has been found."); + return -1; + } + + if (warnManualInjection) + { + var result = MessageBoxW(IntPtr.Zero, $"Take care: you are manually injecting Dalamud into FFXIV({string.Join(", ", processes.Select(x => $"{x.Id}"))}).\n\nIf you are doing this to use plugins before they are officially whitelisted on patch days, things may go wrong and you may get into trouble.\nWe discourage you from doing this and you won't be warned again in-game.", "Dalamud", MessageBoxType.IconWarning | MessageBoxType.OkCancel); + + // IDCANCEL + if (result == 2) + { + Log.Information("User cancelled injection"); + return -2; + } + } + + foreach (var process in processes) + Inject(process, AdjustStartInfo(dalamudStartInfo, process.MainModule.FileName)); + + return 0; } - private static DalamudStartInfo GetStartInfo(string? arg, Process process) + private static int ProcessLaunchCommand(List args, DalamudStartInfo dalamudStartInfo) { - DalamudStartInfo startInfo; + string? gamePath = null; + List gameArguments = new(); + string? mode = null; + var useFakeArguments = false; + var showHelp = args.Count <= 2; + var handleOwner = IntPtr.Zero; - if (arg != default) + var parsingGameArgument = false; + for (var i = 2; i < args.Count; i++) { - startInfo = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(arg))); + if (parsingGameArgument) + { + gameArguments.Add(args[i]); + continue; + } + + if (args[i] == "-h" || args[i] == "--help") + { + showHelp = true; + } + else if (args[i] == "-f" || args[i] == "--fake-arguments") + { + useFakeArguments = true; + } + else if (args[i] == "-g") + { + gamePath = args[++i]; + } + else if (args[i].StartsWith("--game=")) + { + gamePath = args[i].Split('=', 2)[1]; + } + else if (args[i] == "-m") + { + mode = args[++i]; + } + else if (args[i].StartsWith("--mode=")) + { + gamePath = args[i].Split('=', 2)[1]; + } + else if (args[i].StartsWith("--handle-owner=")) + { + handleOwner = IntPtr.Parse(args[i].Split('=', 2)[1]); + } + else if (args[i] == "--") + { + parsingGameArgument = true; + } + else + { + Log.Error("No such command found: {0}", args[i]); + return -1; + } + } + + if (showHelp) + { + ProcessHelpCommand(args, "launch"); + return args.Count <= 2 ? -1 : 0; + } + + mode = mode == null ? "entrypoint" : mode.ToLowerInvariant(); + if (mode.Length > 0 && mode.Length <= 10 && "entrypoint"[0..mode.Length] == mode) + { + mode = "entrypoint"; + } + else if (mode.Length > 0 && mode.Length <= 6 && "inject"[0..mode.Length] == mode) + { + mode = "inject"; } else { - var ffxivDir = Path.GetDirectoryName(process.MainModule.FileName); - var appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); - var xivlauncherDir = Path.Combine(appDataDir, "XIVLauncher"); - - var gameVerStr = File.ReadAllText(Path.Combine(ffxivDir, "ffxivgame.ver")); - var gameVer = GameVersion.Parse(gameVerStr); - - startInfo = new DalamudStartInfo - { - WorkingDirectory = null, - ConfigurationPath = Path.Combine(xivlauncherDir, "dalamudConfig.json"), - PluginDirectory = Path.Combine(xivlauncherDir, "installedPlugins"), - DefaultPluginDirectory = Path.Combine(xivlauncherDir, "devPlugins"), - AssetDirectory = Path.Combine(xivlauncherDir, "dalamudAssets", "dev"), - GameVersion = gameVer, - Language = ClientLanguage.English, - }; - - Log.Debug( - "Creating a new StartInfo with:\n" + - $" WorkingDirectory: {startInfo.WorkingDirectory}\n" + - $" ConfigurationPath: {startInfo.ConfigurationPath}\n" + - $" PluginDirectory: {startInfo.PluginDirectory}\n" + - $" DefaultPluginDirectory: {startInfo.DefaultPluginDirectory}\n" + - $" AssetDirectory: {startInfo.AssetDirectory}\n" + - $" GameVersion: {startInfo.GameVersion}\n" + - $" Language: {startInfo.Language}\n"); - - Log.Information("A Dalamud start info was not found in the program arguments. One has been generated for you."); - Log.Information("Copy the following contents into the program arguments:"); - - var startInfoJson = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(startInfo))); - Log.Information(startInfoJson); + Log.Error("Invalid mode: {0}", mode); + return -1; } - return startInfo; + if (gamePath == null) + { + try + { + var appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var xivlauncherDir = Path.Combine(appDataDir, "XIVLauncher"); + var launcherConfigPath = Path.Combine(xivlauncherDir, "launcherConfigV3.json"); + gamePath = Path.Combine(JsonSerializer.CreateDefault().Deserialize>(new JsonTextReader(new StringReader(File.ReadAllText(launcherConfigPath))))["GamePath"], "game", "ffxiv_dx11.exe"); + Log.Information("Using game installation path configuration from from XIVLauncher: {0}", gamePath); + } + catch (Exception) + { + gamePath = @"C:\Program Files (x86)\SquareEnix\FINAL FANTASY XIV - A Realm Reborn\game\ffxiv_dx11.exe"; + Log.Warning("Failed to read launcherConfigV3.json. Using default game installation path: {0}", gamePath); + } + + if (!File.Exists(gamePath)) + { + Log.Error("File not found: {0}", gamePath); + return -1; + } + } + + if (useFakeArguments) + { + var gameVersion = File.ReadAllText(Path.Combine(Directory.GetParent(gamePath).FullName, "ffxivgame.ver")); + var sqpackPath = Path.Combine(Directory.GetParent(gamePath).FullName, "sqpack"); + var maxEntitledExpansionId = 0; + while (File.Exists(Path.Combine(sqpackPath, $"ex{maxEntitledExpansionId + 1}", $"ex{maxEntitledExpansionId + 1}.ver"))) + maxEntitledExpansionId++; + + gameArguments.InsertRange(0, new string[] + { + "DEV.TestSID=0", + "DEV.UseSqPack=1", + "DEV.DataPathType=1", + "DEV.LobbyHost01=127.0.0.1", + "DEV.LobbyPort01=54994", + "DEV.LobbyHost02=127.0.0.2", + "DEV.LobbyPort02=54994", + "DEV.LobbyHost03=127.0.0.3", + "DEV.LobbyPort03=54994", + "DEV.LobbyHost04=127.0.0.4", + "DEV.LobbyPort04=54994", + "DEV.LobbyHost05=127.0.0.5", + "DEV.LobbyPort05=54994", + "DEV.LobbyHost06=127.0.0.6", + "DEV.LobbyPort06=54994", + "DEV.LobbyHost07=127.0.0.7", + "DEV.LobbyPort07=54994", + "DEV.LobbyHost08=127.0.0.8", + "DEV.LobbyPort08=54994", + "DEV.LobbyHost09=127.0.0.9", + "DEV.LobbyPort09=54994", + "SYS.Region=0", + "language=1", + $"ver={gameVersion}", + $"DEV.MaxEntitledExpansionID={maxEntitledExpansionId}", + "DEV.GMServerHost=127.0.0.100", + "DEV.GameQuitMessageBox=0", + }); + } + + var gameArgumentString = string.Join(" ", gameArguments.Select(x => EncodeParameterArgument(x))); + var process = NativeAclFix.LaunchGame(Path.GetDirectoryName(gamePath), gamePath, gameArgumentString, (Process p) => + { + if (mode == "entrypoint") + { + var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath); + Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo)); + if (RewriteRemoteEntryPointW(p.Handle, gamePath, JsonConvert.SerializeObject(startInfo)) != 0) + { + Log.Error("[HOOKS] RewriteRemoteEntryPointW failed"); + throw new Exception("RewriteRemoteEntryPointW failed"); + } + } + }); + + if (mode == "inject") + { + var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath); + Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo)); + Inject(process, startInfo); + } + + var processHandleForOwner = IntPtr.Zero; + if (handleOwner != IntPtr.Zero) + { + if (!DuplicateHandle(Process.GetCurrentProcess().Handle, process.Handle, handleOwner, out processHandleForOwner, 0, false, DuplicateOptions.SameAccess)) + Log.Warning("Failed to call DuplicateHandle: Win32 error code {0}", Marshal.GetLastWin32Error()); + } + + Console.WriteLine($"{{\"pid\": {process.Id}, \"handle\": {processHandleForOwner}}}"); + + return 0; + } + + private static Process GetInheritableCurrentProcessHandle() + { + if (!DuplicateHandle(Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, out var inheritableCurrentProcessHandle, 0, true, DuplicateOptions.SameAccess)) + { + Log.Error("Failed to call DuplicateHandle: Win32 error code {0}", Marshal.GetLastWin32Error()); + return null; + } + + return new NativeAclFix.ExistingProcess(inheritableCurrentProcessHandle); + } + + private static int ProcessLaunchTestCommand(List args) + { + Console.WriteLine("Testing launch command."); + args[0] = Process.GetCurrentProcess().MainModule.FileName; + args[1] = "launch"; + + var inheritableCurrentProcess = GetInheritableCurrentProcessHandle(); // so that it closes the handle when it's done + args.Insert(2, $"--handle-owner={inheritableCurrentProcess.Handle}"); + + for (var i = 0; i < args.Count; i++) + Console.WriteLine("Argument {0}: {1}", i, args[i]); + + Process helperProcess = new(); + helperProcess.StartInfo.FileName = args[0]; + for (var i = 1; i < args.Count; i++) + helperProcess.StartInfo.ArgumentList.Add(args[i]); + helperProcess.StartInfo.RedirectStandardOutput = true; + helperProcess.StartInfo.RedirectStandardError = true; + helperProcess.StartInfo.UseShellExecute = false; + helperProcess.ErrorDataReceived += new DataReceivedEventHandler((sendingProcess, errLine) => Console.WriteLine($"stderr: \"{errLine.Data}\"")); + helperProcess.Start(); + helperProcess.BeginErrorReadLine(); + helperProcess.WaitForExit(); + if (helperProcess.ExitCode != 0) + return -1; + + var result = JsonSerializer.CreateDefault().Deserialize>(new JsonTextReader(helperProcess.StandardOutput)); + var pid = result["pid"]; + var handle = (IntPtr)result["handle"]; + var resultProcess = new NativeAclFix.ExistingProcess(handle); + Console.WriteLine("PID: {0}, Handle: {1}", pid, handle); + Console.WriteLine("Press Enter to force quit"); + Console.ReadLine(); + resultProcess.Kill(); + return 0; + } + + private static DalamudStartInfo AdjustStartInfo(DalamudStartInfo startInfo, string gamePath) + { + var ffxivDir = Path.GetDirectoryName(gamePath); + var gameVerStr = File.ReadAllText(Path.Combine(ffxivDir, "ffxivgame.ver")); + var gameVer = GameVersion.Parse(gameVerStr); + + return new() + { + WorkingDirectory = startInfo.WorkingDirectory, + ConfigurationPath = startInfo.ConfigurationPath, + PluginDirectory = startInfo.PluginDirectory, + DefaultPluginDirectory = startInfo.DefaultPluginDirectory, + AssetDirectory = startInfo.AssetDirectory, + Language = ClientLanguage.English, + GameVersion = gameVer, + DelayInitializeMs = startInfo.DelayInitializeMs, + }; } private static void Inject(Process process, DalamudStartInfo startInfo) @@ -380,5 +671,73 @@ namespace Dalamud.Injector Log.Information("Done"); } + + [DllImport("Dalamud.Boot.dll")] + private static extern int RewriteRemoteEntryPointW(IntPtr hProcess, [MarshalAs(UnmanagedType.LPWStr)] string gamePath, [MarshalAs(UnmanagedType.LPWStr)] string loadInfoJson); + + /// + /// This routine appends the given argument to a command line such that + /// CommandLineToArgvW will return the argument string unchanged. Arguments + /// in a command line should be separated by spaces; this function does + /// not add these spaces. + /// + /// Taken from https://stackoverflow.com/questions/5510343/escape-command-line-arguments-in-c-sharp + /// and https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/. + /// + /// Supplies the argument to encode. + /// + /// Supplies an indication of whether we should quote the argument even if it + /// does not contain any characters that would ordinarily require quoting. + /// + private static string EncodeParameterArgument(string argument, bool force = false) + { + if (argument == null) throw new ArgumentNullException(nameof(argument)); + + // Unless we're told otherwise, don't quote unless we actually + // need to do so --- hopefully avoid problems if programs won't + // parse quotes properly + if (force == false + && argument.Length > 0 + && argument.IndexOfAny(" \t\n\v\"".ToCharArray()) == -1) + { + return argument; + } + + var quoted = new StringBuilder(); + quoted.Append('"'); + + var numberBackslashes = 0; + + foreach (var chr in argument) + { + switch (chr) + { + case '\\': + numberBackslashes++; + continue; + case '"': + // Escape all backslashes and the following + // double quotation mark. + quoted.Append('\\', (numberBackslashes * 2) + 1); + quoted.Append(chr); + break; + default: + // Backslashes aren't special here. + quoted.Append('\\', numberBackslashes); + quoted.Append(chr); + break; + } + + numberBackslashes = 0; + } + + // Escape all backslashes, but let the terminating + // double quotation mark we add below be interpreted + // as a metacharacter. + quoted.Append('\\', numberBackslashes * 2); + quoted.Append('"'); + + return quoted.ToString(); + } } } diff --git a/Dalamud.Injector/NativeAclFix.cs b/Dalamud.Injector/NativeAclFix.cs new file mode 100644 index 000000000..ec93b6d63 --- /dev/null +++ b/Dalamud.Injector/NativeAclFix.cs @@ -0,0 +1,544 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Linq; +using System.Diagnostics; +using System.Reflection; +using Microsoft.Win32.SafeHandles; +using System.Threading; +using Serilog; + +// ReSharper disable InconsistentNaming + +namespace Dalamud.Injector +{ + public static class NativeAclFix + { + // Definitions taken from PInvoke.net (with some changes) + private static class PInvoke + { + #region Constants + public const UInt32 STANDARD_RIGHTS_ALL = 0x001F0000; + public const UInt32 SPECIFIC_RIGHTS_ALL = 0x0000FFFF; + public const UInt32 PROCESS_VM_WRITE = 0x0020; + + public const UInt32 GRANT_ACCESS = 1; + + public const UInt32 SECURITY_DESCRIPTOR_REVISION = 1; + + public const UInt32 CREATE_SUSPENDED = 0x00000004; + + public const UInt32 TOKEN_QUERY = 0x0008; + public const UInt32 TOKEN_ADJUST_PRIVILEGES = 0x0020; + + public const UInt32 PRIVILEGE_SET_ALL_NECESSARY = 1; + + public const UInt32 SE_PRIVILEGE_ENABLED = 0x00000002; + public const UInt32 SE_PRIVILEGE_REMOVED = 0x00000004; + + + public enum MULTIPLE_TRUSTEE_OPERATION + { + NO_MULTIPLE_TRUSTEE, + TRUSTEE_IS_IMPERSONATE + } + + public enum TRUSTEE_FORM + { + TRUSTEE_IS_SID, + TRUSTEE_IS_NAME, + TRUSTEE_BAD_FORM, + TRUSTEE_IS_OBJECTS_AND_SID, + TRUSTEE_IS_OBJECTS_AND_NAME + } + + public enum TRUSTEE_TYPE + { + TRUSTEE_IS_UNKNOWN, + TRUSTEE_IS_USER, + TRUSTEE_IS_GROUP, + TRUSTEE_IS_DOMAIN, + TRUSTEE_IS_ALIAS, + TRUSTEE_IS_WELL_KNOWN_GROUP, + TRUSTEE_IS_DELETED, + TRUSTEE_IS_INVALID, + TRUSTEE_IS_COMPUTER + } + + public enum SE_OBJECT_TYPE + { + SE_UNKNOWN_OBJECT_TYPE, + SE_FILE_OBJECT, + SE_SERVICE, + SE_PRINTER, + SE_REGISTRY_KEY, + SE_LMSHARE, + SE_KERNEL_OBJECT, + SE_WINDOW_OBJECT, + SE_DS_OBJECT, + SE_DS_OBJECT_ALL, + SE_PROVIDER_DEFINED_OBJECT, + SE_WMIGUID_OBJECT, + SE_REGISTRY_WOW64_32KEY + } + public enum SECURITY_INFORMATION + { + OWNER_SECURITY_INFORMATION = 1, + GROUP_SECURITY_INFORMATION = 2, + DACL_SECURITY_INFORMATION = 4, + SACL_SECURITY_INFORMATION = 8, + UNPROTECTED_SACL_SECURITY_INFORMATION = 0x10000000, + UNPROTECTED_DACL_SECURITY_INFORMATION = 0x20000000, + PROTECTED_SACL_SECURITY_INFORMATION = 0x40000000 + } + #endregion + + + #region Structures + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 0)] + public struct TRUSTEE : IDisposable + { + public IntPtr pMultipleTrustee; + public MULTIPLE_TRUSTEE_OPERATION MultipleTrusteeOperation; + public TRUSTEE_FORM TrusteeForm; + public TRUSTEE_TYPE TrusteeType; + private IntPtr ptstrName; + + void IDisposable.Dispose() + { + if (ptstrName != IntPtr.Zero) Marshal.Release(ptstrName); + } + + public string Name { get { return Marshal.PtrToStringAuto(ptstrName); } } + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 0)] + public struct EXPLICIT_ACCESS + { + uint grfAccessPermissions; + uint grfAccessMode; + uint grfInheritance; + TRUSTEE Trustee; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SECURITY_DESCRIPTOR + { + public byte Revision; + public byte Sbz1; + public UInt16 Control; + public IntPtr Owner; + public IntPtr Group; + public IntPtr Sacl; + public IntPtr Dacl; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct STARTUPINFO + { + public Int32 cb; + public string lpReserved; + public string lpDesktop; + public string lpTitle; + public Int32 dwX; + public Int32 dwY; + public Int32 dwXSize; + public Int32 dwYSize; + public Int32 dwXCountChars; + public Int32 dwYCountChars; + public Int32 dwFillAttribute; + public Int32 dwFlags; + public Int16 wShowWindow; + public Int16 cbReserved2; + public IntPtr lpReserved2; + public IntPtr hStdInput; + public IntPtr hStdOutput; + public IntPtr hStdError; + } + + [StructLayout(LayoutKind.Sequential)] + public struct PROCESS_INFORMATION + { + public IntPtr hProcess; + public IntPtr hThread; + public int dwProcessId; + public UInt32 dwThreadId; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SECURITY_ATTRIBUTES + { + public int nLength; + public IntPtr lpSecurityDescriptor; + public bool bInheritHandle; + } + + [StructLayout(LayoutKind.Sequential)] + public struct LUID + { + public UInt32 LowPart; + public Int32 HighPart; + } + + [StructLayout(LayoutKind.Sequential)] + public struct PRIVILEGE_SET + { + public UInt32 PrivilegeCount; + public UInt32 Control; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] + public LUID_AND_ATTRIBUTES[] Privilege; + } + + public struct LUID_AND_ATTRIBUTES + { + public LUID Luid; + public UInt32 Attributes; + } + + [StructLayout(LayoutKind.Sequential)] + public struct TOKEN_PRIVILEGES + { + public UInt32 PrivilegeCount; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] + public LUID_AND_ATTRIBUTES[] Privileges; + } + #endregion + + + #region Methods + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern void BuildExplicitAccessWithName( + ref EXPLICIT_ACCESS pExplicitAccess, + string pTrusteeName, + uint AccessPermissions, + uint AccessMode, + uint Inheritance); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern int SetEntriesInAcl( + int cCountOfExplicitEntries, + ref EXPLICIT_ACCESS pListOfExplicitEntries, + IntPtr OldAcl, + out IntPtr NewAcl); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool InitializeSecurityDescriptor( + out SECURITY_DESCRIPTOR pSecurityDescriptor, + uint dwRevision); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool SetSecurityDescriptorDacl( + ref SECURITY_DESCRIPTOR pSecurityDescriptor, + bool bDaclPresent, + IntPtr pDacl, + bool bDaclDefaulted); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + public static extern bool CreateProcess( + string lpApplicationName, + string lpCommandLine, + ref SECURITY_ATTRIBUTES lpProcessAttributes, + IntPtr lpThreadAttributes, + bool bInheritHandles, + UInt32 dwCreationFlags, + IntPtr lpEnvironment, + string lpCurrentDirectory, + [In] ref STARTUPINFO lpStartupInfo, + out PROCESS_INFORMATION lpProcessInformation); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool CloseHandle(IntPtr hObject); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern uint ResumeThread(IntPtr hThread); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool OpenProcessToken( + IntPtr ProcessHandle, + UInt32 DesiredAccess, + out IntPtr TokenHandle); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, ref LUID lpLuid); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool PrivilegeCheck( + IntPtr ClientToken, + ref PRIVILEGE_SET RequiredPrivileges, + out bool pfResult); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool AdjustTokenPrivileges( + IntPtr TokenHandle, + bool DisableAllPrivileges, + ref TOKEN_PRIVILEGES NewState, + UInt32 BufferLengthInBytes, + IntPtr PreviousState, + UInt32 ReturnLengthInBytes); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern uint GetSecurityInfo( + IntPtr handle, + SE_OBJECT_TYPE ObjectType, + SECURITY_INFORMATION SecurityInfo, + IntPtr pSidOwner, + IntPtr pSidGroup, + out IntPtr pDacl, + IntPtr pSacl, + IntPtr pSecurityDescriptor); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern uint SetSecurityInfo( + IntPtr handle, + SE_OBJECT_TYPE ObjectType, + SECURITY_INFORMATION SecurityInfo, + IntPtr psidOwner, + IntPtr psidGroup, + IntPtr pDacl, + IntPtr pSacl); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr GetCurrentProcess(); + #endregion + } + + public class ExistingProcess : Process + { + public ExistingProcess(IntPtr handle) + { + SetHandle(handle); + } + + private void SetHandle(IntPtr handle) + { + var baseType = GetType().BaseType; + if (baseType == null) + return; + + var setProcessHandleMethod = baseType.GetMethod("SetProcessHandle", + BindingFlags.NonPublic | BindingFlags.Instance); + setProcessHandleMethod?.Invoke(this, new object[] {new SafeProcessHandle(handle, true)}); + } + } + + public class GameExitedException : Exception + { + public GameExitedException() + : base("Game exited prematurely.") + { + } + } + + public static Process LaunchGame(string workingDir, string exePath, string arguments, Action beforeResume) + { + Process process = null; + + var userName = Environment.UserName; + + var pExplicitAccess = new PInvoke.EXPLICIT_ACCESS(); + PInvoke.BuildExplicitAccessWithName( + ref pExplicitAccess, + userName, + PInvoke.STANDARD_RIGHTS_ALL | PInvoke.SPECIFIC_RIGHTS_ALL & ~PInvoke.PROCESS_VM_WRITE, + PInvoke.GRANT_ACCESS, + 0); + + if (PInvoke.SetEntriesInAcl(1, ref pExplicitAccess, IntPtr.Zero, out var newAcl) != 0) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + var secDesc = new PInvoke.SECURITY_DESCRIPTOR(); + if (!PInvoke.InitializeSecurityDescriptor(out secDesc, PInvoke.SECURITY_DESCRIPTOR_REVISION)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + if (!PInvoke.SetSecurityDescriptorDacl(ref secDesc, true, newAcl, false)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + var psecDesc = Marshal.AllocHGlobal(Marshal.SizeOf()); + Marshal.StructureToPtr(secDesc, psecDesc, true); + + var lpProcessInformation = new PInvoke.PROCESS_INFORMATION(); + try + { + var lpProcessAttributes = new PInvoke.SECURITY_ATTRIBUTES + { + nLength = Marshal.SizeOf(), + lpSecurityDescriptor = psecDesc, + bInheritHandle = false + }; + + var lpStartupInfo = new PInvoke.STARTUPINFO + { + cb = Marshal.SizeOf() + }; + + var compatLayerPrev = Environment.GetEnvironmentVariable("__COMPAT_LAYER"); + + Environment.SetEnvironmentVariable("__COMPAT_LAYER", "RunAsInvoker"); + try + { + if (!PInvoke.CreateProcess( + null, + $"\"{exePath}\" {arguments}", + ref lpProcessAttributes, + IntPtr.Zero, + false, + PInvoke.CREATE_SUSPENDED, + IntPtr.Zero, + workingDir, + ref lpStartupInfo, + out lpProcessInformation)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + finally + { + Environment.SetEnvironmentVariable("__COMPAT_LAYER", compatLayerPrev); + } + + DisableSeDebug(lpProcessInformation.hProcess); + + process = new ExistingProcess(lpProcessInformation.hProcess); + + beforeResume?.Invoke(process); + + PInvoke.ResumeThread(lpProcessInformation.hThread); + + // Ensure that the game main window is prepared + try + { + do + { + process.WaitForInputIdle(); + + Thread.Sleep(100); + } while (IntPtr.Zero == TryFindGameWindow(process)); + } + catch (InvalidOperationException) + { + throw new GameExitedException(); + } + + if (PInvoke.GetSecurityInfo( + PInvoke.GetCurrentProcess(), + PInvoke.SE_OBJECT_TYPE.SE_KERNEL_OBJECT, + PInvoke.SECURITY_INFORMATION.DACL_SECURITY_INFORMATION, + IntPtr.Zero, IntPtr.Zero, + out var pACL, + IntPtr.Zero, IntPtr.Zero) != 0) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + if (PInvoke.SetSecurityInfo( + lpProcessInformation.hProcess, + PInvoke.SE_OBJECT_TYPE.SE_KERNEL_OBJECT, + PInvoke.SECURITY_INFORMATION.DACL_SECURITY_INFORMATION | PInvoke.SECURITY_INFORMATION.UNPROTECTED_DACL_SECURITY_INFORMATION, + IntPtr.Zero, IntPtr.Zero, pACL, IntPtr.Zero) != 0) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + catch (Exception ex) + { + Log.Error(ex, "[NativeAclFix] Uncaught error during initialization, trying to kill process"); + + try + { + process?.Kill(); + } + catch (Exception killEx) + { + Log.Error(killEx, "[NativeAclFix] Could not kill process"); + } + + throw; + } + finally + { + Marshal.FreeHGlobal(psecDesc); + PInvoke.CloseHandle(lpProcessInformation.hThread); + } + + return process; + } + + private static void DisableSeDebug(IntPtr ProcessHandle) + { + if (!PInvoke.OpenProcessToken(ProcessHandle, PInvoke.TOKEN_QUERY | PInvoke.TOKEN_ADJUST_PRIVILEGES, out var TokenHandle)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + var luidDebugPrivilege = new PInvoke.LUID(); + if (!PInvoke.LookupPrivilegeValue(null, "SeDebugPrivilege", ref luidDebugPrivilege)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + var RequiredPrivileges = new PInvoke.PRIVILEGE_SET + { + PrivilegeCount = 1, + Control = PInvoke.PRIVILEGE_SET_ALL_NECESSARY, + Privilege = new PInvoke.LUID_AND_ATTRIBUTES[1] + }; + + RequiredPrivileges.Privilege[0].Luid = luidDebugPrivilege; + RequiredPrivileges.Privilege[0].Attributes = PInvoke.SE_PRIVILEGE_ENABLED; + + if (!PInvoke.PrivilegeCheck(TokenHandle, ref RequiredPrivileges, out bool bResult)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + if (bResult) // SeDebugPrivilege is enabled; try disabling it + { + var TokenPrivileges = new PInvoke.TOKEN_PRIVILEGES + { + PrivilegeCount = 1, + Privileges = new PInvoke.LUID_AND_ATTRIBUTES[1] + }; + + TokenPrivileges.Privileges[0].Luid = luidDebugPrivilege; + TokenPrivileges.Privileges[0].Attributes = PInvoke.SE_PRIVILEGE_REMOVED; + + if (!PInvoke.AdjustTokenPrivileges(TokenHandle, false, ref TokenPrivileges, 0, IntPtr.Zero, 0)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + + PInvoke.CloseHandle(TokenHandle); + } + + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr FindWindowEx(IntPtr parentHandle, IntPtr hWndChildAfter, string className, IntPtr windowTitle); + [DllImport("user32.dll", SetLastError = true)] + private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool IsWindowVisible(IntPtr hWnd); + + private static IntPtr TryFindGameWindow(Process process) + { + IntPtr hwnd = IntPtr.Zero; + while (IntPtr.Zero != (hwnd = FindWindowEx(IntPtr.Zero, hwnd, "FFXIVGAME", IntPtr.Zero))) + { + GetWindowThreadProcessId(hwnd, out uint pid); + + if (pid == process.Id && IsWindowVisible(hwnd)) + { + break; + } + } + return hwnd; + } + } +} diff --git a/Dalamud.Injector/NativeFunctions.cs b/Dalamud.Injector/NativeFunctions.cs index b9ed9f33d..f1749ae0a 100644 --- a/Dalamud.Injector/NativeFunctions.cs +++ b/Dalamud.Injector/NativeFunctions.cs @@ -364,6 +364,23 @@ namespace Dalamud.Injector StackSizeParamIsReservation = 0x10000, } + /// + /// DUPLICATE_* values for DuplicateHandle's dwDesiredAccess. + /// + [Flags] + public enum DuplicateOptions : uint + { + /// + /// Closes the source handle. This occurs regardless of any error status returned. + /// + CloseSource = 0x00000001, + + /// + /// Ignores the dwDesiredAccess parameter. The duplicate handle has the same access as the source handle. + /// + SameAccess = 0x00000002, + } + /// /// PAGE_* from memoryapi. /// @@ -833,5 +850,65 @@ namespace Dalamud.Injector byte[] lpBuffer, int dwSize, out IntPtr lpNumberOfBytesWritten); + + /// + /// Duplicates an object handle. + /// + /// + /// A handle to the process with the handle to be duplicated. + /// + /// The handle must have the PROCESS_DUP_HANDLE access right. + /// + /// + /// The handle to be duplicated. This is an open object handle that is valid in the context of the source process. + /// For a list of objects whose handles can be duplicated, see the following Remarks section. + /// + /// + /// A handle to the process that is to receive the duplicated handle. + /// + /// The handle must have the PROCESS_DUP_HANDLE access right. + /// + /// + /// A pointer to a variable that receives the duplicate handle. This handle value is valid in the context of the target process. + /// + /// If hSourceHandle is a pseudo handle returned by GetCurrentProcess or GetCurrentThread, DuplicateHandle converts it to a real handle to a process or thread, respectively. + /// + /// If lpTargetHandle is NULL, the function duplicates the handle, but does not return the duplicate handle value to the caller. This behavior exists only for backward compatibility with previous versions of this function. You should not use this feature, as you will lose system resources until the target process terminates. + /// + /// This parameter is ignored if hTargetProcessHandle is NULL. + /// + /// + /// The access requested for the new handle. For the flags that can be specified for each object type, see the following Remarks section. + /// + /// This parameter is ignored if the dwOptions parameter specifies the DUPLICATE_SAME_ACCESS flag. Otherwise, the flags that can be specified depend on the type of object whose handle is to be duplicated. + /// + /// This parameter is ignored if hTargetProcessHandle is NULL. + /// + /// + /// A variable that indicates whether the handle is inheritable. If TRUE, the duplicate handle can be inherited by new processes created by the target process. If FALSE, the new handle cannot be inherited. + /// + /// This parameter is ignored if hTargetProcessHandle is NULL. + /// + /// + /// Optional actions. + /// + /// + /// If the function succeeds, the return value is nonzero. + /// + /// If the function fails, the return value is zero. To get extended error information, call GetLastError. + /// + /// + /// See https://docs.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-duplicatehandle. + /// + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool DuplicateHandle( + IntPtr hSourceProcessHandle, + IntPtr hSourceHandle, + IntPtr hTargetProcessHandle, + out IntPtr lpTargetHandle, + uint dwDesiredAccess, + [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, + DuplicateOptions dwOptions); } }