IT
OmnvertImage • Document • Network

Inspect an EXE without running it: reading PE headers

7 min read
A laptop screen filled with dense minified code in blue, green and yellow, far too small to read line by line.

A Windows executable is a documented container. Every field the loader reads to map the file into memory is one you can read too, long before you decide whether the thing should ever run.

What static triage buys you

Someone hands you setup_v3_final.exe from a mirror you have never heard of, or a build lands in your release folder and you want to know it is the artifact you think it is. Double-clicking is the one option that cannot be undone. Everything else can: a Windows executable is a documented container called Portable Executable, and every field the loader reads to map the file into memory is a field you can read too.

Be clear about the limit. Static inspection is not a verdict on safety. A clean-looking PE can be malicious and an odd-looking PE is often just an old compiler. Reading headers produces questions, not answers. If the questions do not resolve, the file goes to an isolated sandbox or a multi-engine scanning service, not onto your work machine.

The anatomy of a PE file

DOS header and stub

Every PE starts with the two bytes MZ. The 64-byte DOS header that follows ends in e_lfanew, the file offset of the real PE header. Between them sits the DOS stub, a tiny program whose only job is to print "This program cannot be run in DOS mode" if the file is ever launched under DOS. The stub itself is boring. What is occasionally interesting is the gap: some toolchains leave extra data between the stub and the PE signature, and an unexpectedly large gap is a hint that the file was touched after it was linked.

PE signature and COFF header

At e_lfanew you find four bytes, PE\0\0, then the 20-byte COFF file header. Four fields there are worth your attention. Machine gives the target architecture: 0x014c for x86, 0x8664 for x64, 0xAA64 for ARM64. A 32-bit binary in 2026 is not suspicious, but it is a fact worth having before you wonder why a driver will not load. NumberOfSections tells you how many rows the section table has, typically four to eight for ordinary compiler output. TimeDateStamp holds the link time as a Unix epoch value. The Characteristics flags say whether the image is an executable or a DLL and whether it handles addresses above 2 GB.

The optional header, which is not optional

For an image file the optional header is mandatory. Its magic value is 0x10b for PE32 and 0x20b for PE32+. The difference is more than bit width: PE32+ drops the BaseOfData field and widens ImageBase and the stack and heap reserve fields to eight bytes, which is why naive parsers written for 32-bit binaries silently misread 64-bit ones. ImageBase is the module's preferred load address. AddressOfEntryPoint is the RVA of the first instruction to run. Subsystem says whether this is a console program (3), a windowed GUI program (2) or a driver. A file presented as a silent installer but marked as a console subsystem deserves a second look.

The section table

After the headers comes one 40-byte record per section: name, virtual size, virtual address, raw size, raw offset and characteristics. The familiar names are .text, .rdata, .data, .rsrc and .reloc. Two things stand out when they go wrong. A section marked writable and executable at the same time is the classic footprint of code that rewrites or unpacks itself. A virtual size far larger than the raw size means the image reserves room at load that is not in the file, which is exactly what a packer needs.

Imports: the fastest read on behaviour

The densest information in a binary is the import table. A PE has to name every DLL and every function it wants resolved, so the loader can wire them up. Within a minute you can tell whether the program speaks to the network, writes to the registry, or spawns other processes.

  • ws2_32.dll, wininet.dll, winhttp.dll: network capability. In a tool advertised as offline, that needs explaining.
  • advapi32.dll with RegSetValueEx and friends: persistence or configuration writes.
  • CreateProcess, ShellExecute, WinExec: it starts other programs.
  • VirtualAlloc, VirtualProtect, WriteProcessMemory: memory permission changes and injection patterns.
  • Almost nothing except LoadLibrary and GetProcAddress: the real import table is hidden and resolved at runtime.

That last line matters more than the others. A short import table is rarely a sign of a lean program; it is usually a sign of packing. Opening the file in the EXE and PE viewer puts the headers, section table, imports, resources and per-section entropy on one screen. The parsing happens in your browser from the file bytes, and nothing about the file is executed.

Entropy and packers

Per-section Shannon entropy is a number between 0 and 8 that measures how unpredictable the bytes are. Compiled x86 code usually sits somewhere around 6.0 to 6.8. Compressed or encrypted data pushes past 7.5. A .text section reading 7.9 is not machine code; it is a blob waiting to be unpacked.

