Skip to content

Commit e726754

Browse files
committed
patch ILT
1 parent 812cc8d commit e726754

19 files changed

Lines changed: 138 additions & 262 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
*/obj
33
*/bin
44
/build
5+
*/build
56
/bin
67
/publish
78
/out

Common/build/net8.0/Amethyst.Common.deps.json

Lines changed: 0 additions & 73 deletions
This file was deleted.
-29 KB
Binary file not shown.
-24.3 KB
Binary file not shown.

ModuleTweaker/Commands/MainCommand.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,14 +172,24 @@ ulong ParseAddress(string? address)
172172
return default;
173173
}
174174
Logger.Info($"Loaded module '{ModulePath}' as PE file.");
175-
var patcher = new PEPatcher(peFile, [..symbols.Values], includeDebugNames);
175+
var patcher = new PEPatcher(peFile, [..symbols.Values], includeDebugNames, Obfuscate);
176176

177177
if (patcher.Patch())
178178
{
179-
File.Copy(ModulePath, ModulePath + ".bak", true);
179+
if (!Obfuscate)
180+
File.Copy(ModulePath, ModulePath + ".bak", true);
180181
using var ms = new MemoryStream();
181182
peFile.Write(ms);
182183
var newBytes = ms.ToArray();
184+
foreach (var (off, len) in patcher.PostWriteZeros) {
185+
if (off + len > newBytes.Length) {
186+
Logger.Warn($"Post-write zero 0x{off:X}+{len} exceeds output size {newBytes.Length}, skipping.");
187+
continue;
188+
}
189+
Array.Clear(newBytes, (int)off, (int)len);
190+
}
191+
if (patcher.PostWriteZeros.Count > 0)
192+
Logger.Info($"Applied {patcher.PostWriteZeros.Count} post-write zero range(s).");
183193
ulong newHash = XXH64.DigestOf(newBytes);
184194
File.WriteAllBytes(ModulePath, newBytes);
185195
File.WriteAllText(Path.Combine(PlatformOutput.FullName, "module_hash.txt"), newHash.ToString("X16"));

ModuleTweaker/Patching/PE/PEPatcher.cs

Lines changed: 125 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
using System.Text;
77

88
namespace Amethyst.ModuleTweaker.Patching.PE {
9-
public class PEPatcher(PEFile file, List<AbstractSymbol> symbols, bool includeDebugNames = true) : IPatcher {
9+
public class PEPatcher(PEFile file, List<AbstractSymbol> symbols, bool includeDebugNames = true, bool zeroFillImports = false) : IPatcher {
1010
// Runtime Importer Header
1111
public const string SectionRTIH = ".rtih"; // Runtime Importer Header
1212
public const string SectionRTIS = ".rtis"; // Runtime Importer Storage
@@ -21,6 +21,14 @@ public class PEPatcher(PEFile file, List<AbstractSymbol> symbols, bool includeDe
2121
public PEFile File { get; } = file;
2222
public List<AbstractSymbol> Symbols { get; } = symbols;
2323
public bool IncludeDebugNames { get; } = includeDebugNames;
24+
public bool ZeroFillImports { get; } = zeroFillImports;
25+
26+
private record ZeroRange(uint Rva, uint Length, string Purpose);
27+
28+
// File offsets to zero in the serialized output, computed post-UpdateHeaders and
29+
// applied by the caller after File.Write. Avoids touching AsmResolver section
30+
// Contents, which subtly breaks LoadLibrary even when virtual size is preserved.
31+
public List<(uint FileOffset, uint Length)> PostWriteZeros { get; } = [];
2432

2533
public bool IsCustomSection(string name) {
2634
return CustomSections.Contains(name);
@@ -50,55 +58,82 @@ public bool Patch() {
5058
// Read existing import descriptors
5159
using var importReader = File.CreateDataDirectoryReader(importDirectory).ToReader();
5260
List<ImportDescriptor> importDescriptors = [];
53-
uint targetIATRVA = 0;
54-
uint targetILTRVA = 0;
61+
List<ImportDescriptor> targetDescriptors = [];
5562
while (true) {
5663
var entry = ImportDescriptor.Read(importReader);
5764
if (entry.IsZero)
5865
break;
5966
string name = File.CreateReaderAtRva(entry.Name).ReadAsciiString();
6067
if (name.StartsWith("Minecraft.Windows", StringComparison.OrdinalIgnoreCase)) {
61-
targetIATRVA = entry.OriginalFirstThunk;
62-
targetILTRVA = entry.FirstThunk;
68+
targetDescriptors.Add(entry);
6369
}
6470
importDescriptors.Add(entry);
6571
}
6672

67-
if (targetIATRVA == 0 || targetILTRVA == 0) {
73+
if (targetDescriptors.Count == 0) {
6874
Logger.Warn("PE file does not import from 'Minecraft.Windows', skipping patch.");
6975
return false;
7076
}
77+
if (targetDescriptors.Count > 1) {
78+
Logger.Warn($"PE file has {targetDescriptors.Count} 'Minecraft.Windows' descriptors; all will be patched.");
79+
}
7180

72-
// Map import names to their target RVAs
81+
// Map import names to their target RVAs across all matching descriptors
7382
Dictionary<string, uint> importNameToTarget = [];
7483
List<AbstractSymbol> symbolsToWrite = [];
75-
var targetILTReader = File.CreateReaderAtRva(targetILTRVA);
76-
var targetIATReader = File.CreateReaderAtRva(targetIATRVA);
77-
uint index = 0;
78-
while (true) {
79-
ulong iltEntry = targetILTReader.ReadUInt64();
80-
if (iltEntry == 0)
81-
break;
82-
index++;
83-
ulong iatEntry = targetIATReader.ReadUInt64();
84-
bool isOrdinal = (iltEntry & 0x8000000000000000) != 0;
85-
if (isOrdinal) {
86-
continue;
84+
List<ZeroRange> rangesToZero = [];
85+
HashSet<uint> targetHintNameRvas = [];
86+
uint totalImportCount = 0;
87+
88+
foreach (var target in targetDescriptors) {
89+
uint targetILTRVA = target.OriginalFirstThunk; // hint/name RVAs
90+
uint targetIATRVA = target.FirstThunk; // code calls through this; runtime writes here
91+
92+
var targetILTReader = File.CreateReaderAtRva(targetILTRVA);
93+
var targetIATReader = File.CreateReaderAtRva(targetIATRVA);
94+
uint descriptorEntryCount = 0;
95+
while (true) {
96+
ulong iltEntry = targetILTReader.ReadUInt64();
97+
if (iltEntry == 0)
98+
break;
99+
descriptorEntryCount++;
100+
totalImportCount++;
101+
ulong iatEntry = targetIATReader.ReadUInt64();
102+
bool isOrdinal = (iltEntry & 0x8000000000000000) != 0;
103+
if (isOrdinal)
104+
continue;
105+
uint hintNameRVA = (uint)(iltEntry & 0x7FFFFFFFFFFFFFFF);
106+
var hintNameReader = File.CreateReaderAtRva(hintNameRVA);
107+
ushort hint = hintNameReader.ReadUInt16();
108+
string name = hintNameReader.ReadAsciiString();
109+
110+
// Zero-fill: record the hint+name blob (2-byte hint + name bytes + null terminator)
111+
uint hintNameLen = (uint)(2 + name.Length + 1);
112+
rangesToZero.Add(new ZeroRange(hintNameRVA, hintNameLen, $"hint+name:{name}"));
113+
targetHintNameRvas.Add(hintNameRVA);
114+
115+
var symbol = Symbols.OfType<AbstractPESymbol>().FirstOrDefault(s => s.Name == name);
116+
if (symbol is null)
117+
continue;
118+
uint entryRVA = targetIATRVA + ((descriptorEntryCount - 1) * 8);
119+
importNameToTarget[name] = entryRVA;
120+
symbol.TargetOffset = entryRVA;
121+
symbolsToWrite.Add(symbol);
122+
Logger.Debug($"Mapping import {name} to target RVA 0x{entryRVA:X}...");
87123
}
88-
uint hintNameRVA = (uint)(iltEntry & 0x7FFFFFFFFFFFFFFF);
89-
var hintNameReader = File.CreateReaderAtRva(hintNameRVA);
90-
ushort hint = hintNameReader.ReadUInt16();
91-
string name = hintNameReader.ReadAsciiString();
92-
var symbol = Symbols.OfType<AbstractPESymbol>().FirstOrDefault(s => s.Name == name);
93-
if (symbol is null)
94-
continue;
95-
uint entryRVA = targetILTRVA + ((index - 1) * 8);
96-
importNameToTarget[name] = entryRVA;
97-
symbol.TargetOffset = entryRVA;
98-
symbolsToWrite.Add(symbol);
99-
Logger.Debug($"Mapping import {name} to target RVA 0x{entryRVA:X}...");
124+
125+
// Zero-fill: record ILT and IAT slot ranges (entries + 8-byte null terminator)
126+
uint slotTableBytes = (descriptorEntryCount + 1) * 8;
127+
rangesToZero.Add(new ZeroRange(targetILTRVA, slotTableBytes, $"ILT ({descriptorEntryCount} entries)"));
128+
rangesToZero.Add(new ZeroRange(targetIATRVA, slotTableBytes, $"IAT ({descriptorEntryCount} entries)"));
129+
130+
// Zero-fill: record DLL name string
131+
string dllName = File.CreateReaderAtRva(target.Name).ReadAsciiString();
132+
rangesToZero.Add(new ZeroRange(target.Name, (uint)(dllName.Length + 1), $"dll-name:{dllName}"));
100133
}
101134

135+
uint index = totalImportCount;
136+
102137
foreach (var s in Symbols.Where(s => s.IsShadowSymbol && !symbolsToWrite.Contains(s))) {
103138
symbolsToWrite.Add(s);
104139
Logger.Debug($"Mapping shadow symbol {s.Name}...");
@@ -169,10 +204,14 @@ public bool Patch() {
169204
using var ms = new MemoryStream();
170205
using var writer = new BinaryWriter(ms, Encoding.UTF8);
171206
foreach (var entry in importDescriptors) {
172-
if (entry.OriginalFirstThunk == targetIATRVA || entry.FirstThunk == targetILTRVA)
207+
if (targetDescriptors.Any(t => t.OriginalFirstThunk == entry.OriginalFirstThunk && t.FirstThunk == entry.FirstThunk))
173208
continue;
174209
entry.Write(writer);
175210
}
211+
// Null-terminator descriptor: Windows PE loader iterates until it hits a
212+
// zero-filled IMAGE_IMPORT_DESCRIPTOR (20 bytes). Without this, the loader
213+
// reads past the section and LoadLibrary fails.
214+
writer.Write(new byte[20]);
176215
var data = new DataSegment(ms.ToArray());
177216
nidtSec.Contents = data;
178217
File.Sections.Add(nidtSec);
@@ -182,6 +221,60 @@ public bool Patch() {
182221
File.OptionalHeader.SetDataDirectory(DataDirectoryIndex.ImportDirectory, new DataDirectory(nidtSec.Rva, data.GetVirtualSize()));
183222
}
184223

224+
if (ZeroFillImports) {
225+
// Cross-check: filter out any hint/name RVAs that are shared with non-target descriptors
226+
HashSet<uint> sharedHintNameRvas = [];
227+
foreach (var entry in importDescriptors) {
228+
if (targetDescriptors.Any(t => t.OriginalFirstThunk == entry.OriginalFirstThunk && t.FirstThunk == entry.FirstThunk))
229+
continue;
230+
var iltReader = File.CreateReaderAtRva(entry.OriginalFirstThunk);
231+
while (true) {
232+
ulong ilt = iltReader.ReadUInt64();
233+
if (ilt == 0) break;
234+
if ((ilt & 0x8000000000000000) != 0) continue;
235+
uint hnRva = (uint)(ilt & 0x7FFFFFFFFFFFFFFF);
236+
if (targetHintNameRvas.Contains(hnRva))
237+
sharedHintNameRvas.Add(hnRva);
238+
}
239+
}
240+
241+
var filteredRanges = rangesToZero
242+
.Where(r => !(r.Purpose.StartsWith("hint+name:") && sharedHintNameRvas.Contains(r.Rva)))
243+
.ToList();
244+
245+
// Defer zero-fill to post-write: compute file offsets now, apply to output bytes later.
246+
// This sidesteps AsmResolver's Contents replacement, which corrupts LoadLibrary.
247+
File.UpdateHeaders();
248+
foreach (var range in filteredRanges) {
249+
var sec = File.Sections.FirstOrDefault(s =>
250+
range.Rva >= s.Rva && (range.Rva + range.Length) <= (s.Rva + s.GetVirtualSize()));
251+
if (sec is null) {
252+
Logger.Warn($"Range 0x{range.Rva:X}+{range.Length} ({range.Purpose}) has no containing section, skipping.");
253+
continue;
254+
}
255+
uint fileOffset = (uint)sec.Offset + (range.Rva - sec.Rva);
256+
PostWriteZeros.Add((fileOffset, range.Length));
257+
Logger.Debug($"Queued post-write zero: file 0x{fileOffset:X}+0x{range.Length:X} ({range.Purpose})");
258+
}
259+
Logger.Info($"Queued {PostWriteZeros.Count} post-write zero range(s).");
260+
261+
var boundDir = File.OptionalHeader.GetDataDirectory(DataDirectoryIndex.BoundImportDirectory);
262+
if (boundDir.IsPresentInPE)
263+
File.OptionalHeader.SetDataDirectory(DataDirectoryIndex.BoundImportDirectory, new DataDirectory(0, 0));
264+
var delayDir = File.OptionalHeader.GetDataDirectory(DataDirectoryIndex.DelayImportDescrDirectory);
265+
if (delayDir.IsPresentInPE)
266+
File.OptionalHeader.SetDataDirectory(DataDirectoryIndex.DelayImportDescrDirectory, new DataDirectory(0, 0));
267+
268+
// Clear the Debug data directory entry so external tools don't find CodeView/PDB info.
269+
// We deliberately do NOT touch the raw bytes the directory points at: they live in .rdata
270+
// adjacent to LoadConfig/CFG structures that the Windows loader actively reads, and
271+
// zeroing them crashes LoadLibrary. The build-time audit in mod_build.lua fails if any
272+
// RSDS or .pdb string actually appears in the output, which is the real defense.
273+
// var debugDir = File.OptionalHeader.GetDataDirectory(DataDirectoryIndex.DebugDirectory);
274+
// if (debugDir.IsPresentInPE)
275+
// File.OptionalHeader.SetDataDirectory(DataDirectoryIndex.DebugDirectory, new DataDirectory(0, 0));
276+
}
277+
185278
File.UpdateHeaders();
186279
Logger.Info("PE file patched successfully.");
187280
return true;
-29 KB
Binary file not shown.
-24.3 KB
Binary file not shown.

0 commit comments

Comments
 (0)