Skip to content

Instantly share code, notes, and snippets.

@Quackster
Created January 18, 2026 13:23
Show Gist options
  • Select an option

  • Save Quackster/31006851b64072472a5558cc86f66da5 to your computer and use it in GitHub Desktop.

Select an option

Save Quackster/31006851b64072472a5558cc86f66da5 to your computer and use it in GitHub Desktop.
aspack v2.12 dumper
import struct, os, ctypes, time, pefile
from ctypes import wintypes
CREATE_SUSPENDED = 0x4
STILL_ACTIVE = 259
class STARTUPINFOW(ctypes.Structure):
_fields_ = [
("cb", wintypes.DWORD),
("lpReserved", wintypes.LPWSTR),
("lpDesktop", wintypes.LPWSTR),
("lpTitle", wintypes.LPWSTR),
("dwX", wintypes.DWORD),
("dwY", wintypes.DWORD),
("dwXSize", wintypes.DWORD),
("dwYSize", wintypes.DWORD),
("dwXCountChars", wintypes.DWORD),
("dwYCountChars", wintypes.DWORD),
("dwFillAttribute", wintypes.DWORD),
("dwFlags", wintypes.DWORD),
("wShowWindow", wintypes.WORD),
("cbReserved2", wintypes.WORD),
("lpReserved2", ctypes.c_void_p),
("hStdInput", wintypes.HANDLE),
("hStdOutput", wintypes.HANDLE),
("hStdError", wintypes.HANDLE),
]
class PROCESS_INFORMATION(ctypes.Structure):
_fields_ = [
("hProcess", wintypes.HANDLE),
("hThread", wintypes.HANDLE),
("dwProcessId", wintypes.DWORD),
("dwThreadId", wintypes.DWORD),
]
class MEMORY_BASIC_INFORMATION32(ctypes.Structure):
_fields_ = [
("BaseAddress", wintypes.DWORD),
("AllocationBase", wintypes.DWORD),
("AllocationProtect", wintypes.DWORD),
("RegionSize", wintypes.DWORD),
("State", wintypes.DWORD),
("Protect", wintypes.DWORD),
("Type", wintypes.DWORD),
]
k32 = ctypes.windll.kernel32
def read_mem(hproc, addr, size):
"""Read memory, returning bytes or None"""
buf = ctypes.create_string_buffer(size)
nread = ctypes.c_size_t()
if k32.ReadProcessMemory(hproc, ctypes.c_void_p(addr), buf, size, ctypes.byref(nread)):
return buf.raw[:nread.value]
return None
def dump_unpacked(exe_path, output_path, wait_time=3.0):
"""Dump ASPack-unpacked 32-bit VB6 executable from memory"""
pe = pefile.PE(exe_path)
image_base = pe.OPTIONAL_HEADER.ImageBase
image_size = pe.OPTIONAL_HEADER.SizeOfImage
print(f"[*] Image base: 0x{image_base:08X}, size: 0x{image_size:X}")
# Create suspended process
si = STARTUPINFOW()
si.cb = ctypes.sizeof(STARTUPINFOW)
pi = PROCESS_INFORMATION()
exe_abs = os.path.abspath(exe_path)
if not k32.CreateProcessW(exe_abs, None, None, None, False, CREATE_SUSPENDED,
None, os.path.dirname(exe_abs), ctypes.byref(si), ctypes.byref(pi)):
print(f"[-] CreateProcess failed: {k32.GetLastError()}")
return False
print(f"[*] PID: {pi.dwProcessId}, TID: {pi.dwThreadId}")
try:
# Let ASPack unpack
print(f"[*] Running for {wait_time}s to let ASPack decompress...")
k32.ResumeThread(pi.hThread)
time.sleep(wait_time)
k32.SuspendThread(pi.hThread)
# Check process alive
ec = wintypes.DWORD()
k32.GetExitCodeProcess(pi.hProcess, ctypes.byref(ec))
if ec.value != STILL_ACTIVE:
print(f"[-] Process exited: {ec.value}")
return False
print("[*] Process still running")
# Read image page by page
dump = bytearray(image_size)
pages_read = 0
for off in range(0, image_size, 0x1000):
data = read_mem(pi.hProcess, image_base + off, min(0x1000, image_size - off))
if data:
dump[off:off+len(data)] = data
pages_read += 1
print(f"[*] Read {pages_read} pages ({pages_read * 0x1000} bytes)")
if pages_read == 0:
print("[-] No memory read!")
return False
# Verify MZ header
if dump[:2] != b'MZ':
print("[-] Invalid MZ header")
return False
# Get PE offset
e_lfanew = struct.unpack("<I", dump[0x3C:0x40])[0]
if dump[e_lfanew:e_lfanew+2] != b'PE':
print("[-] Invalid PE signature")
return False
print(f"[*] Valid PE at offset 0x{e_lfanew:X}")
# Find OEP - VB6 apps push address of VB5! header
vb5_pos = bytes(dump).find(b'VB5!')
if vb5_pos >= 0:
vb5_va = image_base + vb5_pos
print(f"[*] VB5! header at 0x{vb5_va:08X} (RVA 0x{vb5_pos:X})")
# Search for PUSH <vb5_va> (0x68 + 4-byte address)
push_target = struct.pack("<I", vb5_va)
oep_rva = None
for i in range(len(dump) - 5):
if dump[i] == 0x68 and bytes(dump[i+1:i+5]) == push_target:
oep_rva = i
print(f"[*] Found OEP: PUSH at RVA 0x{oep_rva:X}")
break
if oep_rva is None:
# Fallback: use section start
for sec in pe.sections:
if sec.Characteristics & 0x20: # CODE
oep_rva = sec.VirtualAddress
break
else:
oep_rva = pe.sections[0].VirtualAddress
print(f"[*] Using fallback OEP: 0x{oep_rva:X}")
else:
oep_rva = pe.OPTIONAL_HEADER.AddressOfEntryPoint
print(f"[*] No VB5! found, keeping original OEP: 0x{oep_rva:X}")
# Patch entry point
struct.pack_into("<I", dump, e_lfanew + 0x28, oep_rva)
# Fix section headers for memory-mapped layout
num_secs = struct.unpack("<H", dump[e_lfanew + 6:e_lfanew + 8])[0]
opt_size = struct.unpack("<H", dump[e_lfanew + 0x14:e_lfanew + 0x16])[0]
sec_off = e_lfanew + 0x18 + opt_size
print(f"[*] Fixing {num_secs} sections:")
for i in range(num_secs):
s = sec_off + i * 0x28
name = dump[s:s+8].rstrip(b'\x00').decode(errors='ignore')
vsize = struct.unpack("<I", dump[s+8:s+12])[0]
vaddr = struct.unpack("<I", dump[s+12:s+16])[0]
# Set RawSize = VirtualSize, RawAddress = VirtualAddress
struct.pack_into("<I", dump, s + 0x10, vsize)
struct.pack_into("<I", dump, s + 0x14, vaddr)
print(f" {name:8s} VA=0x{vaddr:05X} Size=0x{vsize:X}")
# Write dump
with open(output_path, 'wb') as f:
f.write(dump)
print(f"[+] Saved unpacked PE: {output_path}")
print(f"[+] Entry point: 0x{oep_rva:X}")
return True
finally:
k32.TerminateProcess(pi.hProcess, 0)
k32.CloseHandle(pi.hProcess)
k32.CloseHandle(pi.hThread)
if __name__ == "__main__":
# Try very short intervals since ASPack unpacks very fast
for wait in [0.05, 0.1, 0.15, 0.2, 0.3]:
print(f"\n=== Trying wait_time={wait}s ===")
if dump_unpacked("H-Retro.exe", f"H-Retro_unpacked.exe", wait_time=wait):
break
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment