Skip to content

Resource Directory

Data directory 2

IMAGE_DIRECTORY_ENTRY_RESOURCE: DataDirectory[2] in the Optional header. All data directories

Resources hold data compiled into the binary: icons, dialogs, version information, manifests, and any blob the author wants. That makes the resource section the favorite hiding place for dropper payloads, encrypted configs and second stages, and version info is also the first thing a masquerading sample fakes.

How the structure works

The directory is a three-level tree:

Type (RT_RCDATA, RT_ICON, "IMAGE", …)
└── Name or ID (DATA.DLL, #101, …)
    └── Language (#1033 = en-US, #0 = neutral)  ──►  IMAGE_RESOURCE_DATA_ENTRY: RVA, size, code page
Standard type ID What's in it
RT_ICON / RT_GROUP_ICON 3 / 14 Icons. Stolen icons (PDF, Word) are a classic lure
RT_VERSION 16 Company, product, original filename, all attacker-controlled
RT_MANIFEST 24 UAC level, DPI, dependencies (see Manifest)
RT_RCDATA 10 Raw application data, the usual place for payloads
RT_DIALOG / RT_STRING / RT_MENU 5 / 6 / 4 UI
Custom string types For example "IMAGE", "MUI", "TYPELIB", "REGISTRY"

What PPEE shows

Linux screenshot: Resource view with version resource details

Resource view with version resource details

  • Tree: every type under DIR_ENTRY_RESOURCE (n) (#16 (RT_VERSION), "IMAGE", …).
  • Upper list for a type: Resource Name/ID · OffsetToData · Type Detected · Size ratio (each resource's share of the type's bytes).
  • Lower list for a resource: each language with OffsetToData, Size, CodePage, Entropy, MD5, Type Detected and a first-bytes hex/ASCII preview.
  • Dump… on a language row saves the raw resource. PE payloads found in resources (and in appended data) are labelled with their bitness and kind, such as 32-bit EXE, not just PE File, so you can tell a 32-bit implant from a 64-bit one at a glance. Follow in Hex View (Ctrl+H) selects its bytes.

Content-based type detection

PPEE identifies each blob from its bytes, not from its declared type, so an executable disguised as a bitmap still shows up. Detected types include PE File, Zip Archive, Rar Archive, GZIP Compressed file, Microsoft Cabinet file, AutoIt compiled script file, Rich Text Format, XML, SVG, PNG, JPEG, GIF Image, BMP Image, Icon, Cursor, WAV, AVI, Macromedia flash compressed/uncompressed, Borland Delphi Form, OLE or Visual C++ type library file, Registry script, Version Resource, Security Certificate, Text string and more. Unknown content shows as null in JSON, which is itself interesting when combined with high entropy.

Reading resources like an analyst

Observation What it suggests
RT_RCDATA (or a custom type) with entropy > 7.5, typeDetected: null, tens of KB or more Encrypted or compressed payload: a dropper, an encrypted config, or a crypter stub's inner PE
typeDetected: "PE File" inside any type Embedded executable, dropped or injected at run time
Resource names like DATA.DLL, API.DLL, PAYLOAD, random strings The author's own labels for what they unpack
AutoIt compiled script file AutoIt-compiled malware (the script can be decompiled)
DVCLAL, PACKAGEINFO, Borland Delphi Form Built with Delphi, which tells you what kind of code and RTTI to expect
Resource data RVA not backed by file data The resource directory survived packing, but the data was moved into a packed section (UPX does this)
RT_VERSION claiming Microsoft/Adobe, but the file is unsigned or signed by someone else Masquerading. Cross-check with Security
An icon of a document type on an EXE Social-engineering lure

Walkthrough: a dropper's embedded executable

3c6b036f2eebc124c17db51960d9f6c9b39e236e9a33d5ac0c3a3a2cabe36833.exe (RemusStealer, 1.8 MB):

$ ppee-cli --resources 3c6b036f….exe
Resources: 4 type(s), 9 name(s), 9 language variant(s):
  #10 (RT_RCDATA) [...]
    #100 [...]
      lang=#1033 offset=4C500 size=1515568 codePage=0 entropy=6.49614 md5=AC7A167EE7269BD790F220BB104CCA22 typeDetected=PE File
        first bytes: 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00 | MZ.........ÿÿ..
  1. What is it? typeDetected=PE File and MZ in the first bytes: a 1.5 MB executable, 83% of the file. MCP extract_payload adds embeddedPe: {machine: 0x8664, bitness: 64-bit (PE32+), kind: EXE}.
  2. Is it used? ppee-cli --xrefs FindResourceW 3c6b036f….exe finds one call, with mov r8d, 0xA (lpType = RT_RCDATA) just before it. The code loads this resource (details).
  3. Analyze it where it is: ppee-cli --analysis '3c6b036f….exe#resource:RT_RCDATA/100' reads the next stage straight from the resource (layers). Look it up by MD5 AC7A167EE7269BD790F220BB104CCA22, or carve it (below) if you need the file.

More samples

  • QuasarRAT builds (2eac9624….exe, 37d82058….exe) carry two small PEs (6 KB and 9 KB) in RT_RCDATA #101 and #102: helper modules dropped at run time.
  • A protected crackme carries RT_RCDATA entries named API.DLL, ASMG.DLL, 17.DLL, … and a 38,607-byte DATA.DLL with entropy 7.89 and no recognizable type: encrypted modules the protector unpacks at run time.
  • A UPX-packed Rufus build lists 50+ RT_RCDATA entries with entropy ≈ 7.99. Their data RVAs fall inside UPX0, a section with no raw data: the directory stayed readable, the bytes were packed.
  • A Delphi binary contains RT_RCDATA/DVCLAL, the Delphi license-check resource.

High entropy is not proof

PNG icons are compressed, so entropy 7.6–7.98 is normal for them. That's why PPEE shows Type Detected next to entropy. The signal is high entropy + unknown type + large size, especially in RT_RCDATA.

Dialogs, version info and string tables, decoded

Selecting a resource shows its language variants; for a dialog, version info or string table, the rows end with what it holds: the dialog's caption, font and every control (ID, class, text), the version values (CompanyName, OriginalFilename, …) and file versions, and the strings by ID. PPEE reads these formats itself, so it works the same on Windows and Linux.

A fake antivirus whose version info names another program

[email protected] presents itself as "Security Essentials 2011". Its version info says Kernel Mode Driver Manager by Four-F, OriginalFilename KmdManager.exe: a freeware driver tool's identity, reused. Compare it with the file name, the signer and the code.

For an assistant, get_resources returns the same, and gives each control or string ID its codeUses: the instructions using the ID, which get_xrefs with imm: lists.

Carving resources out

The GUI's Dump… saves one resource. From the command line, the resource's RVA and size are in the JSON, and ppee-dump-resource.sh converts the RVA to a file offset using the section table, carves the bytes, and verifies the MD5 against what PPEE reported:

$ ppee-dump-resource.sh explorer.exe IMAGE 100 icon.png
icon.png: 120 bytes at file offset 0x4FE980, md5 927E8608714F8F45FD337184873D56C9 OK
$ file icon.png
icon.png: PNG image data, 16 x 16, 8-bit gray+alpha, non-interlaced

$ ppee-dump-resource.sh explorer.exe RT_MANIFEST 1 manifest.xml
manifest.xml: 1327 bytes at file offset 0x45D500, md5 48C1E399D28E49E2E457C8AAC64FB5E3 OK

$ ppee-dump-resource.sh 3c6b036f….exe RT_RCDATA 100 stage2.bin
stage2.bin: 1515568 bytes at file offset 0x47300, md5 AC7A167EE7269BD790F220BB104CCA22 OK
$ file stage2.bin
stage2.bin: PE32+ executable (GUI) x86-64, for MS Windows, 7 sections

$ ppee-dump-resource.sh rufus.exe RT_RCDATA 300
resource RT_RCDATA/300 is not backed by file data (packed/virtual section); dump it from memory instead

Carved payloads are live malware

A resource carved from a sample is as dangerous as the sample. Carve inside an isolated analysis VM, never on a machine you work on.

Source of ppee-dump-resource.sh
#!/usr/bin/env bash
# ppee-dump-resource.sh FILE TYPE NAME [OUT] -- carve one resource (first language) out of a PE using ppee-cli's JSON.
# TYPE/NAME are as ppee-cli prints them: RT_RCDATA / DATA.DLL, RT_VERSION / 1, "IMAGE" -> IMAGE, …
set -euo pipefail
f=$1 type=$2 name=$3 out=${4:-"$name.bin"}
read -r off size md5 < <(ppee-cli --no-update-check --no-similarity --json --resources --sections "$f" | jq -r --arg t "$type" --arg n "$name" '
  def h: ascii_downcase | explode | reduce .[] as $c (0; . * 16 + (if $c >= 97 then $c - 87 else $c - 48 end));
  .sections as $s
  | [range(0; 96) | select($s["Section[\(.)].VirtualAddress"] != null)
     | {va: ($s["Section[\(.)].VirtualAddress"] | h), raw: ($s["Section[\(.)].PointerToRawData"] | h),
        size: ($s["Section[\(.)].SizeOfRawData"] | h)}] as $secs
  | .resources.types[] | select(((.typeName // .name // .id) | tostring) == $t)
  | .names[] | select(((.name // .id) | tostring) == $n) | .languages[0]
  | (.offsetToData | h) as $rva
  | [$secs[] | select($rva >= .va and $rva < .va + .size)][0] as $sec
  | if $sec == null then "NOTINFILE \(.size) \(.md5)" else "\($rva - $sec.va + $sec.raw) \(.size) \(.md5)" end' | head -1) || true
[ -n "${off:-}" ] || { echo "resource $type/$name not found (check the names with: ppee-cli --resources $f)" >&2; exit 1; }
[ "$off" != NOTINFILE ] || { echo "resource $type/$name is not backed by file data (packed/virtual section); dump it from memory instead" >&2; exit 2; }
dd if="$f" of="$out" bs=1 skip="$off" count="$size" status=none
got=$(md5sum "$out" | cut -c1-32 | tr a-f A-F)
echo "$out: $size bytes at file offset 0x$(printf %X "$off"), md5 $got $([ "$got" = "$md5" ] && echo OK || echo "MISMATCH (expected $md5)")"

CLI and JSON

$ ppee-cli --resources explorer.exe
Resources: 7 type(s), 683 name(s), 683 language variant(s):
  "IMAGE" [... NumberOfIdEntries=425]
    #100 [...]
      lang=#1033 offset=507580 size=120 codePage=0 entropy=5.66008 md5=927E8608714F8F45FD337184873D56C9 typeDetected=PNG
        first bytes: 89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 | PNG........IHDR

(explorer.exe: an ordinary icon, for comparison with the dropper above.) offset is the resource's RVA, as the format stores it.

JSON: resources.types[] → id/name, typeName, names[] → id/name, languages[] → id, offsetToData (an RVA), size, codePage, entropy, md5, firstBytesHex, firstBytesAscii, typeDetected.

Hunting recipes

# Payload candidates: big, high-entropy, unrecognized blobs
ppee-cli --json --resources f.exe | jq -r '.resources.types[] as $t | $t.names[] as $n | $n.languages[]
  | select(.entropy > 7.5 and .size > 10000 and (.typeDetected == null or .typeDetected == "PE File"))
  | "\($t.typeName // $t.name // $t.id)/\($n.name // $n.id)\t\(.size)\t\(.entropy)\t\(.typeDetected)"'

# Embedded executables anywhere in a folder
for f in samples/*; do
  ppee-cli --no-similarity --json --resources "$f" 2>/dev/null | jq -r --arg f "$f" \
    '.resources.types[]? as $t | $t.names[] as $n | $n.languages[] | select(.typeDetected == "PE File")
     | "\($f)\t\($t.typeName // $t.name // $t.id)/\($n.name // $n.id)\t\(.md5)"'
done

# MD5s of every resource, for IOC lookups of dropped components
ppee-cli --json --resources f.exe | jq -r '[.resources.types[].names[].languages[] | .md5] | unique[]'

# Delphi?
ppee-cli --json --resources f.exe | jq -e '[.resources.types[].names[] | .name // empty] | index("DVCLAL")' >/dev/null && echo Delphi

Related: --resources · Manifest · Strings · Hashes & entropy

References