加壳样本脱壳实录(三):利用内存转储(Memory Dump)提取解密代码

在对恶意软件样本、勒索病毒或加固商业客户端进行逆向分析时,经常会遇到各种压缩壳、加密壳或自研保护壳。这些外壳程序通常使用高度混淆的汇编指令、反调试陷阱和多层加密算法来阻碍静态反编译。
然而,无论外壳的加密算法多么复杂,CPU 在执行真实业务代码之前,必须将原始程序解密并还原到内存中。利用内存转储(Memory Dump)在程序执行流到达原始入口点(OEP, Original Entry Point)的瞬间提取解密后的内存镜像,是快速剥离外壳的核心技术。
壳的解密执行流程与 OEP 定位
一个标准加壳可执行文件的运行过程如下:
[操作系统加载器 Loader]
│
▼
[外壳入口点 EP (Stub)] ──> [反调试/反虚拟机检测]
│
▼
[内存解密循环 / 展开 Section] ──> [动态加载 DLL / 修复 IAT]
│
▼
[跨段大跳转 JMP/CALL] ──> [原始入口点 OEP]
│
▼
[执行真实业务逻辑]
核心难点:定位 OEP 的三种经典方法
- 外壳初始代码通常会使用 pushad / pushal 保存所有通用寄存器状态。
- 在执行解密前,在 ESP 寄存器所指向的栈顶地址设置硬件写入/访问断点(Hardware Breakpoint on Access)。
- 当外壳完成解密并执行 popad 恢复寄存器时,命中该硬件断点,随后的 jmp OEP 即为跳转到真实入口点的跨段指令。
- 对原始程序的代码段(如 .text)设置“内存执行(Execute)”断点。
- 当外壳在数据区完成解密并首次将指令指针切回 .text 段时触发中断。
- 绝大多数壳在解密完成后,必须调用 GetProcAddress 和 LoadLibraryA 来重建被破坏的导入表(IAT)。在此类 API 返回处跟踪即可快速锁定 OEP。
内存转储与 PE/ELF 镜像修复流程
直接从内存中 Dump 出来的二进制文件并不能直接在磁盘上运行,原因在于:
自动化内存 Dump 脚本实战
以下 Python 脚本展示了如何利用 Windows API(OpenProcess, VirtualQueryEx, ReadProcessMemory)连接到挂起状态的目标进程,遍历其虚拟内存页并提取解密的代码段:
import ctypes
from ctypes import wintypes
import struct
import sys
# Windows API 基础常量
PROCESS_ALL_ACCESS = 0x1F0FFF
MEM_COMMIT = 0x1000
PAGE_EXECUTE_READWRITE = 0x40
PAGE_EXECUTE_READ = 0x20
# 声明 Win32 结构体
class MEMORY_BASIC_INFORMATION(ctypes.Structure):
_fields_ = [
("BaseAddress", wintypes.LPVOID),
("AllocationBase", wintypes.LPVOID),
("AllocationProtect", wintypes.DWORD),
("RegionSize", ctypes.c_size_t),
("State", wintypes.DWORD),
("Protect", wintypes.DWORD),
("Type", wintypes.DWORD),
]
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
def dump_process_memory_at_oep(pid: int, output_file: str):
print(f"[*] 正在挂钩目标进程 PID: {pid}…")
# 1. 打开目标进程句柄
h_process = kernel32.OpenProcess(PROCESS_ALL_ACCESS, False, pid)
if not h_process:
raise ctypes.WinError(ctypes.get_last_error())
address = 0
mbi = MEMORY_BASIC_INFORMATION()
dumped_bytes = bytearray()
print("[*] 正在扫描可执行内存段…")
# 2. 遍历进程虚拟地址空间
while kernel32.VirtualQueryEx(h_process, ctypes.c_void_p(address), ctypes.byref(mbi), ctypes.sizeof(mbi)):
# 仅提取已提交且具备可执行权限的代码段
if mbi.State == MEM_COMMIT and (mbi.Protect in [PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE]):
base = mbi.BaseAddress
size = mbi.RegionSize
print(f"[+] 发现可执行内存块: 基址 = 0x{base:08X}, 大小 = {size / 1024:.2f} KB")
buffer = (ctypes.c_char * size)()
bytes_read = ctypes.c_size_t(0)
# 3. 从目标进程内存读取数据
if kernel32.ReadProcessMemory(h_process, ctypes.c_void_p(base), buffer, size, ctypes.byref(bytes_read)):
dumped_bytes.extend(buffer.raw)
else:
print(f"[-] 读取内存块失败: 0x{base:08X}")
address += mbi.RegionSize
if address >= 0x7FFFFFFF: # 32 位用户态地址上限
break
kernel32.CloseHandle(h_process)
# 4. 转储到本地文件
with open(output_file, "wb") as f:
f.write(dumped_bytes)
print(f"[+] 内存代码段转储完成,已保存至: {output_file} (总计 {len(dumped_bytes)} 字节)")
if __name__ == "__main__":
if len(sys.argv) < 3:
print("用法: python dump_mem.py <PID> <OUTPUT_PATH>")
sys.exit(1)
target_pid = int(sys.argv[1])
out_path = sys.argv[2]
dump_process_memory_at_oep(target_pid, out_path)
防御性加固与反脱壳对抗
从软件保护视角,单靠静态壳已无法抵御内存 Dump,现代代码防护应升级为更深维度的防御:
网硕互联帮助中心
评论前必须登录!
注册