pe

Read Windows executables (PE: .exe, .dll, .sys) without running them: architecture, compile time, entry point, sections with their entropy and permissions, imports and exports, whether the file is signed, and a list of suspicious indicators (packer section names, writable+executable sections, injection or keylogging imports, odd entry points). Pure Python; also works in the playground on bytes.

import pe

Example

import binary
import pe

# A tiny 32-bit Windows program, built byte by byte: a DOS header, the PE
# headers and one section named like a packer's (UPX0), writable and runnable.
let dos = binary.concat(b"MZ", binary.zeros(58), binary.pack("<I", 64))
let coff = binary.pack("<4sHHIIIHH", b"PE\x00\x00", 0x14c, 1, 1700000000, 0, 0, 224, 0x0102)
let optional = binary.concat(
  binary.pack(
    "<HBBIIIIIIIIIHHHHHHIIIIHHIIIIII",
    0x10b,
    14,
    0,
    512,
    0,
    0,
    0x1000,
    0x1000,
    0x2000,
    0x400000,
    0x1000,
    512,
    6,
    0,
    0,
    0,
    6,
    0,
    0,
    0x2000,
    512,
    0,
    3,
    0,
    0x100000,
    0x1000,
    0x100000,
    0x1000,
    0,
    16,
  ),
  binary.zeros(128),
)
let section = binary.pack("<8sIIIIIIHHI", b"UPX0", 512, 0x1000, 512, 512, 0, 0, 0, 0, 0xE0000020)
let code = binary.concat(b"\x6a\x00\xff\x15", binary.zeros(508))
let header = binary.concat(dos, coff, optional, section)
let data = binary.concat(header, binary.zeros(512 - len(header)), code)

let info = pe.parse_bytes(data)
say(f"{info.format} for {info.architecture}, built {info.compiled}")
for s in info.sections {
  say(f"  section {s.name}: {s.size} bytes, entropy {s.entropy}")
}
for hit in pe.indicators(info) {
  say(f"  [{hit.level}] {hit.detail}")
}

Functions

parse(path)

returns map

Everything pe knows about a file. Fails if the file isn't a PE.

Fields

  • "format" pe32 or pe64.
  • "architecture" the CPU type, like "x86" or "x64".
  • "machine" the CPU's longer name, like "x86-64 (64-bit)".
  • "is_dll", "is_driver", "is_dotnet", "is_signed" true or false.
  • "subsystem"
  • "timestamp", "compiled" when it was built, as a number and as text.
  • "entry_point" the address where the program starts.
  • "entry_hex" the same address in hex, like "0x401000".
  • "entry_section" the section the entry point is in.
  • "image_base"
  • "sections" a list of maps with name, virtual_address, virtual_size, raw_size, raw_offset, size, entropy, flags, readable, writable and executable.
  • "imports" a map of dll -> list of names.
  • "import_count"
  • "exports" a list of names.
  • "export_name", "pdb", "directories", "overlay_size"
  • "imphash" the import hash pefile and VirusTotal show (nil without imports).
  • "rich_header" nil, or {offset, key, checksum_valid, hash, entries}. Each entry is {product_id, build, count}: a Microsoft tool that built the file.
  • "resources" a list of {type, name, language, size, offset, codepage, entropy}. At most 16384 tree entries are read; entropy is nil past the first 16 MB of resource data.
  • "signature" nil if unsigned, else {type, digest_algorithm, signer, certificates, verified, problem}. Each certificate has subject, issuer, serial, not_before, not_after and is_signer. Nothing is checked: verified is always false.
  • "truncated" true if the file is cut off.
  • "entropy", "size"
  • "indicators" a list of {level, name, detail} findings.
Visualizehow pe.parse works

