云计算百科
云计算领域专业知识百科平台

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

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

封面信息图

在对恶意软件样本、勒索病毒或加固商业客户端进行逆向分析时,经常会遇到各种压缩壳、加密壳或自研保护壳。这些外壳程序通常使用高度混淆的汇编指令、反调试陷阱和多层加密算法来阻碍静态反编译。

然而,无论外壳的加密算法多么复杂,CPU 在执行真实业务代码之前,必须将原始程序解密并还原到内存中。利用内存转储(Memory Dump)在程序执行流到达原始入口点(OEP, Original Entry Point)的瞬间提取解密后的内存镜像,是快速剥离外壳的核心技术。

壳的解密执行流程与 OEP 定位

一个标准加壳可执行文件的运行过程如下:

[操作系统加载器 Loader]


[外壳入口点 EP (Stub)] ──> [反调试/反虚拟机检测]


[内存解密循环 / 展开 Section] ──> [动态加载 DLL / 修复 IAT]


[跨段大跳转 JMP/CALL] ──> [原始入口点 OEP]


[执行真实业务逻辑]

核心难点:定位 OEP 的三种经典方法

  • ESP 定律法(单步堆栈平衡法):
    • 外壳初始代码通常会使用 pushad / pushal 保存所有通用寄存器状态。
    • 在执行解密前,在 ESP 寄存器所指向的栈顶地址设置硬件写入/访问断点(Hardware Breakpoint on Access)。
    • 当外壳完成解密并执行 popad 恢复寄存器时,命中该硬件断点,随后的 jmp OEP 即为跳转到真实入口点的跨段指令。
  • 内存访问断点法(Memory Breakpoint):
    • 对原始程序的代码段(如 .text)设置“内存执行(Execute)”断点。
    • 当外壳在数据区完成解密并首次将指令指针切回 .text 段时触发中断。
  • API 监控法:
    • 绝大多数壳在解密完成后,必须调用 GetProcAddress 和 LoadLibraryA 来重建被破坏的导入表(IAT)。在此类 API 返回处跟踪即可快速锁定 OEP。
  • 内存转储与 PE/ELF 镜像修复流程

    直接从内存中 Dump 出来的二进制文件并不能直接在磁盘上运行,原因在于:

  • 内存对齐(SectionAlignment)与文件对齐(FileAlignment)不一致:内存中通常是 0x1000(4KB)对齐,磁盘文件通常是 0x200(512B)对齐。
  • 导入地址表(IAT)未重绑定:内存中的 IAT 已经被解析为宿主系统的具体 API 虚拟地址,静态脱壳必须逆向提取 API 名称并重建导入描述符表(Import Directory Table)。
  • PE 镜像基址(ImageBase)与重定位表修复。
  • 自动化内存 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,现代代码防护应升级为更深维度的防御:

  • 动态代码抹除(Code Slicing & Erasing):函数执行完毕后立即用随机垃圾数据覆盖内存,避免全量代码同时停留在内存中。
  • 代码虚拟化保护(VMP / Code Virtualization):将真实 x86 机器码转换为私有的虚拟机字节码指令集,依靠解释器动态执行,即使 Dump 内存也只能得到虚拟字节码而非原生机器码。
  • 系统调用指纹校验:在关键解密路径中避开标准 API,直接使用内联汇编触发底层系统调用(Direct Syscall),增加动态 Hook 和断点监控的捕获难度。
  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » 加壳样本脱壳实录(三):利用内存转储(Memory Dump)提取解密代码
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!