From 4f78563a2f075d23ce115095c8c406d40bb6bbec Mon Sep 17 00:00:00 2001 From: Raony Fernandes dos Reis Date: Sat, 11 Oct 2025 02:41:32 -0300 Subject: [PATCH 01/14] Lots of op stuff --- ModuleTweaker/Commands/MainCommand.cs | 49 +- ModuleTweaker/Patching/IPatcher.cs | 15 + ModuleTweaker/Patching/ImportedSymbol.cs | 31 ++ ModuleTweaker/Patching/ImporterHeader.cs | 70 +++ .../{Models => PE}/ImportDescriptor.cs | 2 +- ModuleTweaker/Patching/PE/PEImporterHeader.cs | 35 ++ ModuleTweaker/Patching/PE/PEPatcher.cs | 211 +++++++++ ModuleTweaker/Patching/PEFileHelper.cs | 446 ------------------ .../Patching/Symbols/FunctionSymbol.cs | 65 +++ .../Patching/Symbols/VariableSymbol.cs | 26 + .../Patching/Symbols/VirtualPointerSymbol.cs | 26 + 11 files changed, 519 insertions(+), 457 deletions(-) create mode 100644 ModuleTweaker/Patching/IPatcher.cs create mode 100644 ModuleTweaker/Patching/ImportedSymbol.cs create mode 100644 ModuleTweaker/Patching/ImporterHeader.cs rename ModuleTweaker/Patching/{Models => PE}/ImportDescriptor.cs (98%) create mode 100644 ModuleTweaker/Patching/PE/PEImporterHeader.cs create mode 100644 ModuleTweaker/Patching/PE/PEPatcher.cs delete mode 100644 ModuleTweaker/Patching/PEFileHelper.cs create mode 100644 ModuleTweaker/Patching/Symbols/FunctionSymbol.cs create mode 100644 ModuleTweaker/Patching/Symbols/VariableSymbol.cs create mode 100644 ModuleTweaker/Patching/Symbols/VirtualPointerSymbol.cs diff --git a/ModuleTweaker/Commands/MainCommand.cs b/ModuleTweaker/Commands/MainCommand.cs index 16dc34b..73df92e 100644 --- a/ModuleTweaker/Commands/MainCommand.cs +++ b/ModuleTweaker/Commands/MainCommand.cs @@ -1,11 +1,14 @@ using Amethyst.Common.Diagnostics; using Amethyst.Common.Models; using Amethyst.ModuleTweaker.Patching; +using Amethyst.ModuleTweaker.Patching.PE; +using Amethyst.ModuleTweaker.Patching.Symbols; using AsmResolver.PE.File; using CliFx; using CliFx.Attributes; using CliFx.Infrastructure; using Newtonsoft.Json; +using System.Globalization; namespace Amethyst.ModuleTweaker.Commands { @@ -34,12 +37,20 @@ public ValueTask ExecuteAsync(IConsole console) return default; } + ulong ParseAddress(string? address) + { + if (string.IsNullOrEmpty(address)) + return 0x0; + if (address.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + address = address[2..]; + if (!ulong.TryParse(address, NumberStyles.HexNumber, null, out var addr)) + return 0x0; + return addr; + } + // Collect all symbol files and accumulate mangled names IEnumerable symbolFiles = symbols.EnumerateFiles("*.json", SearchOption.AllDirectories); - HashSet methods = []; - HashSet variables = []; - HashSet vtables = []; - HashSet vfuncs = []; + List importedSymbols = []; foreach (var symbolFile in symbolFiles) { using var stream = symbolFile.OpenRead(); @@ -54,25 +65,43 @@ public ValueTask ExecuteAsync(IConsole console) { if (string.IsNullOrEmpty(function.Name)) continue; - methods.Add(function); + + importedSymbols.Add(new FunctionSymbol { + Name = function.Name, + IsVirtual = false, + IsSignature = function.Signature is not null, + Address = ParseAddress(function.Address), + Signature = function.Signature ?? string.Empty + }); } foreach (var variable in symbolJson.Variables) { if (string.IsNullOrEmpty(variable.Name)) continue; - variables.Add(variable); + importedSymbols.Add(new VariableSymbol { + Name = variable.Name, + Address = ParseAddress(variable.Address) + }); } foreach (var vtable in symbolJson.VirtualTables) { if (string.IsNullOrEmpty(vtable.Name)) continue; - vtables.Add(vtable); + importedSymbols.Add(new VirtualPointerSymbol { + Name = vtable.Name, + Address = ParseAddress(vtable.Address) + }); } foreach (var vfunc in symbolJson.VirtualFunctions) { if (string.IsNullOrEmpty(vfunc.Name)) continue; - vfuncs.Add(vfunc); + importedSymbols.Add(new FunctionSymbol { + Name = vfunc.Name, + IsVirtual = true, + VirtualIndex = vfunc.Index, + VirtualTable = vfunc.VirtualTable ?? "this" + }); } break; } @@ -83,8 +112,8 @@ public ValueTask ExecuteAsync(IConsole console) { // Patch the module var file = PEFile.FromFile(ModulePath); - PEFileHelper helper = new(file); - if (helper.Patch(methods, variables, vtables, vfuncs)) + var patcher = new PEPatcher(file, importedSymbols); + if (patcher.Patch()) { file.AlignSections(); File.Copy(ModulePath, ModulePath + ".backup", true); diff --git a/ModuleTweaker/Patching/IPatcher.cs b/ModuleTweaker/Patching/IPatcher.cs new file mode 100644 index 0000000..ad95f55 --- /dev/null +++ b/ModuleTweaker/Patching/IPatcher.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching { + public interface IPatcher { + bool IsPatched(); + bool RemoveSection(string name); + void Unpatch(); + bool Patch(); + bool IsCustomSection(string name); + } +} diff --git a/ModuleTweaker/Patching/ImportedSymbol.cs b/ModuleTweaker/Patching/ImportedSymbol.cs new file mode 100644 index 0000000..cb07ccb --- /dev/null +++ b/ModuleTweaker/Patching/ImportedSymbol.cs @@ -0,0 +1,31 @@ +using AsmResolver.IO; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching { + public enum SymbolType { + Function, + Variable, + VirtualPointer + } + + public abstract class ImportedSymbol { + public string Name { get; set; } = string.Empty; + + public abstract SymbolType GetSymbolType(); + + public virtual void Write(BinaryWriter writer) { + // Version 1 layout: + // [1 byte ] SymbolType (0 = Function, 1 = Variable, 2 = VirtualPointer) + // [4 bytes] Name length (N) + // [N bytes] Name (UTF-8) + + writer.Write((byte)GetSymbolType()); + writer.Write(Encoding.UTF8.GetByteCount(Name)); + writer.Write(Encoding.UTF8.GetBytes(Name)); + } + } +} diff --git a/ModuleTweaker/Patching/ImporterHeader.cs b/ModuleTweaker/Patching/ImporterHeader.cs new file mode 100644 index 0000000..d11fc5d --- /dev/null +++ b/ModuleTweaker/Patching/ImporterHeader.cs @@ -0,0 +1,70 @@ +using AsmResolver.IO; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching { + public enum HeaderFormat { + PE + } + + public interface IHeaderContext { + HeaderFormat GetFormat(); + } + + public class ImporterHeader { + public const string Magic = "RTIH"; + public HeaderFormat Format { get; set; } = HeaderFormat.PE; + public uint Version { get; set; } = 1; + public List Symbols { get; set; } = []; + + public virtual void Write(IHeaderContext ctx, BinaryWriter writer) { + // Version 1 layout: + // [1 byte ] Format (0 = PE) + // [4 bytes] Magic "RTIH" + // [4 bytes] Version (1) + // [8 bytes] Size of symbols section + // [4 bytes] Number of symbols + // [N bytes] Symbols + // [Classes that inherit can add more data here] + + // Validate context format + if (ctx.GetFormat() != Format) + throw new InvalidOperationException($"Header format mismatch. Context format is {ctx.GetFormat()}, but header format is {Format}."); + + // Write magic, format and version + writer.Write(Encoding.ASCII.GetBytes(Magic)); + writer.Write((byte)Format); + writer.Write(Version); + + // Reserve space for symbol block size + long sizePos = writer.BaseStream.Position; + writer.Write(0L); + + // Write symbol count and symbols + writer.Write(Symbols.Count); + + // Remember position to calculate size later + long lastPos = writer.BaseStream.Position; + + // Write symbols + foreach (var symbol in Symbols) + symbol.Write(writer); + + // Get end position + long endPos = writer.BaseStream.Position; + + // Calculate symbol block size + long symbolsSize = endPos - lastPos; + + // Seek back and write symbol block size + writer.BaseStream.Seek(sizePos, SeekOrigin.Begin); + writer.Write(symbolsSize); + + // Seek back to end + writer.BaseStream.Seek(endPos, SeekOrigin.Begin); + } + } +} diff --git a/ModuleTweaker/Patching/Models/ImportDescriptor.cs b/ModuleTweaker/Patching/PE/ImportDescriptor.cs similarity index 98% rename from ModuleTweaker/Patching/Models/ImportDescriptor.cs rename to ModuleTweaker/Patching/PE/ImportDescriptor.cs index 7372f2c..68af6ed 100644 --- a/ModuleTweaker/Patching/Models/ImportDescriptor.cs +++ b/ModuleTweaker/Patching/PE/ImportDescriptor.cs @@ -6,7 +6,7 @@ using System.Text; using System.Threading.Tasks; -namespace Amethyst.ModuleTweaker.Patching.Models +namespace Amethyst.ModuleTweaker.Patching.PE { [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct ImportDescriptor() diff --git a/ModuleTweaker/Patching/PE/PEImporterHeader.cs b/ModuleTweaker/Patching/PE/PEImporterHeader.cs new file mode 100644 index 0000000..e6477e4 --- /dev/null +++ b/ModuleTweaker/Patching/PE/PEImporterHeader.cs @@ -0,0 +1,35 @@ +using AsmResolver.PE.File; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching.PE { + public class PEHeaderContext(PEFile file, uint iatVirtualAddress, uint iatCount) : IHeaderContext { + public HeaderFormat GetFormat() => HeaderFormat.PE; + + public PEFile File { get; } = file; + public uint IATVirtualAddress { get; } = iatVirtualAddress; + public uint IATCount { get; } = iatCount; + } + + public class PEImporterHeader : ImporterHeader { + public override void Write(IHeaderContext ctx, BinaryWriter writer) { + // Version 1 PE-specific layout: + // [Base ImporterHeader layout] + // [4 bytes] "Minecraft.Windows.exe" IAT Virtual Address + // [4 bytes] "Minecraft.Windows.exe" IAT Count + + if (ctx is not PEHeaderContext peCtx) + throw new InvalidOperationException($"Invalid header context type. Expected PEHeaderContext, got {ctx.GetType()}."); + + // Call base to write common layout + base.Write(ctx, writer); + + // Write PE-specific fields + writer.Write(peCtx.IATVirtualAddress); + writer.Write(peCtx.IATCount); + } + } +} diff --git a/ModuleTweaker/Patching/PE/PEPatcher.cs b/ModuleTweaker/Patching/PE/PEPatcher.cs new file mode 100644 index 0000000..e276269 --- /dev/null +++ b/ModuleTweaker/Patching/PE/PEPatcher.cs @@ -0,0 +1,211 @@ +using Amethyst.Common.Diagnostics; +using AsmResolver; +using AsmResolver.IO; +using AsmResolver.PE.File; +using AsmResolver.PE.File.Headers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching.PE { + public class PEPatcher(PEFile file, List symbols) : IPatcher { + // Runtime Importer Header + public const string SectionRTIH = ".rtih"; + + // New Import Descriptor Table + public const string SectionIDNEW = ".idnew"; + + public static HashSet CustomSections { get; } = [ + SectionRTIH, + SectionIDNEW + ]; + + public PEFile File { get; } = file; + public List Symbols { get; } = symbols; + + public bool IsCustomSection(string name) { + return CustomSections.Contains(name); + } + + public bool IsPatched() { + foreach (var section in File.Sections) { + if (IsCustomSection(section.Name)) + return true; + } + return false; + } + + public bool Patch() { + // Version 1 full PE-specific layout: + // .rtih - Runtime Importer Header + // [PEImporterHeader data] + // .idnew - New Import Descriptor Table + // [4 bytes] Original IDT RVA + // [4 bytes] Original IDT Size + // [New IDT data (old IDT minus "Minecraft.Windows.exe")] + + if (IsPatched()) { + Logger.Debug("PE file is already patched, unpatching it first."); + Unpatch(); + } + + Logger.Debug("Patching PE file for runtime importing..."); + + // Get the import directory entries + var importDirectory = File.OptionalHeader.GetDataDirectory(DataDirectoryIndex.ImportDirectory); + if (!importDirectory.IsPresentInPE) { + Logger.Warn("PE file has no import directory, no reason to patch it."); + return false; + } + + // Read existing import descriptors + List importDescriptors = []; + { + var importDirectoryReader = File.CreateDataDirectoryReader(importDirectory); + while (importDirectoryReader.CanRead(ImportDescriptor.Size)) { + var descriptor = ImportDescriptor.Read(ref importDirectoryReader); + if (descriptor.IsZero) + break; + importDescriptors.Add(descriptor); + } + } + + // Find the import descriptor for "Minecraft.Windows.exe" + ImportDescriptor? minecraftWindowsImportDescriptor = null; + foreach (var descriptor in importDescriptors) { + var nameRva = descriptor.Name; + var name = File.CreateReaderAtRva(nameRva).ReadAsciiString(); + if (name.StartsWith("Minecraft.Windows", StringComparison.OrdinalIgnoreCase)) { + minecraftWindowsImportDescriptor = descriptor; + break; + } + } + + // If not found, no reason to patch + if (minecraftWindowsImportDescriptor is null) { + Logger.Warn("PE file does not import from 'Minecraft.Windows.exe', no reason to patch it."); + return false; + } + + // Map all mangled names for "Minecraft.Windows.exe" functions to IAT offsets + uint iatRva = minecraftWindowsImportDescriptor.Value.FirstThunk; + uint iatCount = 0; + Dictionary iatIndices = []; + { + var reader = File.CreateReaderAtRva(minecraftWindowsImportDescriptor.Value.OriginalFirstThunk); + uint iatIndex = 0; + while (reader.CanRead(sizeof(ulong))) { + ulong iltEntry = reader.ReadUInt64(); + if (iltEntry == 0) + break; + iatIndex++; + bool isOrdinal = (iltEntry & 0x8000000000000000) != 0; + if (isOrdinal) + continue; + + uint hintNameRva = (uint)(iltEntry & 0x7FFFFFFFFFFFFFFF); + var hintNameReader = File.CreateReaderAtRva(hintNameRva); + ushort hint = hintNameReader.ReadUInt16(); + string functionName = hintNameReader.ReadAsciiString(); + iatIndices[functionName] = iatIndex - 1; + } + iatCount = iatIndex; + } + + // Create the PEHeaderContext + var context = new PEHeaderContext(File, iatRva, iatCount); + + // Create the RTIH section + { + PESection section = new( + SectionRTIH, + SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); + using var ms = new MemoryStream(); + var writer = new BinaryWriter(ms); + + // Create the PEImporterHeader + var header = new PEImporterHeader() { + Format = HeaderFormat.PE, + Version = 1, + Symbols = Symbols + }; + + // Write the entire header + header.Write(context, writer); + + // Finalize the section + section.Contents = new DataSegment(ms.ToArray()); + File.Sections.Add(section); + } + + // Create new import descriptor table section + { + PESection section = new( + SectionIDNEW, + SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); + + using var ms = new MemoryStream(); + var writer = new BinaryStreamWriter(ms); + + // Write original import directory RVA and size at the start + writer.WriteUInt32(importDirectory.VirtualAddress); + writer.WriteUInt32(importDirectory.Size); + + foreach (var descriptor in importDescriptors) { + if (descriptor.Equals(minecraftWindowsImportDescriptor.Value)) + continue; + descriptor.Write(writer); + } + + // Write null descriptor at the end + ImportDescriptor.Empty.Write(writer); + section.Contents = new DataSegment(ms.ToArray()); + File.Sections.Add(section); + File.AlignSections(); + + // Update import directory to point to the new table + File.OptionalHeader.SetDataDirectory( + DataDirectoryIndex.ImportDirectory, + new(section.Rva + sizeof(uint) * 2, (uint)ms.Length)); + Logger.Debug("Removed import from 'Minecraft.Windows.exe'."); + } + Logger.Debug("Patching completed."); + return true; + } + + public bool RemoveSection(string name) { + var section = File.Sections.FirstOrDefault(s => s.Name == name); + if (section is not null) { + return File.Sections.Remove(section); + } + return false; + } + + public void Unpatch() { + if (!IsPatched()) + return; + Logger.Debug("Unpatching PE file..."); + for (int i = File.Sections.Count - 1; i >= 0; i--) { + var section = File.Sections[i]; + if (section.Name == SectionIDNEW) { + BinaryStreamReader reader = section.CreateReader(); + uint originalIDTRva = reader.ReadUInt32(); + uint originalIDTSize = reader.ReadUInt32(); + + File.OptionalHeader.SetDataDirectory( + DataDirectoryIndex.ImportDirectory, + new(originalIDTRva, originalIDTSize) + ); + } + + if (IsCustomSection(section.Name)) { + Logger.Debug($"Removing custom section '{section.Name}'..."); + File.Sections.RemoveAt(i); + } + } + File.AlignSections(); + } + } +} diff --git a/ModuleTweaker/Patching/PEFileHelper.cs b/ModuleTweaker/Patching/PEFileHelper.cs deleted file mode 100644 index d7127a3..0000000 --- a/ModuleTweaker/Patching/PEFileHelper.cs +++ /dev/null @@ -1,446 +0,0 @@ -using Amethyst.Common.Diagnostics; -using Amethyst.Common.Models; -using Amethyst.ModuleTweaker.Patching.Models; -using AsmResolver; -using AsmResolver.IO; -using AsmResolver.PE.File; -using AsmResolver.PE.File.Headers; -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using static System.Runtime.InteropServices.JavaScript.JSType; - -namespace Amethyst.ModuleTweaker.Patching -{ - public class PEFileHelper(PEFile file) - { - public const string StringTableName = ".strt"; - public const string FunctionDescriptorSectionName = ".fndt"; - public const string VirtualFunctionDescriptorSectionName = ".vfndt"; - public const string VariableDescriptorSectionName = ".vardt"; - public const string VirtualTableDescriptorSectionName = ".vtbdt"; - public const string NewImportDescriptorSectionName = ".idnew"; - public static HashSet SectionNames = [ - VariableDescriptorSectionName, - FunctionDescriptorSectionName, - StringTableName, - VirtualFunctionDescriptorSectionName, - VirtualTableDescriptorSectionName, - NewImportDescriptorSectionName - ]; - - public PEFile File { get; private set; } = file; - - public bool IsPatched() - { - foreach (var section in File.Sections) - { - if (IsCustomSectionName(section.Name)) - return true; - } - return false; - } - - public void RemoveSection(string name) - { - var section = File.Sections.FirstOrDefault(s => s.Name == name); - if (section is not null) - File.Sections.Remove(section); - } - - public void Unpatch() - { - if (!IsPatched()) - return; - Logger.Debug("Unpatching PE file..."); - for (int i = File.Sections.Count - 1; i >= 0; i--) - { - var section = File.Sections[i]; - if (section.Name == NewImportDescriptorSectionName) - { - BinaryStreamReader reader = section.CreateReader(); - uint originalIDTRva = reader.ReadUInt32(); - uint originalIDTSize = reader.ReadUInt32(); - - File.OptionalHeader.SetDataDirectory( - DataDirectoryIndex.ImportDirectory, - new(originalIDTRva, originalIDTSize) - ); - } - - if (IsCustomSectionName(section.Name)) - { - Logger.Debug($"Removing custom section '{section.Name}'..."); - File.Sections.RemoveAt(i); - } - } - File.AlignSections(); - } - - public bool Patch( - IEnumerable functions, - IEnumerable variables, - IEnumerable vtables, - IEnumerable vfunctions) - { - if (IsPatched()) - { - Logger.Debug("PE file is already patched, unpatching it first."); - Unpatch(); - } - - Logger.Debug("Patching PE file for runtime importing..."); - - // Get the import directory entries - var importDirectory = File.OptionalHeader.GetDataDirectory(DataDirectoryIndex.ImportDirectory); - if (!importDirectory.IsPresentInPE) - { - Logger.Warn("PE file has no import directory, no reason to patch it."); - return false; - } - - // Read existing import descriptors - List importDescriptors = []; - { - var importDirectoryReader = File.CreateDataDirectoryReader(importDirectory); - while (importDirectoryReader.CanRead(ImportDescriptor.Size)) - { - var descriptor = ImportDescriptor.Read(ref importDirectoryReader); - if (descriptor.IsZero) - break; - importDescriptors.Add(descriptor); - } - } - - // Find the import descriptor for "Minecraft.Windows.exe" - ImportDescriptor? minecraftWindowsImportDescriptor = null; - foreach (var descriptor in importDescriptors) - { - var nameRva = descriptor.Name; - var name = File.CreateReaderAtRva(nameRva).ReadAsciiString(); - if (name.StartsWith("Minecraft.Windows", StringComparison.OrdinalIgnoreCase)) - { - minecraftWindowsImportDescriptor = descriptor; - break; - } - } - - // If not found, no reason to patch - if (minecraftWindowsImportDescriptor is null) - { - Logger.Warn("PE file does not import from 'Minecraft.Windows.exe', no reason to patch it."); - return false; - } - - // Map all mangled names for "Minecraft.Windows.exe" functions to IAT offsets - uint iatRva = minecraftWindowsImportDescriptor.Value.FirstThunk; - uint iatSize = 0; - Dictionary iatIndices = []; - { - var reader = File.CreateReaderAtRva(minecraftWindowsImportDescriptor.Value.OriginalFirstThunk); - uint iatIndex = 0; - while (reader.CanRead(sizeof(ulong))) - { - ulong iltEntry = reader.ReadUInt64(); - if (iltEntry == 0) - break; - iatIndex++; - bool isOrdinal = (iltEntry & 0x8000000000000000) != 0; - if (isOrdinal) - continue; - - uint hintNameRva = (uint)(iltEntry & 0x7FFFFFFFFFFFFFFF); - var hintNameReader = File.CreateReaderAtRva(hintNameRva); - ushort hint = hintNameReader.ReadUInt16(); - string functionName = hintNameReader.ReadAsciiString(); - iatIndices[functionName] = iatIndex - 1; - } - iatSize = iatIndex; - } - - Dictionary stringToIndex = []; - { - // Create string table section - // Contains all strings (eg. mangled names, signatures) to be resolved at runtime - PESection section = new( - StringTableName, - SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); - - using var ms = new MemoryStream(); - var writer = new BinaryStreamWriter(ms); - uint countPosition = (uint)ms.Position; - writer.WriteUInt32(0u); // Placeholder for count - uint index = 0; - - // Write all function names - foreach (var method in functions) - { - writer.WriteBytes([.. Encoding.ASCII.GetBytes(method.Name), 0]); - stringToIndex[method.Name] = index++; - - } - - // Write all function signatures - foreach (var method in functions.Where(m => m.Signature is not null)) - { - writer.WriteBytes([.. Encoding.ASCII.GetBytes(method.Signature!), 0]); - stringToIndex[method.Signature!] = index++; - } - - // Write all variable names - foreach (var variable in variables) - { - writer.WriteBytes([.. Encoding.ASCII.GetBytes(variable.Name), 0]); - stringToIndex[variable.Name] = index++; - } - - // Write all virtual table names - foreach (var vtable in vtables) - { - writer.WriteBytes([.. Encoding.ASCII.GetBytes(vtable.Name), 0]); - stringToIndex[vtable.Name] = index++; - } - - // Write all virtual function names - foreach (var vfunc in vfunctions) - { - writer.WriteBytes([.. Encoding.ASCII.GetBytes(vfunc.Name), 0]); - stringToIndex[vfunc.Name] = index++; - } - - // Go back and write the count - ms.Seek(countPosition, SeekOrigin.Begin); - writer.WriteUInt32(index); - - section.Contents = new DataSegment(ms.ToArray()); - File.Sections.Add(section); - Logger.Info($"Added {index} strings."); - } - - // Create function descriptor table section - // Contains descriptors for all functions to be resolved at runtime - { - PESection section = new( - FunctionDescriptorSectionName, - SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); - using var ms = new MemoryStream(); - var writer = new BinaryStreamWriter(ms); - uint countPosition = (uint)ms.Position; - writer.WriteUInt32(0u); // Placeholder for count - writer.WriteUInt32(iatRva); - writer.WriteUInt32(iatSize); - uint count = 0; - foreach (var function in functions) - { - if (!iatIndices.TryGetValue(function.Name, out uint iatIndex)) - continue; - uint nameIndex = stringToIndex[function.Name]; - bool usesSignature = function.Signature is not null; - - writer.WriteUInt32(nameIndex); - writer.WriteUInt32(iatIndex); - writer.WriteByte((byte)(usesSignature ? 1 : 0)); - - if (usesSignature) - { - uint signatureIndex = stringToIndex[function.Signature!]; - writer.WriteUInt64(signatureIndex); - } - else - { - if (!ulong.TryParse(function.Address?.Replace("0x", "") ?? "0", out ulong address)) - writer.WriteUInt64(0x0); - else - writer.WriteUInt64(address); - } - - // uint: NameIndex - // uint: IATIndex - // byte: UsesSignature - // ulong: SignatureIndex or Address - count++; - Logger.Debug($"Tweaked: '{function.Name}'."); - } - - // Go back and write the count - ms.Seek(countPosition, SeekOrigin.Begin); - writer.WriteUInt32(count); - - section.Contents = new DataSegment(ms.ToArray()); - File.Sections.Add(section); - } - - // Create variable descriptor table section - // Contains descriptors for all variables to be resolved at runtime - { - PESection section = new( - VariableDescriptorSectionName, - SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); - using var ms = new MemoryStream(); - var writer = new BinaryStreamWriter(ms); - uint countPosition = (uint)ms.Position; - writer.WriteUInt32(0u); // Placeholder for count - writer.WriteUInt32(iatRva); - writer.WriteUInt32(iatSize); - uint count = 0; - foreach (var variable in variables) - { - if (!iatIndices.TryGetValue(variable.Name, out uint iatIndex)) - continue; - uint nameIndex = stringToIndex[variable.Name]; - - writer.WriteUInt32(nameIndex); - writer.WriteUInt32(iatIndex); - string address = variable.Address.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? variable.Address[2..] : variable.Address; - - try - { - ulong addressValue = ulong.Parse(address, NumberStyles.HexNumber); - writer.WriteUInt64(addressValue); - } - catch (Exception) - { - writer.WriteUInt64(0x0); - } - - // uint: NameIndex - // uint: IATIndex - // ulong: Address - count++; - Logger.Debug($"Tweaked: '{variable.Name}'."); - } - - // Go back and write the count - ms.Seek(countPosition, SeekOrigin.Begin); - writer.WriteUInt32(count); - - section.Contents = new DataSegment(ms.ToArray()); - File.Sections.Add(section); - } - - // Create virtual table descriptor table section - { - PESection section = new( - VirtualTableDescriptorSectionName, - SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); - using var ms = new MemoryStream(); - var writer = new BinaryStreamWriter(ms); - uint countPosition = (uint)ms.Position; - writer.WriteUInt32(0u); // Placeholder for count - uint count = 0; - foreach (var vtable in vtables) - { - uint nameIndex = stringToIndex[vtable.Name]; - - writer.WriteUInt32(nameIndex); - string address = vtable.Address.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? vtable.Address[2..] : vtable.Address; - - try - { - ulong addressValue = ulong.Parse(address, NumberStyles.HexNumber); - writer.WriteUInt64(addressValue); - } - catch (Exception) - { - writer.WriteUInt64(0x0); - } - - // uint: NameIndex - // ulong: Address - count++; - Logger.Debug($"Tweaked: '{vtable.Name}'."); - } - - // Go back and write the count - ms.Seek(countPosition, SeekOrigin.Begin); - writer.WriteUInt32(count); - - section.Contents = new DataSegment(ms.ToArray()); - File.Sections.Add(section); - } - - // Create virtual function descriptor table section - { - PESection section = new( - VirtualFunctionDescriptorSectionName, - SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); - using var ms = new MemoryStream(); - var writer = new BinaryStreamWriter(ms); - uint countPosition = (uint)ms.Position; - writer.WriteUInt32(0u); // Placeholder for count - writer.WriteUInt32(iatRva); - writer.WriteUInt32(iatSize); - uint count = 0; - foreach (var vfunc in vfunctions) - { - if (!iatIndices.TryGetValue(vfunc.Name, out uint iatIndex)) - continue; - if (!stringToIndex.TryGetValue(vfunc.VirtualTable, out uint vtableIndex)) - continue; - - uint nameIndex = stringToIndex[vfunc.Name]; - - writer.WriteUInt32(nameIndex); - writer.WriteUInt32(iatIndex); - writer.WriteUInt32(vtableIndex); - writer.WriteUInt32(vfunc.Index); - - // uint: NameIndex - // uint: IATIndex - // uint: VirtualTableNameIndex - // uint: FunctionIndex - count++; - Logger.Debug($"Tweaked: '{vfunc.Name}'."); - } - - // Go back and write the count - ms.Seek(countPosition, SeekOrigin.Begin); - writer.WriteUInt32(count); - - section.Contents = new DataSegment(ms.ToArray()); - File.Sections.Add(section); - } - - // Create new import descriptor table section - // Contains a copy of the original import descriptor table without the "Minecraft.Windows.exe" entry - { - PESection section = new( - NewImportDescriptorSectionName, - SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); - using var ms = new MemoryStream(); - var writer = new BinaryStreamWriter(ms); - - // Write original import directory RVA and size at the start - writer.WriteUInt32(importDirectory.VirtualAddress); - writer.WriteUInt32(importDirectory.Size); - - foreach (var descriptor in importDescriptors) - { - if (descriptor.Equals(minecraftWindowsImportDescriptor.Value)) - continue; - descriptor.Write(writer); - } - - // Write null descriptor at the end - ImportDescriptor.Empty.Write(writer); - section.Contents = new DataSegment(ms.ToArray()); - File.Sections.Add(section); - File.AlignSections(); - - // Update import directory to point to the new table - File.OptionalHeader.SetDataDirectory( - DataDirectoryIndex.ImportDirectory, - new(section.Rva + sizeof(uint) * 2, (uint)ms.Length)); - Logger.Debug("Removed import from 'Minecraft.Windows.exe'."); - } - Logger.Debug("Patching completed."); - return true; - } - - public static bool IsCustomSectionName(string name) => SectionNames.Contains(name); - } -} diff --git a/ModuleTweaker/Patching/Symbols/FunctionSymbol.cs b/ModuleTweaker/Patching/Symbols/FunctionSymbol.cs new file mode 100644 index 0000000..05972cf --- /dev/null +++ b/ModuleTweaker/Patching/Symbols/FunctionSymbol.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching.Symbols { + public class FunctionSymbol : ImportedSymbol { + public bool IsVirtual { get; set; } = false; + + // If IsVirtual is true, these fields are used. + public uint VirtualIndex { get; set; } = 0; + public string VirtualTable { get; set; } = string.Empty; + + // If IsVirtual is false, these fields are used. + public bool IsSignature { get; set; } = false; + + // If IsSignature is true, this field is used. + public string Signature { get; set; } = string.Empty; + + // If IsSignature is false, this field is used. + public ulong Address { get; set; } = 0; + + public override SymbolType GetSymbolType() { + return SymbolType.Function; + } + + public override void Write(BinaryWriter writer) { + // Version 1 FunctionSymbol-specific format: + // [Base ImporterSymbol layout] + // [1 byte ] IsVirtual (0 = false, 1 = true) + // If IsVirtual == 1: + // [4 bytes] VirtualIndex + // [4 bytes] VirtualTable length (N) + // [N bytes] VirtualTable (UTF-8) + // Else (IsVirtual == 0): + // [1 byte ] IsSignature (0 = false, 1 = true) + // If IsSignature == 1: + // [4 bytes] Signature length (N) + // [N bytes] Signature (UTF-8) + // Else (IsSignature == 0): + // [8 bytes] Address + + // Write base class data first + base.Write(writer); + + writer.Write((byte)(IsVirtual ? 1 : 0)); + if (IsVirtual) { + writer.Write(VirtualIndex); + writer.Write(Encoding.UTF8.GetByteCount(VirtualTable)); + writer.Write(Encoding.UTF8.GetBytes(VirtualTable)); + } + else { + writer.Write((byte)(IsSignature ? 1 : 0)); + if (IsSignature) { + writer.Write(Encoding.UTF8.GetByteCount(Signature)); + writer.Write(Encoding.UTF8.GetBytes(Signature)); + } + else { + writer.Write(Address); + } + } + } + } +} diff --git a/ModuleTweaker/Patching/Symbols/VariableSymbol.cs b/ModuleTweaker/Patching/Symbols/VariableSymbol.cs new file mode 100644 index 0000000..d878cb7 --- /dev/null +++ b/ModuleTweaker/Patching/Symbols/VariableSymbol.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching.Symbols { + public class VariableSymbol : ImportedSymbol { + public ulong Address { get; set; } = 0; + + public override SymbolType GetSymbolType() { + return SymbolType.Variable; + } + + public override void Write(BinaryWriter writer) { + // Version 1 VariableSymbol-specific format: + // [Base ImporterSymbol layout] + // [8 bytes] Address + + // Write base class data first + base.Write(writer); + + writer.Write(Address); + } + } +} diff --git a/ModuleTweaker/Patching/Symbols/VirtualPointerSymbol.cs b/ModuleTweaker/Patching/Symbols/VirtualPointerSymbol.cs new file mode 100644 index 0000000..0136f1d --- /dev/null +++ b/ModuleTweaker/Patching/Symbols/VirtualPointerSymbol.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching.Symbols { + public class VirtualPointerSymbol : ImportedSymbol { + public ulong Address { get; set; } = 0; + + public override SymbolType GetSymbolType() { + return SymbolType.VirtualPointer; + } + + public override void Write(BinaryWriter writer) { + // Version 1 VirtualPointerSymbol-specific format: + // [Base ImporterSymbol layout] + // [8 bytes] Address + + // Write base class data first + base.Write(writer); + + writer.Write(Address); + } + } +} From dbc3a917e629587adcdf829c2cd622cea8247c95 Mon Sep 17 00:00:00 2001 From: Raony Fernandes dos Reis Date: Sat, 11 Oct 2025 13:32:13 -0300 Subject: [PATCH 02/14] Remove PCH for debugging and removed some \n --- ModuleTweaker/Patching/PE/PEImporterHeader.cs | 3 +-- ModuleTweaker/Patching/Symbols/FunctionSymbol.cs | 1 - ModuleTweaker/Patching/Symbols/VariableSymbol.cs | 1 - ModuleTweaker/Patching/Symbols/VirtualPointerSymbol.cs | 1 - SymbolGenerator/Properties/launchSettings.json | 2 +- 5 files changed, 2 insertions(+), 6 deletions(-) diff --git a/ModuleTweaker/Patching/PE/PEImporterHeader.cs b/ModuleTweaker/Patching/PE/PEImporterHeader.cs index e6477e4..19f644a 100644 --- a/ModuleTweaker/Patching/PE/PEImporterHeader.cs +++ b/ModuleTweaker/Patching/PE/PEImporterHeader.cs @@ -25,9 +25,8 @@ public override void Write(IHeaderContext ctx, BinaryWriter writer) { throw new InvalidOperationException($"Invalid header context type. Expected PEHeaderContext, got {ctx.GetType()}."); // Call base to write common layout - base.Write(ctx, writer); - // Write PE-specific fields + base.Write(ctx, writer); writer.Write(peCtx.IATVirtualAddress); writer.Write(peCtx.IATCount); } diff --git a/ModuleTweaker/Patching/Symbols/FunctionSymbol.cs b/ModuleTweaker/Patching/Symbols/FunctionSymbol.cs index 05972cf..504d37c 100644 --- a/ModuleTweaker/Patching/Symbols/FunctionSymbol.cs +++ b/ModuleTweaker/Patching/Symbols/FunctionSymbol.cs @@ -43,7 +43,6 @@ public override void Write(BinaryWriter writer) { // Write base class data first base.Write(writer); - writer.Write((byte)(IsVirtual ? 1 : 0)); if (IsVirtual) { writer.Write(VirtualIndex); diff --git a/ModuleTweaker/Patching/Symbols/VariableSymbol.cs b/ModuleTweaker/Patching/Symbols/VariableSymbol.cs index d878cb7..104c713 100644 --- a/ModuleTweaker/Patching/Symbols/VariableSymbol.cs +++ b/ModuleTweaker/Patching/Symbols/VariableSymbol.cs @@ -19,7 +19,6 @@ public override void Write(BinaryWriter writer) { // Write base class data first base.Write(writer); - writer.Write(Address); } } diff --git a/ModuleTweaker/Patching/Symbols/VirtualPointerSymbol.cs b/ModuleTweaker/Patching/Symbols/VirtualPointerSymbol.cs index 0136f1d..efd6e38 100644 --- a/ModuleTweaker/Patching/Symbols/VirtualPointerSymbol.cs +++ b/ModuleTweaker/Patching/Symbols/VirtualPointerSymbol.cs @@ -19,7 +19,6 @@ public override void Write(BinaryWriter writer) { // Write base class data first base.Write(writer); - writer.Write(Address); } } diff --git a/SymbolGenerator/Properties/launchSettings.json b/SymbolGenerator/Properties/launchSettings.json index 4ccf029..2a964cb 100644 --- a/SymbolGenerator/Properties/launchSettings.json +++ b/SymbolGenerator/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Generate Symbols": { "commandName": "Project", - "commandLineArgs": "-i \"./input/src\"\r\n-o \"./output\"\r\n-f \"mc\"\r\n-p \"win-client\"\r\n--\r\n-x c++\r\n-std=c++23\r\n-include-pch \"./input/src/pch.hpp.pch\"\r\n-fms-extensions\r\n-fms-compatibility\r\n-fms-define-stdc\r\n-I\"./input/include\"\r\n-I\"./input/src\"" + "commandLineArgs": "-i \"./input/src\"\r\n-o \"./output\"\r\n-f \"mc\"\r\n-p \"win-client\"\r\n--\r\n-x c++\r\n-std=c++23\r\n\r\n-fms-extensions\r\n-fms-compatibility\r\n-fms-define-stdc\r\n-I\"./input/include\"\r\n-I\"./input/src\"" } } } \ No newline at end of file From 993db45e363087679ba8044b30bb4568c6244031 Mon Sep 17 00:00:00 2001 From: Raony Fernandes dos Reis Date: Sat, 11 Oct 2025 13:38:37 -0300 Subject: [PATCH 03/14] Bump versions for ModuleTweaker and SymbolGenerator Updated Amethyst.ModuleTweaker to version 2.0.0 and Amethyst.SymbolGenerator to version 1.0.9 to reflect new releases or changes. Amethyst.ModuleTweaker has a new section and patching model, so... breaking changes --- ModuleTweaker/Amethyst.ModuleTweaker.csproj | 2 +- SymbolGenerator/Amethyst.SymbolGenerator.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ModuleTweaker/Amethyst.ModuleTweaker.csproj b/ModuleTweaker/Amethyst.ModuleTweaker.csproj index 90eab50..e1fcf33 100644 --- a/ModuleTweaker/Amethyst.ModuleTweaker.csproj +++ b/ModuleTweaker/Amethyst.ModuleTweaker.csproj @@ -7,7 +7,7 @@ enable Amethyst.ModuleTweaker Amethyst.ModuleTweaker - 1.0.6 + 2.0.0 diff --git a/SymbolGenerator/Amethyst.SymbolGenerator.csproj b/SymbolGenerator/Amethyst.SymbolGenerator.csproj index 6a156f2..57c43f3 100644 --- a/SymbolGenerator/Amethyst.SymbolGenerator.csproj +++ b/SymbolGenerator/Amethyst.SymbolGenerator.csproj @@ -8,7 +8,7 @@ enable True Amethyst.SymbolGenerator - 1.0.8 + 1.0.9 From 2d5dc3dfa21628ac6bdfa8d20140d5bf4d4755ed Mon Sep 17 00:00:00 2001 From: Raony Fernandes dos Reis Date: Sat, 11 Oct 2025 14:28:38 -0300 Subject: [PATCH 04/14] Stuff --- Common/Tracking/FileTracker.cs | 2 +- SymbolGenerator/Parsing/ASTVisitor.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Common/Tracking/FileTracker.cs b/Common/Tracking/FileTracker.cs index 44c8df7..1b05cf1 100644 --- a/Common/Tracking/FileTracker.cs +++ b/Common/Tracking/FileTracker.cs @@ -72,7 +72,7 @@ public FileTracker(DirectoryInfo inputDirectory, FileInfo checksumFile, string[] if (Filters.Any() && !Filters.Any(f => Path.GetRelativePath(InputDirectory.FullName, file.FullName).StartsWith(f))) continue; string filePath = file.FullName.NormalizeSlashes(); -#if !DEBUG +#if DEBUG string content = File.ReadAllText(file.FullName); ulong hash = XXH64.DigestOf(Encoding.UTF8.GetBytes(content)); diff --git a/SymbolGenerator/Parsing/ASTVisitor.cs b/SymbolGenerator/Parsing/ASTVisitor.cs index 07de0b5..af41290 100644 --- a/SymbolGenerator/Parsing/ASTVisitor.cs +++ b/SymbolGenerator/Parsing/ASTVisitor.cs @@ -84,7 +84,7 @@ public bool PrintErrors() public string GetUsr(CXCursor cursor) { - return cursor.Usr.ToString() + cursor.IsDefinition; + return cursor.Usr.ToString() + "@" + cursor.IsDefinition; } public string GetSpelling(CXCursor cursor) From 753a34a72d269ec8b876fe3490b2d56fcc69adc0 Mon Sep 17 00:00:00 2001 From: Raony Fernandes dos Reis Date: Sat, 11 Oct 2025 22:41:31 -0300 Subject: [PATCH 05/14] Remove AsmResolver and move to LibObjectFile --- ModuleTweaker/Amethyst.ModuleTweaker.csproj | 6 +- ModuleTweaker/Commands/MainCommand.cs | 12 +- ModuleTweaker/Patching/IPatcher.cs | 1 - ModuleTweaker/Patching/ImportedSymbol.cs | 3 +- ModuleTweaker/Patching/ImporterHeader.cs | 3 +- ModuleTweaker/Patching/PE/ImportDescriptor.cs | 13 +- ModuleTweaker/Patching/PE/PEImporterHeader.cs | 2 +- ModuleTweaker/Patching/PE/PEPatcher.cs | 189 +++++------------- ModuleTweaker/Properties/launchSettings.json | 2 +- 9 files changed, 73 insertions(+), 158 deletions(-) diff --git a/ModuleTweaker/Amethyst.ModuleTweaker.csproj b/ModuleTweaker/Amethyst.ModuleTweaker.csproj index e1fcf33..d2ed894 100644 --- a/ModuleTweaker/Amethyst.ModuleTweaker.csproj +++ b/ModuleTweaker/Amethyst.ModuleTweaker.csproj @@ -11,13 +11,11 @@ - + - - - + diff --git a/ModuleTweaker/Commands/MainCommand.cs b/ModuleTweaker/Commands/MainCommand.cs index 73df92e..4cb7078 100644 --- a/ModuleTweaker/Commands/MainCommand.cs +++ b/ModuleTweaker/Commands/MainCommand.cs @@ -3,10 +3,10 @@ using Amethyst.ModuleTweaker.Patching; using Amethyst.ModuleTweaker.Patching.PE; using Amethyst.ModuleTweaker.Patching.Symbols; -using AsmResolver.PE.File; using CliFx; using CliFx.Attributes; using CliFx.Infrastructure; +using LibObjectFile.PE; using Newtonsoft.Json; using System.Globalization; @@ -111,13 +111,15 @@ ulong ParseAddress(string? address) try { // Patch the module - var file = PEFile.FromFile(ModulePath); + using var str = File.Open(ModulePath, FileMode.Open, FileAccess.ReadWrite); + var file = PEFile.Read(str, new PEImageReaderOptions { EnableStackTrace = true }); var patcher = new PEPatcher(file, importedSymbols); if (patcher.Patch()) { - file.AlignSections(); - File.Copy(ModulePath, ModulePath + ".backup", true); - file.Write(ModulePath); + using var copyStr = File.Create(ModulePath + ".backup"); + str.Seek(0, SeekOrigin.Begin); + str.CopyTo(copyStr); + file.Write(str); } } catch (Exception ex) diff --git a/ModuleTweaker/Patching/IPatcher.cs b/ModuleTweaker/Patching/IPatcher.cs index ad95f55..1188385 100644 --- a/ModuleTweaker/Patching/IPatcher.cs +++ b/ModuleTweaker/Patching/IPatcher.cs @@ -8,7 +8,6 @@ namespace Amethyst.ModuleTweaker.Patching { public interface IPatcher { bool IsPatched(); bool RemoveSection(string name); - void Unpatch(); bool Patch(); bool IsCustomSection(string name); } diff --git a/ModuleTweaker/Patching/ImportedSymbol.cs b/ModuleTweaker/Patching/ImportedSymbol.cs index cb07ccb..8d8d7ec 100644 --- a/ModuleTweaker/Patching/ImportedSymbol.cs +++ b/ModuleTweaker/Patching/ImportedSymbol.cs @@ -1,5 +1,4 @@ -using AsmResolver.IO; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/ModuleTweaker/Patching/ImporterHeader.cs b/ModuleTweaker/Patching/ImporterHeader.cs index d11fc5d..b0c7642 100644 --- a/ModuleTweaker/Patching/ImporterHeader.cs +++ b/ModuleTweaker/Patching/ImporterHeader.cs @@ -1,5 +1,4 @@ -using AsmResolver.IO; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; diff --git a/ModuleTweaker/Patching/PE/ImportDescriptor.cs b/ModuleTweaker/Patching/PE/ImportDescriptor.cs index 68af6ed..7607aca 100644 --- a/ModuleTweaker/Patching/PE/ImportDescriptor.cs +++ b/ModuleTweaker/Patching/PE/ImportDescriptor.cs @@ -1,5 +1,4 @@ -using AsmResolver.IO; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; @@ -31,18 +30,18 @@ public bool IsZero } } - public static ImportDescriptor Read(ref BinaryStreamReader reader) + public static ImportDescriptor Read(BinaryReader reader) { - Span bytes = stackalloc byte[(int)Size]; - if (reader.ReadBytes(bytes) != Size) + var bytes = reader.ReadBytes((int)Size); + if (bytes.Length != Size) throw new ArgumentException("Not enough data to read ImportDescriptor."); return MemoryMarshal.Read(bytes); } - public void Write(BinaryStreamWriter writer) + public void Write(BinaryWriter writer) { Span bytes = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref this, 1)); - writer.WriteBytes(bytes); + writer.Write(bytes); } public override bool Equals(object? obj) diff --git a/ModuleTweaker/Patching/PE/PEImporterHeader.cs b/ModuleTweaker/Patching/PE/PEImporterHeader.cs index 19f644a..9b97e52 100644 --- a/ModuleTweaker/Patching/PE/PEImporterHeader.cs +++ b/ModuleTweaker/Patching/PE/PEImporterHeader.cs @@ -1,4 +1,4 @@ -using AsmResolver.PE.File; +using LibObjectFile.PE; using System; using System.Collections.Generic; using System.Linq; diff --git a/ModuleTweaker/Patching/PE/PEPatcher.cs b/ModuleTweaker/Patching/PE/PEPatcher.cs index e276269..56e629f 100644 --- a/ModuleTweaker/Patching/PE/PEPatcher.cs +++ b/ModuleTweaker/Patching/PE/PEPatcher.cs @@ -1,11 +1,10 @@ using Amethyst.Common.Diagnostics; -using AsmResolver; -using AsmResolver.IO; -using AsmResolver.PE.File; -using AsmResolver.PE.File.Headers; +using LibObjectFile.Diagnostics; +using LibObjectFile.PE; using System; using System.Collections.Generic; using System.Linq; +using System.Reflection.PortableExecutable; using System.Text; using System.Threading.Tasks; @@ -14,12 +13,8 @@ public class PEPatcher(PEFile file, List symbols) : IPatcher { // Runtime Importer Header public const string SectionRTIH = ".rtih"; - // New Import Descriptor Table - public const string SectionIDNEW = ".idnew"; - public static HashSet CustomSections { get; } = [ - SectionRTIH, - SectionIDNEW + SectionRTIH ]; public PEFile File { get; } = file; @@ -37,140 +32,89 @@ public bool IsPatched() { return false; } + static uint AlignSectionRVA(uint value, uint align) => (value + align - 1) & ~(align - 1); + public bool Patch() { // Version 1 full PE-specific layout: // .rtih - Runtime Importer Header // [PEImporterHeader data] - // .idnew - New Import Descriptor Table - // [4 bytes] Original IDT RVA - // [4 bytes] Original IDT Size - // [New IDT data (old IDT minus "Minecraft.Windows.exe")] if (IsPatched()) { - Logger.Debug("PE file is already patched, unpatching it first."); - Unpatch(); + Logger.Debug("PE file is already patched, no reason to repatch."); + return false; } Logger.Debug("Patching PE file for runtime importing..."); // Get the import directory entries - var importDirectory = File.OptionalHeader.GetDataDirectory(DataDirectoryIndex.ImportDirectory); - if (!importDirectory.IsPresentInPE) { + var importDirectory = File.Directories.Import; + if (importDirectory is null) { Logger.Warn("PE file has no import directory, no reason to patch it."); return false; } - // Read existing import descriptors - List importDescriptors = []; - { - var importDirectoryReader = File.CreateDataDirectoryReader(importDirectory); - while (importDirectoryReader.CanRead(ImportDescriptor.Size)) { - var descriptor = ImportDescriptor.Read(ref importDirectoryReader); - if (descriptor.IsZero) - break; - importDescriptors.Add(descriptor); - } - } - - // Find the import descriptor for "Minecraft.Windows.exe" - ImportDescriptor? minecraftWindowsImportDescriptor = null; - foreach (var descriptor in importDescriptors) { - var nameRva = descriptor.Name; - var name = File.CreateReaderAtRva(nameRva).ReadAsciiString(); - if (name.StartsWith("Minecraft.Windows", StringComparison.OrdinalIgnoreCase)) { - minecraftWindowsImportDescriptor = descriptor; - break; - } - } + // Find the import directory entry for "Minecraft.Windows.exe" + PEImportDirectoryEntry? targetImportDirectoryEntry = importDirectory.Entries.FirstOrDefault(e => e.ImportDllNameLink.Resolve()?.StartsWith("Minecraft.Windows", StringComparison.OrdinalIgnoreCase) ?? false); // If not found, no reason to patch - if (minecraftWindowsImportDescriptor is null) { + if (targetImportDirectoryEntry is null) { Logger.Warn("PE file does not import from 'Minecraft.Windows.exe', no reason to patch it."); return false; } // Map all mangled names for "Minecraft.Windows.exe" functions to IAT offsets - uint iatRva = minecraftWindowsImportDescriptor.Value.FirstThunk; + uint iatRva = targetImportDirectoryEntry.ImportAddressTable.RVA; uint iatCount = 0; Dictionary iatIndices = []; - { - var reader = File.CreateReaderAtRva(minecraftWindowsImportDescriptor.Value.OriginalFirstThunk); - uint iatIndex = 0; - while (reader.CanRead(sizeof(ulong))) { - ulong iltEntry = reader.ReadUInt64(); - if (iltEntry == 0) - break; - iatIndex++; - bool isOrdinal = (iltEntry & 0x8000000000000000) != 0; - if (isOrdinal) - continue; - - uint hintNameRva = (uint)(iltEntry & 0x7FFFFFFFFFFFFFFF); - var hintNameReader = File.CreateReaderAtRva(hintNameRva); - ushort hint = hintNameReader.ReadUInt16(); - string functionName = hintNameReader.ReadAsciiString(); - iatIndices[functionName] = iatIndex - 1; + foreach (var import in targetImportDirectoryEntry.ImportLookupTable.Entries) { + iatCount++; + if (import.IsImportByOrdinal) + continue; + var name = import.HintName.Resolve().Name; + if (name is not null) { + iatIndices[name] = iatCount - 1; } - iatCount = iatIndex; } // Create the PEHeaderContext var context = new PEHeaderContext(File, iatRva, iatCount); + var lastSection = File.Sections[^1]!; + var sectionAlignment = File.OptionalHeader.SectionAlignment; + var fileAlignment = File.OptionalHeader.FileAlignment; + var nextRVA = AlignSectionRVA(lastSection.RVA + lastSection.VirtualSize, sectionAlignment); // Create the RTIH section - { - PESection section = new( - SectionRTIH, - SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); - using var ms = new MemoryStream(); - var writer = new BinaryWriter(ms); - - // Create the PEImporterHeader - var header = new PEImporterHeader() { - Format = HeaderFormat.PE, - Version = 1, - Symbols = Symbols - }; - - // Write the entire header - header.Write(context, writer); - - // Finalize the section - section.Contents = new DataSegment(ms.ToArray()); - File.Sections.Add(section); - } - - // Create new import descriptor table section - { - PESection section = new( - SectionIDNEW, - SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); - - using var ms = new MemoryStream(); - var writer = new BinaryStreamWriter(ms); - - // Write original import directory RVA and size at the start - writer.WriteUInt32(importDirectory.VirtualAddress); - writer.WriteUInt32(importDirectory.Size); - - foreach (var descriptor in importDescriptors) { - if (descriptor.Equals(minecraftWindowsImportDescriptor.Value)) - continue; - descriptor.Write(writer); + PESection rtihSection = File.AddSection(SectionRTIH, nextRVA); + rtihSection.Characteristics = SectionCharacteristics.ContainsInitializedData | SectionCharacteristics.MemRead; + var rthiStream = new PEStreamSectionData(); + + var header = new PEImporterHeader() { + Format = HeaderFormat.PE, + Version = 1, + Symbols = Symbols + }; + using var writer = new BinaryWriter(rthiStream.Stream, Encoding.UTF8, true); + header.Write(context, writer); + rtihSection.Content.Add(rthiStream); + + // Remove the target import directory entry from the import directory + importDirectory.Entries.Remove(targetImportDirectoryEntry); + DiagnosticBag diags = new() { EnableStackTrace = true }; + File.Verify(diags); + foreach (var diag in diags.Messages) { + switch (diag.Kind) { + case DiagnosticKind.Error: + Logger.Error(diag.ToString()); + break; + case DiagnosticKind.Warning: + Logger.Warn(diag.ToString()); + break; + default: + Logger.Info(diag.ToString()); + break; } - - // Write null descriptor at the end - ImportDescriptor.Empty.Write(writer); - section.Contents = new DataSegment(ms.ToArray()); - File.Sections.Add(section); - File.AlignSections(); - - // Update import directory to point to the new table - File.OptionalHeader.SetDataDirectory( - DataDirectoryIndex.ImportDirectory, - new(section.Rva + sizeof(uint) * 2, (uint)ms.Length)); - Logger.Debug("Removed import from 'Minecraft.Windows.exe'."); } + Logger.Debug("Removed import from 'Minecraft.Windows.exe'."); Logger.Debug("Patching completed."); return true; } @@ -182,30 +126,5 @@ public bool RemoveSection(string name) { } return false; } - - public void Unpatch() { - if (!IsPatched()) - return; - Logger.Debug("Unpatching PE file..."); - for (int i = File.Sections.Count - 1; i >= 0; i--) { - var section = File.Sections[i]; - if (section.Name == SectionIDNEW) { - BinaryStreamReader reader = section.CreateReader(); - uint originalIDTRva = reader.ReadUInt32(); - uint originalIDTSize = reader.ReadUInt32(); - - File.OptionalHeader.SetDataDirectory( - DataDirectoryIndex.ImportDirectory, - new(originalIDTRva, originalIDTSize) - ); - } - - if (IsCustomSection(section.Name)) { - Logger.Debug($"Removing custom section '{section.Name}'..."); - File.Sections.RemoveAt(i); - } - } - File.AlignSections(); - } } } diff --git a/ModuleTweaker/Properties/launchSettings.json b/ModuleTweaker/Properties/launchSettings.json index 8b4f1e0..7efb724 100644 --- a/ModuleTweaker/Properties/launchSettings.json +++ b/ModuleTweaker/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "ModuleTweaker": { "commandName": "Project", - "commandLineArgs": "-m \"C:\\Users\\bravo\\AppData\\Local\\Packages\\Microsoft.MinecraftUWP_8wekyb3d8bbwe\\LocalState\\games\\com.mojang\\amethyst\\mods\\AmethystRuntime@2.0.0\\AmethystRuntime.dll\"\r\n-s \"C:\\Users\\bravo\\Documents\\GitHub\\Amethyst\\generated\\symbols\"" + "commandLineArgs": "-m \"./input/Amethyst-Runtime.dll\"\r\n-s \"./output/symbols\"" } } } \ No newline at end of file From f138936146663101e5445866f9b895e1f05364ed Mon Sep 17 00:00:00 2001 From: Raony Fernandes dos Reis Date: Tue, 14 Oct 2025 01:58:58 -0300 Subject: [PATCH 06/14] C# side of ModuleTweaker is actually working as intended now --- Common/Diagnostics/Logger.cs | 40 ++-- Common/Utility/CursorLocation.cs | 27 +++ Common/Utility/Utils.cs | 12 ++ ModuleTweaker/Amethyst.ModuleTweaker.csproj | 4 +- ModuleTweaker/Commands/MainCommand.cs | 66 +++--- ModuleTweaker/Patching/AbstractHeader.cs | 69 ++++++ ModuleTweaker/Patching/AbstractSymbol.cs | 53 +++++ ModuleTweaker/Patching/HeaderFactory.cs | 26 +++ ModuleTweaker/Patching/HeaderType.cs | 32 +++ ModuleTweaker/Patching/IPatcher.cs | 1 + ModuleTweaker/Patching/ImportedSymbol.cs | 30 --- ModuleTweaker/Patching/ImporterHeader.cs | 69 ------ .../Patching/PE/AbstractPEImporterHeader.cs | 29 +++ ModuleTweaker/Patching/PE/AbstractPESymbol.cs | 22 ++ ModuleTweaker/Patching/PE/ImportDescriptor.cs | 14 +- ModuleTweaker/Patching/PE/PEImporterHeader.cs | 34 --- ModuleTweaker/Patching/PE/PEPatcher.cs | 202 +++++++++++------- ModuleTweaker/Patching/PE/V1/PEDataSymbol.cs | 29 +++ .../Patching/PE/V1/PEFunctionSymbol.cs | 54 +++++ .../Patching/PE/V1/PEImporterHeader.cs | 11 + ModuleTweaker/Patching/SymbolFactory.cs | 26 +++ ModuleTweaker/Patching/SymbolInfo.cs | 33 +++ ModuleTweaker/Patching/SymbolType.cs | 32 +++ .../Patching/Symbols/FunctionSymbol.cs | 64 ------ .../Patching/Symbols/VariableSymbol.cs | 25 --- .../Patching/Symbols/VirtualPointerSymbol.cs | 25 --- ModuleTweaker/Utility/Utils.cs | 19 ++ SymbolGenerator/Commands/MainCommand.cs | 2 +- SymbolGenerator/Parsing/ASTClass.cs | 5 +- SymbolGenerator/Parsing/ASTCursorLocation.cs | 10 - SymbolGenerator/Parsing/ASTMethod.cs | 5 +- SymbolGenerator/Parsing/ASTPrinter.cs | 6 +- SymbolGenerator/Parsing/ASTVariable.cs | 5 +- SymbolGenerator/Parsing/ASTVisitor.cs | 17 +- .../Annotations/AbstractAnnotationTarget.cs | 6 +- .../Annotations/Comments/CommentParser.cs | 5 +- .../Annotations/Comments/RawAnnotation.cs | 6 +- 37 files changed, 689 insertions(+), 426 deletions(-) create mode 100644 Common/Utility/CursorLocation.cs create mode 100644 ModuleTweaker/Patching/AbstractHeader.cs create mode 100644 ModuleTweaker/Patching/AbstractSymbol.cs create mode 100644 ModuleTweaker/Patching/HeaderFactory.cs create mode 100644 ModuleTweaker/Patching/HeaderType.cs delete mode 100644 ModuleTweaker/Patching/ImportedSymbol.cs delete mode 100644 ModuleTweaker/Patching/ImporterHeader.cs create mode 100644 ModuleTweaker/Patching/PE/AbstractPEImporterHeader.cs create mode 100644 ModuleTweaker/Patching/PE/AbstractPESymbol.cs delete mode 100644 ModuleTweaker/Patching/PE/PEImporterHeader.cs create mode 100644 ModuleTweaker/Patching/PE/V1/PEDataSymbol.cs create mode 100644 ModuleTweaker/Patching/PE/V1/PEFunctionSymbol.cs create mode 100644 ModuleTweaker/Patching/PE/V1/PEImporterHeader.cs create mode 100644 ModuleTweaker/Patching/SymbolFactory.cs create mode 100644 ModuleTweaker/Patching/SymbolInfo.cs create mode 100644 ModuleTweaker/Patching/SymbolType.cs delete mode 100644 ModuleTweaker/Patching/Symbols/FunctionSymbol.cs delete mode 100644 ModuleTweaker/Patching/Symbols/VariableSymbol.cs delete mode 100644 ModuleTweaker/Patching/Symbols/VirtualPointerSymbol.cs create mode 100644 ModuleTweaker/Utility/Utils.cs delete mode 100644 SymbolGenerator/Parsing/ASTCursorLocation.cs diff --git a/Common/Diagnostics/Logger.cs b/Common/Diagnostics/Logger.cs index 285bced..cb3d039 100644 --- a/Common/Diagnostics/Logger.cs +++ b/Common/Diagnostics/Logger.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using Amethyst.Common.Utility; +using System.Reflection; namespace Amethyst.Common.Diagnostics { @@ -10,43 +11,28 @@ public static void WriteLine(object? message = null, ConsoleColor color = Consol Console.WriteLine(message); Console.ResetColor(); } - - public static void Info(object? message) + + public static void Info(object? message, CursorLocation? location = null) { - WriteLine($"{( - Assembly.GetEntryAssembly()?.GetName()?.Name is { } name ? - $"[{name}] " : - "")}[INFO] {message}", ConsoleColor.White); + WriteLine($"{(location is not null && location.File != "" ? location.ToString() + ": " : (Assembly.GetEntryAssembly()?.GetName().Name is not string name ? "Unknown: " : name.Trim() + ": "))} message: {message}", ConsoleColor.White); } - public static void Debug(object? message) + public static void Debug(object? message, CursorLocation? location = null) { #if DEBUG - WriteLine($"{( - Assembly.GetEntryAssembly()?.GetName()?.Name is { } name ? - $"[{name}] " : - "")}[DEBUG] {message}", ConsoleColor.White); + WriteLine($"{(location is not null && location.File != "" ? location.ToString() + ": " : (Assembly.GetEntryAssembly()?.GetName().Name is not string name ? "Unknown: " : name.Trim() + ": "))}message: {message}", ConsoleColor.White); #endif } - public static void Warn(string message) => - WriteLine($"{( - Assembly.GetEntryAssembly()?.GetName()?.Name is { } name ? - $"[{name}] " : - "")}[WARN] {message}", ConsoleColor.Yellow); + public static void Warn(string message, CursorLocation? location = null) => + WriteLine($"{(location is not null && location.File != "" ? location?.ToString() + ": " : (Assembly.GetEntryAssembly()?.GetName().Name is not string name ? "Unknown: " : name.Trim() + ": "))}warning: {message}", ConsoleColor.Yellow); - public static void Error(string message) => - WriteLine($"{( - Assembly.GetEntryAssembly()?.GetName()?.Name is { } name ? - $"[{name}] " : - "")}[ERROR] {message}", ConsoleColor.Red); + public static void Error(string message, CursorLocation? location = null) => + WriteLine($"{(location is not null && location.File != "" ? location?.ToString() + ": " : (Assembly.GetEntryAssembly()?.GetName().Name is not string name ? "Unknown: " : name.Trim() + ": "))}error: {message}", ConsoleColor.Red); - public static void Fatal(string message) + public static void Fatal(string message, CursorLocation? location = null) { - WriteLine($"{( - Assembly.GetEntryAssembly()?.GetName()?.Name is { } name ? - $"[{name}] " : - "")}[FATAL] {message}", ConsoleColor.Magenta); + WriteLine($"{(location is not null && location.File != "" ? location?.ToString() + ": " : (Assembly.GetEntryAssembly()?.GetName().Name is not string name ? "Unknown: " : name.Trim() + ": "))}fatal error: {message}", ConsoleColor.Magenta); Environment.Exit(1); } } diff --git a/Common/Utility/CursorLocation.cs b/Common/Utility/CursorLocation.cs new file mode 100644 index 0000000..fbd4ad5 --- /dev/null +++ b/Common/Utility/CursorLocation.cs @@ -0,0 +1,27 @@ +using Amethyst.Common.Extensions; + +namespace Amethyst.Common.Utility { + public class CursorLocation + { + public string File { get; set; } + public uint Line { get; set; } + public uint Column { get; set; } + + public CursorLocation(string file, uint line, uint column) + { + if (string.IsNullOrEmpty(file) || !System.IO.File.Exists(file)) + File = ""; + else + File = Path.GetFullPath(file).NormalizeSlashes(); + Line = line; + Column = column; + } + + override public string ToString() + { + if (File == "") + return ""; + return $"{File}({Line},{Column})"; + } + } +} diff --git a/Common/Utility/Utils.cs b/Common/Utility/Utils.cs index 87de0c6..34fad97 100644 --- a/Common/Utility/Utils.cs +++ b/Common/Utility/Utils.cs @@ -59,5 +59,17 @@ public static void CreateDefinitionFile(string defFile, IEnumerable mang sb.AppendLine("; End of generated file."); File.WriteAllText(defFile, sb.ToString()); } + + public static void WritePrefixedString(this BinaryWriter writer, string str) { + byte[] bytes = Encoding.UTF8.GetBytes(str); + writer.Write(bytes.Length); + writer.Write(bytes); + } + + public static string ReadPrefixedString(this BinaryReader reader) { + int length = reader.ReadInt32(); + byte[] bytes = reader.ReadBytes(length); + return Encoding.UTF8.GetString(bytes); + } } } diff --git a/ModuleTweaker/Amethyst.ModuleTweaker.csproj b/ModuleTweaker/Amethyst.ModuleTweaker.csproj index d2ed894..ea117d0 100644 --- a/ModuleTweaker/Amethyst.ModuleTweaker.csproj +++ b/ModuleTweaker/Amethyst.ModuleTweaker.csproj @@ -11,7 +11,9 @@ - + + + diff --git a/ModuleTweaker/Commands/MainCommand.cs b/ModuleTweaker/Commands/MainCommand.cs index 4cb7078..a38c5b6 100644 --- a/ModuleTweaker/Commands/MainCommand.cs +++ b/ModuleTweaker/Commands/MainCommand.cs @@ -1,12 +1,11 @@ using Amethyst.Common.Diagnostics; using Amethyst.Common.Models; using Amethyst.ModuleTweaker.Patching; -using Amethyst.ModuleTweaker.Patching.PE; -using Amethyst.ModuleTweaker.Patching.Symbols; +using AsmResolver.PE.File; +using AsmResolver.PE.Imports; using CliFx; using CliFx.Attributes; using CliFx.Infrastructure; -using LibObjectFile.PE; using Newtonsoft.Json; using System.Globalization; @@ -24,14 +23,14 @@ public class MainCommand : ICommand public ValueTask ExecuteAsync(IConsole console) { FileInfo module = new(ModulePath); - DirectoryInfo symbols = new(SymbolsPath); + DirectoryInfo symbolsDir = new(SymbolsPath); if (module.Exists is false) { Logger.Warn("Couldn't patch module, specified module does not exist."); return default; } - if (symbols.Exists is false) + if (symbolsDir.Exists is false) { Logger.Warn("Couldn't patch module, specified symbols directory does not exist."); return default; @@ -48,9 +47,12 @@ ulong ParseAddress(string? address) return addr; } + //SymbolFactory.Register(new SymbolType(1, "function"), () => new PEFunctionSymbol()); + HeaderFactory.Register(new HeaderType(1, "pe32+"), (args) => new Patching.PE.V1.PEImporterHeader()); + // Collect all symbol files and accumulate mangled names - IEnumerable symbolFiles = symbols.EnumerateFiles("*.json", SearchOption.AllDirectories); - List importedSymbols = []; + IEnumerable symbolFiles = symbolsDir.EnumerateFiles("*.json", SearchOption.AllDirectories); + List symbols = []; foreach (var symbolFile in symbolFiles) { using var stream = symbolFile.OpenRead(); @@ -61,12 +63,10 @@ ulong ParseAddress(string? address) switch (symbolJson.FormatVersion) { case 1: - foreach (var function in symbolJson.Functions) - { + foreach (var function in symbolJson.Functions) { if (string.IsNullOrEmpty(function.Name)) continue; - - importedSymbols.Add(new FunctionSymbol { + symbols.Add(new Patching.PE.V1.PEFunctionSymbol { Name = function.Name, IsVirtual = false, IsSignature = function.Signature is not null, @@ -74,35 +74,34 @@ ulong ParseAddress(string? address) Signature = function.Signature ?? string.Empty }); } - foreach (var variable in symbolJson.Variables) - { + foreach (var vfunc in symbolJson.VirtualFunctions) { + if (string.IsNullOrEmpty(vfunc.Name)) + continue; + symbols.Add(new Patching.PE.V1.PEFunctionSymbol { + Name = vfunc.Name, + IsVirtual = true, + VirtualIndex = vfunc.Index, + VirtualTable = vfunc.VirtualTable ?? "this" + }); + } + foreach (var variable in symbolJson.Variables) { if (string.IsNullOrEmpty(variable.Name)) continue; - importedSymbols.Add(new VariableSymbol { + symbols.Add(new Patching.PE.V1.PEDataSymbol { Name = variable.Name, + IsVirtualTable = false, Address = ParseAddress(variable.Address) }); } - foreach (var vtable in symbolJson.VirtualTables) - { + foreach (var vtable in symbolJson.VirtualTables) { if (string.IsNullOrEmpty(vtable.Name)) continue; - importedSymbols.Add(new VirtualPointerSymbol { + symbols.Add(new Patching.PE.V1.PEDataSymbol { Name = vtable.Name, + IsVirtualTable = true, Address = ParseAddress(vtable.Address) }); } - foreach (var vfunc in symbolJson.VirtualFunctions) - { - if (string.IsNullOrEmpty(vfunc.Name)) - continue; - importedSymbols.Add(new FunctionSymbol { - Name = vfunc.Name, - IsVirtual = true, - VirtualIndex = vfunc.Index, - VirtualTable = vfunc.VirtualTable ?? "this" - }); - } break; } } @@ -111,15 +110,12 @@ ulong ParseAddress(string? address) try { // Patch the module - using var str = File.Open(ModulePath, FileMode.Open, FileAccess.ReadWrite); - var file = PEFile.Read(str, new PEImageReaderOptions { EnableStackTrace = true }); - var patcher = new PEPatcher(file, importedSymbols); + var file = PEFile.FromFile(ModulePath); + var patcher = new Patching.PE.PEPatcher(file, symbols); if (patcher.Patch()) { - using var copyStr = File.Create(ModulePath + ".backup"); - str.Seek(0, SeekOrigin.Begin); - str.CopyTo(copyStr); - file.Write(str); + File.Copy(ModulePath, ModulePath + ".bak", true); + file.Write(ModulePath); } } catch (Exception ex) diff --git a/ModuleTweaker/Patching/AbstractHeader.cs b/ModuleTweaker/Patching/AbstractHeader.cs new file mode 100644 index 0000000..ec1986d --- /dev/null +++ b/ModuleTweaker/Patching/AbstractHeader.cs @@ -0,0 +1,69 @@ +using Amethyst.Common.Utility; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching { + // [Header Format] + // [8 bytes ] Magic "AME_RTI" + // [4 bytes ] Format Version (uint) + // [4 bytes ] Format Type length (N) + // [N bytes ] Format Type (UTF-8) + // [4 bytes ] Number of symbols (M) + // [M symbols] + // [Classes that inherit add more data here] + public abstract class AbstractHeader { + public const string MagicSignature = "AME_RTI"; + + public List Symbols { get; set; } = []; + + public abstract string FormatType { get; } + public abstract uint FormatVersion { get; } + + public virtual void WriteTo(BinaryWriter writer) { + writer.WritePrefixedString(MagicSignature); + writer.Write(FormatVersion); + writer.WritePrefixedString(FormatType); + writer.Write(Symbols.Count); + foreach (var sym in Symbols) + sym.WriteTo(writer); + } + + public virtual void ReadFrom(BinaryReader reader) { + string magic = reader.ReadPrefixedString(); + if (magic != MagicSignature) + throw new InvalidOperationException($"Invalid magic signature '{magic}', expected '{MagicSignature}'."); + uint ver = reader.ReadUInt32(); + if (ver != FormatVersion) + throw new InvalidOperationException($"Incompatible header version {ver}, expected {FormatVersion}."); + string fmt = reader.ReadPrefixedString(); + if (fmt != FormatType) + throw new InvalidOperationException($"Incompatible header format {fmt}, expected {FormatType}."); + int count = reader.ReadInt32(); + Symbols.Clear(); + for (int i = 0; i < count; i++) { + SymbolInfo info = AbstractSymbol.PeekInfo(reader); + AbstractSymbol sym = SymbolFactory.Create(info.Type); + sym.ReadFrom(reader); + Symbols.Add(sym); + } + } + + public static HeaderType PeekInfo(BinaryReader reader) { + long initialPos = reader.BaseStream.Position; + try { + string magic = reader.ReadPrefixedString(); + if (magic != MagicSignature) + throw new InvalidOperationException($"Invalid magic signature '{magic}', expected '{MagicSignature}'."); + uint ver = reader.ReadUInt32(); + string fmt = reader.ReadPrefixedString(); + return new(ver, fmt); + } + finally { + reader.BaseStream.Position = initialPos; + } + } + } +} diff --git a/ModuleTweaker/Patching/AbstractSymbol.cs b/ModuleTweaker/Patching/AbstractSymbol.cs new file mode 100644 index 0000000..e5066c3 --- /dev/null +++ b/ModuleTweaker/Patching/AbstractSymbol.cs @@ -0,0 +1,53 @@ +using Amethyst.Common.Utility; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching { + public abstract class AbstractSymbol { + public string Name { get; set; } = string.Empty; + + public abstract string FormatType { get; } + public abstract uint FormatVersion { get; } + public abstract string Kind { get; } + public abstract bool IsShadowSymbol { get; } + + public virtual void WriteTo(BinaryWriter writer) { + writer.Write(FormatVersion); + writer.WritePrefixedString(FormatType); + writer.WritePrefixedString(Kind); + writer.WritePrefixedString(Name); + } + + public virtual void ReadFrom(BinaryReader reader) { + uint ver = reader.ReadUInt32(); + if (ver != FormatVersion) + throw new InvalidOperationException($"Incompatible symbol version {ver}, expected {FormatVersion}."); + string fmt = reader.ReadPrefixedString(); + if (fmt != FormatType) + throw new InvalidOperationException($"Incompatible symbol format {fmt}, expected {FormatType}."); + string kind = reader.ReadPrefixedString(); + if (kind != Kind) + throw new InvalidOperationException($"Incompatible symbol kind {kind}, expected {Kind}."); + Name = reader.ReadPrefixedString(); + } + + public override string ToString() { + return $"Symbol[v{FormatVersion}, {FormatType}, {Name}, {Kind}]"; + } + + public static SymbolInfo PeekInfo(BinaryReader reader) { + long initialPos = reader.BaseStream.Position; + try { + uint ver = reader.ReadUInt32(); + string fmt = reader.ReadPrefixedString(); + string kind = reader.ReadPrefixedString(); + return new(ver, fmt, kind); + } finally { + reader.BaseStream.Position = initialPos; + } + } + } +} diff --git a/ModuleTweaker/Patching/HeaderFactory.cs b/ModuleTweaker/Patching/HeaderFactory.cs new file mode 100644 index 0000000..68e436e --- /dev/null +++ b/ModuleTweaker/Patching/HeaderFactory.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching { + public class HeaderFactory { + private static readonly Dictionary> sConstructors = []; + public static IReadOnlyDictionary> Constructors => sConstructors; + + public static void Register(HeaderType type, Func constructor) { + ArgumentNullException.ThrowIfNull(type); + ArgumentNullException.ThrowIfNull(constructor); + if (sConstructors.ContainsKey(type)) + throw new InvalidOperationException($"A constructor for {type} is already registered."); + sConstructors[type] = constructor; + } + + public static AbstractHeader Create(HeaderType type, params object[] args) { + if (sConstructors.TryGetValue(type, out var constructor)) + return constructor(args); + throw new InvalidOperationException($"No constructor registered for {type}."); + } + } +} diff --git a/ModuleTweaker/Patching/HeaderType.cs b/ModuleTweaker/Patching/HeaderType.cs new file mode 100644 index 0000000..69ade01 --- /dev/null +++ b/ModuleTweaker/Patching/HeaderType.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching { + public class HeaderType(uint version, string type) { + public uint Version { get; set; } = version; + public string Type { get; set; } = type; + + public override bool Equals(object? obj) { + if (obj is not HeaderType other) + return false; + return Version == other.Version && Type == other.Type; + } + + public override int GetHashCode() { + return HashCode.Combine(Version, Type); + } + + public override string ToString() { + return $"HeaderType[v{Version}, {Type}]"; + } + + public static bool operator ==(HeaderType? a, HeaderType? b) => + a?.Equals(b) ?? b is null; + + public static bool operator !=(HeaderType? a, HeaderType? b) => + !(a == b); + } +} diff --git a/ModuleTweaker/Patching/IPatcher.cs b/ModuleTweaker/Patching/IPatcher.cs index 1188385..71f5d7c 100644 --- a/ModuleTweaker/Patching/IPatcher.cs +++ b/ModuleTweaker/Patching/IPatcher.cs @@ -9,6 +9,7 @@ public interface IPatcher { bool IsPatched(); bool RemoveSection(string name); bool Patch(); + bool Unpatch(); bool IsCustomSection(string name); } } diff --git a/ModuleTweaker/Patching/ImportedSymbol.cs b/ModuleTweaker/Patching/ImportedSymbol.cs deleted file mode 100644 index 8d8d7ec..0000000 --- a/ModuleTweaker/Patching/ImportedSymbol.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Amethyst.ModuleTweaker.Patching { - public enum SymbolType { - Function, - Variable, - VirtualPointer - } - - public abstract class ImportedSymbol { - public string Name { get; set; } = string.Empty; - - public abstract SymbolType GetSymbolType(); - - public virtual void Write(BinaryWriter writer) { - // Version 1 layout: - // [1 byte ] SymbolType (0 = Function, 1 = Variable, 2 = VirtualPointer) - // [4 bytes] Name length (N) - // [N bytes] Name (UTF-8) - - writer.Write((byte)GetSymbolType()); - writer.Write(Encoding.UTF8.GetByteCount(Name)); - writer.Write(Encoding.UTF8.GetBytes(Name)); - } - } -} diff --git a/ModuleTweaker/Patching/ImporterHeader.cs b/ModuleTweaker/Patching/ImporterHeader.cs deleted file mode 100644 index b0c7642..0000000 --- a/ModuleTweaker/Patching/ImporterHeader.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Amethyst.ModuleTweaker.Patching { - public enum HeaderFormat { - PE - } - - public interface IHeaderContext { - HeaderFormat GetFormat(); - } - - public class ImporterHeader { - public const string Magic = "RTIH"; - public HeaderFormat Format { get; set; } = HeaderFormat.PE; - public uint Version { get; set; } = 1; - public List Symbols { get; set; } = []; - - public virtual void Write(IHeaderContext ctx, BinaryWriter writer) { - // Version 1 layout: - // [1 byte ] Format (0 = PE) - // [4 bytes] Magic "RTIH" - // [4 bytes] Version (1) - // [8 bytes] Size of symbols section - // [4 bytes] Number of symbols - // [N bytes] Symbols - // [Classes that inherit can add more data here] - - // Validate context format - if (ctx.GetFormat() != Format) - throw new InvalidOperationException($"Header format mismatch. Context format is {ctx.GetFormat()}, but header format is {Format}."); - - // Write magic, format and version - writer.Write(Encoding.ASCII.GetBytes(Magic)); - writer.Write((byte)Format); - writer.Write(Version); - - // Reserve space for symbol block size - long sizePos = writer.BaseStream.Position; - writer.Write(0L); - - // Write symbol count and symbols - writer.Write(Symbols.Count); - - // Remember position to calculate size later - long lastPos = writer.BaseStream.Position; - - // Write symbols - foreach (var symbol in Symbols) - symbol.Write(writer); - - // Get end position - long endPos = writer.BaseStream.Position; - - // Calculate symbol block size - long symbolsSize = endPos - lastPos; - - // Seek back and write symbol block size - writer.BaseStream.Seek(sizePos, SeekOrigin.Begin); - writer.Write(symbolsSize); - - // Seek back to end - writer.BaseStream.Seek(endPos, SeekOrigin.Begin); - } - } -} diff --git a/ModuleTweaker/Patching/PE/AbstractPEImporterHeader.cs b/ModuleTweaker/Patching/PE/AbstractPEImporterHeader.cs new file mode 100644 index 0000000..01c605e --- /dev/null +++ b/ModuleTweaker/Patching/PE/AbstractPEImporterHeader.cs @@ -0,0 +1,29 @@ +namespace Amethyst.ModuleTweaker.Patching.PE { + // [Header Format] + // [Includes AbstractHeader layout] + // [4 bytes ] Old IDT RVA + // [4 bytes ] Old IDT Size + // [4 bytes ] Import Count + + public abstract class AbstractPEImporterHeader() : AbstractHeader { + public override string FormatType => "pe32+"; + + public uint OldIDT { get; set; } = 0; + public uint OldIDTSize { get; set; } = 0; + public uint ImportCount { get; set; } = 0; + + public override void ReadFrom(BinaryReader reader) { + base.ReadFrom(reader); + OldIDT = reader.ReadUInt32(); + OldIDTSize = reader.ReadUInt32(); + ImportCount = reader.ReadUInt32(); + } + + public override void WriteTo(BinaryWriter writer) { + base.WriteTo(writer); + writer.Write(OldIDT); + writer.Write(OldIDTSize); + writer.Write(ImportCount); + } + } +} diff --git a/ModuleTweaker/Patching/PE/AbstractPESymbol.cs b/ModuleTweaker/Patching/PE/AbstractPESymbol.cs new file mode 100644 index 0000000..239d7b7 --- /dev/null +++ b/ModuleTweaker/Patching/PE/AbstractPESymbol.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching.PE { + public abstract class AbstractPESymbol : AbstractSymbol { + public override string FormatType => "pe32+"; + public uint TargetOffset { get; set; } = 0; + + public override void ReadFrom(BinaryReader reader) { + base.ReadFrom(reader); + TargetOffset = reader.ReadUInt32(); + } + + public override void WriteTo(BinaryWriter writer) { + base.WriteTo(writer); + writer.Write(TargetOffset); + } + } +} diff --git a/ModuleTweaker/Patching/PE/ImportDescriptor.cs b/ModuleTweaker/Patching/PE/ImportDescriptor.cs index 7607aca..86d2a6d 100644 --- a/ModuleTweaker/Patching/PE/ImportDescriptor.cs +++ b/ModuleTweaker/Patching/PE/ImportDescriptor.cs @@ -1,12 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; +using System.Runtime.InteropServices; -namespace Amethyst.ModuleTweaker.Patching.PE -{ +namespace Amethyst.ModuleTweaker.Patching.PE { [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct ImportDescriptor() { @@ -32,8 +26,8 @@ public bool IsZero public static ImportDescriptor Read(BinaryReader reader) { - var bytes = reader.ReadBytes((int)Size); - if (bytes.Length != Size) + Span bytes = stackalloc byte[(int)Size]; + if (reader.Read(bytes) != Size) throw new ArgumentException("Not enough data to read ImportDescriptor."); return MemoryMarshal.Read(bytes); } diff --git a/ModuleTweaker/Patching/PE/PEImporterHeader.cs b/ModuleTweaker/Patching/PE/PEImporterHeader.cs deleted file mode 100644 index 9b97e52..0000000 --- a/ModuleTweaker/Patching/PE/PEImporterHeader.cs +++ /dev/null @@ -1,34 +0,0 @@ -using LibObjectFile.PE; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Amethyst.ModuleTweaker.Patching.PE { - public class PEHeaderContext(PEFile file, uint iatVirtualAddress, uint iatCount) : IHeaderContext { - public HeaderFormat GetFormat() => HeaderFormat.PE; - - public PEFile File { get; } = file; - public uint IATVirtualAddress { get; } = iatVirtualAddress; - public uint IATCount { get; } = iatCount; - } - - public class PEImporterHeader : ImporterHeader { - public override void Write(IHeaderContext ctx, BinaryWriter writer) { - // Version 1 PE-specific layout: - // [Base ImporterHeader layout] - // [4 bytes] "Minecraft.Windows.exe" IAT Virtual Address - // [4 bytes] "Minecraft.Windows.exe" IAT Count - - if (ctx is not PEHeaderContext peCtx) - throw new InvalidOperationException($"Invalid header context type. Expected PEHeaderContext, got {ctx.GetType()}."); - - // Call base to write common layout - // Write PE-specific fields - base.Write(ctx, writer); - writer.Write(peCtx.IATVirtualAddress); - writer.Write(peCtx.IATCount); - } - } -} diff --git a/ModuleTweaker/Patching/PE/PEPatcher.cs b/ModuleTweaker/Patching/PE/PEPatcher.cs index 56e629f..bf8d18a 100644 --- a/ModuleTweaker/Patching/PE/PEPatcher.cs +++ b/ModuleTweaker/Patching/PE/PEPatcher.cs @@ -1,24 +1,24 @@ using Amethyst.Common.Diagnostics; -using LibObjectFile.Diagnostics; -using LibObjectFile.PE; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection.PortableExecutable; +using Amethyst.ModuleTweaker.Patching.PE.V1; +using Amethyst.ModuleTweaker.Utility; +using AsmResolver; +using AsmResolver.PE.File; +using AsmResolver.PE.File.Headers; using System.Text; -using System.Threading.Tasks; namespace Amethyst.ModuleTweaker.Patching.PE { - public class PEPatcher(PEFile file, List symbols) : IPatcher { + public class PEPatcher(PEFile file, List symbols) : IPatcher { // Runtime Importer Header - public const string SectionRTIH = ".rtih"; + public const string SectionRTIH = ".rtih"; // Runtime Importer Header + public const string SectionNIDT = ".nidt"; // New Import Directory Table public static HashSet CustomSections { get; } = [ - SectionRTIH + SectionRTIH, + SectionNIDT ]; public PEFile File { get; } = file; - public List Symbols { get; } = symbols; + public List Symbols { get; } = symbols; public bool IsCustomSection(string name) { return CustomSections.Contains(name); @@ -32,90 +32,121 @@ public bool IsPatched() { return false; } - static uint AlignSectionRVA(uint value, uint align) => (value + align - 1) & ~(align - 1); - public bool Patch() { // Version 1 full PE-specific layout: // .rtih - Runtime Importer Header // [PEImporterHeader data] if (IsPatched()) { - Logger.Debug("PE file is already patched, no reason to repatch."); - return false; + if (!Unpatch()) + Logger.Fatal("Failed to unpatch existing patch, cannot re-patch."); } - Logger.Debug("Patching PE file for runtime importing..."); - - // Get the import directory entries - var importDirectory = File.Directories.Import; - if (importDirectory is null) { - Logger.Warn("PE file has no import directory, no reason to patch it."); + Logger.Debug("Patching PE file..."); + var importDirectory = File.OptionalHeader.GetDataDirectory(DataDirectoryIndex.ImportDirectory); + if (!importDirectory.IsPresentInPE) { + Logger.Warn("PE file has no import directory, skipping patch."); return false; } - // Find the import directory entry for "Minecraft.Windows.exe" - PEImportDirectoryEntry? targetImportDirectoryEntry = importDirectory.Entries.FirstOrDefault(e => e.ImportDllNameLink.Resolve()?.StartsWith("Minecraft.Windows", StringComparison.OrdinalIgnoreCase) ?? false); + // Read existing import descriptors + using var importReader = File.CreateDataDirectoryReader(importDirectory).ToReader(); + List importDescriptors = []; + uint targetIATRVA = 0; + uint targetILTRVA = 0; + while (true) { + var entry = ImportDescriptor.Read(importReader); + if (entry.IsZero) + break; + string name = File.CreateReaderAtRva(entry.Name).ReadAsciiString(); + if (name.StartsWith("Minecraft.Windows", StringComparison.OrdinalIgnoreCase)) { + targetIATRVA = entry.OriginalFirstThunk; + targetILTRVA = entry.FirstThunk; + } + importDescriptors.Add(entry); + } - // If not found, no reason to patch - if (targetImportDirectoryEntry is null) { - Logger.Warn("PE file does not import from 'Minecraft.Windows.exe', no reason to patch it."); + if (targetIATRVA == 0 || targetILTRVA == 0) { + Logger.Warn("PE file does not import from 'Minecraft.Windows', skipping patch."); return false; } - // Map all mangled names for "Minecraft.Windows.exe" functions to IAT offsets - uint iatRva = targetImportDirectoryEntry.ImportAddressTable.RVA; - uint iatCount = 0; - Dictionary iatIndices = []; - foreach (var import in targetImportDirectoryEntry.ImportLookupTable.Entries) { - iatCount++; - if (import.IsImportByOrdinal) + // Map import names to their target RVAs + Dictionary importNameToTarget = []; + List symbolsToWrite = []; + var targetILTReader = File.CreateReaderAtRva(targetILTRVA); + var targetIATReader = File.CreateReaderAtRva(targetIATRVA); + uint index = 0; + while (true) { + ulong iltEntry = targetILTReader.ReadUInt64(); + if (iltEntry == 0) + break; + index++; + ulong iatEntry = targetIATReader.ReadUInt64(); + bool isOrdinal = (iltEntry & 0x8000000000000000) != 0; + if (isOrdinal) { continue; - var name = import.HintName.Resolve().Name; - if (name is not null) { - iatIndices[name] = iatCount - 1; } + uint hintNameRVA = (uint)(iltEntry & 0x7FFFFFFFFFFFFFFF); + var hintNameReader = File.CreateReaderAtRva(hintNameRVA); + ushort hint = hintNameReader.ReadUInt16(); + string name = hintNameReader.ReadAsciiString(); + var symbol = Symbols.OfType().FirstOrDefault(s => s.Name == name); + if (symbol is null) + continue; + uint entryRVA = targetIATReader.Rva - 8; + importNameToTarget[name] = entryRVA; + symbol.TargetOffset = entryRVA; + symbolsToWrite.Add(symbol); + Logger.Debug($"Mapping import {name} to target RVA 0x{entryRVA:X}..."); } - // Create the PEHeaderContext - var context = new PEHeaderContext(File, iatRva, iatCount); - var lastSection = File.Sections[^1]!; - var sectionAlignment = File.OptionalHeader.SectionAlignment; - var fileAlignment = File.OptionalHeader.FileAlignment; - var nextRVA = AlignSectionRVA(lastSection.RVA + lastSection.VirtualSize, sectionAlignment); + foreach (var s in Symbols.Where(s => s.IsShadowSymbol && !symbolsToWrite.Contains(s))) { + symbolsToWrite.Add(s); + Logger.Debug($"Mapping shadow symbol {s.Name}..."); + } + symbolsToWrite.AddRange(Symbols.Where(s => s.IsShadowSymbol && !symbolsToWrite.Contains(s))); + Logger.Info($"Mapped {symbolsToWrite.Count} symbols to import targets."); // Create the RTIH section - PESection rtihSection = File.AddSection(SectionRTIH, nextRVA); - rtihSection.Characteristics = SectionCharacteristics.ContainsInitializedData | SectionCharacteristics.MemRead; - var rthiStream = new PEStreamSectionData(); - - var header = new PEImporterHeader() { - Format = HeaderFormat.PE, - Version = 1, - Symbols = Symbols - }; - using var writer = new BinaryWriter(rthiStream.Stream, Encoding.UTF8, true); - header.Write(context, writer); - rtihSection.Content.Add(rthiStream); - - // Remove the target import directory entry from the import directory - importDirectory.Entries.Remove(targetImportDirectoryEntry); - DiagnosticBag diags = new() { EnableStackTrace = true }; - File.Verify(diags); - foreach (var diag in diags.Messages) { - switch (diag.Kind) { - case DiagnosticKind.Error: - Logger.Error(diag.ToString()); - break; - case DiagnosticKind.Warning: - Logger.Warn(diag.ToString()); - break; - default: - Logger.Info(diag.ToString()); - break; + { + PESection rtihSec = new(SectionRTIH, SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); + using var ms = new MemoryStream(); + using var writer = new BinaryWriter(ms, Encoding.UTF8); + var header = new PEImporterHeader() { + Symbols = symbolsToWrite, + OldIDT = importDirectory.VirtualAddress, + OldIDTSize = importDirectory.Size, + ImportCount = index + }; + header.WriteTo(writer); + var data = new DataSegment(ms.ToArray()); + rtihSec.Contents = data; + File.Sections.Add(rtihSec); + File.AlignSections(); + } + + // Create new NIDT section + { + PESection nidtSec = new(SectionNIDT, SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); + using var ms = new MemoryStream(); + using var writer = new BinaryWriter(ms, Encoding.UTF8); + foreach (var entry in importDescriptors) { + if (entry.OriginalFirstThunk == targetIATRVA || entry.FirstThunk == targetILTRVA) + continue; + entry.Write(writer); } + var data = new DataSegment(ms.ToArray()); + nidtSec.Contents = data; + File.Sections.Add(nidtSec); + File.AlignSections(); + + // Update import directory to point to new NIDT + File.OptionalHeader.SetDataDirectory(DataDirectoryIndex.ImportDirectory, new DataDirectory(nidtSec.Rva, data.GetVirtualSize())); } - Logger.Debug("Removed import from 'Minecraft.Windows.exe'."); - Logger.Debug("Patching completed."); + + File.UpdateHeaders(); + Logger.Info("PE file patched successfully."); return true; } @@ -126,5 +157,32 @@ public bool RemoveSection(string name) { } return false; } + + public bool Unpatch() { + for (int i = File.Sections.Count - 1; i >= 0; i--) { + var section = File.Sections[i]!; + if (section.Name == SectionRTIH) { + using var reader = section.CreateReader().ToReader(); + var type = AbstractHeader.PeekInfo(reader); + var header = HeaderFactory.Create(type); + header.ReadFrom(reader); + + if (header is not AbstractPEImporterHeader peHeader) { + Logger.Warn($"RTIH section does not contain a valid PE Importer Header, cannot unpatch."); + return false; + } + + // Restore the old import directory + File.OptionalHeader.SetDataDirectory(DataDirectoryIndex.ImportDirectory, new DataDirectory(peHeader.OldIDT, peHeader.OldIDTSize)); + } + + if (IsCustomSection(section.Name)) { + Logger.Debug($"Removing custom section '{section.Name}'..."); + File.Sections.RemoveAt(i); + } + } + File.AlignSections(); + return true; + } } } diff --git a/ModuleTweaker/Patching/PE/V1/PEDataSymbol.cs b/ModuleTweaker/Patching/PE/V1/PEDataSymbol.cs new file mode 100644 index 0000000..6e57741 --- /dev/null +++ b/ModuleTweaker/Patching/PE/V1/PEDataSymbol.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching.PE.V1 { + public class PEDataSymbol : AbstractPESymbol { + public override uint FormatVersion => 1; + public override string Kind => "data"; + + public bool IsVirtualTable { get; set; } = false; + public ulong Address { get; set; } = 0x0; + + public override bool IsShadowSymbol => IsVirtualTable; + + public override void WriteTo(BinaryWriter writer) { + base.WriteTo(writer); + writer.Write((byte)(IsVirtualTable ? 1 : 0)); + writer.Write(Address); + } + + public override void ReadFrom(BinaryReader reader) { + base.ReadFrom(reader); + IsVirtualTable = reader.ReadByte() != 0; + Address = reader.ReadUInt64(); + } + } +} diff --git a/ModuleTweaker/Patching/PE/V1/PEFunctionSymbol.cs b/ModuleTweaker/Patching/PE/V1/PEFunctionSymbol.cs new file mode 100644 index 0000000..75ecca4 --- /dev/null +++ b/ModuleTweaker/Patching/PE/V1/PEFunctionSymbol.cs @@ -0,0 +1,54 @@ +using Amethyst.Common.Utility; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching.PE.V1 { + public class PEFunctionSymbol : AbstractPESymbol { + public override uint FormatVersion => 1; + public override string Kind => "function"; + + public override bool IsShadowSymbol => false; + + public bool IsVirtual { get; set; } = false; + public uint VirtualIndex { get; set; } = 0; + public string VirtualTable { get; set; } = string.Empty; + public bool IsSignature { get; set; } = false; + public string Signature { get; set; } = string.Empty; + public ulong Address { get; set; } = 0x0; + + public override void ReadFrom(BinaryReader reader) { + base.ReadFrom(reader); + IsVirtual = reader.ReadBoolean(); + if (IsVirtual) { + VirtualIndex = reader.ReadUInt32(); + VirtualTable = reader.ReadPrefixedString(); + } + else { + IsSignature = reader.ReadBoolean(); + if (IsSignature) + Signature = reader.ReadPrefixedString(); + else + Address = reader.ReadUInt64(); + } + } + + public override void WriteTo(BinaryWriter writer) { + base.WriteTo(writer); + writer.Write(IsVirtual); + if (IsVirtual) { + writer.Write(VirtualIndex); + writer.WritePrefixedString(VirtualTable); + } + else { + writer.Write(IsSignature); + if (IsSignature) + writer.WritePrefixedString(Signature); + else + writer.Write(Address); + } + } + } +} diff --git a/ModuleTweaker/Patching/PE/V1/PEImporterHeader.cs b/ModuleTweaker/Patching/PE/V1/PEImporterHeader.cs new file mode 100644 index 0000000..23da440 --- /dev/null +++ b/ModuleTweaker/Patching/PE/V1/PEImporterHeader.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching.PE.V1 { + public class PEImporterHeader : AbstractPEImporterHeader { + public override uint FormatVersion => 1; + } +} diff --git a/ModuleTweaker/Patching/SymbolFactory.cs b/ModuleTweaker/Patching/SymbolFactory.cs new file mode 100644 index 0000000..462c102 --- /dev/null +++ b/ModuleTweaker/Patching/SymbolFactory.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching { + public static class SymbolFactory { + private static readonly Dictionary> sConstructors = []; + public static IReadOnlyDictionary> Constructors => sConstructors; + + public static void Register(SymbolType type, Func constructor) { + ArgumentNullException.ThrowIfNull(type); + ArgumentNullException.ThrowIfNull(constructor); + if (sConstructors.ContainsKey(type)) + throw new InvalidOperationException($"A constructor for {type} is already registered."); + sConstructors[type] = constructor; + } + + public static AbstractSymbol Create(SymbolType type) { + if (sConstructors.TryGetValue(type, out var constructor)) + return constructor(); + throw new InvalidOperationException($"No constructor registered for {type}."); + } + } +} diff --git a/ModuleTweaker/Patching/SymbolInfo.cs b/ModuleTweaker/Patching/SymbolInfo.cs new file mode 100644 index 0000000..f1da67c --- /dev/null +++ b/ModuleTweaker/Patching/SymbolInfo.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching { + public class SymbolInfo(uint version, string kind, string name) { + public SymbolType Type => new(version, kind); + public string Name { get; set; } = name; + + public override bool Equals(object? obj) { + if (obj is not SymbolInfo other) + return false; + return Type == other.Type && + Name == other.Name; + } + + public override int GetHashCode() { + return HashCode.Combine(Type, Name); + } + + public override string ToString() { + return $"SymbolInfo[{Type}, {Name}]"; + } + + public static bool operator ==(SymbolInfo? a, SymbolInfo? b) => + a?.Equals(b) ?? b is null; + + public static bool operator !=(SymbolInfo? a, SymbolInfo? b) => + !(a == b); + } +} diff --git a/ModuleTweaker/Patching/SymbolType.cs b/ModuleTweaker/Patching/SymbolType.cs new file mode 100644 index 0000000..7d9e73c --- /dev/null +++ b/ModuleTweaker/Patching/SymbolType.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Patching { + public class SymbolType(uint version, string kind) { + public uint Version { get; set; } = version; + public string Kind { get; set; } = kind; + + public override bool Equals(object? obj) { + if (obj is not SymbolType other) + return false; + return Version == other.Version && Kind == other.Kind; + } + + public override int GetHashCode() { + return HashCode.Combine(Version, Kind); + } + + public override string ToString() { + return $"SymbolType[v{Version}, {Kind}]"; + } + + public static bool operator ==(SymbolType? a, SymbolType? b) => + a?.Equals(b) ?? b is null; + + public static bool operator !=(SymbolType? a, SymbolType? b) => + !(a == b); + } +} diff --git a/ModuleTweaker/Patching/Symbols/FunctionSymbol.cs b/ModuleTweaker/Patching/Symbols/FunctionSymbol.cs deleted file mode 100644 index 504d37c..0000000 --- a/ModuleTweaker/Patching/Symbols/FunctionSymbol.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Amethyst.ModuleTweaker.Patching.Symbols { - public class FunctionSymbol : ImportedSymbol { - public bool IsVirtual { get; set; } = false; - - // If IsVirtual is true, these fields are used. - public uint VirtualIndex { get; set; } = 0; - public string VirtualTable { get; set; } = string.Empty; - - // If IsVirtual is false, these fields are used. - public bool IsSignature { get; set; } = false; - - // If IsSignature is true, this field is used. - public string Signature { get; set; } = string.Empty; - - // If IsSignature is false, this field is used. - public ulong Address { get; set; } = 0; - - public override SymbolType GetSymbolType() { - return SymbolType.Function; - } - - public override void Write(BinaryWriter writer) { - // Version 1 FunctionSymbol-specific format: - // [Base ImporterSymbol layout] - // [1 byte ] IsVirtual (0 = false, 1 = true) - // If IsVirtual == 1: - // [4 bytes] VirtualIndex - // [4 bytes] VirtualTable length (N) - // [N bytes] VirtualTable (UTF-8) - // Else (IsVirtual == 0): - // [1 byte ] IsSignature (0 = false, 1 = true) - // If IsSignature == 1: - // [4 bytes] Signature length (N) - // [N bytes] Signature (UTF-8) - // Else (IsSignature == 0): - // [8 bytes] Address - - // Write base class data first - base.Write(writer); - writer.Write((byte)(IsVirtual ? 1 : 0)); - if (IsVirtual) { - writer.Write(VirtualIndex); - writer.Write(Encoding.UTF8.GetByteCount(VirtualTable)); - writer.Write(Encoding.UTF8.GetBytes(VirtualTable)); - } - else { - writer.Write((byte)(IsSignature ? 1 : 0)); - if (IsSignature) { - writer.Write(Encoding.UTF8.GetByteCount(Signature)); - writer.Write(Encoding.UTF8.GetBytes(Signature)); - } - else { - writer.Write(Address); - } - } - } - } -} diff --git a/ModuleTweaker/Patching/Symbols/VariableSymbol.cs b/ModuleTweaker/Patching/Symbols/VariableSymbol.cs deleted file mode 100644 index 104c713..0000000 --- a/ModuleTweaker/Patching/Symbols/VariableSymbol.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Amethyst.ModuleTweaker.Patching.Symbols { - public class VariableSymbol : ImportedSymbol { - public ulong Address { get; set; } = 0; - - public override SymbolType GetSymbolType() { - return SymbolType.Variable; - } - - public override void Write(BinaryWriter writer) { - // Version 1 VariableSymbol-specific format: - // [Base ImporterSymbol layout] - // [8 bytes] Address - - // Write base class data first - base.Write(writer); - writer.Write(Address); - } - } -} diff --git a/ModuleTweaker/Patching/Symbols/VirtualPointerSymbol.cs b/ModuleTweaker/Patching/Symbols/VirtualPointerSymbol.cs deleted file mode 100644 index efd6e38..0000000 --- a/ModuleTweaker/Patching/Symbols/VirtualPointerSymbol.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Amethyst.ModuleTweaker.Patching.Symbols { - public class VirtualPointerSymbol : ImportedSymbol { - public ulong Address { get; set; } = 0; - - public override SymbolType GetSymbolType() { - return SymbolType.VirtualPointer; - } - - public override void Write(BinaryWriter writer) { - // Version 1 VirtualPointerSymbol-specific format: - // [Base ImporterSymbol layout] - // [8 bytes] Address - - // Write base class data first - base.Write(writer); - writer.Write(Address); - } - } -} diff --git a/ModuleTweaker/Utility/Utils.cs b/ModuleTweaker/Utility/Utils.cs new file mode 100644 index 0000000..5ed7089 --- /dev/null +++ b/ModuleTweaker/Utility/Utils.cs @@ -0,0 +1,19 @@ +using AsmResolver.IO; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Amethyst.ModuleTweaker.Utility { + public static class Utils { + public static BinaryReader ToReader(this BinaryStreamReader reader) { + var ms = new MemoryStream(reader.ReadToEnd()); + return new BinaryReader(ms, Encoding.UTF8, false); + } + + public static BinaryWriter ToWriter(this BinaryStreamWriter writer) { + return new BinaryWriter(writer.BaseStream, Encoding.UTF8, false); + } + } +} diff --git a/SymbolGenerator/Commands/MainCommand.cs b/SymbolGenerator/Commands/MainCommand.cs index 851ac7c..bf8bd61 100644 --- a/SymbolGenerator/Commands/MainCommand.cs +++ b/SymbolGenerator/Commands/MainCommand.cs @@ -149,7 +149,7 @@ public ValueTask ExecuteAsync(IConsole console) if (processed is null) continue; - if (processed.Target.Location is not { } location || location.File == "Unknown File") + if (processed.Target.Location is not { } location || location.File == "") { Logger.Warn($"Skipping annotation for '{processed.Target}' due to unknown location."); continue; diff --git a/SymbolGenerator/Parsing/ASTClass.cs b/SymbolGenerator/Parsing/ASTClass.cs index 6646fc6..c68d578 100644 --- a/SymbolGenerator/Parsing/ASTClass.cs +++ b/SymbolGenerator/Parsing/ASTClass.cs @@ -1,4 +1,5 @@ -using Amethyst.SymbolGenerator.Parsing.Annotations; +using Amethyst.Common.Utility; +using Amethyst.SymbolGenerator.Parsing.Annotations; namespace Amethyst.SymbolGenerator.Parsing { @@ -6,7 +7,7 @@ public class ASTClass : AbstractAnnotationTarget { public string? Name { get; set; } public string? Namespace { get; set; } - public override ASTCursorLocation? Location { get; set; } + public override CursorLocation? Location { get; set; } public ASTBaseSpecifier[] DirectBaseClasses { get; set; } = []; public ASTMethod[] Methods { get; set; } = []; public ASTVariable[] Variables { get; set; } = []; diff --git a/SymbolGenerator/Parsing/ASTCursorLocation.cs b/SymbolGenerator/Parsing/ASTCursorLocation.cs deleted file mode 100644 index 07ecd55..0000000 --- a/SymbolGenerator/Parsing/ASTCursorLocation.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Amethyst.SymbolGenerator.Parsing -{ - public record ASTCursorLocation(string File, uint Line, uint Column, uint Offset) - { - override public string ToString() - { - return $"{File}:{Line}:{Column}:{Offset}"; - } - } -} diff --git a/SymbolGenerator/Parsing/ASTMethod.cs b/SymbolGenerator/Parsing/ASTMethod.cs index 82a1335..42e123c 100644 --- a/SymbolGenerator/Parsing/ASTMethod.cs +++ b/SymbolGenerator/Parsing/ASTMethod.cs @@ -1,4 +1,5 @@ -using Amethyst.SymbolGenerator.Parsing.Annotations; +using Amethyst.Common.Utility; +using Amethyst.SymbolGenerator.Parsing.Annotations; namespace Amethyst.SymbolGenerator.Parsing { @@ -8,7 +9,7 @@ public class ASTMethod : AbstractAnnotationTarget public string MangledName { get; set; } = null!; public string? Namespace { get; set; } = null; public ASTClass? DeclaringClass { get; set; } = null; - public override ASTCursorLocation? Location { get; set; } = null; + public override CursorLocation? Location { get; set; } = null; public bool IsVirtual { get; set; } = false; public bool IsImported { get; set; } = false; public bool IsConstructor { get; set; } = false; diff --git a/SymbolGenerator/Parsing/ASTPrinter.cs b/SymbolGenerator/Parsing/ASTPrinter.cs index 7c6e648..060e333 100644 --- a/SymbolGenerator/Parsing/ASTPrinter.cs +++ b/SymbolGenerator/Parsing/ASTPrinter.cs @@ -9,7 +9,7 @@ public static void PrintVariable(ASTVariable variable, string inputDir) var kind = variable.IsFreeVariable ? "Free Variable" : "Variable"; Logger.Debug($"{kind}: {variable.FullName}, Imported: {variable.IsImported}, Static: {variable.IsStatic}"); if (variable.Location is not null && !string.IsNullOrEmpty(variable.Location.File)) - Logger.Debug($" at {Path.GetRelativePath(inputDir, variable.Location.File)}:{variable.Location.Line}:{variable.Location.Column}:{variable.Location.Offset}"); + Logger.Debug($" at {Path.GetRelativePath(inputDir, variable.Location.File)}:{variable.Location.Line}:{variable.Location.Column}"); } public static void PrintMethod(ASTMethod method, string inputDir) @@ -17,7 +17,7 @@ public static void PrintMethod(ASTMethod method, string inputDir) var kind = method.IsFreeFunction ? "Free Function" : "Method"; Logger.Debug($"{kind}: {method.FullName}, Virtual: {method.IsVirtual}, Imported: {method.IsImported}"); if (method.Location is not null && !string.IsNullOrEmpty(method.Location.File)) - Logger.Debug($" at {Path.GetRelativePath(inputDir, method.Location.File)}:{method.Location.Line}:{method.Location.Column}:{method.Location.Offset}"); + Logger.Debug($" at {Path.GetRelativePath(inputDir, method.Location.File)}:{method.Location.Line}:{method.Location.Column}"); } public static void PrintClass(ASTClass cls, string inputDir) @@ -31,7 +31,7 @@ public static void PrintClass(ASTClass cls, string inputDir) if (!string.IsNullOrEmpty(cls.Namespace)) Logger.Debug($" FullName: {cls.FullName}"); if (cls.Location != null) - Logger.Debug($" Location: {Path.GetRelativePath(inputDir, cls.Location.File)}:{cls.Location.Line}:{cls.Location.Column}:{cls.Location.Offset}"); + Logger.Debug($" Location: {Path.GetRelativePath(inputDir, cls.Location.File)}:{cls.Location.Line}:{cls.Location.Column}"); // Print methods foreach (var method in cls.Methods) diff --git a/SymbolGenerator/Parsing/ASTVariable.cs b/SymbolGenerator/Parsing/ASTVariable.cs index 76d79ad..fcb4592 100644 --- a/SymbolGenerator/Parsing/ASTVariable.cs +++ b/SymbolGenerator/Parsing/ASTVariable.cs @@ -1,4 +1,5 @@ -using Amethyst.SymbolGenerator.Parsing.Annotations; +using Amethyst.Common.Utility; +using Amethyst.SymbolGenerator.Parsing.Annotations; namespace Amethyst.SymbolGenerator.Parsing { @@ -8,7 +9,7 @@ public class ASTVariable : AbstractAnnotationTarget public string MangledName { get; set; } = string.Empty; public string? Namespace { get; set; } = null; public ASTClass? DeclaringClass { get; set; } = null; - public override ASTCursorLocation? Location { get; set; } = null; + public override CursorLocation? Location { get; set; } = null; public bool IsImported { get; set; } = false; public string? RawComment { get; set; } = null; public bool IsStatic { get; set; } = false; diff --git a/SymbolGenerator/Parsing/ASTVisitor.cs b/SymbolGenerator/Parsing/ASTVisitor.cs index af41290..a04dfd4 100644 --- a/SymbolGenerator/Parsing/ASTVisitor.cs +++ b/SymbolGenerator/Parsing/ASTVisitor.cs @@ -1,5 +1,6 @@ using Amethyst.Common.Diagnostics; using Amethyst.Common.Extensions; +using Amethyst.Common.Utility; using ClangSharp.Interop; using System.Linq; @@ -9,7 +10,7 @@ public class ASTVisitor { private readonly Dictionary SpellingCache = []; private readonly Dictionary MangleCache = []; - private readonly Dictionary LocationCache = []; + private readonly Dictionary LocationCache = []; private readonly Dictionary RawCommentCache = []; private readonly Dictionary IsImportedCache = []; private readonly Dictionary FullNamespaceCache = []; @@ -64,7 +65,7 @@ public bool PrintErrors() return false; foreach (var diag in GetDiagnostics()) { - Action? log = diag.Severity switch + Action? log = diag.Severity switch { CXDiagnosticSeverity.CXDiagnostic_Error => Logger.Error, CXDiagnosticSeverity.CXDiagnostic_Fatal => Logger.Error, @@ -75,14 +76,14 @@ public bool PrintErrors() var location = diag.Location; location.GetFileLocation(out var file, out var line, out var column, out var offset); string filePath = file.ToString() ?? "Unknown file"; - string message = diag.Format(CXDiagnostic.DefaultDisplayOptions).ToString() ?? "Unknown diagnostic message"; - log($"{filePath}({line},{column}): {message}"); + string message = diag.Spelling.ToString() ?? "Unknown diagnostic message"; + log(message, new(filePath, line, column)); } return true; } #endregion - public string GetUsr(CXCursor cursor) + public static string GetUsr(CXCursor cursor) { return cursor.Usr.ToString() + "@" + cursor.IsDefinition; } @@ -99,7 +100,7 @@ public string GetSpelling(CXCursor cursor) return spelling; } - public ASTCursorLocation? GetLocation(CXCursor cursor) + public CursorLocation? GetLocation(CXCursor cursor) { string usr = GetUsr(cursor); if (LocationCache.TryGetValue(usr, out var cached)) @@ -111,7 +112,7 @@ public string GetSpelling(CXCursor cursor) if (!string.IsNullOrEmpty(path)) path = Path.GetFullPath(path.ToString()).NormalizeSlashes(); - var location = new ASTCursorLocation(path, line, column, offset); + var location = new CursorLocation(path, line, column); LocationCache[usr] = location; return location; @@ -411,7 +412,7 @@ public ASTVariable[] GetVariables() if (ClassCache.TryGetValue(usr, out var cached)) return (CXChildVisitResult.CXChildVisit_Continue, cached); - ASTCursorLocation? location = GetLocation(cursor); + CursorLocation? location = GetLocation(cursor); // Collect members var (methodsCursors, variableCursors, baseCursors) = CollectClassMembers(cursor); diff --git a/SymbolGenerator/Parsing/Annotations/AbstractAnnotationTarget.cs b/SymbolGenerator/Parsing/Annotations/AbstractAnnotationTarget.cs index 33edaf3..5e83763 100644 --- a/SymbolGenerator/Parsing/Annotations/AbstractAnnotationTarget.cs +++ b/SymbolGenerator/Parsing/Annotations/AbstractAnnotationTarget.cs @@ -1,8 +1,10 @@ -namespace Amethyst.SymbolGenerator.Parsing.Annotations +using Amethyst.Common.Utility; + +namespace Amethyst.SymbolGenerator.Parsing.Annotations { public abstract class AbstractAnnotationTarget { - public virtual ASTCursorLocation? Location { get; set; } + public virtual CursorLocation? Location { get; set; } public HashSet Annotations { get; set; } = []; diff --git a/SymbolGenerator/Parsing/Annotations/Comments/CommentParser.cs b/SymbolGenerator/Parsing/Annotations/Comments/CommentParser.cs index 8674fa6..275f76a 100644 --- a/SymbolGenerator/Parsing/Annotations/Comments/CommentParser.cs +++ b/SymbolGenerator/Parsing/Annotations/Comments/CommentParser.cs @@ -1,11 +1,12 @@ -using Amethyst.SymbolGenerator.Parsing.Annotations.Comments; +using Amethyst.Common.Utility; +using Amethyst.SymbolGenerator.Parsing.Annotations.Comments; using System.Text.RegularExpressions; namespace Amethyst.SymbolGenerator.Parsing.Annotations { public static partial class CommentParser { - public static IEnumerable ParseAnnotations(AbstractAnnotationTarget target, string comment, ASTCursorLocation location) + public static IEnumerable ParseAnnotations(AbstractAnnotationTarget target, string comment, CursorLocation location) { using var sr = new StringReader(comment); string? line; diff --git a/SymbolGenerator/Parsing/Annotations/Comments/RawAnnotation.cs b/SymbolGenerator/Parsing/Annotations/Comments/RawAnnotation.cs index 0bfce13..4d2670f 100644 --- a/SymbolGenerator/Parsing/Annotations/Comments/RawAnnotation.cs +++ b/SymbolGenerator/Parsing/Annotations/Comments/RawAnnotation.cs @@ -1,6 +1,8 @@ -namespace Amethyst.SymbolGenerator.Parsing.Annotations.Comments +using Amethyst.Common.Utility; + +namespace Amethyst.SymbolGenerator.Parsing.Annotations.Comments { - public record RawAnnotation(string Tag, IEnumerable Arguments, ASTCursorLocation Location, AbstractAnnotationTarget Target) + public record RawAnnotation(string Tag, IEnumerable Arguments, CursorLocation Location, AbstractAnnotationTarget Target) { public override string ToString() { From 22ba706097a09c8081baa877854c2693aa65de10 Mon Sep 17 00:00:00 2001 From: Raony Fernandes dos Reis Date: Tue, 14 Oct 2025 02:25:31 -0300 Subject: [PATCH 07/14] Actually working now --- ModuleTweaker/Commands/MainCommand.cs | 3 ++- ModuleTweaker/Patching/AbstractSymbol.cs | 3 ++- ModuleTweaker/Patching/SymbolInfo.cs | 4 ++-- ModuleTweaker/Patching/SymbolType.cs | 9 +++++---- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/ModuleTweaker/Commands/MainCommand.cs b/ModuleTweaker/Commands/MainCommand.cs index a38c5b6..76f8452 100644 --- a/ModuleTweaker/Commands/MainCommand.cs +++ b/ModuleTweaker/Commands/MainCommand.cs @@ -47,7 +47,8 @@ ulong ParseAddress(string? address) return addr; } - //SymbolFactory.Register(new SymbolType(1, "function"), () => new PEFunctionSymbol()); + SymbolFactory.Register(new SymbolType(1, "pe32+", "data"), () => new Patching.PE.V1.PEDataSymbol()); + SymbolFactory.Register(new SymbolType(1, "pe32+", "function"), () => new Patching.PE.V1.PEFunctionSymbol()); HeaderFactory.Register(new HeaderType(1, "pe32+"), (args) => new Patching.PE.V1.PEImporterHeader()); // Collect all symbol files and accumulate mangled names diff --git a/ModuleTweaker/Patching/AbstractSymbol.cs b/ModuleTweaker/Patching/AbstractSymbol.cs index e5066c3..9ed2455 100644 --- a/ModuleTweaker/Patching/AbstractSymbol.cs +++ b/ModuleTweaker/Patching/AbstractSymbol.cs @@ -44,7 +44,8 @@ public static SymbolInfo PeekInfo(BinaryReader reader) { uint ver = reader.ReadUInt32(); string fmt = reader.ReadPrefixedString(); string kind = reader.ReadPrefixedString(); - return new(ver, fmt, kind); + string name = reader.ReadPrefixedString(); + return new(ver, fmt, kind, name); } finally { reader.BaseStream.Position = initialPos; } diff --git a/ModuleTweaker/Patching/SymbolInfo.cs b/ModuleTweaker/Patching/SymbolInfo.cs index f1da67c..7b63729 100644 --- a/ModuleTweaker/Patching/SymbolInfo.cs +++ b/ModuleTweaker/Patching/SymbolInfo.cs @@ -5,8 +5,8 @@ using System.Threading.Tasks; namespace Amethyst.ModuleTweaker.Patching { - public class SymbolInfo(uint version, string kind, string name) { - public SymbolType Type => new(version, kind); + public class SymbolInfo(uint version, string format, string kind, string name) { + public SymbolType Type => new(version, format, kind); public string Name { get; set; } = name; public override bool Equals(object? obj) { diff --git a/ModuleTweaker/Patching/SymbolType.cs b/ModuleTweaker/Patching/SymbolType.cs index 7d9e73c..a02d381 100644 --- a/ModuleTweaker/Patching/SymbolType.cs +++ b/ModuleTweaker/Patching/SymbolType.cs @@ -5,22 +5,23 @@ using System.Threading.Tasks; namespace Amethyst.ModuleTweaker.Patching { - public class SymbolType(uint version, string kind) { + public class SymbolType(uint version, string format, string kind) { public uint Version { get; set; } = version; + public string Format { get; set; } = format; public string Kind { get; set; } = kind; public override bool Equals(object? obj) { if (obj is not SymbolType other) return false; - return Version == other.Version && Kind == other.Kind; + return Version == other.Version && Format == other.Format && Kind == other.Kind; } public override int GetHashCode() { - return HashCode.Combine(Version, Kind); + return HashCode.Combine(Version, Format, Kind); } public override string ToString() { - return $"SymbolType[v{Version}, {Kind}]"; + return $"SymbolType[v{Version}, {Format}, {Kind}]"; } public static bool operator ==(SymbolType? a, SymbolType? b) => From 3d7c30b7256aebad20f2eff9931f03ce43b10217 Mon Sep 17 00:00:00 2001 From: Raony Fernandes dos Reis Date: Tue, 14 Oct 2025 14:08:20 -0300 Subject: [PATCH 08/14] Update MainCommand.cs --- ModuleTweaker/Commands/MainCommand.cs | 52 +++++++++++++++++++++------ 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/ModuleTweaker/Commands/MainCommand.cs b/ModuleTweaker/Commands/MainCommand.cs index 76f8452..189c040 100644 --- a/ModuleTweaker/Commands/MainCommand.cs +++ b/ModuleTweaker/Commands/MainCommand.cs @@ -6,6 +6,7 @@ using CliFx; using CliFx.Attributes; using CliFx.Infrastructure; +using K4os.Hash.xxHash; using Newtonsoft.Json; using System.Globalization; @@ -14,28 +15,34 @@ namespace Amethyst.ModuleTweaker.Commands [Command(Description = "Patches or unpatches modules for runtime importing support.")] public class MainCommand : ICommand { - [CommandOption("module", 'm', Description = "The specified module path to patch.")] + [CommandOption("module", 'm', Description = "The specified module path to patch.", IsRequired = true)] public string ModulePath { get; set; } = null!; - [CommandOption("symbols", 's', Description = "Path to directory containing *.symbols.json to use for patching.")] + [CommandOption("symbols", 's', Description = "Path to directory containing *.symbols.json to use for patching.", IsRequired = true)] public string SymbolsPath { get; set; } = null!; + [CommandOption("output", 'o', Description = "Path to save temporary files, don't confuse with -m.")] + public string OutputPath { get; set; } = null!; + public ValueTask ExecuteAsync(IConsole console) { FileInfo module = new(ModulePath); DirectoryInfo symbolsDir = new(SymbolsPath); - if (module.Exists is false) - { - Logger.Warn("Couldn't patch module, specified module does not exist."); + if (module.Exists is false) { + Logger.Fatal("Couldn't patch module, specified module does not exist."); return default; } - if (symbolsDir.Exists is false) - { - Logger.Warn("Couldn't patch module, specified symbols directory does not exist."); + if (symbolsDir.Exists is false) { + Logger.Fatal("Couldn't patch module, specified symbols directory does not exist."); return default; } + if (string.IsNullOrEmpty(OutputPath)) { + OutputPath = Path.GetFullPath(Path.Combine(SymbolsPath, "../")); + } + DirectoryInfo outDir = new(OutputPath); + ulong ParseAddress(string? address) { if (string.IsNullOrEmpty(address)) @@ -111,12 +118,35 @@ ulong ParseAddress(string? address) try { // Patch the module - var file = PEFile.FromFile(ModulePath); - var patcher = new Patching.PE.PEPatcher(file, symbols); + var bytes = File.ReadAllBytes(ModulePath); + ulong hash = XXH64.DigestOf(bytes); + if (File.Exists(Path.Combine(outDir.FullName, "module_hash.txt"))) { + var existingHash = File.ReadAllText(Path.Combine(outDir.FullName, "module_hash.txt")); + if (ulong.TryParse(existingHash, NumberStyles.HexNumber, null, out var existingHashValue)) { + if (existingHashValue == hash) { + Logger.Info("Module hash matches previous hash, skipping patch."); + return default; + } + } + } + + var peFile = PEFile.FromBytes(bytes); + if (peFile is null) { + Logger.Fatal("Failed to read module as a PE file."); + return default; + } + Logger.Info($"Loaded module '{ModulePath}' as PE file."); + var patcher = new Patching.PE.PEPatcher(peFile, symbols); + if (patcher.Patch()) { File.Copy(ModulePath, ModulePath + ".bak", true); - file.Write(ModulePath); + using var ms = new MemoryStream(); + peFile.Write(ms); + var newBytes = ms.ToArray(); + ulong newHash = XXH64.DigestOf(newBytes); + File.WriteAllBytes(ModulePath, newBytes); + File.WriteAllText(Path.Combine(outDir.FullName, "module_hash.txt"), newHash.ToString("X16")); } } catch (Exception ex) From a8457a65f227347ba8f473b11219a57431cbcb2f Mon Sep 17 00:00:00 2001 From: Raony Fernandes dos Reis Date: Wed, 15 Oct 2025 00:57:56 -0300 Subject: [PATCH 09/14] OP stuff --- Common/Models/VariableSymbolModel.cs | 3 +++ Common/Models/VirtualFunctionSymbolModel.cs | 3 +++ ModuleTweaker/Commands/MainCommand.cs | 6 ++++-- ModuleTweaker/Patching/PE/PEPatcher.cs | 2 +- ModuleTweaker/Patching/PE/V1/PEDataSymbol.cs | 3 +++ ModuleTweaker/Patching/PE/V1/PEFunctionSymbol.cs | 11 +++++++---- ModuleTweaker/Patching/SymbolInfo.cs | 2 +- SymbolGenerator/Commands/MainCommand.cs | 3 ++- .../Handlers/VirtualIndexAnnotationHandler.cs | 3 ++- 9 files changed, 26 insertions(+), 10 deletions(-) diff --git a/Common/Models/VariableSymbolModel.cs b/Common/Models/VariableSymbolModel.cs index 850e518..aaa7d4c 100644 --- a/Common/Models/VariableSymbolModel.cs +++ b/Common/Models/VariableSymbolModel.cs @@ -14,5 +14,8 @@ public class VariableSymbolModel [JsonProperty("address")] public string Address { get; set; } = string.Empty; + + [JsonProperty("is_vaddress")] + public bool IsVirtualTableAddress { get; set; } = false; } } diff --git a/Common/Models/VirtualFunctionSymbolModel.cs b/Common/Models/VirtualFunctionSymbolModel.cs index 332e336..d7f39ab 100644 --- a/Common/Models/VirtualFunctionSymbolModel.cs +++ b/Common/Models/VirtualFunctionSymbolModel.cs @@ -23,5 +23,8 @@ public class VirtualFunctionSymbolModel [JsonProperty("index")] public uint Index { get; set; } = 0; + + [JsonProperty("is_vdtor")] + public bool IsVirtualDestructor { get; set; } = false; } } diff --git a/ModuleTweaker/Commands/MainCommand.cs b/ModuleTweaker/Commands/MainCommand.cs index 189c040..2e13525 100644 --- a/ModuleTweaker/Commands/MainCommand.cs +++ b/ModuleTweaker/Commands/MainCommand.cs @@ -89,7 +89,8 @@ ulong ParseAddress(string? address) Name = vfunc.Name, IsVirtual = true, VirtualIndex = vfunc.Index, - VirtualTable = vfunc.VirtualTable ?? "this" + VirtualTable = vfunc.VirtualTable ?? "this", + IsDestructor = vfunc.IsVirtualDestructor }); } foreach (var variable in symbolJson.Variables) { @@ -98,7 +99,8 @@ ulong ParseAddress(string? address) symbols.Add(new Patching.PE.V1.PEDataSymbol { Name = variable.Name, IsVirtualTable = false, - Address = ParseAddress(variable.Address) + Address = ParseAddress(variable.Address), + IsVirtualTableAddress = variable.IsVirtualTableAddress }); } foreach (var vtable in symbolJson.VirtualTables) { diff --git a/ModuleTweaker/Patching/PE/PEPatcher.cs b/ModuleTweaker/Patching/PE/PEPatcher.cs index bf8d18a..81170d3 100644 --- a/ModuleTweaker/Patching/PE/PEPatcher.cs +++ b/ModuleTweaker/Patching/PE/PEPatcher.cs @@ -94,7 +94,7 @@ public bool Patch() { var symbol = Symbols.OfType().FirstOrDefault(s => s.Name == name); if (symbol is null) continue; - uint entryRVA = targetIATReader.Rva - 8; + uint entryRVA = targetILTRVA + ((index - 1) * 8); importNameToTarget[name] = entryRVA; symbol.TargetOffset = entryRVA; symbolsToWrite.Add(symbol); diff --git a/ModuleTweaker/Patching/PE/V1/PEDataSymbol.cs b/ModuleTweaker/Patching/PE/V1/PEDataSymbol.cs index 6e57741..9862714 100644 --- a/ModuleTweaker/Patching/PE/V1/PEDataSymbol.cs +++ b/ModuleTweaker/Patching/PE/V1/PEDataSymbol.cs @@ -9,6 +9,7 @@ public class PEDataSymbol : AbstractPESymbol { public override uint FormatVersion => 1; public override string Kind => "data"; + public bool IsVirtualTableAddress { get; set; } = false; public bool IsVirtualTable { get; set; } = false; public ulong Address { get; set; } = 0x0; @@ -16,12 +17,14 @@ public class PEDataSymbol : AbstractPESymbol { public override void WriteTo(BinaryWriter writer) { base.WriteTo(writer); + writer.Write((byte)(IsVirtualTableAddress ? 1 : 0)); writer.Write((byte)(IsVirtualTable ? 1 : 0)); writer.Write(Address); } public override void ReadFrom(BinaryReader reader) { base.ReadFrom(reader); + IsVirtualTableAddress = reader.ReadByte() != 0; IsVirtualTable = reader.ReadByte() != 0; Address = reader.ReadUInt64(); } diff --git a/ModuleTweaker/Patching/PE/V1/PEFunctionSymbol.cs b/ModuleTweaker/Patching/PE/V1/PEFunctionSymbol.cs index 75ecca4..315f2b6 100644 --- a/ModuleTweaker/Patching/PE/V1/PEFunctionSymbol.cs +++ b/ModuleTweaker/Patching/PE/V1/PEFunctionSymbol.cs @@ -12,6 +12,7 @@ public class PEFunctionSymbol : AbstractPESymbol { public override bool IsShadowSymbol => false; + public bool IsDestructor { get; set; } = false; public bool IsVirtual { get; set; } = false; public uint VirtualIndex { get; set; } = 0; public string VirtualTable { get; set; } = string.Empty; @@ -21,13 +22,14 @@ public class PEFunctionSymbol : AbstractPESymbol { public override void ReadFrom(BinaryReader reader) { base.ReadFrom(reader); - IsVirtual = reader.ReadBoolean(); + IsDestructor = reader.ReadByte() != 0; + IsVirtual = reader.ReadByte() != 0; if (IsVirtual) { VirtualIndex = reader.ReadUInt32(); VirtualTable = reader.ReadPrefixedString(); } else { - IsSignature = reader.ReadBoolean(); + IsSignature = reader.ReadByte() != 0; if (IsSignature) Signature = reader.ReadPrefixedString(); else @@ -37,13 +39,14 @@ public override void ReadFrom(BinaryReader reader) { public override void WriteTo(BinaryWriter writer) { base.WriteTo(writer); - writer.Write(IsVirtual); + writer.Write((byte)(IsDestructor ? 1 : 0)); + writer.Write((byte)(IsVirtual ? 1 : 0)); if (IsVirtual) { writer.Write(VirtualIndex); writer.WritePrefixedString(VirtualTable); } else { - writer.Write(IsSignature); + writer.Write((byte)(IsSignature ? 1 : 0)); if (IsSignature) writer.WritePrefixedString(Signature); else diff --git a/ModuleTweaker/Patching/SymbolInfo.cs b/ModuleTweaker/Patching/SymbolInfo.cs index 7b63729..aad7eb6 100644 --- a/ModuleTweaker/Patching/SymbolInfo.cs +++ b/ModuleTweaker/Patching/SymbolInfo.cs @@ -6,7 +6,7 @@ namespace Amethyst.ModuleTweaker.Patching { public class SymbolInfo(uint version, string format, string kind, string name) { - public SymbolType Type => new(version, format, kind); + public SymbolType Type { get; init; } = new(version, format, kind); public string Name { get; set; } = name; public override bool Equals(object? obj) { diff --git a/SymbolGenerator/Commands/MainCommand.cs b/SymbolGenerator/Commands/MainCommand.cs index bf8bd61..0d84022 100644 --- a/SymbolGenerator/Commands/MainCommand.cs +++ b/SymbolGenerator/Commands/MainCommand.cs @@ -178,7 +178,8 @@ public ValueTask ExecuteAsync(IConsole console) annotationsData[location.File].Add(new VariableSymbolModel { Name = vtable.VtableMangledLabel, - Address = vtable.Address + Address = vtable.Address, + IsVirtualTableAddress = true }); } } diff --git a/SymbolGenerator/Parsing/Annotations/Handlers/VirtualIndexAnnotationHandler.cs b/SymbolGenerator/Parsing/Annotations/Handlers/VirtualIndexAnnotationHandler.cs index 8df3411..a6cec96 100644 --- a/SymbolGenerator/Parsing/Annotations/Handlers/VirtualIndexAnnotationHandler.cs +++ b/SymbolGenerator/Parsing/Annotations/Handlers/VirtualIndexAnnotationHandler.cs @@ -48,7 +48,8 @@ public override ProcessedAnnotation Handle(RawAnnotation annotation) Index = ParameterPack.Index, VirtualTable = $"{target.DeclaringClass!.FullName}::vtable::'{ParameterPack.TargetVirtualTable}'", Inherit = ParameterPack.ShouldInherit, - Overrides = ParameterPack.ShouldInherit ? target.OverrideOf!.MangledName : null + Overrides = ParameterPack.ShouldInherit ? target.OverrideOf!.MangledName : null, + IsVirtualDestructor = target.IsDestructor }, Resolve ); From b510cb495cd821c085470e7c8b84aa5ce28db7d1 Mon Sep 17 00:00:00 2001 From: Raony Fernandes dos Reis Date: Wed, 15 Oct 2025 01:59:01 -0300 Subject: [PATCH 10/14] Add symbol storage support for PE patching Introduces HasStorage and StorageOffset to AbstractPESymbol and implements storage allocation for function destructors and virtual table addresses. Adds a new .rtis section for symbol storage in PE files, updates patcher logic to assign and fix up storage offsets, and provides alignment helpers. This enables runtime storage for certain symbols, such as disabling virtual destructor deletion. --- Common/Utility/Utils.cs | 7 ++++ ModuleTweaker/Commands/MainCommand.cs | 6 ++-- ModuleTweaker/Patching/PE/AbstractPESymbol.cs | 8 +++++ ModuleTweaker/Patching/PE/PEPatcher.cs | 33 +++++++++++++++++++ ModuleTweaker/Patching/PE/V1/PEDataSymbol.cs | 11 ++++++- .../Patching/PE/V1/PEFunctionSymbol.cs | 16 +++++++++ SymbolGenerator/Commands/MainCommand.cs | 6 ++-- 7 files changed, 81 insertions(+), 6 deletions(-) diff --git a/Common/Utility/Utils.cs b/Common/Utility/Utils.cs index 34fad97..cf08a90 100644 --- a/Common/Utility/Utils.cs +++ b/Common/Utility/Utils.cs @@ -71,5 +71,12 @@ public static string ReadPrefixedString(this BinaryReader reader) { byte[] bytes = reader.ReadBytes(length); return Encoding.UTF8.GetString(bytes); } + + public static void Align(this BinaryWriter writer, int alignment = 16, byte pad = 0x00) { + long pos = writer.BaseStream.Position; + int padding = (int)((alignment - (pos % alignment)) % alignment); + for (int i = 0; i < padding; i++) + writer.Write(pad); + } } } diff --git a/ModuleTweaker/Commands/MainCommand.cs b/ModuleTweaker/Commands/MainCommand.cs index 2e13525..ae782d0 100644 --- a/ModuleTweaker/Commands/MainCommand.cs +++ b/ModuleTweaker/Commands/MainCommand.cs @@ -90,7 +90,8 @@ ulong ParseAddress(string? address) IsVirtual = true, VirtualIndex = vfunc.Index, VirtualTable = vfunc.VirtualTable ?? "this", - IsDestructor = vfunc.IsVirtualDestructor + IsDestructor = vfunc.IsVirtualDestructor, + HasStorage = vfunc.IsVirtualDestructor }); } foreach (var variable in symbolJson.Variables) { @@ -100,7 +101,8 @@ ulong ParseAddress(string? address) Name = variable.Name, IsVirtualTable = false, Address = ParseAddress(variable.Address), - IsVirtualTableAddress = variable.IsVirtualTableAddress + IsVirtualTableAddress = variable.IsVirtualTableAddress, + HasStorage = variable.IsVirtualTableAddress }); } foreach (var vtable in symbolJson.VirtualTables) { diff --git a/ModuleTweaker/Patching/PE/AbstractPESymbol.cs b/ModuleTweaker/Patching/PE/AbstractPESymbol.cs index 239d7b7..934e51f 100644 --- a/ModuleTweaker/Patching/PE/AbstractPESymbol.cs +++ b/ModuleTweaker/Patching/PE/AbstractPESymbol.cs @@ -8,15 +8,23 @@ namespace Amethyst.ModuleTweaker.Patching.PE { public abstract class AbstractPESymbol : AbstractSymbol { public override string FormatType => "pe32+"; public uint TargetOffset { get; set; } = 0; + public bool HasStorage { get; set; } = false; + public uint StorageOffset { get; set; } = 0; public override void ReadFrom(BinaryReader reader) { base.ReadFrom(reader); TargetOffset = reader.ReadUInt32(); + HasStorage = reader.ReadByte() != 0; + StorageOffset = reader.ReadUInt32(); } public override void WriteTo(BinaryWriter writer) { base.WriteTo(writer); writer.Write(TargetOffset); + writer.Write((byte)(HasStorage ? 1 : 0)); + writer.Write(StorageOffset); } + + public abstract void SetStorage(BinaryWriter writer); } } diff --git a/ModuleTweaker/Patching/PE/PEPatcher.cs b/ModuleTweaker/Patching/PE/PEPatcher.cs index 81170d3..df08e99 100644 --- a/ModuleTweaker/Patching/PE/PEPatcher.cs +++ b/ModuleTweaker/Patching/PE/PEPatcher.cs @@ -10,10 +10,12 @@ namespace Amethyst.ModuleTweaker.Patching.PE { public class PEPatcher(PEFile file, List symbols) : IPatcher { // Runtime Importer Header public const string SectionRTIH = ".rtih"; // Runtime Importer Header + public const string SectionRTIS = ".rtis"; // Runtime Importer Storage public const string SectionNIDT = ".nidt"; // New Import Directory Table public static HashSet CustomSections { get; } = [ SectionRTIH, + SectionRTIS, SectionNIDT ]; @@ -108,6 +110,37 @@ public bool Patch() { symbolsToWrite.AddRange(Symbols.Where(s => s.IsShadowSymbol && !symbolsToWrite.Contains(s))); Logger.Info($"Mapped {symbolsToWrite.Count} symbols to import targets."); + // Create the RTIS section + { + PESection rtisSec = new(SectionRTIS, SectionFlags.ContentInitializedData | SectionFlags.MemoryRead | SectionFlags.MemoryWrite | SectionFlags.MemoryExecute); + using var ms = new MemoryStream(); + using var writer = new BinaryWriter(ms, Encoding.UTF8); + foreach (var sym in symbolsToWrite) { + if (sym is AbstractPESymbol peSym) { + if (peSym.HasStorage) { + peSym.SetStorage(writer); + Logger.Debug($"Assigned storage offset 0x{peSym.StorageOffset:X} to symbol {peSym.Name}..."); + } + } + } + var data = new DataSegment(ms.ToArray()); + rtisSec.Contents = data; + File.Sections.Add(rtisSec); + File.AlignSections(); + } + + // Update storage offsets to be section-relative + uint rtisRVA = File.Sections.First(s => s.Name == SectionRTIS).Rva; + foreach (var sym in symbolsToWrite) { + if (sym is AbstractPESymbol peSym) { + if (peSym.HasStorage) { + // StorageOffset is RVO now, convert to RVA + peSym.StorageOffset += rtisRVA; + Logger.Debug($"Fixed up storage RVA to 0x{peSym.StorageOffset:X} for symbol {peSym.Name}..."); + } + } + } + // Create the RTIH section { PESection rtihSec = new(SectionRTIH, SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); diff --git a/ModuleTweaker/Patching/PE/V1/PEDataSymbol.cs b/ModuleTweaker/Patching/PE/V1/PEDataSymbol.cs index 9862714..af4e638 100644 --- a/ModuleTweaker/Patching/PE/V1/PEDataSymbol.cs +++ b/ModuleTweaker/Patching/PE/V1/PEDataSymbol.cs @@ -1,4 +1,5 @@ -using System; +using Amethyst.Common.Utility; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -28,5 +29,13 @@ public override void ReadFrom(BinaryReader reader) { IsVirtualTable = reader.ReadByte() != 0; Address = reader.ReadUInt64(); } + + public override void SetStorage(BinaryWriter writer) { + if (!HasStorage) + throw new InvalidOperationException("Cannot set storage on a symbol without storage."); + writer.Align(8, 0x00); // Align to 8 bytes with zeros + StorageOffset = (uint)writer.BaseStream.Position; + writer.Write(new byte[8]); + } } } diff --git a/ModuleTweaker/Patching/PE/V1/PEFunctionSymbol.cs b/ModuleTweaker/Patching/PE/V1/PEFunctionSymbol.cs index 315f2b6..d4c7ca6 100644 --- a/ModuleTweaker/Patching/PE/V1/PEFunctionSymbol.cs +++ b/ModuleTweaker/Patching/PE/V1/PEFunctionSymbol.cs @@ -7,6 +7,12 @@ namespace Amethyst.ModuleTweaker.Patching.PE.V1 { public class PEFunctionSymbol : AbstractPESymbol { + public static readonly byte[] VirtualDestructorDeletingDisableBlock = [ + 0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, // mov rax, 0x1000000000000000 + 0x31, 0xD2, // xor edx, edx (sets delete flag to false) + 0xFF, 0xE0 // jmp rax + ]; + public override uint FormatVersion => 1; public override string Kind => "function"; @@ -53,5 +59,15 @@ public override void WriteTo(BinaryWriter writer) { writer.Write(Address); } } + + public override void SetStorage(BinaryWriter writer) { + if (!HasStorage) + throw new InvalidOperationException("Cannot set storage on a symbol without storage."); + if (!IsDestructor) + throw new InvalidOperationException("Cannot set storage on a non-destructor function symbol."); + writer.Align(16, 0x90); // Align to 16 bytes with NOPs + StorageOffset = (uint)writer.BaseStream.Position; + writer.Write(VirtualDestructorDeletingDisableBlock); + } } } diff --git a/SymbolGenerator/Commands/MainCommand.cs b/SymbolGenerator/Commands/MainCommand.cs index 0d84022..1d0bed0 100644 --- a/SymbolGenerator/Commands/MainCommand.cs +++ b/SymbolGenerator/Commands/MainCommand.cs @@ -168,7 +168,8 @@ public ValueTask ExecuteAsync(IConsole console) annotationsData[location.File].Add(new VariableSymbolModel { Name = mangled, - Address = vtable.Address + Address = vtable.Address, + IsVirtualTableAddress = true }); } @@ -178,8 +179,7 @@ public ValueTask ExecuteAsync(IConsole console) annotationsData[location.File].Add(new VariableSymbolModel { Name = vtable.VtableMangledLabel, - Address = vtable.Address, - IsVirtualTableAddress = true + Address = vtable.Address }); } } From 1f3ba3f90ff65432d17ec42ba49685ef5eb204ce Mon Sep 17 00:00:00 2001 From: Raony Fernandes dos Reis Date: Wed, 15 Oct 2025 02:25:09 -0300 Subject: [PATCH 11/14] Update PEPatcher.cs --- ModuleTweaker/Patching/PE/PEPatcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ModuleTweaker/Patching/PE/PEPatcher.cs b/ModuleTweaker/Patching/PE/PEPatcher.cs index df08e99..b66294b 100644 --- a/ModuleTweaker/Patching/PE/PEPatcher.cs +++ b/ModuleTweaker/Patching/PE/PEPatcher.cs @@ -107,7 +107,6 @@ public bool Patch() { symbolsToWrite.Add(s); Logger.Debug($"Mapping shadow symbol {s.Name}..."); } - symbolsToWrite.AddRange(Symbols.Where(s => s.IsShadowSymbol && !symbolsToWrite.Contains(s))); Logger.Info($"Mapped {symbolsToWrite.Count} symbols to import targets."); // Create the RTIS section @@ -115,6 +114,7 @@ public bool Patch() { PESection rtisSec = new(SectionRTIS, SectionFlags.ContentInitializedData | SectionFlags.MemoryRead | SectionFlags.MemoryWrite | SectionFlags.MemoryExecute); using var ms = new MemoryStream(); using var writer = new BinaryWriter(ms, Encoding.UTF8); + writer.Write(0ul); // Runtime state (8 bytes) foreach (var sym in symbolsToWrite) { if (sym is AbstractPESymbol peSym) { if (peSym.HasStorage) { From efb99615527edc130c941633277efa6db14afe93 Mon Sep 17 00:00:00 2001 From: Raony Fernandes dos Reis Date: Wed, 15 Oct 2025 12:45:55 -0300 Subject: [PATCH 12/14] Add logging for RTIS and RTIH section sizes Introduced log messages to report the number of symbols and the total size in bytes for the generated RTIS and RTIH sections. This provides better visibility into the patching process and helps with debugging and analysis. --- ModuleTweaker/Patching/PE/PEPatcher.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ModuleTweaker/Patching/PE/PEPatcher.cs b/ModuleTweaker/Patching/PE/PEPatcher.cs index b66294b..5c5634e 100644 --- a/ModuleTweaker/Patching/PE/PEPatcher.cs +++ b/ModuleTweaker/Patching/PE/PEPatcher.cs @@ -109,6 +109,7 @@ public bool Patch() { } Logger.Info($"Mapped {symbolsToWrite.Count} symbols to import targets."); + uint rtisRealSize = 0; // Create the RTIS section { PESection rtisSec = new(SectionRTIS, SectionFlags.ContentInitializedData | SectionFlags.MemoryRead | SectionFlags.MemoryWrite | SectionFlags.MemoryExecute); @@ -123,11 +124,14 @@ public bool Patch() { } } } - var data = new DataSegment(ms.ToArray()); + byte[] msData = ms.ToArray(); + rtisRealSize = (uint)msData.Length; + var data = new DataSegment(msData); rtisSec.Contents = data; File.Sections.Add(rtisSec); File.AlignSections(); } + Logger.Info($"Generated storage for {symbolsToWrite.Count(s => (s is AbstractPESymbol peSym) && peSym.HasStorage)} symbols, total size 0x{rtisRealSize:X} bytes."); // Update storage offsets to be section-relative uint rtisRVA = File.Sections.First(s => s.Name == SectionRTIS).Rva; @@ -141,6 +145,7 @@ public bool Patch() { } } + uint rtihRealSize = 0; // Create the RTIH section { PESection rtihSec = new(SectionRTIH, SectionFlags.ContentInitializedData | SectionFlags.MemoryRead); @@ -153,11 +158,14 @@ public bool Patch() { ImportCount = index }; header.WriteTo(writer); - var data = new DataSegment(ms.ToArray()); + byte[] msData = ms.ToArray(); + rtihRealSize = (uint)msData.Length; + var data = new DataSegment(msData); rtihSec.Contents = data; File.Sections.Add(rtihSec); File.AlignSections(); } + Logger.Info($"Mapped {symbolsToWrite.Count} symbols, total size 0x{rtisRealSize:X} bytes."); // Create new NIDT section { From 29403ac4bf90a858259abcdde7709cddd0e38e35 Mon Sep 17 00:00:00 2001 From: Raony Fernandes dos Reis Date: Wed, 15 Oct 2025 13:07:38 -0300 Subject: [PATCH 13/14] Update PEPatcher.cs --- ModuleTweaker/Patching/PE/PEPatcher.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ModuleTweaker/Patching/PE/PEPatcher.cs b/ModuleTweaker/Patching/PE/PEPatcher.cs index 5c5634e..caae6c6 100644 --- a/ModuleTweaker/Patching/PE/PEPatcher.cs +++ b/ModuleTweaker/Patching/PE/PEPatcher.cs @@ -107,7 +107,6 @@ public bool Patch() { symbolsToWrite.Add(s); Logger.Debug($"Mapping shadow symbol {s.Name}..."); } - Logger.Info($"Mapped {symbolsToWrite.Count} symbols to import targets."); uint rtisRealSize = 0; // Create the RTIS section @@ -165,7 +164,7 @@ public bool Patch() { File.Sections.Add(rtihSec); File.AlignSections(); } - Logger.Info($"Mapped {symbolsToWrite.Count} symbols, total size 0x{rtisRealSize:X} bytes."); + Logger.Info($"Mapped {symbolsToWrite.Count} symbols, total size 0x{rtihRealSize:X} bytes."); // Create new NIDT section { From 914242ad71dead2b5d625edaa668f64e5cd4d660 Mon Sep 17 00:00:00 2001 From: Raony Fernandes dos Reis Date: Wed, 15 Oct 2025 13:44:43 -0300 Subject: [PATCH 14/14] Update Amethyst.SymbolGenerator.csproj --- SymbolGenerator/Amethyst.SymbolGenerator.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SymbolGenerator/Amethyst.SymbolGenerator.csproj b/SymbolGenerator/Amethyst.SymbolGenerator.csproj index 57c43f3..ca60b4d 100644 --- a/SymbolGenerator/Amethyst.SymbolGenerator.csproj +++ b/SymbolGenerator/Amethyst.SymbolGenerator.csproj @@ -8,7 +8,7 @@ enable True Amethyst.SymbolGenerator - 1.0.9 + 1.0.10