Created
January 18, 2026 13:23
-
-
Save Quackster/31006851b64072472a5558cc86f66da5 to your computer and use it in GitHub Desktop.
aspack v2.12 dumper
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 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