feat: Add TerraSharp for Terraria and tModLoader
This commit is contained in:
commit
58eb32c7ed
70 files changed
+8678
No files matched your search
@@ -0,0 +1,93 @@
|
||||
option casemap:none
|
||||
|
||||
DataStage equ 0
|
||||
DataResult equ 4
|
||||
DataManagedResult equ 8
|
||||
DataCompleted equ 12
|
||||
DataAssembly equ 16
|
||||
DataTypeName equ 24
|
||||
DataMethodName equ 32
|
||||
DataGetRuntimeDelegate equ 64
|
||||
|
||||
StageGetDelegates equ 7
|
||||
StageLoadAssembly equ 8
|
||||
StageGetEntryPoint equ 9
|
||||
StageInvoke equ 10
|
||||
|
||||
DelegateGetFunctionPointer equ 6
|
||||
DelegateLoadAssembly equ 7
|
||||
|
||||
LocalLoadAssembly equ 30h
|
||||
LocalGetFunction equ 38h
|
||||
LocalEntry equ 40h
|
||||
|
||||
.code
|
||||
Bootstrap proc frame
|
||||
push rbx
|
||||
.pushreg rbx
|
||||
sub rsp, 50h
|
||||
.allocstack 50h
|
||||
.endprolog
|
||||
|
||||
mov rbx, rcx
|
||||
mov dword ptr [rbx + DataResult], 80004005h
|
||||
mov dword ptr [rbx + DataManagedResult], -1
|
||||
mov dword ptr [rbx + DataCompleted], 0
|
||||
xor eax, eax
|
||||
mov [rsp + LocalLoadAssembly], rax
|
||||
mov [rsp + LocalGetFunction], rax
|
||||
mov [rsp + LocalEntry], rax
|
||||
|
||||
mov dword ptr [rbx + DataStage], StageGetDelegates
|
||||
xor ecx, ecx
|
||||
mov edx, DelegateLoadAssembly
|
||||
lea r8, [rsp + LocalLoadAssembly]
|
||||
call qword ptr [rbx + DataGetRuntimeDelegate]
|
||||
mov [rbx + DataResult], eax
|
||||
test eax, eax
|
||||
js Complete
|
||||
|
||||
xor ecx, ecx
|
||||
mov edx, DelegateGetFunctionPointer
|
||||
lea r8, [rsp + LocalGetFunction]
|
||||
call qword ptr [rbx + DataGetRuntimeDelegate]
|
||||
mov [rbx + DataResult], eax
|
||||
test eax, eax
|
||||
js Complete
|
||||
|
||||
mov dword ptr [rbx + DataStage], StageLoadAssembly
|
||||
mov rcx, [rbx + DataAssembly]
|
||||
xor edx, edx
|
||||
xor r8d, r8d
|
||||
call qword ptr [rsp + LocalLoadAssembly]
|
||||
mov [rbx + DataResult], eax
|
||||
test eax, eax
|
||||
js Complete
|
||||
|
||||
mov dword ptr [rbx + DataStage], StageGetEntryPoint
|
||||
mov rcx, [rbx + DataTypeName]
|
||||
mov rdx, [rbx + DataMethodName]
|
||||
xor r8d, r8d
|
||||
xor r9d, r9d
|
||||
mov qword ptr [rsp + 20h], 0
|
||||
lea rax, [rsp + LocalEntry]
|
||||
mov [rsp + 28h], rax
|
||||
call qword ptr [rsp + LocalGetFunction]
|
||||
mov [rbx + DataResult], eax
|
||||
test eax, eax
|
||||
js Complete
|
||||
|
||||
mov dword ptr [rbx + DataStage], StageInvoke
|
||||
xor ecx, ecx
|
||||
xor edx, edx
|
||||
call qword ptr [rsp + LocalEntry]
|
||||
mov [rbx + DataManagedResult], eax
|
||||
|
||||
Complete:
|
||||
mov dword ptr [rbx + DataCompleted], 1
|
||||
mov eax, [rbx + DataResult]
|
||||
add rsp, 50h
|
||||
pop rbx
|
||||
ret
|
||||
Bootstrap endp
|
||||
end
|
||||
@@ -0,0 +1,134 @@
|
||||
.386
|
||||
.model flat
|
||||
option casemap:none
|
||||
|
||||
StageOffset EQU 0
|
||||
ResultOffset EQU 4
|
||||
ManagedResultOffset EQU 8
|
||||
CompletedOffset EQU 12
|
||||
AssemblyOffset EQU 16
|
||||
TypeOffset EQU 24
|
||||
MethodOffset EQU 32
|
||||
ArgumentOffset EQU 40
|
||||
VersionOffset EQU 48
|
||||
CreateInstanceOffset EQU 56
|
||||
ClsidMetaOffset EQU 72
|
||||
IidMetaOffset EQU 80
|
||||
IidInfoOffset EQU 88
|
||||
ClsidHostOffset EQU 96
|
||||
IidHostOffset EQU 104
|
||||
|
||||
CreateInstanceStage EQU 1
|
||||
GetRuntimeStage EQU 2
|
||||
GetInterfaceStage EQU 3
|
||||
StartStage EQU 4
|
||||
ExecuteStage EQU 5
|
||||
|
||||
; Vtable offsets come from metahost.h and mscoree.h in the .NET Framework SDK.
|
||||
ReleaseOffset EQU 8
|
||||
GetRuntimeOffset EQU 12
|
||||
GetInterfaceOffset EQU 36
|
||||
StartOffset EQU 12
|
||||
ExecuteOffset EQU 44
|
||||
|
||||
.code
|
||||
Bootstrap PROC
|
||||
push ebp
|
||||
mov ebp, esp
|
||||
push esi
|
||||
push edi
|
||||
sub esp, 12
|
||||
mov edi, [ebp + 8]
|
||||
xor eax, eax
|
||||
mov [ebp - 12], eax
|
||||
mov [ebp - 16], eax
|
||||
mov [ebp - 20], eax
|
||||
|
||||
mov DWORD PTR [edi + StageOffset], CreateInstanceStage
|
||||
lea eax, [ebp - 12]
|
||||
push eax
|
||||
push DWORD PTR [edi + IidMetaOffset]
|
||||
push DWORD PTR [edi + ClsidMetaOffset]
|
||||
call DWORD PTR [edi + CreateInstanceOffset]
|
||||
mov [edi + ResultOffset], eax
|
||||
test eax, eax
|
||||
js cleanup
|
||||
|
||||
mov DWORD PTR [edi + StageOffset], GetRuntimeStage
|
||||
mov esi, [ebp - 12]
|
||||
lea eax, [ebp - 16]
|
||||
push eax
|
||||
push DWORD PTR [edi + IidInfoOffset]
|
||||
push DWORD PTR [edi + VersionOffset]
|
||||
push esi
|
||||
mov eax, [esi]
|
||||
call DWORD PTR [eax + GetRuntimeOffset]
|
||||
mov [edi + ResultOffset], eax
|
||||
test eax, eax
|
||||
js cleanup
|
||||
|
||||
mov DWORD PTR [edi + StageOffset], GetInterfaceStage
|
||||
mov esi, [ebp - 16]
|
||||
lea eax, [ebp - 20]
|
||||
push eax
|
||||
push DWORD PTR [edi + IidHostOffset]
|
||||
push DWORD PTR [edi + ClsidHostOffset]
|
||||
push esi
|
||||
mov eax, [esi]
|
||||
call DWORD PTR [eax + GetInterfaceOffset]
|
||||
mov [edi + ResultOffset], eax
|
||||
test eax, eax
|
||||
js cleanup
|
||||
|
||||
mov DWORD PTR [edi + StageOffset], StartStage
|
||||
mov esi, [ebp - 20]
|
||||
push esi
|
||||
mov eax, [esi]
|
||||
call DWORD PTR [eax + StartOffset]
|
||||
mov [edi + ResultOffset], eax
|
||||
test eax, eax
|
||||
js cleanup
|
||||
|
||||
mov DWORD PTR [edi + StageOffset], ExecuteStage
|
||||
lea eax, [edi + ManagedResultOffset]
|
||||
push eax
|
||||
push DWORD PTR [edi + ArgumentOffset]
|
||||
push DWORD PTR [edi + MethodOffset]
|
||||
push DWORD PTR [edi + TypeOffset]
|
||||
push DWORD PTR [edi + AssemblyOffset]
|
||||
push esi
|
||||
mov eax, [esi]
|
||||
call DWORD PTR [eax + ExecuteOffset]
|
||||
mov [edi + ResultOffset], eax
|
||||
|
||||
cleanup:
|
||||
mov esi, [ebp - 20]
|
||||
test esi, esi
|
||||
jz releaseInfo
|
||||
push esi
|
||||
mov eax, [esi]
|
||||
call DWORD PTR [eax + ReleaseOffset]
|
||||
releaseInfo:
|
||||
mov esi, [ebp - 16]
|
||||
test esi, esi
|
||||
jz releaseMeta
|
||||
push esi
|
||||
mov eax, [esi]
|
||||
call DWORD PTR [eax + ReleaseOffset]
|
||||
releaseMeta:
|
||||
mov esi, [ebp - 12]
|
||||
test esi, esi
|
||||
jz done
|
||||
push esi
|
||||
mov eax, [esi]
|
||||
call DWORD PTR [eax + ReleaseOffset]
|
||||
done:
|
||||
mov DWORD PTR [edi + CompletedOffset], 1
|
||||
mov eax, [edi + ResultOffset]
|
||||
add esp, 12
|
||||
pop edi
|
||||
pop esi
|
||||
pop ebp
|
||||
ret 4
|
||||
Bootstrap ENDP
|
||||
END
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
#define NOMINMAX
|
||||
#include <windows.h>
|
||||
#include <cstddef>
|
||||
|
||||
enum class BootstrapStage : DWORD
|
||||
{
|
||||
Pending,
|
||||
CreateInstance,
|
||||
GetRuntime,
|
||||
GetInterface,
|
||||
Start,
|
||||
Execute,
|
||||
RegisterUnwind,
|
||||
GetDelegates,
|
||||
LoadAssembly,
|
||||
GetEntryPoint,
|
||||
Invoke,
|
||||
RemoveUnwind,
|
||||
};
|
||||
|
||||
// Layout and stages match bootstrap-x86.asm and bootstrap-x64.asm; pointer slots are always 64 bits.
|
||||
struct BootstrapData
|
||||
{
|
||||
BootstrapStage stage;
|
||||
HRESULT result;
|
||||
INT32 managedResult;
|
||||
BOOL completed;
|
||||
UINT64 assembly;
|
||||
UINT64 typeName;
|
||||
UINT64 methodName;
|
||||
UINT64 argument;
|
||||
UINT64 runtimeVersion;
|
||||
UINT64 clrCreateInstance;
|
||||
UINT64 getRuntimeDelegate;
|
||||
UINT64 clsidMetaHost;
|
||||
UINT64 iidMetaHost;
|
||||
UINT64 iidRuntimeInfo;
|
||||
UINT64 clsidRuntimeHost;
|
||||
UINT64 iidRuntimeHost;
|
||||
};
|
||||
|
||||
static_assert(offsetof(BootstrapData, assembly) == 16);
|
||||
static_assert(offsetof(BootstrapData, typeName) == 24);
|
||||
static_assert(offsetof(BootstrapData, methodName) == 32);
|
||||
static_assert(offsetof(BootstrapData, argument) == 40);
|
||||
static_assert(offsetof(BootstrapData, runtimeVersion) == 48);
|
||||
static_assert(offsetof(BootstrapData, clrCreateInstance) == 56);
|
||||
static_assert(offsetof(BootstrapData, getRuntimeDelegate) == 64);
|
||||
static_assert(offsetof(BootstrapData, clsidMetaHost) == 72);
|
||||
static_assert(offsetof(BootstrapData, iidMetaHost) == 80);
|
||||
static_assert(offsetof(BootstrapData, iidRuntimeInfo) == 88);
|
||||
static_assert(offsetof(BootstrapData, clsidRuntimeHost) == 96);
|
||||
static_assert(offsetof(BootstrapData, iidRuntimeHost) == 104);
|
||||
static_assert(sizeof(BootstrapData) == 112);
|
||||
@@ -0,0 +1,155 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$X86Object,
|
||||
[Parameter(Mandatory = $true)][string]$X64Object,
|
||||
[Parameter(Mandatory = $true)][string]$Output
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Assert-Range([byte[]]$Bytes, [long]$Offset, [long]$Size) {
|
||||
if ($Offset -lt 0 -or $Size -lt 0 -or $Offset -gt $Bytes.Length -or $Size -gt $Bytes.Length - $Offset) {
|
||||
throw 'COFF data extends beyond the object file.'
|
||||
}
|
||||
}
|
||||
|
||||
function Read-UInt16([byte[]]$Bytes, [long]$Offset) {
|
||||
Assert-Range $Bytes $Offset 2
|
||||
return [BitConverter]::ToUInt16($Bytes, [int]$Offset)
|
||||
}
|
||||
|
||||
function Read-UInt32([byte[]]$Bytes, [long]$Offset) {
|
||||
Assert-Range $Bytes $Offset 4
|
||||
return [BitConverter]::ToUInt32($Bytes, [int]$Offset)
|
||||
}
|
||||
|
||||
function Read-CoffObject([string]$Path, [uint16]$Machine) {
|
||||
$bytes = [IO.File]::ReadAllBytes([IO.Path]::GetFullPath($Path))
|
||||
Assert-Range $bytes 0 20
|
||||
if ((Read-UInt16 $bytes 0) -ne $Machine -or (Read-UInt16 $bytes 16) -ne 0) {
|
||||
throw "Unexpected COFF object format: $Path"
|
||||
}
|
||||
$sectionCount = Read-UInt16 $bytes 2
|
||||
$symbolOffset = Read-UInt32 $bytes 8
|
||||
$symbolCount = Read-UInt32 $bytes 12
|
||||
$sectionTableEnd = 20 + [long]$sectionCount * 40
|
||||
Assert-Range $bytes 20 ([long]$sectionCount * 40)
|
||||
$sections = @()
|
||||
for ($index = 0; $index -lt $sectionCount; $index++) {
|
||||
$offset = 20 + $index * 40
|
||||
$name = [Text.Encoding]::ASCII.GetString($bytes, $offset, 8).TrimEnd([char]0)
|
||||
if ($name.StartsWith('/')) {
|
||||
$stringTableOffset = [long]$symbolOffset + [long]$symbolCount * 18
|
||||
$stringTableSize = Read-UInt32 $bytes $stringTableOffset
|
||||
Assert-Range $bytes $stringTableOffset $stringTableSize
|
||||
$nameOffset = [uint32]0
|
||||
if (-not [uint32]::TryParse($name.Substring(1), [ref]$nameOffset) -or $nameOffset -lt 4 -or $nameOffset -ge $stringTableSize) { throw "Invalid COFF section name: $Path" }
|
||||
$nameStart = $stringTableOffset + $nameOffset
|
||||
$nameEnd = $nameStart
|
||||
while ($nameEnd -lt $stringTableOffset + $stringTableSize -and $bytes[$nameEnd] -ne 0) { $nameEnd++ }
|
||||
if ($nameEnd -eq $stringTableOffset + $stringTableSize) { throw "Unterminated COFF section name: $Path" }
|
||||
$name = [Text.Encoding]::ASCII.GetString($bytes, [int]$nameStart, [int]($nameEnd - $nameStart))
|
||||
}
|
||||
$size = Read-UInt32 $bytes ($offset + 16)
|
||||
$dataOffset = Read-UInt32 $bytes ($offset + 20)
|
||||
$relocationOffset = Read-UInt32 $bytes ($offset + 24)
|
||||
$relocationCount = Read-UInt16 $bytes ($offset + 32)
|
||||
$characteristics = Read-UInt32 $bytes ($offset + 36)
|
||||
if ($relocationCount -eq 0xffff -or ($characteristics -band 0x01000000) -ne 0) {
|
||||
throw "COFF relocation overflow is unsupported: $Path"
|
||||
}
|
||||
if ($size -gt 0) {
|
||||
Assert-Range $bytes $dataOffset $size
|
||||
if ($dataOffset -lt $sectionTableEnd) { throw "Invalid section data offset: $Path" }
|
||||
}
|
||||
if ($relocationCount -gt 0) {
|
||||
Assert-Range $bytes $relocationOffset ([long]$relocationCount * 10)
|
||||
if ($relocationOffset -lt $sectionTableEnd) { throw "Invalid relocation offset: $Path" }
|
||||
}
|
||||
if (($name -in '.text', '.xdata' -or $name.StartsWith('.text$', [StringComparison]::Ordinal)) -and $relocationCount -ne 0) {
|
||||
throw "$name must be relocation-free: $Path"
|
||||
}
|
||||
$sections += [pscustomobject]@{ Name = $name; Index = $index + 1; Size = $size; DataOffset = $dataOffset; RelocationOffset = $relocationOffset; RelocationCount = $relocationCount }
|
||||
}
|
||||
$symbols = @{}
|
||||
if ($symbolCount -gt 0) {
|
||||
Assert-Range $bytes $symbolOffset ([long]$symbolCount * 18)
|
||||
if ($symbolOffset -lt $sectionTableEnd) { throw "Invalid symbol table offset: $Path" }
|
||||
for ($index = 0; $index -lt $symbolCount; $index++) {
|
||||
$offset = $symbolOffset + [long]$index * 18
|
||||
$auxiliaryCount = $bytes[$offset + 17]
|
||||
if ([long]$index + $auxiliaryCount -ge $symbolCount) { throw "Invalid auxiliary symbol count: $Path" }
|
||||
$symbols[$index] = [pscustomobject]@{ Value = Read-UInt32 $bytes ($offset + 8); Section = [BitConverter]::ToInt16($bytes, [int]($offset + 12)) }
|
||||
$index += $auxiliaryCount
|
||||
}
|
||||
}
|
||||
return [pscustomobject]@{ Path = $Path; Bytes = $bytes; Sections = $sections; Symbols = $symbols }
|
||||
}
|
||||
|
||||
function Get-Section($Object, [string]$Name) {
|
||||
$matches = @($Object.Sections | Where-Object { $_.Size -gt 0 -and ($_.Name -eq $Name -or ($Name -eq '.text' -and $_.Name.StartsWith('.text$', [StringComparison]::Ordinal))) })
|
||||
if ($matches.Count -ne 1) {
|
||||
throw "Expected one nonempty $Name section: $($Object.Path)"
|
||||
}
|
||||
return $matches[0]
|
||||
}
|
||||
|
||||
function Get-SectionBytes($Object, $Section) {
|
||||
$bytes = New-Object byte[] $Section.Size
|
||||
[Array]::Copy($Object.Bytes, [int]$Section.DataOffset, $bytes, 0, [int]$Section.Size)
|
||||
return ,$bytes
|
||||
}
|
||||
|
||||
function Resolve-UnwindReference($Object, $Section, [int]$Offset, [int]$ExpectedSection) {
|
||||
$relocations = @()
|
||||
for ($index = 0; $index -lt $Section.RelocationCount; $index++) {
|
||||
$position = $Section.RelocationOffset + $index * 10
|
||||
if ((Read-UInt32 $Object.Bytes $position) -eq $Offset) { $relocations += $position }
|
||||
}
|
||||
if ($relocations.Count -ne 1) { throw 'Each RUNTIME_FUNCTION field must have exactly one relocation.' }
|
||||
$position = $relocations[0]
|
||||
if ((Read-UInt16 $Object.Bytes ($position + 8)) -ne 3) { throw 'RUNTIME_FUNCTION requires AMD64_ADDR32NB relocations.' }
|
||||
$symbolIndex = Read-UInt32 $Object.Bytes ($position + 4)
|
||||
if (-not $Object.Symbols.ContainsKey([int]$symbolIndex)) { throw 'RUNTIME_FUNCTION references an invalid or auxiliary symbol.' }
|
||||
$symbol = $Object.Symbols[[int]$symbolIndex]
|
||||
if ($symbol.Section -ne $ExpectedSection) { throw 'RUNTIME_FUNCTION references the wrong section.' }
|
||||
return [long]$symbol.Value + (Read-UInt32 $Object.Bytes ($Section.DataOffset + $Offset))
|
||||
}
|
||||
|
||||
function Append-ByteArray([Text.StringBuilder]$Builder, [string]$Name, [byte[]]$Bytes) {
|
||||
[void]$Builder.AppendLine("inline constexpr unsigned char $Name[] =")
|
||||
[void]$Builder.AppendLine('{')
|
||||
for ($index = 0; $index -lt $Bytes.Length; $index += 16) {
|
||||
$values = for ($position = $index; $position -lt [Math]::Min($index + 16, $Bytes.Length); $position++) { '0x{0:X2}' -f $Bytes[$position] }
|
||||
[void]$Builder.AppendLine(' ' + ($values -join ', ') + ',')
|
||||
}
|
||||
[void]$Builder.AppendLine('};')
|
||||
[void]$Builder.AppendLine()
|
||||
}
|
||||
|
||||
$x86 = Read-CoffObject $X86Object 0x014c
|
||||
$x64 = Read-CoffObject $X64Object 0x8664
|
||||
$x86Code = Get-SectionBytes $x86 (Get-Section $x86 '.text')
|
||||
$x64Text = Get-Section $x64 '.text'
|
||||
$x64Unwind = Get-Section $x64 '.xdata'
|
||||
$x64Functions = Get-Section $x64 '.pdata'
|
||||
if ($x64Functions.Size -ne 12 -or $x64Functions.RelocationCount -ne 3) { throw 'The x64 bootstrap must have exactly one RUNTIME_FUNCTION.' }
|
||||
$begin = Resolve-UnwindReference $x64 $x64Functions 0 $x64Text.Index
|
||||
$end = Resolve-UnwindReference $x64 $x64Functions 4 $x64Text.Index
|
||||
$unwindData = Resolve-UnwindReference $x64 $x64Functions 8 $x64Unwind.Index
|
||||
if ($begin -ne 0 -or $end -ne $x64Text.Size -or $unwindData -ne 0) { throw 'The x64 frame must cover all code and reference the first UNWIND_INFO.' }
|
||||
$x64Code = Get-SectionBytes $x64 $x64Text
|
||||
$x64UnwindBytes = Get-SectionBytes $x64 $x64Unwind
|
||||
if ($x64UnwindBytes.Length -lt 4 -or $x64UnwindBytes[0] -ne 1 -or ((4 + 2 * $x64UnwindBytes[2] + 3) -band -4) -gt $x64UnwindBytes.Length) { throw 'Unsupported or truncated x64 UNWIND_INFO.' }
|
||||
$header = New-Object Text.StringBuilder
|
||||
[void]$header.AppendLine('#pragma once')
|
||||
[void]$header.AppendLine()
|
||||
Append-ByteArray $header 'BootstrapX86' $x86Code
|
||||
Append-ByteArray $header 'BootstrapX64' $x64Code
|
||||
Append-ByteArray $header 'BootstrapX64Unwind' $x64UnwindBytes
|
||||
[void]$header.AppendLine('inline constexpr DWORD BootstrapX64Begin = 0x{0:X8};' -f $begin)
|
||||
[void]$header.AppendLine('inline constexpr DWORD BootstrapX64End = 0x{0:X8};' -f $end)
|
||||
[void]$header.AppendLine('inline constexpr DWORD BootstrapX64UnwindData = 0x{0:X8};' -f $unwindData)
|
||||
$outputPath = [IO.Path]::GetFullPath($Output)
|
||||
[void][IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($outputPath))
|
||||
[IO.File]::WriteAllText($outputPath, $header.ToString(), [Text.Encoding]::ASCII)
|
||||
@@ -0,0 +1,574 @@
|
||||
// Usage: TerraSharp.Injector.exe [TerraSharp.<Vanilla|tModLoader>.dll | build directory]; injects into Terraria (x86, CLR v4) or tModLoader (x64, .NET 8).
|
||||
#include "bootstrap.h"
|
||||
#include "bootstrap.generated.h"
|
||||
#include <metahost.h>
|
||||
#include <tlhelp32.h>
|
||||
#include <psapi.h>
|
||||
#include <winternl.h>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifndef _M_X64
|
||||
#error The injector must be built for x64.
|
||||
#endif
|
||||
|
||||
enum class Game { Terraria, TModLoader };
|
||||
|
||||
struct Target
|
||||
{
|
||||
Game game;
|
||||
const wchar_t* name;
|
||||
const wchar_t* assembly;
|
||||
bool x86;
|
||||
};
|
||||
|
||||
static const Target Targets[] =
|
||||
{
|
||||
{ Game::Terraria, L"Terraria", L"TerraSharp.Vanilla", true },
|
||||
{ Game::TModLoader, L"tModLoader", L"TerraSharp.tModLoader", false },
|
||||
};
|
||||
|
||||
struct Handle
|
||||
{
|
||||
HANDLE value;
|
||||
explicit Handle(HANDLE handle) : value(handle) {}
|
||||
Handle(const Handle&) = delete;
|
||||
Handle& operator=(const Handle&) = delete;
|
||||
~Handle() { if (value && value != INVALID_HANDLE_VALUE) CloseHandle(value); }
|
||||
};
|
||||
|
||||
struct RemoteMemory
|
||||
{
|
||||
HANDLE process;
|
||||
void* address;
|
||||
RemoteMemory(HANDLE target, SIZE_T size, bool x86 = false) : process(target), address(VirtualAllocEx(target, nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE))
|
||||
{
|
||||
if (address && x86 && reinterpret_cast<UINT64>(address) + size > (UINT64{ 1 } << 32))
|
||||
{
|
||||
VirtualFreeEx(process, address, 0, MEM_RELEASE);
|
||||
address = nullptr;
|
||||
SetLastError(ERROR_INVALID_ADDRESS);
|
||||
}
|
||||
}
|
||||
RemoteMemory(const RemoteMemory&) = delete;
|
||||
RemoteMemory& operator=(const RemoteMemory&) = delete;
|
||||
~RemoteMemory() { if (address) VirtualFreeEx(process, address, 0, MEM_RELEASE); }
|
||||
};
|
||||
|
||||
static bool ContainsNoCase(std::wstring text, std::wstring part)
|
||||
{
|
||||
for (wchar_t& character : text)
|
||||
character = towlower(character);
|
||||
for (wchar_t& character : part)
|
||||
character = towlower(character);
|
||||
return text.find(part) != std::wstring::npos;
|
||||
}
|
||||
|
||||
static std::wstring CommandLine(DWORD processId)
|
||||
{
|
||||
using QueryFunction = NTSTATUS(NTAPI*)(HANDLE, ULONG, PVOID, ULONG, PULONG);
|
||||
static auto query = reinterpret_cast<QueryFunction>(GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQueryInformationProcess"));
|
||||
const ULONG ProcessCommandLineInformation = 60;
|
||||
Handle process(OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId));
|
||||
if (!process.value || !query)
|
||||
return {};
|
||||
ULONG size = 0;
|
||||
query(process.value, ProcessCommandLineInformation, nullptr, 0, &size);
|
||||
if (!size)
|
||||
return {};
|
||||
std::vector<unsigned char> buffer(size);
|
||||
if (query(process.value, ProcessCommandLineInformation, buffer.data(), size, &size) < 0)
|
||||
return {};
|
||||
auto text = reinterpret_cast<UNICODE_STRING*>(buffer.data());
|
||||
return std::wstring(text->Buffer, text->Length / sizeof(wchar_t));
|
||||
}
|
||||
|
||||
static DWORD FindProcess(Game game)
|
||||
{
|
||||
Handle snapshot(CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0));
|
||||
if (snapshot.value == INVALID_HANDLE_VALUE)
|
||||
return 0;
|
||||
PROCESSENTRY32W entry{ sizeof(entry) };
|
||||
for (BOOL hasProcess = Process32FirstW(snapshot.value, &entry); hasProcess; hasProcess = Process32NextW(snapshot.value, &entry))
|
||||
{
|
||||
if (game == Game::Terraria && _wcsicmp(entry.szExeFile, L"Terraria.exe") == 0)
|
||||
return entry.th32ProcessID;
|
||||
// tModLoader runs as "dotnet tModLoader.dll"; its Steam client helper and dedicated server use the same command.
|
||||
if (game == Game::TModLoader && _wcsicmp(entry.szExeFile, L"dotnet.exe") == 0)
|
||||
{
|
||||
std::wstring command = CommandLine(entry.th32ProcessID);
|
||||
if (ContainsNoCase(command, L"tModLoader.dll") && !ContainsNoCase(command, L"-server") && !ContainsNoCase(command, L"-terrariasteamclient"))
|
||||
return entry.th32ProcessID;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool ReadAll(const std::wstring& path, std::vector<unsigned char>& bytes)
|
||||
{
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file)
|
||||
return false;
|
||||
bytes.assign(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
|
||||
return true;
|
||||
}
|
||||
|
||||
struct PeImage
|
||||
{
|
||||
std::vector<unsigned char> bytes;
|
||||
const IMAGE_NT_HEADERS32* headers = nullptr;
|
||||
|
||||
bool Load(const std::wstring& path)
|
||||
{
|
||||
if (!ReadAll(path, bytes) || bytes.size() < sizeof(IMAGE_DOS_HEADER))
|
||||
return false;
|
||||
auto dosHeader = reinterpret_cast<const IMAGE_DOS_HEADER*>(bytes.data());
|
||||
if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE || dosHeader->e_lfanew <= 0 || static_cast<size_t>(dosHeader->e_lfanew) + sizeof(IMAGE_NT_HEADERS64) > bytes.size())
|
||||
return false;
|
||||
headers = reinterpret_cast<const IMAGE_NT_HEADERS32*>(bytes.data() + dosHeader->e_lfanew);
|
||||
return headers->Signature == IMAGE_NT_SIGNATURE;
|
||||
}
|
||||
|
||||
WORD Machine() const { return headers->FileHeader.Machine; }
|
||||
|
||||
const IMAGE_DATA_DIRECTORY& Directory(int index) const
|
||||
{
|
||||
if (headers->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC)
|
||||
return reinterpret_cast<const IMAGE_NT_HEADERS64*>(headers)->OptionalHeader.DataDirectory[index];
|
||||
return headers->OptionalHeader.DataDirectory[index];
|
||||
}
|
||||
|
||||
const unsigned char* At(DWORD relativeAddress, DWORD size) const
|
||||
{
|
||||
auto section = IMAGE_FIRST_SECTION(headers);
|
||||
for (WORD i = 0; i < headers->FileHeader.NumberOfSections; i++, section++)
|
||||
{
|
||||
DWORD extent = std::max(section->Misc.VirtualSize, section->SizeOfRawData);
|
||||
if (relativeAddress >= section->VirtualAddress && relativeAddress - section->VirtualAddress + size <= extent)
|
||||
{
|
||||
size_t offset = static_cast<size_t>(section->PointerToRawData) + (relativeAddress - section->VirtualAddress);
|
||||
return offset + size <= bytes.size() ? bytes.data() + offset : nullptr;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Forwarded exports have an RVA inside the export directory; none of the names used here are forwarded.
|
||||
DWORD Export(const char* name) const
|
||||
{
|
||||
const IMAGE_DATA_DIRECTORY& directory = Directory(IMAGE_DIRECTORY_ENTRY_EXPORT);
|
||||
auto exports = reinterpret_cast<const IMAGE_EXPORT_DIRECTORY*>(At(directory.VirtualAddress, sizeof(IMAGE_EXPORT_DIRECTORY)));
|
||||
if (!exports)
|
||||
return 0;
|
||||
auto names = reinterpret_cast<const DWORD*>(At(exports->AddressOfNames, exports->NumberOfNames * sizeof(DWORD)));
|
||||
auto ordinals = reinterpret_cast<const WORD*>(At(exports->AddressOfNameOrdinals, exports->NumberOfNames * sizeof(WORD)));
|
||||
auto functions = reinterpret_cast<const DWORD*>(At(exports->AddressOfFunctions, exports->NumberOfFunctions * sizeof(DWORD)));
|
||||
if (!names || !ordinals || !functions)
|
||||
return 0;
|
||||
for (DWORD i = 0; i < exports->NumberOfNames; i++)
|
||||
{
|
||||
auto exportName = reinterpret_cast<const char*>(At(names[i], 1));
|
||||
if (!exportName || strcmp(exportName, name) != 0 || ordinals[i] >= exports->NumberOfFunctions)
|
||||
continue;
|
||||
DWORD relativeAddress = functions[ordinals[i]];
|
||||
bool forwarded = relativeAddress >= directory.VirtualAddress && relativeAddress < directory.VirtualAddress + directory.Size;
|
||||
return forwarded ? 0 : relativeAddress;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
struct ModuleInfo
|
||||
{
|
||||
unsigned char* address = nullptr;
|
||||
std::wstring path;
|
||||
};
|
||||
|
||||
static std::wstring ModuleFilePath(std::wstring path, bool x86)
|
||||
{
|
||||
if (!x86)
|
||||
return path;
|
||||
wchar_t system[MAX_PATH];
|
||||
UINT systemLength = GetSystemDirectoryW(system, MAX_PATH);
|
||||
if (!systemLength || systemLength >= MAX_PATH || path.size() <= systemLength || path[systemLength] != L'\\' || _wcsnicmp(path.c_str(), system, systemLength) != 0)
|
||||
return path;
|
||||
// WOW64 module paths can retain System32 even when the mapped DLL comes from SysWOW64.
|
||||
wchar_t wow64[MAX_PATH];
|
||||
UINT wow64Length = GetSystemWow64DirectoryW(wow64, MAX_PATH);
|
||||
return wow64Length && wow64Length < MAX_PATH ? std::wstring(wow64, wow64Length) + path.substr(systemLength) : path;
|
||||
}
|
||||
|
||||
static ModuleInfo RemoteModule(HANDLE process, bool x86, const wchar_t* name)
|
||||
{
|
||||
std::vector<HMODULE> modules(1024);
|
||||
DWORD needed = 0;
|
||||
while (true)
|
||||
{
|
||||
if (!EnumProcessModulesEx(process, modules.data(), static_cast<DWORD>(modules.size() * sizeof(HMODULE)), &needed, x86 ? LIST_MODULES_32BIT : LIST_MODULES_64BIT))
|
||||
return {};
|
||||
if (needed <= modules.size() * sizeof(HMODULE))
|
||||
break;
|
||||
modules.resize(needed / sizeof(HMODULE));
|
||||
}
|
||||
for (DWORD i = 0; i < needed / sizeof(HMODULE); i++)
|
||||
{
|
||||
wchar_t base[MAX_PATH];
|
||||
if (GetModuleBaseNameW(process, modules[i], base, MAX_PATH) && _wcsicmp(base, name) == 0)
|
||||
{
|
||||
std::vector<wchar_t> path(32768);
|
||||
DWORD length = GetModuleFileNameExW(process, modules[i], path.data(), static_cast<DWORD>(path.size()));
|
||||
if (length && length < path.size())
|
||||
return { reinterpret_cast<unsigned char*>(modules[i]), ModuleFilePath(std::wstring(path.data(), length), x86) };
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
static void* RemoteExport(HANDLE process, bool x86, const wchar_t* moduleName, const char* name)
|
||||
{
|
||||
ModuleInfo module = RemoteModule(process, x86, moduleName);
|
||||
PeImage image;
|
||||
if (!module.address)
|
||||
return nullptr;
|
||||
if (!image.Load(module.path) || image.Machine() != (x86 ? IMAGE_FILE_MACHINE_I386 : IMAGE_FILE_MACHINE_AMD64))
|
||||
{
|
||||
wprintf(L"cannot read an %s module image: %s\n", x86 ? L"x86" : L"x64", module.path.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
DWORD relativeAddress = image.Export(name);
|
||||
return relativeAddress ? module.address + relativeAddress : nullptr;
|
||||
}
|
||||
|
||||
enum class ThreadResult { NotStarted, Completed, Uncertain };
|
||||
|
||||
static ThreadResult RemoteCall(HANDLE process, void* function, void* argument, DWORD& exitCode)
|
||||
{
|
||||
Handle thread(CreateRemoteThread(process, nullptr, 0, reinterpret_cast<LPTHREAD_START_ROUTINE>(function), argument, 0, nullptr));
|
||||
if (!thread.value)
|
||||
{
|
||||
wprintf(L"CreateRemoteThread failed: %lu\n", GetLastError());
|
||||
return ThreadResult::NotStarted;
|
||||
}
|
||||
if (WaitForSingleObject(thread.value, INFINITE) != WAIT_OBJECT_0 || !GetExitCodeThread(thread.value, &exitCode))
|
||||
{
|
||||
wprintf(L"waiting for the remote thread failed: %lu\n", GetLastError());
|
||||
return ThreadResult::Uncertain;
|
||||
}
|
||||
return ThreadResult::Completed;
|
||||
}
|
||||
|
||||
static const wchar_t* StageName(BootstrapStage stage)
|
||||
{
|
||||
switch (stage)
|
||||
{
|
||||
case BootstrapStage::CreateInstance: return L"CLRCreateInstance";
|
||||
case BootstrapStage::GetRuntime: return L"GetRuntime";
|
||||
case BootstrapStage::GetInterface: return L"GetInterface";
|
||||
case BootstrapStage::Start: return L"Start";
|
||||
case BootstrapStage::Execute: return L"ExecuteInDefaultAppDomain";
|
||||
case BootstrapStage::RegisterUnwind: return L"RtlAddFunctionTable";
|
||||
case BootstrapStage::GetDelegates: return L"hostfxr_get_runtime_delegate";
|
||||
case BootstrapStage::LoadAssembly: return L"load_assembly";
|
||||
case BootstrapStage::GetEntryPoint: return L"get_function_pointer";
|
||||
case BootstrapStage::Invoke: return L"TerraSharp.Entry.Init";
|
||||
case BootstrapStage::RemoveUnwind: return L"RtlDeleteFunctionTable";
|
||||
default: return L"bootstrap";
|
||||
}
|
||||
}
|
||||
|
||||
#pragma pack(push, 1)
|
||||
// Tail-calling RtlAddFunctionTable needs no unwind entry of its own.
|
||||
struct UnwindRegistration
|
||||
{
|
||||
BYTE loadTable[2]{ 0x48, 0xB9 };
|
||||
UINT64 table;
|
||||
BYTE loadCount{ 0xBA };
|
||||
DWORD count{ 1 };
|
||||
BYTE loadBase[2]{ 0x49, 0xB8 };
|
||||
UINT64 base;
|
||||
BYTE loadFunction[2]{ 0x48, 0xB8 };
|
||||
UINT64 function;
|
||||
BYTE tailCall[2]{ 0xFF, 0xE0 };
|
||||
};
|
||||
#pragma pack(pop)
|
||||
static_assert(sizeof(UnwindRegistration) == 37);
|
||||
|
||||
static int Inject(HANDLE process, DWORD processId, const Target& target, const std::wstring& assembly)
|
||||
{
|
||||
BOOL wow64 = FALSE;
|
||||
if (!IsWow64Process(process, &wow64))
|
||||
{
|
||||
wprintf(L"IsWow64Process failed: %lu\n", GetLastError());
|
||||
return 4;
|
||||
}
|
||||
if ((wow64 != FALSE) != target.x86)
|
||||
{
|
||||
wprintf(L"%s must be an %s process\n", target.name, target.x86 ? L"x86" : L"x64");
|
||||
return 4;
|
||||
}
|
||||
BootstrapData data{};
|
||||
data.result = E_PENDING;
|
||||
data.managedResult = -1;
|
||||
void* addFunctionTable = nullptr;
|
||||
void* deleteFunctionTable = nullptr;
|
||||
if (target.x86)
|
||||
{
|
||||
data.clrCreateInstance = reinterpret_cast<UINT64>(RemoteExport(process, true, L"mscoree.dll", "CLRCreateInstance"));
|
||||
if (!data.clrCreateInstance)
|
||||
{
|
||||
wprintf(L"CLRCreateInstance is unavailable in target; wait for Terraria to finish starting\n");
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
data.getRuntimeDelegate = reinterpret_cast<UINT64>(RemoteExport(process, false, L"hostfxr.dll", "hostfxr_get_runtime_delegate"));
|
||||
addFunctionTable = RemoteExport(process, false, L"ntdll.dll", "RtlAddFunctionTable");
|
||||
deleteFunctionTable = RemoteExport(process, false, L"ntdll.dll", "RtlDeleteFunctionTable");
|
||||
if (!data.getRuntimeDelegate || !addFunctionTable || !deleteFunctionTable)
|
||||
{
|
||||
wprintf(L"hostfxr or unwind APIs are unavailable in target; wait for tModLoader to finish starting\n");
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring type = L"TerraSharp.Entry";
|
||||
if (!target.x86)
|
||||
{
|
||||
size_t start = assembly.find_last_of(L"\\/") + 1;
|
||||
type += L", " + assembly.substr(start, assembly.size() - start - 4);
|
||||
}
|
||||
const std::wstring version = L"v4.0.30319", method = L"Init", argument;
|
||||
SIZE_T dataSize = sizeof(BootstrapData) + (assembly.size() + type.size() + version.size() + method.size() + argument.size() + 5) * sizeof(wchar_t) + 5 * sizeof(GUID) + 4;
|
||||
SIZE_T codeSize = target.x86 ? sizeof(BootstrapX86) : sizeof(BootstrapX64);
|
||||
SIZE_T unwindOffset = (codeSize + 3) & ~SIZE_T{ 3 };
|
||||
SIZE_T tableOffset = (unwindOffset + sizeof(BootstrapX64Unwind) + 3) & ~SIZE_T{ 3 };
|
||||
SIZE_T registrationOffset = tableOffset + sizeof(RUNTIME_FUNCTION);
|
||||
SIZE_T allocationSize = target.x86 ? codeSize : registrationOffset + sizeof(UnwindRegistration);
|
||||
RemoteMemory remoteData(process, dataSize, target.x86);
|
||||
RemoteMemory remoteCode(process, allocationSize, target.x86);
|
||||
if (!remoteData.address || !remoteCode.address)
|
||||
{
|
||||
wprintf(L"remote allocation failed: %lu\n", GetLastError());
|
||||
return 5;
|
||||
}
|
||||
std::vector<unsigned char> payload(sizeof(BootstrapData));
|
||||
auto append = [&](const void* bytes, SIZE_T size)
|
||||
{
|
||||
UINT64 address = reinterpret_cast<UINT64>(remoteData.address) + payload.size();
|
||||
auto begin = static_cast<const unsigned char*>(bytes);
|
||||
payload.insert(payload.end(), begin, begin + size);
|
||||
return address;
|
||||
};
|
||||
auto text = [&](const std::wstring& value) { return append(value.c_str(), (value.size() + 1) * sizeof(wchar_t)); };
|
||||
data.assembly = text(assembly);
|
||||
data.typeName = text(type);
|
||||
data.methodName = text(method);
|
||||
data.argument = text(argument);
|
||||
data.runtimeVersion = text(version);
|
||||
payload.resize((payload.size() + 3) & ~SIZE_T{ 3 });
|
||||
const GUID guids[]{ CLSID_CLRMetaHost, __uuidof(ICLRMetaHost), __uuidof(ICLRRuntimeInfo), CLSID_CLRRuntimeHost, __uuidof(ICLRRuntimeHost) };
|
||||
data.clsidMetaHost = append(&guids[0], sizeof(GUID));
|
||||
data.iidMetaHost = append(&guids[1], sizeof(GUID));
|
||||
data.iidRuntimeInfo = append(&guids[2], sizeof(GUID));
|
||||
data.clsidRuntimeHost = append(&guids[3], sizeof(GUID));
|
||||
data.iidRuntimeHost = append(&guids[4], sizeof(GUID));
|
||||
memcpy(payload.data(), &data, sizeof(data));
|
||||
|
||||
std::vector<unsigned char> code(allocationSize);
|
||||
memcpy(code.data(), target.x86 ? BootstrapX86 : BootstrapX64, codeSize);
|
||||
auto codeBase = static_cast<unsigned char*>(remoteCode.address);
|
||||
void* functionTable = codeBase + tableOffset;
|
||||
if (!target.x86)
|
||||
{
|
||||
memcpy(code.data() + unwindOffset, BootstrapX64Unwind, sizeof(BootstrapX64Unwind));
|
||||
RUNTIME_FUNCTION function{ BootstrapX64Begin, BootstrapX64End, static_cast<DWORD>(unwindOffset) + BootstrapX64UnwindData };
|
||||
memcpy(code.data() + tableOffset, &function, sizeof(function));
|
||||
UnwindRegistration registration{};
|
||||
registration.table = reinterpret_cast<UINT64>(functionTable);
|
||||
registration.base = reinterpret_cast<UINT64>(codeBase);
|
||||
registration.function = reinterpret_cast<UINT64>(addFunctionTable);
|
||||
memcpy(code.data() + registrationOffset, ®istration, sizeof(registration));
|
||||
}
|
||||
if (!WriteProcessMemory(process, remoteData.address, payload.data(), payload.size(), nullptr) || !WriteProcessMemory(process, remoteCode.address, code.data(), code.size(), nullptr))
|
||||
{
|
||||
wprintf(L"remote write failed: %lu\n", GetLastError());
|
||||
return 5;
|
||||
}
|
||||
|
||||
DWORD oldProtect;
|
||||
if (!VirtualProtectEx(process, remoteCode.address, allocationSize, PAGE_EXECUTE_READ, &oldProtect) || !FlushInstructionCache(process, remoteCode.address, allocationSize))
|
||||
{
|
||||
wprintf(L"preparing bootstrap code failed: %lu\n", GetLastError());
|
||||
return 5;
|
||||
}
|
||||
|
||||
DWORD exitCode = 0;
|
||||
if (!target.x86)
|
||||
{
|
||||
ThreadResult registered = RemoteCall(process, codeBase + registrationOffset, nullptr, exitCode);
|
||||
if (registered != ThreadResult::Completed || !(exitCode & 0xFF))
|
||||
{
|
||||
if (registered == ThreadResult::Uncertain)
|
||||
remoteCode.address = remoteData.address = nullptr;
|
||||
wprintf(L"RtlAddFunctionTable failed\n");
|
||||
return 6;
|
||||
}
|
||||
}
|
||||
ThreadResult ran = RemoteCall(process, codeBase + (target.x86 ? 0 : BootstrapX64Begin), remoteData.address, exitCode);
|
||||
if (ran == ThreadResult::Uncertain)
|
||||
{
|
||||
// A running thread or registered unwind table can still reference these allocations.
|
||||
remoteCode.address = remoteData.address = nullptr;
|
||||
return 6;
|
||||
}
|
||||
bool read = ran == ThreadResult::Completed && ReadProcessMemory(process, remoteData.address, &data, sizeof(data), nullptr);
|
||||
DWORD readError = read ? ERROR_SUCCESS : GetLastError();
|
||||
if (!target.x86)
|
||||
{
|
||||
DWORD removed = 0;
|
||||
if (RemoteCall(process, deleteFunctionTable, functionTable, removed) != ThreadResult::Completed || !(removed & 0xFF))
|
||||
{
|
||||
remoteCode.address = remoteData.address = nullptr;
|
||||
wprintf(L"RtlDeleteFunctionTable failed; keeping the remote bootstrap allocated\n");
|
||||
return 6;
|
||||
}
|
||||
}
|
||||
if (ran == ThreadResult::NotStarted)
|
||||
return 6;
|
||||
if (!read)
|
||||
{
|
||||
wprintf(L"reading bootstrap result failed: %lu\n", readError);
|
||||
return 7;
|
||||
}
|
||||
if (!data.completed)
|
||||
{
|
||||
wprintf(L"bootstrap stopped during %s (exit=0x%08lX)\n", StageName(data.stage), exitCode);
|
||||
return 7;
|
||||
}
|
||||
if (FAILED(data.result))
|
||||
{
|
||||
wprintf(L"%s failed (hr=0x%08lX)\n", StageName(data.stage), static_cast<DWORD>(data.result));
|
||||
return 7;
|
||||
}
|
||||
if (data.managedResult != 0)
|
||||
{
|
||||
wprintf(L"TerraSharp.Entry.Init failed (return=0x%08lX)\n", static_cast<DWORD>(data.managedResult));
|
||||
return 8;
|
||||
}
|
||||
wprintf(L"injected into %s pid %lu\n", target.name, processId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static std::wstring FullPath(const wchar_t* path)
|
||||
{
|
||||
DWORD length = GetFullPathNameW(path, 0, nullptr, nullptr);
|
||||
if (!length)
|
||||
return {};
|
||||
std::vector<wchar_t> buffer(length);
|
||||
DWORD written = GetFullPathNameW(path, length, buffer.data(), nullptr);
|
||||
return written && written < length ? std::wstring(buffer.data(), written) : std::wstring();
|
||||
}
|
||||
|
||||
static bool IsFile(const std::wstring& path)
|
||||
{
|
||||
DWORD attributes = GetFileAttributesW(path.c_str());
|
||||
return attributes != INVALID_FILE_ATTRIBUTES && !(attributes & FILE_ATTRIBUTE_DIRECTORY);
|
||||
}
|
||||
|
||||
static std::wstring WithSlash(std::wstring path)
|
||||
{
|
||||
if (!path.empty() && path.back() != L'\\' && path.back() != L'/')
|
||||
path += L'\\';
|
||||
return path;
|
||||
}
|
||||
|
||||
// Matched by prefix: build-and-inject.bat suffixes the assembly and file name with a per-build stamp.
|
||||
static std::wstring FindAssembly(const std::wstring& directory, const Target& target)
|
||||
{
|
||||
WIN32_FIND_DATAW found;
|
||||
HANDLE search = FindFirstFileW((directory + target.assembly + L"*.dll").c_str(), &found);
|
||||
if (search == INVALID_HANDLE_VALUE)
|
||||
return {};
|
||||
std::wstring result = directory + found.cFileName;
|
||||
bool single = !FindNextFileW(search, &found);
|
||||
FindClose(search);
|
||||
return single ? result : std::wstring();
|
||||
}
|
||||
|
||||
int wmain(int argc, wchar_t** argv)
|
||||
{
|
||||
if (argc > 2)
|
||||
{
|
||||
wprintf(L"usage: TerraSharp.Injector.exe [TerraSharp.<Vanilla|tModLoader>.dll | build directory]\n");
|
||||
return 1;
|
||||
}
|
||||
std::vector<wchar_t> self(32768);
|
||||
DWORD selfLength = GetModuleFileNameW(nullptr, self.data(), static_cast<DWORD>(self.size()));
|
||||
std::wstring exeDirectory = selfLength && selfLength < self.size() ? std::wstring(self.data(), selfLength) : std::wstring();
|
||||
exeDirectory.resize(exeDirectory.find_last_of(L"\\/") + 1);
|
||||
std::wstring input = argc == 2 ? FullPath(argv[1]) : exeDirectory;
|
||||
if (input.empty() || exeDirectory.empty())
|
||||
{
|
||||
wprintf(L"resolving paths failed: %lu\n", GetLastError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Target* target = nullptr;
|
||||
DWORD processId = 0;
|
||||
std::wstring assembly;
|
||||
if (IsFile(input))
|
||||
{
|
||||
PeImage image;
|
||||
WORD machine = image.Load(input) ? image.Machine() : 0;
|
||||
target = machine == IMAGE_FILE_MACHINE_I386 ? &Targets[0] : machine == IMAGE_FILE_MACHINE_AMD64 ? &Targets[1] : nullptr;
|
||||
if (!target)
|
||||
{
|
||||
wprintf(L"not an x86 or x64 TerraSharp build: %s\n", input.c_str());
|
||||
return 1;
|
||||
}
|
||||
assembly = input;
|
||||
processId = FindProcess(target->game);
|
||||
}
|
||||
else
|
||||
{
|
||||
DWORD terraria = FindProcess(Game::Terraria), tModLoader = FindProcess(Game::TModLoader);
|
||||
if (terraria && tModLoader)
|
||||
{
|
||||
wprintf(L"Terraria and tModLoader are both running; pass the DLL to inject\n");
|
||||
return 2;
|
||||
}
|
||||
target = tModLoader ? &Targets[1] : &Targets[0];
|
||||
processId = tModLoader ? tModLoader : terraria;
|
||||
std::wstring directory = WithSlash(input);
|
||||
assembly = FindAssembly(directory, *target);
|
||||
if (processId && assembly.empty())
|
||||
{
|
||||
wprintf(L"expected exactly one %s*.dll in %s\n", target->assembly, directory.c_str());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (!IsFile(assembly))
|
||||
{
|
||||
wprintf(L"managed DLL not found: %s\n", assembly.c_str());
|
||||
return 1;
|
||||
}
|
||||
if (!processId)
|
||||
{
|
||||
wprintf(L"%s is not running\n", target->name);
|
||||
return 2;
|
||||
}
|
||||
Handle process(OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, processId));
|
||||
if (!process.value)
|
||||
{
|
||||
wprintf(L"OpenProcess failed: %lu\n", GetLastError());
|
||||
return 3;
|
||||
}
|
||||
return Inject(process.value, processId, *target, assembly);
|
||||
}
|
||||
Reference in new issue
Block a user