All posts
TutorialAugust 4, 20268 min read

Read a PE header by hand (and why it still matters)

Tools will parse a PE for you. Knowing which fields matter — and which ones lie — is what lets you tell a packed sample from a legitimate binary in about thirty seconds.

Malware Analysis Academy

Editorial team

Every Windows executable you will ever triage starts with the same two structures, and almost every quick judgement you make about a sample comes from four or five fields inside them. Tools surface these instantly. The reason to know them by hand is that when a tool's answer looks wrong, you need to be able to say why.

Two headers, one purpose

A PE file opens with a DOS header — a 64-byte relic whose only job today is to hold a pointer. The last four bytes of it, at offset 0x3C, give the offset of the real header:

00000000  4d 5a 90 00 03 00 00 00  ...  MZ......
0000003c  f8 00 00 00                   -> PE header at 0xF8
000000f8  50 45 00 00 4c 01 06 00       PE..L...

MZ at offset 0 and PE\0\0 at the offset named by 0x3C. If the second signature is missing, you are not looking at a PE, whatever the extension claims.

The fields that carry the signal

Machine (0x4C = i386, 0x8664 = x64). Cheap, and occasionally the whole answer: an x86 DLL that a 64-bit process is supposedly loading is a contradiction worth chasing.

TimeDateStamp. A Unix timestamp of the build. Treat it as a claim, not a fact — it is trivially editable, and some toolchains zero it deliberately for reproducible builds. A timestamp in 1970, or in the future, tells you someone touched it. A plausible one tells you very little.

NumberOfSections. Ordinary binaries have four to seven. One or two sections with unusual names is a strong packing hint.

AddressOfEntryPoint. Where execution begins, as an offset from the image base. Compare it to the section ranges: a legitimate compiler puts the entry point inside .text. An entry point sitting in the last section, or in a section marked writable, is the classic shape of an unpacking stub that decompresses the real code and then jumps to it.

Sections: where packing shows itself

For each section you get a name, a virtual size, a raw size, and characteristics flags. Two comparisons do most of the work.

Raw size versus virtual size. A section that occupies almost nothing on disk but demands a large allocation in memory is asking for room to unpack into. When SizeOfRawData is 0 and VirtualSize is 400 KB, that space gets filled at runtime by something.

Writable and executable together. Compilers do not emit sections marked both WRITE and EXECUTE. Code that intends to rewrite itself does.

Names are the weakest signal of the three. UPX0 / UPX1 is a giveaway, but any competent packer renames its sections to .text and .data, so an innocent-looking name proves nothing. Judge the numbers, not the labels.

Imports say more than strings

The import table lists the DLLs and functions the binary resolves at load time. You are reading for combinations, not individual entries. VirtualAlloc plus WriteProcessMemory plus CreateRemoteThread is the textbook injection triad. CryptEncrypt alongside directory enumeration reads very differently from CryptEncrypt inside a backup utility.

The most informative import table is a nearly empty one. A binary that imports only LoadLibraryA and GetProcAddress is telling you it resolves everything else at runtime, specifically so that a static list like this one stays empty. That absence is itself the finding.

What this looks like in practice

Roughly thirty seconds of header reading gets you to a defensible first opinion:

  • PE\0\0 present, machine i386 — a 32-bit Windows binary
  • three sections, entry point in the last one — probable unpacking stub
  • that section: 512 bytes raw, 380 KB virtual, WRITE|EXECUTE — unpacks at runtime
  • imports: LoadLibraryA, GetProcAddress, nothing else — resolves APIs dynamically

None of that is proof of malice. Commercial software is packed too, for licensing and size. What you have is a shape, and a reason to keep going rather than to close the file.

Where to go next

The natural next step is dynamic analysis: let the stub unpack itself in an isolated VM and dump the result. That is a different discipline with its own safety rules, and it is the one place where "read the headers first" pays off most — you already know what you expect to find.

#pe-format#static-analysis#packing

Keep reading

Read a PE header by hand (and why it still matters) | Malware Analysis Academy