Skip to content

Releases: techlinn/MemPE

v0.5.6

Choose a tag to compare

@github-actions github-actions released this 21 Oct 03:28

Changes

  • named forwarded imports after the module that exports them

Full Changelog: v0.5.5...v0.5.6

v0.5.5

Choose a tag to compare

@github-actions github-actions released this 20 Oct 19:45

Changes

  • labelled where each recovered import came from and scored call-site agreement

Full Changelog: v0.5.4...v0.5.5

v0.5.4

Choose a tag to compare

@github-actions github-actions released this 20 Oct 05:37

Changes

  • added a build and test workflow
  • bumped version
  • stopped importing from itself and counted slots outside data sections
  • bumped version
  • counted why referenced import slots failed to resolve
  • bumped version
  • stopped losing recovered imports the headers could not describe

Full Changelog: v0.5.1...v0.5.4

v0.5.1

Choose a tag to compare

@github-actions github-actions released this 20 Oct 00:12

Changes

  • bumped version
  • stopped mapping an RVA into a section with no raw data
  • kept the exports it can read when one ordinal overflows
  • generated release notes from commits instead of pull requests

Full Changelog: v0.5.0...v0.5.1

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 19 Oct 15:20

Full Changelog: v0.4.0...v0.5.0

MemPE v0.4.0

Choose a tag to compare

@techlinn techlinn released this 04 Aug 22:52

Imports

mempe used to claim two imports kernel32 does not have. The decoder that reads
mov r64,[rip+disp32] treated any cached pointer to an API as a link-time
dependency. Those candidates now need a neighboring pointer slot resolving into
the same module before they count; pointers reached through call or jmp are
accepted as before. Checked against the file on disk, the rebuilt kernel32
import table is an exact match at 1274 entries. Across every dumped System32
DLL, false positives dropped from 748 to 691 and nothing real was lost: 0
missing out of 10700.

An image can no longer be recorded as importing from itself. ntdll was claiming
five imports from ntdll.

Dumps that used to fail

Denied module enumeration used to stop mempe before it captured anything. The
loader module list is now enrichment rather than a requirement: the main image
is identified by its mapped file path, and the degraded mode is reported instead
of inferred.

The 4096-image and 4 GiB safety caps used to abort. They now capture what fits,
always including the main image, and report how many images were skipped.

New

--only <name> or --only 0x<base> writes just the images you want. Every
image is still captured and indexed, so imports resolve exactly as they would in
a full dump; only the files written are filtered.

--raw-regions now writes a sidecar beside each blob giving its load address,
allocation base, committed size and page protections, so a disassembler can be
pointed at the right address. No header is invented for those bytes.

Full mapped paths are kept instead of basenames, which is enough to tell two
same-named DLLs in different directories apart. On explorer it found three.

Measured and left alone

Four suspected limitations turned out not to be. Thunk chains never exceed one
hop across explorer, RuntimeBroker and five test binaries, and nothing was ever
abandoned at the depth limit. Export indexing costs 72 ms of a 5 second explorer
dump. Capture is under a quarter of wall time, so parallelising the reads would
not buy anything. Watch mode never declared a target settled early: private page
counts were identical four seconds later on all four packed samples.

Bound imports turned out to be common rather than legacy, 10836 of 29267 PE
files on a stock Windows 11 install, but mempe never clears that directory. It
came off the list of clears not worth naming, so a real one would be reported.

MemPE v0.3.0

Choose a tag to compare

@techlinn techlinn released this 31 Jul 18:44

Two fixes about the main image, and they pull in opposite directions in a way that
turns out to be the point.

The first is that mempe stops guessing. If the process path doesn't match anything in
the loader's module list, it used to just take the first module and dump that under
the target's name. It now tries the exact path, falls back to a unique basename, and
refuses with an error if neither is conclusive. Two modules with the same basename
count as inconclusive.

The second is that it stops giving up. An unmapped main image used to abort the whole
dump, which is the worst possible moment to throw everything away, because a hollowed
process is exactly when the surviving modules matter. Unmapping the main image of a
live process now produces all 33 remaining DLLs and exit code 3 instead of nothing and
exit code 2.

Import recovery accepts a lone resolved pointer when the IAT directory actually points
at the range it sits in. Requiring two adjacent entries makes sense for a range found
only by matching a section name, and is too strict for one the header vouches for.

Delay-load slots that were declared but never called are now counted and reported
rather than quietly skipped. A normal target shows 3319 unresolved against the 3959 its
modules declare on disk, which is roughly what you'd expect from a process that has
only just started. Two other silent drops also became visible: executable sections that
came out entirely zero, and unreadable pages inside raw regions.

Tests went from 56 to 66, and every new one was checked by deliberately breaking the
code it covers to confirm it actually fails.

Worth knowing: the basename fallback and the lone-pointer recovery are both correct and
neither fires on the test corpus. Import counts are unchanged at 1342, 816, 819 and 816.
The all-zero-section warning does fire, on a deliberately early dump of the VMProtect
sample.