pe.parse("demo.exe") gives {"format": "pe64", "architecture": "x64", "machine": "x86-64 (64-bit)", "is_dll": false, …

pe.parse: inputs flowing into the function and the resultpathdemo.exeMZ … PE\0\0 … 4 sectionspe.parseresult · everything pe knows (some keys shown)format"pe64"architecture"x64"subsystem"windows_console"compiled"2024-01-11 14:49:36 UTC"entry_section".text"import_count7is_signedfalse

Real result: worked out by running this example with TB while the site was built.

parse_bytes(data)

returns map

Like pe.parse() but for bytes already in memory.

Visualizehow pe.parse_bytes works

pe.parse_bytes(exe) gives {"format": "pe64", "architecture": "x64", "machine": "x86-64 (64-bit)", "is_dll": false, …

pe.parse_bytes: inputs flowing into the function and the resultdatabytes in memory4d 5a 90 00 … (MZ)pe.parse_bytesresult · the same map as parse()format"pe64"architecture"x64"is_dllfalseentry_hex"0x1000"

Real result: worked out by running this example with TB while the site was built.

is_pe(path)

returns bool

True if the file starts like a Windows PE executable (MZ header and PE signature).

Visualizehow pe.is_pe works

pe.is_pe("demo.exe") gives true

pe.is_pe: inputs flowing into the function and the resultpathdemo.exepe.is_peresult · booltruepathnotes.txtpe.is_peresult · boolfalse

Real result: worked out by running this example with TB while the site was built.

sections(path)

returns list

Just the sections: name, sizes, offsets, entropy and permissions.

Visualizehow pe.sections works

pe.sections("demo.exe") gives [{"name": ".text", "virtual_address": 4096, "virtual_size": 204, "raw_size": 512, "raw_of…

pe.sections: inputs flowing into the function and the resultpathdemo.exepe.sectionsresult · code, data and import sectionsnamesizeentropy.text5121.041.rdata5120.454.data5120.431.idata5122.237

Real result: worked out by running this example with TB while the site was built.

imports(path)

returns map

The import table: {"KERNEL32.dll": ["CreateFileW", ...], ...}.

Visualizehow pe.imports works

pe.imports("demo.exe") gives {"KERNEL32.dll": ["CreateFileW", "ReadFile", "VirtualAllocEx", "WriteProcessMemory", "Cre…

pe.imports: inputs flowing into the function and the resultpathdemo.exepe.importsresult · DLL name -> functions it callsKERNEL32.dll["CreateFileW", "ReadFile", "V…WS2_32.dll["connect", "send"]

Real result: worked out by running this example with TB while the site was built.

exports(path)

returns list

The names a DLL exports (empty for most .exe files).

Visualizehow pe.exports works

pe.exports("demo.dll") gives ["Install", "Run", "Uninstall"]

pe.exports: inputs flowing into the function and the resultpathdemo.dllpe.exportsresult · what the DLL offers"Install""Run""Uninstall"pathdemo.exepe.exportsresult · what the DLL offers[] (empty list)

Real result: worked out by running this example with TB while the site was built.

indicators(path_or_info)

returns list

Suspicious indicators for a file (or for a map from pe.parse()): [{"level": high/medium/low/info, "name", "detail"}].

Visualizehow pe.indicators works

pe.indicators("demo.exe") gives [{"level": "high", "name": "imports_injection", "detail": "Imports injection functions: C…

pe.indicators: inputs flowing into the function and the resultpath_or_infodemo.exechecksinjection APIs? packed? ASLR, DEP?signed? entry point odd?pe.indicatorsresult · suspicious things, most serious firstlevelnamehighimports_injectionlowimports_networklowno_aslrlowno_depinfounsigned

Real result: worked out by running this example with TB while the site was built.

summary(path)

returns text

A short human-readable description, like "pe64 x64 executable, 3 sections, 42 imports, compiled 2024-01-05, unsigned".

Visualizehow pe.summary works

pe.summary("demo.exe") gives "pe64 x64 executable, 4 sections, 7 imports, compiled 2024-01-11, unsigned, 1 high-risk i…

pe.summary: inputs flowing into the function and the resultpathdemo.exepe.summaryresult · one line for a reportpe64 x64 executable, 4 sections, 7 imports, compiled 2024-01-11, unsigned, 1 high-risk indicator

Real result: worked out by running this example with TB while the site was built.