High entropy on its own proves nothing. Embedded PNGs, compressed resources and the digital signature blob are all high entropy. Read it in context: section names such as UPX0 and UPX1, a two-entry import table and a 7.9 code section together tell a consistent story, while any one of them alone does not. Packing is not inherently hostile either, since commercial licensing and anti-tamper products compress and encrypt code for their own reasons.

The compile timestamp is not evidence

TimeDateStamp is a plain 32-bit integer sitting in the file. Changing it takes a hex editor and ten seconds. When you see a 1992 date, the conclusion is not "old program" but "this field was zeroed or written by hand". Some toolchains deliberately pin it to a fixed value so that builds are reproducible, which is a good practice that also destroys the field's forensic value. It is still worth reading, because a timestamp that contradicts the resource version or the file's claimed release date is a thread to pull. The timestamp converter turns the raw epoch value into a date you can compare.

Present is not the same as valid

One of the optional header's data directories points at the certificate table. A non-empty entry means the file carries a signature blob. It does not mean the signature checks out. Validation requires recomputing the authenticated hash, walking the certificate chain to a trusted root, checking revocation and weighing the countersigned timestamp, and the operating system is the component that does all of that. When an inspection tool says "signed", read it as "a signature block exists here". Even a signature that validates only proves who signed the file; stolen and abused code-signing certificates are a recurring problem, and a valid signature says nothing about what the code does.

Why strings mislead

Pulling printable text out of a binary is easy and habit-forming. The trouble is that most of those strings are not addressed to you. Compiler runtime messages, assert text, licence blocks in embedded resources and accidental runs of ASCII inside compressed data all pad the list. Seeing a domain name does not mean the program contacts it; the string may sit in a data section no code path ever reaches. The inverse is worse: a program that builds its destination at runtime from fragments has perfectly innocent strings.

Treat strings as a hypothesis generator. If a URL or an address looks interesting, check it separately with whois and DNS lookup, and remember that the only way to learn where a program actually connects is to run it under observation and read the resulting capture.

A fifteen-minute triage workflow

  1. Hash the file with SHA-256 and write it down. The hash generator does this in the browser, and the digest is the cleanest way to search a multi-engine scanning service without uploading anything.
  2. Read the headers: architecture, subsystem, PE32 versus PE32+, section count.
  3. Scan the section table for writable-and-executable flags and for entropy outliers.
  4. Read the imports and ask whether the capabilities match the stated purpose.
  5. Compare resources and version info against the identity the file claims: company name, product name, icon.
  6. Note whether a signature block is present, and let the operating system tell you if it validates.
  7. If doubt remains, stop. Detonate in a sandbox or submit to multi-engine scanning.

Most of the time this ends the question without the file ever running. If you cannot explain why a PDF converter needs process creation and raw sockets, do not look for the answer on your laptop. The same discipline shows up elsewhere in this toolkit: decoding a JWT is not verifying it, and reading a PE header is not clearing a binary.

Frequently asked questions

Is inspecting an EXE as risky as running it?

No. Parsing a file means reading its bytes; no code from the binary executes. The realistic risks are accidentally double-clicking it while you work and archives that contain auto-running components. Keep the sample in its own folder and consider renaming the extension while you study it.

Does high entropy mean the file is malicious?

It does not. Compressed resources, embedded images, signature blobs and commercial protection layers are all high entropy. Entropy only becomes meaningful alongside a very short import table, unusual section names and sections that are both writable and executable.

Can I trust the compile timestamp?

Not as evidence. TimeDateStamp is a plain integer in the file and trivial to forge, and some toolchains pin it to a constant so that builds are reproducible. Record it as a hint and corroborate it against version resources and the claimed release date.

The file is signed. Is that enough?

A present signature block is not a validated signature. Validation means recomputing the hash, chaining the certificate to a trusted root and checking revocation, which is the operating system's job. Even a valid signature identifies the signer, not the behaviour of the code.

When should I stop reading headers and move on?

Stop when the imports and resources do not match the job the file claims to do, or when packing hides the interior. The next step is detonation in an isolated sandbox or submission to a multi-engine scanner, never a trial run on a working machine.

Tools used in this post

Sources

MethodologyImage credits