MemPE v0.2.0

Choose a tag to compare

@techlinn techlinn released this 31 Jul 06:33

Import recovery got a lot better. mempe reads the delay-load directory now, and
it understands the register-load form of an import reference instead of only
call and jmp. Dumping kernel32 recovers 1274 of the 1292 imports the file
actually has. The packed test binaries each picked up around 205.

The checksum has been wrong this whole time. mempe copied the header block out
of memory and never updated the field, so every file it wrote claimed a checksum
that described completely different bytes. It is recomputed now and agrees with
what Windows calculates for the same file.

Two things got added to the summary that describe what mempe saw rather than
what it did. It reports whether the entry point sits inside surviving unwind
data, which ordinary compiler output almost always has and hand-written stubs
usually do not. It also counts sections that were writable and executable at the
same moment. Neither one is a judgement about the target.

There is a new warning for executable sections that are high-entropy but were
never written after the image was mapped. That combination usually means the
section was still encrypted when the snapshot happened, so what mempe wrote is
not real code. The warning is about mempe's own output, not about the program.

Dumps of a stomped module used to be fabricated: if the headers in memory were
unreadable, mempe read the original file off disk and spliced its headers onto
whatever was there. It now checks the memory is still backed by that file first.

Carving looks at non-image memory, which is where a .NET assembly loaded from a
byte array lives. New --raw-regions flag writes executable memory that held no
complete PE, for headerless payloads. Off by default, it can produce a lot of
files.

Test count went from 40 to 56, and each new test was checked by deliberately
breaking the code it covers to confirm it fails.

Worth knowing: the wider instruction decoding picks up two entries on kernel32
that are runtime function pointers rather than genuine imports, 2 out of 1276.
Both point at real APIs, they are just not load-time dependencies.

MemPE v0.1.5

Choose a tag to compare

@techlinn techlinn released this 30 Jul 02:13

This release gets more out of a single snapshot.

PEs stored inside other images are written out as their own files now. Packers and
protectors often carry a second payload in a resource directory, and until now
those bytes ended up inside the dumped host with nothing saying they were there.
Every candidate has to parse as a real disk-layout PE before it is written, and
the extension follows the subsystem, so a native subsystem image becomes .sys.
Across fourteen unrelated processes the only files carved were genuine embedded
images.

The output reports how much of an image was written after it loaded, counted
through the working set. On an ordinary program that is a couple of percent. On a
packed one it was a third or more. The count has to be taken before mempe makes
its own snapshot, because the clone marks private pages as shared and they do not
revert, so a repeat dump of the same process says the figure is not measurable
instead of printing zero.

Images the loader does not list are named from their backing file and reported. An
export name RVA of zero is no longer read as a string. It resolved to the DOS
signature, so an image with a stripped export name was written out as MZ.exe and
every export it held was attributed to a module that does not exist.

Rebuilt files carry more of what was in memory. The debug directory is kept rather
than discarded and its file offsets are repointed at the new layout, so pdb paths
and build GUIDs survive. Sections that were executable in memory get their code
flag back, since marking the real code section as data is a common way to keep
analysis tools on the stub. A relocation directory has to walk as real relocation
blocks before it survives, and ASLR is cleared on images with no usable
relocations. Section names made of spaces or raw bytes are replaced, TLS callbacks
are counted, and the entry point is shown with the section holding it.

A process that refuses memory access now says so, and suggests an elevated
terminal, instead of blaming the module snapshot. A single bad import descriptor
no longer discards the descriptors that parsed before it.

The PID and process name workflows work the same as before. The release build
passed all 40 tests.

MemPE v0.1.4

Choose a tag to compare

@techlinn techlinn released this 28 Jul 01:38

This release makes mempe say more about the images it dumps, and fixes several
bugs found while going back over the code.

The output now names the entry point and the section holding it. On a packed
target that shows whether execution still begins inside the stub. A normal
process lands in .text; the packed test binary lands in the packer's own
section. TLS callbacks are counted as well, since protectors use them to run
code before the entry point. Section names made of raw bytes are replaced with
.secNN, so nothing unprintable reaches the tools that read the dump.

Recovered imports no longer cost a working dump. 0.1.3 ran import recovery on
images that already had a valid import table, and when the headers had no room
for another section the rebuild failed outright. Large images such as
mscorlib.ni.dll were lost that way. The existing table is now kept instead.

An export name RVA of zero is no longer treated as a string. It resolved to the
DOS signature, so an image with a stripped export name was written out as
MZ.exe and its exports were attributed to a module that does not exist.
Packers often clear that field.

Hidden image discovery no longer dumps a second PE that the first one already
covers, and an allocation that fails the size limit is skipped rather than
ending the capture.

Watch mode no longer creates the output folder before it starts waiting.
Invalid command line values are quoted in errors, and the help text states the
24 hour watch limit.

The PID and process name workflows work the same as before. The release build
passed all 34 tests.