feat: Add TerraSharp for Terraria and tModLoader

This commit is contained in:
VAC committed 2026-10-14 23:22:54 -03:00
commit 58eb32c7ed
70 files changed
+8678

No files matched your search

+93
View File
@@ -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
+134
View File
@@ -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
+55
View File
@@ -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);
+155
View File
@@ -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)
+574
View File
@@ -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, &registration, 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);
}