Skip to content

Commit

Permalink
add new hybrid game injection
Browse files Browse the repository at this point in the history
  • Loading branch information
marzent committed Feb 18, 2024
1 parent 6296dcf commit a4ca85f
Show file tree
Hide file tree
Showing 10 changed files with 492 additions and 16 deletions.
8 changes: 5 additions & 3 deletions Dalamud.Boot/DalamudStartInfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,13 @@ void from_json(const nlohmann::json& json, DalamudStartInfo::LoadMethod& value)

}
else if (json.is_string()) {
const auto langstr = unicode::convert<std::string>(json.get<std::string>(), &unicode::lower);
if (langstr == "entrypoint")
const auto loadstr = unicode::convert<std::string>(json.get<std::string>(), &unicode::lower);
if (loadstr == "entrypoint")
value = DalamudStartInfo::LoadMethod::Entrypoint;
else if (langstr == "inject")
else if (loadstr == "inject")
value = DalamudStartInfo::LoadMethod::DllInject;
else if (loadstr == "hybrid")
value = DalamudStartInfo::LoadMethod::Hybrid;
}
}

Expand Down
1 change: 1 addition & 0 deletions Dalamud.Boot/DalamudStartInfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ struct DalamudStartInfo {
enum class LoadMethod : int {
Entrypoint,
DllInject,
Hybrid,
};
friend void from_json(const nlohmann::json&, LoadMethod&);

Expand Down
5 changes: 3 additions & 2 deletions Dalamud.Boot/module.def
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
LIBRARY Dalamud.Boot
EXPORTS
Initialize @1
RewriteRemoteEntryPointW @2
RewrittenEntryPoint @3
GetRemoteEntryPointW @2
RewriteRemoteEntryPointW @3
RewrittenEntryPoint @4
8 changes: 4 additions & 4 deletions Dalamud.Boot/rewrite_entrypoint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,9 @@ void* get_mapped_image_base_address(HANDLE hProcess, const std::filesystem::path
/// @brief Get the target process' entry point.
/// @param hProcess Process handle.
/// @param pcwzPath Path to target process.
/// @return address to entry point; null if successful.
/// @return address to entry point; null if unsuccessful.
///
extern "C" void* WINAPI GetRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath) {
extern "C" char* WINAPI GetRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath) {
try {
const auto base_address = static_cast<char*>(get_mapped_image_base_address(hProcess, pcwzPath));

Expand All @@ -246,7 +246,7 @@ extern "C" void* WINAPI GetRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcw
const auto entrypoint = base_address + (nt_header32.OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC
? nt_header32.OptionalHeader.AddressOfEntryPoint
: nt_header64.OptionalHeader.AddressOfEntryPoint);
return static_cast<void*>(entrypoint);
return entrypoint;
}
catch (const std::exception& e) {
logging::E("Failed to retrieve entry point for 0x{:X}: {}", hProcess, e.what());
Expand Down Expand Up @@ -295,7 +295,7 @@ extern "C" HRESULT WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_

auto& params = *reinterpret_cast<RewrittenEntryPointParameters*>(buffer.data());
params.entrypointLength = entrypoint_replacement.size();
params.pEntrypoint = static_cast<char*>(entrypoint);
params.pEntrypoint = entrypoint;

// Backup original entry point.
last_operation = std::format(L"read_process_memory_or_throw(entrypoint, {}b)", entrypoint_replacement.size());
Expand Down
3 changes: 3 additions & 0 deletions Dalamud.Boot/veh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ static void append_injector_launch_args(std::vector<std::wstring>& args)
break;
case DalamudStartInfo::LoadMethod::DllInject:
args.emplace_back(L"--mode=inject");
break;
case DalamudStartInfo::LoadMethod::Hybrid:
args.emplace_back(L"--mode=hybrid");
}
args.emplace_back(L"--logpath=\"" + unicode::convert<std::wstring>(g_startInfo.BootLogPath) + L"\"");
args.emplace_back(L"--dalamud-working-directory=\"" + unicode::convert<std::wstring>(g_startInfo.WorkingDirectory) + L"\"");
Expand Down
5 changes: 5 additions & 0 deletions Dalamud.Common/LoadMethod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,9 @@ public enum LoadMethod
/// Load Dalamud via DLL-injection.
/// </summary>
DllInject,

/// <summary>
/// Load Dalamud via DLL-injection at the suspended entrypoint.
/// </summary>
Hybrid,
}
132 changes: 130 additions & 2 deletions Dalamud.Injector/EntryPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,10 @@ private static int ProcessLaunchCommand(List<string> args, DalamudStartInfo dala
{
dalamudStartInfo.LoadMethod = LoadMethod.DllInject;
}
else if (mode.Length > 0 && mode.Length <= 6 && "hybrid"[0..mode.Length] == mode)
{
dalamudStartInfo.LoadMethod = LoadMethod.Hybrid;
}
else
{
throw new CommandLineException($"\"{mode}\" is not a valid Dalamud load mode.");
Expand Down Expand Up @@ -804,6 +808,12 @@ private static int ProcessLaunchCommand(List<string> args, DalamudStartInfo dala
gameArgumentString = string.Join(" ", gameArguments.Select(x => EncodeParameterArgument(x)));
}

var entryPoint = nint.Zero;
var entryPointInstruction = new byte[1];

// waiting with a suspended main thread will always deadlock
waitForGameWindow = (dalamudStartInfo.LoadMethod != LoadMethod.Hybrid) && waitForGameWindow;

var (process, mainThreadHandle) = GameStart.LaunchGame(
Path.GetDirectoryName(gamePath),
gamePath,
Expand All @@ -819,12 +829,54 @@ private static int ProcessLaunchCommand(List<string> args, DalamudStartInfo dala
RewriteRemoteEntryPointW(p.Handle, gamePath, JsonConvert.SerializeObject(startInfo)));
Log.Verbose("RewriteRemoteEntryPointW called!");
}

if (!withoutDalamud && dalamudStartInfo.LoadMethod == LoadMethod.Hybrid)
{
DebugSetProcessKillOnExit(false);
entryPoint = GetRemoteEntryPointW(p.Handle, gamePath);
if (entryPoint == nint.Zero)
throw new EntryPointNotFoundException("Couldn't get game process entry point");
Log.Verbose("Got remote entry point {0}", entryPoint);
var int3 = new byte[] { 0xCC };
VirtualProtectEx(p.Handle, entryPoint, 1, MemoryProtection.ExecuteReadWrite, out var entryPointProtection);
Log.Verbose("Original entry point protection {0}", entryPointProtection);
ReadProcessMemory(p.Handle, entryPoint, entryPointInstruction, 1, out var numberOfBytesRead);
Log.Verbose("Original entry point instruction {0}", entryPointInstruction[0]);
WriteProcessMemory(p.Handle, entryPoint, int3, 1, out var numberOfBytesWritten);
if (numberOfBytesWritten != 1)
throw new Exception("Failed to set breakpoint at entry point");
VirtualProtectEx(p.Handle, entryPoint, 1, entryPointProtection, out _);
FlushInstructionCache(p.Handle, entryPoint, 1);
}
},
(p, mt) =>
{
if (!withoutDalamud && dalamudStartInfo.LoadMethod == LoadMethod.Hybrid)
{
var dwThreadId = WaitForBreakpointDebugEvent(entryPoint);
DecrementRip(mt);
VirtualProtectEx(p.Handle, entryPoint, 1, MemoryProtection.ExecuteReadWrite, out var entryPointProtection);
WriteProcessMemory(p.Handle, entryPoint, entryPointInstruction, 1, out var numberOfBytesWritten);
if (numberOfBytesWritten != 1)
throw new Exception("Failed to restore entry point instruction");
VirtualProtectEx(p.Handle, entryPoint, 1, entryPointProtection, out _);
FlushInstructionCache(p.Handle, entryPoint, 1);
SuspendThread(mt);
if (!ContinueDebugEvent((uint)p.Id, dwThreadId, DBG_CONTINUE))
throw new Exception("ContinueDebugEvent failed");
if (DbgUiStopDebugging(p.Handle) != 0)
{
throw new Exception("Couldn't detach from target");
}
}
},
waitForGameWindow);
waitForGameWindow,
!withoutDalamud && dalamudStartInfo.LoadMethod == LoadMethod.Hybrid);

