66using System . Text ;
77
88namespace 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 ;
0 commit comments