Turn Dalamud Injector into a console app (#811)

This commit is contained in:
kizer 2022-04-23 23:46:58 +09:00 committed by GitHub
parent 8c6b599a07
commit 4d7b3bca9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 1125 additions and 161 deletions

View file

@ -45,7 +45,7 @@
<PreprocessorDefinitions>CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalLibraryDirectories>..\lib\CoreCLR;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>

View file

@ -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<custom_component_entry_point_fn>(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;
}

View file

@ -67,6 +67,7 @@
<PackageReference Include="Reloaded.Memory.Buffers" Version="1.3.5" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">
<PrivateAssets>all</PrivateAssets>

View file

@ -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
/// <param name="argvPtr">byte** string arguments.</param>
public static void Main(int argc, IntPtr argvPtr)
{
InitUnhandledException();
InitLogging();
var args = new string[argc];
Init();
List<string> 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")
{
args[i] = Marshal.PtrToStringUni(argv[i]);
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)
{
startInfo = JsonConvert.DeserializeObject<DalamudStartInfo>(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,7 +224,274 @@ namespace Dalamud.Injector
}
}
private static Process GetProcessFromExecuting(string gamePath)
private static DalamudStartInfo ExtractAndInitializeStartInfoFromArguments(DalamudStartInfo? startInfo, List<string> args)
{
if (startInfo == null)
startInfo = new();
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++)
{
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;
args.RemoveAt(i);
i--;
}
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()
{
WorkingDirectory = workingDirectory,
ConfigurationPath = configurationPath,
PluginDirectory = pluginDirectory,
DefaultPluginDirectory = defaultPluginDirectory,
AssetDirectory = assetDirectory,
Language = ClientLanguage.English,
GameVersion = null,
DelayInitializeMs = delayInitializeMs,
};
}
private static int ProcessHelpCommand(List<string> args, string? particularCommand = default)
{
var exeName = Path.GetFileName(args[0]);
var exeSpaces = string.Empty;
for (var i = exeName.Length; i > 0; i--)
exeSpaces += " ";
if (particularCommand is null or "help")
Console.WriteLine("{0} help [command]", exeName);
if (particularCommand is null or "inject")
Console.WriteLine("{0} inject [-h/--help] [-a/--all] [--warn] [pid1] [pid2] [pid3] ...", exeName);
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);
}
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<string> args, DalamudStartInfo dalamudStartInfo)
{
List<Process> 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
{
processes.Add(Process.GetProcessById(pid));
}
catch (ArgumentException)
{
Log.Error("Could not find process with PID: {Pid}", pid);
}
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;
}
}
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 int ProcessLaunchCommand(List<string> args, DalamudStartInfo dalamudStartInfo)
{
string? gamePath = null;
List<string> gameArguments = new();
string? mode = null;
var useFakeArguments = false;
var showHelp = args.Count <= 2;
var handleOwner = IntPtr.Zero;
var parsingGameArgument = false;
for (var i = 2; i < args.Count; i++)
{
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
{
Log.Error("Invalid mode: {0}", mode);
return -1;
}
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<Dictionary<string, string>>(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");
@ -197,7 +499,7 @@ namespace Dalamud.Injector
while (File.Exists(Path.Combine(sqpackPath, $"ex{maxEntitledExpansionId + 1}", $"ex{maxEntitledExpansionId + 1}.ver")))
maxEntitledExpansionId++;
var process = Process.Start(gamePath, new string[]
gameArguments.InsertRange(0, new string[]
{
"DEV.TestSID=0",
"DEV.UseSqPack=1",
@ -227,118 +529,107 @@ namespace Dalamud.Injector
"DEV.GMServerHost=127.0.0.100",
"DEV.GameQuitMessageBox=0",
});
Thread.Sleep(1000);
return process;
}
private static Process? GetProcess(string? arg)
var gameArgumentString = string.Join(" ", gameArguments.Select(x => EncodeParameterArgument(x)));
var process = NativeAclFix.LaunchGame(Path.GetDirectoryName(gamePath), gamePath, gameArgumentString, (Process p) =>
{
Process process = null;
var pid = -1;
if (arg != default)
if (mode == "entrypoint")
{
try
var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath);
Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo));
if (RewriteRemoteEntryPointW(p.Handle, gamePath, JsonConvert.SerializeObject(startInfo)) != 0)
{
pid = int.Parse(arg);
}
catch (FormatException)
{
if (File.Exists(arg))
return GetProcessFromExecuting(arg);
Log.Error("[HOOKS] RewriteRemoteEntryPointW failed");
throw new Exception("RewriteRemoteEntryPointW failed");
}
}
});
switch (pid)
if (mode == "inject")
{
case -1:
process = Process.GetProcessesByName("ffxiv_dx11").FirstOrDefault();
if (process == default)
{
throw new Exception("Could not find process");
var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath);
Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo));
Inject(process, startInfo);
}
#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);
// IDCANCEL
if (result == 2)
var processHandleForOwner = IntPtr.Zero;
if (handleOwner != IntPtr.Zero)
{
Log.Information("User cancelled injection");
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;
}
#endif
break;
case -2:
return GetProcessFromExecuting("C:\\Program Files (x86)\\SquareEnix\\FINAL FANTASY XIV - A Realm Reborn\\game\\ffxiv_dx11.exe");
default:
try
{
process = Process.GetProcessById(pid);
}
catch (ArgumentException)
{
Log.Error("Could not find process with PID: {Pid}", pid);
return new NativeAclFix.ExistingProcess(inheritableCurrentProcessHandle);
}
break;
private static int ProcessLaunchTestCommand(List<string> 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<Dictionary<string, int>>(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;
}
return process;
}
private static DalamudStartInfo GetStartInfo(string? arg, Process process)
private static DalamudStartInfo AdjustStartInfo(DalamudStartInfo startInfo, string gamePath)
{
DalamudStartInfo startInfo;
if (arg != default)
{
startInfo = JsonConvert.DeserializeObject<DalamudStartInfo>(Encoding.UTF8.GetString(Convert.FromBase64String(arg)));
}
else
{
var ffxivDir = Path.GetDirectoryName(process.MainModule.FileName);
var appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var xivlauncherDir = Path.Combine(appDataDir, "XIVLauncher");
var ffxivDir = Path.GetDirectoryName(gamePath);
var gameVerStr = File.ReadAllText(Path.Combine(ffxivDir, "ffxivgame.ver"));
var gameVer = GameVersion.Parse(gameVerStr);
startInfo = new DalamudStartInfo
return new()
{
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,
WorkingDirectory = startInfo.WorkingDirectory,
ConfigurationPath = startInfo.ConfigurationPath,
PluginDirectory = startInfo.PluginDirectory,
DefaultPluginDirectory = startInfo.DefaultPluginDirectory,
AssetDirectory = startInfo.AssetDirectory,
Language = ClientLanguage.English,
GameVersion = gameVer,
DelayInitializeMs = startInfo.DelayInitializeMs,
};
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);
}
return startInfo;
}
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);
/// <summary>
/// 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/.
/// </summary>
/// <param name="argument">Supplies the argument to encode.</param>
/// <param name="force">
/// Supplies an indication of whether we should quote the argument even if it
/// does not contain any characters that would ordinarily require quoting.
/// </param>
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();
}
}
}

