Last active
August 27, 2026 21:02
-
-
Save UserUnknownFactor/4cf7d1f0ef82c7de7f3d34cab27f12ba to your computer and use it in GitHub Desktop.
Fixes mscorlib.dll to always use the ja-JP locale for all ConstructCurrentCulture calls
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import pefile | |
| import struct | |
| import re | |
| import sys | |
| import os | |
| import shutil | |
| LOCALE_NAME = "ja-JP" # this should exist in the string table or patch it instead of a 5-wchar string | |
| def patch_mscorlib(input_path): | |
| print(f"[#] Loading PE file: {input_path}") | |
| pe = pefile.PE(input_path) | |
| # 1. Locate the .NET Metadata Root | |
| clr_dir = pe.OPTIONAL_HEADER.DATA_DIRECTORY[14] # COM Descriptor / CLR Header | |
| if clr_dir.VirtualAddress == 0: | |
| raise ValueError("Not a valid .NET assembly (CLR Header not found).") | |
| clr_offset = pe.get_offset_from_rva(clr_dir.VirtualAddress) | |
| meta_rva = struct.unpack_from('<I', pe.__data__, clr_offset + 8)[0] | |
| meta_offset = pe.get_offset_from_rva(meta_rva) | |
| # Verify metadata signature | |
| sig = struct.unpack_from('<I', pe.__data__, meta_offset)[0] | |
| if sig != 0x424A5342: | |
| raise ValueError("Invalid .NET metadata signature.") | |
| # 2. Parse Stream Headers to find the #US (User Strings) heap | |
| ver_length = struct.unpack_from('<I', pe.__data__, meta_offset + 12)[0] | |
| flags_offset = meta_offset + 16 + ((ver_length + 3) & ~3) | |
| num_streams = struct.unpack_from('<H', pe.__data__, flags_offset + 2)[0] | |
| stream_offset = flags_offset + 4 | |
| us_offset = None | |
| us_size = None | |
| for _ in range(num_streams): | |
| s_offset = struct.unpack_from('<I', pe.__data__, stream_offset)[0] | |
| s_size = struct.unpack_from('<I', pe.__data__, stream_offset + 4)[0] | |
| name_start = stream_offset + 8 | |
| name_end = pe.__data__.find(b'\x00', name_start) | |
| name = pe.__data__[name_start:name_end].decode('utf-8') | |
| if name == '#US': | |
| us_offset = meta_offset + s_offset | |
| us_size = s_size | |
| break | |
| name_len = name_end - name_start + 1 | |
| name_len_padded = (name_len + 3) & ~3 | |
| stream_offset += 8 + name_len_padded | |
| if us_offset is None: | |
| raise ValueError("#US (User Strings) stream not found in metadata.") | |
| # 3. Find the LOCALE_NAME string token in the #US heap | |
| us_data = pe.__data__[us_offset:us_offset+us_size] | |
| target_bytes = LOCALE_NAME.encode('utf-16-le') | |
| found_token = None | |
| offset = 1 # standard #US heap offset 0 is the reserved empty string | |
| while offset < len(us_data): | |
| b0 = us_data[offset] | |
| # Decode compressed unsigned integer (string length) | |
| if (b0 & 0x80) == 0: | |
| length = b0 | |
| prefix_size = 1 | |
| elif (b0 & 0xC0) == 0x80: | |
| if offset + 1 >= len(us_data): | |
| break | |
| length = ((b0 & 0x3F) << 8) | us_data[offset + 1] | |
| prefix_size = 2 | |
| else: | |
| if offset + 3 >= len(us_data): | |
| break | |
| length = ( | |
| ((b0 & 0x1F) << 24) | |
| | (us_data[offset + 1] << 16) | |
| | (us_data[offset + 2] << 8) | |
| | us_data[offset + 3] | |
| ) | |
| prefix_size = 4 | |
| # Invalid/truncated entry | |
| if length == 0 or offset + prefix_size + length > len(us_data): | |
| break | |
| # Last byte of a #US entry is the special flag byte. | |
| str_data = us_data[ | |
| offset + prefix_size: | |
| offset + prefix_size + length - 1 | |
| ] | |
| if str_data == target_bytes: | |
| found_token = 0x70000000 | offset | |
| break | |
| # Move directly to the next #US entry. | |
| offset += prefix_size + length | |
| if found_token is None: | |
| print(f"[-] String '{LOCALE_NAME}' not found in the #US heap.") | |
| else: | |
| print(f"[+] Found '{LOCALE_NAME}' in #US heap. Token: 0x{found_token:08X}") | |
| # 4. Find the target IL pattern in the .text section | |
| text_section = next((s for s in pe.sections if b'.text' in s.Name), None) | |
| if not text_section: | |
| raise ValueError(".text section not found.") | |
| text_data = pe.__data__[text_section.PointerToRawData : text_section.PointerToRawData + text_section.SizeOfRawData] | |
| # There are 2 but the necessary goes 1st and patching second is "safe" (if we run the py twice accidentally). | |
| pattern = re.compile(rb'\x28....\x0A\x14\x0B\x06\x2C.\x06\x28....', re.DOTALL) | |
| match = pattern.search(text_data) | |
| if not match: | |
| raise ValueError("Target IL pattern of ConstructCurrentCulture not found in .text section.") | |
| # Calculate exact file offset of the target 'call' instruction | |
| target_file_offset = text_section.PointerToRawData + match.start() | |
| pe.close() | |
| with open(input_path, 'rb') as f: | |
| file_data = bytearray(f.read()) | |
| # Safety check: ensure the byte at our calculated offset is indeed 'call' | |
| if file_data[target_file_offset] != 0x28: # 'call' | |
| raise ValueError("Calculated offset does not point to a 'call' (0x28) instruction. Aborting to prevent corruption.") | |
| # Change opcode from 'call' to 'ldstr' to load our CultureInfo | |
| # or to 'ldnull' which'll enable InvariantCulture branch if no locale found | |
| replacement_info = "" | |
| if found_token is not None: | |
| file_data[target_file_offset] = 0x72 # 'ldstr' | |
| # Inject the new metadata token for the locale string for ldstr | |
| struct.pack_into('<I', file_data, target_file_offset + 1, found_token) | |
| replacement_info = f"'ldstr \"{LOCALE_NAME}\"'" | |
| else: | |
| file_data[target_file_offset] = 0x14 # 'ldnull' | |
| # Inject nop (0x00) instructions for ldnull padding | |
| struct.pack_into('<I', file_data, target_file_offset + 1, 0) | |
| replacement_info = f"'ldnull' + 4*'nop'" | |
| backup_path = input_path + '.bak' | |
| if not os.path.exists(backup_path): | |
| print(f"[*] Backing up original '{input_path}' to '{backup_path}'") | |
| shutil.move(input_path, backup_path) | |
| with open(input_path, 'wb') as f: | |
| f.write(file_data) | |
| print(f"[√] Successfully patched and saved as '{input_path}'") | |
| print(f"[+] Replaced 'call' with {replacement_info} at file offset 0x{target_file_offset:X}") | |
| if __name__ == '__main__': | |
| input_file = 'mscorlib.dll' | |
| if len(sys.argv) > 1: | |
| input_file = sys.argv[1] | |
| if not os.path.exists(input_file): | |
| print(f"[-] Error: File '{input_file}' not found.") | |
| sys.exit(1) | |
| try: | |
| patch_mscorlib(input_file) | |
| except Exception as e: | |
| print(f"[-] Error: {e}") | |
| sys.exit(1) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment