Skip to content

Instantly share code, notes, and snippets.

@UserUnknownFactor
Last active August 27, 2026 21:02
Show Gist options
  • Select an option

  • Save UserUnknownFactor/4cf7d1f0ef82c7de7f3d34cab27f12ba to your computer and use it in GitHub Desktop.

Select an option

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
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