Log.Verbose("Game process started with PID {0}", process.Id);

if (!withoutDalamud && dalamudStartInfo.LoadMethod == LoadMethod.DllInject)
if (!withoutDalamud && (dalamudStartInfo.LoadMethod == LoadMethod.DllInject ||
dalamudStartInfo.LoadMethod == LoadMethod.Hybrid))
{
var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath);
Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo));
Expand All @@ -846,6 +898,79 @@ private static int ProcessLaunchCommand(List<string> args, DalamudStartInfo dala
return 0;
}

private static uint WaitForBreakpointDebugEvent(nint exceptionAddress)
{
var dwThreadId = 0U;
var debugEventPtr = Marshal.AllocHGlobal(188);

Log.Verbose("Waiting for debug breakpoint event...");

while (WaitForDebugEvent(debugEventPtr, uint.MaxValue))
{
var debugEvent = Marshal.PtrToStructure<DebugEvent>(debugEventPtr);

Log.Verbose("Got debug event code: {0}", debugEvent.dwDebugEventCode);

if (debugEvent.dwDebugEventCode != EXCEPTION_DEBUG_EVENT)
{
ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);
continue;
}

if (debugEvent.ExceptionDebugInfo.ExceptionRecord.ExceptionCode != EXCEPTION_BREAKPOINT)
{
ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);
continue;
}

Log.Debug("Got debug breakpoint event with adress {0}", debugEvent.ExceptionDebugInfo.ExceptionRecord.ExceptionAddress);

if (debugEvent.ExceptionDebugInfo.ExceptionRecord.ExceptionAddress != exceptionAddress)
{
ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);
continue;
}

dwThreadId = debugEvent.dwThreadId;
Log.Information("Got entry point debug breakpoint event for TID {0}", dwThreadId);
break;
}

Marshal.FreeHGlobal(debugEventPtr);
if (dwThreadId == 0)
{
Log.Error("Failed to wait for debug event");
throw new Win32Exception(Marshal.GetLastWin32Error());
}

return dwThreadId;
}

private static void DecrementRip(nint threadHandle)
{
// Similar to the debug event struct above, instead of fully defining every union and all
// structs, just allocate the size of the struct and then marshall into our partial one.
var context64Ptr = Marshal.AllocHGlobal(1232);

try
{
var emptyContext64 = Marshal.PtrToStructure<Context64>(context64Ptr);
emptyContext64.ContextFlags = ContextFlags.CONTEXT_CONTROL;
Marshal.StructureToPtr(emptyContext64, context64Ptr, false);
if (!GetThreadContext(threadHandle, context64Ptr))
throw new Win32Exception(Marshal.GetLastWin32Error());
var context64 = Marshal.PtrToStructure<Context64>(context64Ptr);
context64.Rip--;
Marshal.StructureToPtr(context64, context64Ptr, false);
if (!SetThreadContext(threadHandle, context64Ptr))
throw new Win32Exception(Marshal.GetLastWin32Error());
}
finally
{
Marshal.FreeHGlobal(context64Ptr);
}
}