View file

@ -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<Process> 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<PInvoke.SECURITY_DESCRIPTOR>());
Marshal.StructureToPtr<PInvoke.SECURITY_DESCRIPTOR>(secDesc, psecDesc, true);
var lpProcessInformation = new PInvoke.PROCESS_INFORMATION();
try
{
var lpProcessAttributes = new PInvoke.SECURITY_ATTRIBUTES
{
nLength = Marshal.SizeOf<PInvoke.SECURITY_ATTRIBUTES>(),
lpSecurityDescriptor = psecDesc,
bInheritHandle = false
};
var lpStartupInfo = new PInvoke.STARTUPINFO
{
cb = Marshal.SizeOf<PInvoke.STARTUPINFO>()
};
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;
}
}
}

View file

@ -364,6 +364,23 @@ namespace Dalamud.Injector
StackSizeParamIsReservation = 0x10000,
}
/// <summary>
/// DUPLICATE_* values for DuplicateHandle's dwDesiredAccess.
/// </summary>
[Flags]
public enum DuplicateOptions : uint
{
/// <summary>
/// Closes the source handle. This occurs regardless of any error status returned.
/// </summary>
CloseSource = 0x00000001,
/// <summary>
/// Ignores the dwDesiredAccess parameter. The duplicate handle has the same access as the source handle.
/// </summary>
SameAccess = 0x00000002,
}
/// <summary>
/// PAGE_* from memoryapi.
/// </summary>
@ -833,5 +850,65 @@ namespace Dalamud.Injector
byte[] lpBuffer,
int dwSize,
out IntPtr lpNumberOfBytesWritten);
/// <summary>
/// Duplicates an object handle.
/// </summary>
/// <param name="hSourceProcessHandle">
/// A handle to the process with the handle to be duplicated.
///
/// The handle must have the PROCESS_DUP_HANDLE access right.
/// </param>
/// <param name="hSourceHandle">
/// 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.
/// </param>
/// <param name="hTargetProcessHandle">
/// A handle to the process that is to receive the duplicated handle.
///
/// The handle must have the PROCESS_DUP_HANDLE access right.
/// </param>
/// <param name="lpTargetHandle">
/// 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.
/// </param>
/// <param name="dwDesiredAccess">
/// 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.
/// </param>
/// <param name="bInheritHandle">
/// 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.
/// </param>
/// <param name="dwOptions">
/// Optional actions.
/// </param>
/// <returns>
/// 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.
/// </returns>
/// <remarks>
/// See https://docs.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-duplicatehandle.
/// </remarks>
[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);
}
}