Recover and statically analyze manually-mapped DLLs whose PE headers are
wiped at runtime. Works against any process you can OpenProcess() on. No
driver, no debugger, no symbols required. Pure-stdlib Python 3.
Common targets: malware loaders, in-process implants, packers, anti-cheat modules, and any other code mapped by a custom user-mode loader rather than the Windows loader.
Authorized use only. Only point this at processes you own or are explicitly authorized to analyze (your own code, malware in a lab, samples from a sandbox). Don't use it against software you don't have rights to.
Manually-mapped DLLs commonly:
- Get loaded by a custom user-mode loader (not the Windows loader)
- Wipe their
MZ/PE\0\0headers post-DllMainso a casual memory scan doesn't see them - Sometimes also wipe the section table, leaving only raw RX pages
A normal dumper (scylla, pe-sieve) finds them inconsistently or not at
all when the headers are gone. PEReconstruct finds the regions by their
runtime characteristics (executable + private) and forges a synthetic PE
header so IDA can analyze the dump as a normal DLL.
┌─────────────────────────────────┐
│ Target process (running) │
│ PID = whatever │
└────────────┬────────────────────┘
│
┌────────────────────────┼────────────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────────┐ ┌────────────────────┐
│ scan_exec_ │ │ scan_pe_deep.py │ │ dump_manualmap.py │
│ private.py │ │ │ │ │
│ │ │ Aggressive PE-sig │ │ Looks for MZ at │
│ Lists every RX │ │ sweep — finds wiped │ │ region start (the │
│ MEM_PRIVATE │ │ DLLs by their PE\0\0 │ │ "well-behaved" │
│ region. The big │ │ at +e_lfanew. │ │ case where headers │
│ one (~1-10 MB, │ │ Catches anti-dumping │ │ survive). │
│ entropy ~7) is │ │ tricks where MZ is │ │ │
│ usually it. │ │ zeroed but PE isn't. │ │ │
└────────┬─────────┘ └──────────┬───────────┘ └─────────┬──────────┘
│ │ │
└───────────────────────┼────────────────────────┘
│ (you pick the right region from the surveys)
▼
┌────────────────────────────────┐
│ raw .bin file on disk │
│ "execpriv_<HEX_BASE>_ │
│ 0x<SIZE>.bin" │
│ filename encodes runtime VA │
│ you'll feed to step 3. │
└────────────┬───────────────────┘
│
▼
┌────────────────────────────────┐
│ rebuild_headerless.py │
│ │
│ Forges a synthetic PE header │
│ in the first 0x1000 bytes: │
│ - DOS stub │
│ - PE32+ NT headers │
│ - Single .text section RX │
│ covering the whole image. │
└────────────┬───────────────────┘
│
▼
┌────────────────────────────────┐
│ <name>_reconstructed.dll │
│ (loadable PE — open in IDA) │
└────────────┬───────────────────┘
│
▼
┌────────────────────────────────┐
│ analyze_hooks.py │
│ resolve_exports.py │
│ │
│ Reconstruct the IAT from │
│ absolute addresses found in │
│ the dump; name every API │
│ call by module+offset. │
└────────────────────────────────┘
# 1. Find your target process
tasklist | findstr /i "<your_target>"
# Note the PID
# 2. Survey executable private regions (the headerless catcher)
py 01_acquire/scan_exec_private.py --pid <PID> --dump
# 2b. Optionally also try the deep PE-sig sweep
py 01_acquire/scan_pe_deep.py --pid <PID>
# 2c. If the target HAS valid MZ headers (rare for hidden modules), this is fastest
py 01_acquire/dump_manualmap.py --pid <PID>
# Output: a folder of .bin files at C:\path\to\execprivate_<PID>\
# Identify the candidate by size (1-10 MB) and entropy (~6-7).
# Filename pattern: execpriv_<runtime_base_hex>_0x<size_hex>.bin
# 3. Reconstruct the PE — pass the runtime base from the filename
py 02_reconstruct/rebuild_headerless.py \
execprivate_<PID>/execpriv_<BASE_HEX>_<SIZE_HEX>.bin \
0x<BASE_HEX> \
target_reconstructed.dll
# 4. Open target_reconstructed.dll in IDA. Auto-analyze. Done.
# 5. Optional: enrich with imports + named API calls
py 03_enrich/analyze_hooks.py --pid <PID> \
--target target_reconstructed.dll \
--target-base 0x<BASE_HEX> \
--out target_imports.md| Folder | Script | Purpose |
|---|---|---|
01_acquire/ |
scan_exec_private.py |
Primary tool. Lists every RX MEM_PRIVATE region. Use this first — works on headerless dumps. |
01_acquire/ |
scan_pe_deep.py |
Aggressive PE-signature sweep across all committed memory. Catches dumps where MZ was zeroed but PE\0\0 wasn't. |
01_acquire/ |
dump_manualmap.py |
Strict MZ-at-region-start detector. Use only if the target preserves headers. |
01_acquire/ |
dump_contiguous_image.py |
Stitch a contiguous VA range from a target into a single dump, zero-filling unmapped gaps. |
02_reconstruct/ |
rebuild_headerless.py |
Core tool. Forges DOS+NT+section-table over the first 0x1000 bytes of a raw dump. |
03_enrich/ |
analyze_hooks.py |
Walks trampoline pages, identifies hooked functions by absolute-address pattern. Produces a markdown report. |
03_enrich/ |
resolve_exports.py |
Resolves module.dll:0xOFFSET → kernel32!CreateFileA+0x12 style names. |
No section recovery. rebuild_headerless.py declares ONE .text
section covering the whole image RX. The original may have had separate
.rdata / .data / .pdata sections — those become indistinguishable
from code. Side effects:
- IDA tries to disassemble data regions as code (look like garbage)
- High-entropy embedded data (encrypted blobs, compressed assets) appears as a stretch of "invalid instructions" — that's fine, just don't try to interpret them as functions
.idataimport table is GONE — you can't follow API calls by name unless you reconstruct imports (useanalyze_hooks.py)
No relocation table. The reconstructed DLL hardcodes the original
runtime ImageBase. If you try to actually LOAD it via LoadLibrary,
ASLR will rebase it and absolute pointers internal to the dump will be
broken. Only use the output for static analysis in IDA, not for
re-injection.
Wiped padding. If the loader zeroed regions inside the dump (PE header padding, freed allocations), those bytes are lost. Reconstruction won't recover them — the IDB will have zero-filled gaps where things used to be.
Hardened processes.
OpenProcess(PROCESS_VM_READ) is blocked when the target installs a
kernel callback (e.g. ObRegisterCallbacks). You'll get
ERROR_ACCESS_DENIED (5) or a stripped-rights handle. To proceed:
- Suspend the protective service before launching (only works for protections that run as a separate user-mode service)
- Or use a kernel-mode read primitive — your own driver via
kdmapper, or any\Device\PhysicalMemory-style helper. The dump scripts here all assume user-mode RPM and will silently fail otherwise.
Stdlib only. Tested with CPython 3.12 on Windows. No pip installs needed.
PEReconstruct is built to be driven from a coding agent — every script
is argparse-based, prints structured output, and writes deterministic
filenames. A typical Claude Code session looks like:
You: Dump the manually-mapped DLL inside PID 24468 and reconstruct it as a loadable PE for IDA.
Claude Code: (runs
scan_exec_private.py --pid 24468 --dump, reads the entropy + size table, picks the highest-entropy region in the 1-10 MB range, runsrebuild_headerless.pyagainst the dumped.binwith the base parsed from the filename, outputs the path to the reconstructed.dll)
Helpful prompts:
- "Survey PID
<N>for hidden modules and tell me which regions look like manual-mapped DLLs." - "Reconstruct
<path-to-bin>at base0x<BASE>and open it in IDA via the IDA-Pro MCP server." - "Run
analyze_hooks.pyagainst the reconstructed DLL and summarize which Win32 modules it imports from."
Tip: this pairs well with ida-pro-mcp. After reconstruction, point Claude at the IDA MCP server to drive disassembly, decompilation, and symbol-naming over MCP.
The skills/pe-reconstruct/ directory in this repo is a ready-to-use
Claude Code skill.
Once installed, Claude Code automatically invokes it when you ask about
dumping, recovering, or analyzing manually-mapped/headerless DLLs.
# Install for the current user (all projects)
mkdir -p ~/.claude/skills
cp -r skills/pe-reconstruct ~/.claude/skills/
# Or install for one project only
mkdir -p .claude/skills
cp -r skills/pe-reconstruct .claude/skills/The skill's SKILL.md tells Claude exactly which script to run for which
task and how to chain them.
MIT — see LICENSE.