private static Process GetInheritableCurrentProcessHandle()
{
if (!DuplicateHandle(Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, Process.GetCurrentProcess().Handle, out var inheritableCurrentProcessHandle, 0, true, DuplicateOptions.SameAccess))
Expand Down Expand Up @@ -975,6 +1100,9 @@ private static void Inject(Process process, DalamudStartInfo startInfo, nint mai
Log.Information("Done");
}

[DllImport("Dalamud.Boot.dll")]
private static extern nint GetRemoteEntryPointW(IntPtr hProcess, [MarshalAs(UnmanagedType.LPWStr)] string gamePath);

[DllImport("Dalamud.Boot.dll")]
private static extern int RewriteRemoteEntryPointW(IntPtr hProcess, [MarshalAs(UnmanagedType.LPWStr)] string gamePath, [MarshalAs(UnmanagedType.LPWStr)] string loadInfoJson);

Expand Down
19 changes: 16 additions & 3 deletions Dalamud.Injector/GameStart.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ public static class GameStart
/// <param name="arguments">Arguments to pass to the executable file.</param>
/// <param name="dontFixAcl">Don't actually fix the ACL.</param>
/// <param name="beforeResume">Action to execute before the process is started.</param>
/// <param name="afterResume">Action to execute after the process is started.</param>
/// <param name="waitForGameWindow">Wait for the game window to be ready before proceeding.</param>
/// <param name="attach">Attach to the game process as debugger.</param>
/// <returns>The started process and handle to the started main thread.</returns>
/// <exception cref="Win32Exception">Thrown when a win32 error occurs.</exception>
/// <exception cref="GameStartException">Thrown when the process did not start correctly.</exception>
public static (Process GameProcess, nint MainThreadHandle) LaunchGame(string workingDir, string exePath, string arguments, bool dontFixAcl, Action<Process> beforeResume, bool waitForGameWindow = true)
public static (Process GameProcess, nint MainThreadHandle) LaunchGame(string workingDir, string exePath, string arguments, bool dontFixAcl, Action<Process> beforeResume, Action<Process, nint> afterResume, bool waitForGameWindow = true, bool attach = false)
{
Process process = null;

Expand Down Expand Up @@ -98,7 +100,7 @@ public static (Process GameProcess, nint MainThreadHandle) LaunchGame(string wor
ref lpProcessAttributes,
IntPtr.Zero,
false,
PInvoke.CREATE_SUSPENDED,
attach ? PInvoke.DEBUG_ONLY_THIS_PROCESS : PInvoke.CREATE_SUSPENDED,
IntPtr.Zero,
workingDir,
ref lpStartupInfo,
Expand All @@ -121,6 +123,8 @@ public static (Process GameProcess, nint MainThreadHandle) LaunchGame(string wor

PInvoke.ResumeThread(lpProcessInformation.hThread);

afterResume?.Invoke(process, lpProcessInformation.hThread);

// Ensure that the game main window is prepared
if (waitForGameWindow)
{
Expand Down Expand Up @@ -174,7 +178,15 @@ public static (Process GameProcess, nint MainThreadHandle) LaunchGame(string wor
Marshal.FreeHGlobal(psecDesc);
}

NativeFunctions.DuplicateHandle(PInvoke.GetCurrentProcess(), lpProcessInformation.hThread, lpProcessInformation.hProcess, out var mainThreadHandle, 0, false, NativeFunctions.DuplicateOptions.SameAccess);
NativeFunctions.DuplicateHandle(
PInvoke.GetCurrentProcess(),
lpProcessInformation.hThread,
lpProcessInformation.hProcess,
out var mainThreadHandle,
0,
false,
NativeFunctions.DuplicateOptions.SameAccess);

PInvoke.CloseHandle(lpProcessInformation.hThread);

return (process, mainThreadHandle);
Expand Down Expand Up @@ -360,6 +372,7 @@ private static class PInvoke

public const UInt32 SECURITY_DESCRIPTOR_REVISION = 1;

public const UInt32 DEBUG_ONLY_THIS_PROCESS = 0x00000002;
public const UInt32 CREATE_SUSPENDED = 0x00000004;

public const UInt32 TOKEN_QUERY = 0x0008;
Expand Down
Loading

0 comments on commit a4ca85f

Please sign in to comment.