Lesson 2.4 · Binary Formats· 35 min
Resources, Overlays and Other Hiding Places
Where PE files carry data outside their code: the resource tree, version info, overlays, Authenticode signatures, and the .NET and TLS directories.
Objectives
- Navigate the three-level resource tree and recognise the resource types that matter in triage
- Use version information as metadata and IOC while knowing how easily it is forged
- Compute where a PE's overlay starts and explain why droppers and installers use it
- Interpret an Authenticode signature correctly, including signed-but-malicious files
- Spot the .NET and TLS data directories and know what each one means for your next step
So far the PE format has looked tidy: headers describe sections, sections hold
code and data, and the import table says which APIs the program needs. But a PE
file has plenty of room for bytes that are neither code nor ordinary data. Icons,
version strings, manifests, certificates, whole archives glued to the end of the
file. Legitimate software uses every one of these, and so does malware, because
a second executable hidden in an "icon" or appended after the last section is
invisible to anyone who only looks at .text.
This lesson covers those places. They matter mostly during triage: finding an embedded payload early can save hours of reversing the code that unpacks it.
The data directories, revisited
In PE Headers you met the data directory array at the
end of the optional header. Each entry is an (address, size) pair pointing at a
structure the loader or other Windows components consume. Four of them are the
subject of this lesson:
| Index | Directory | Address is… | Why an analyst cares |
|---|---|---|---|
| 2 | Resource | RVA | Icons, dialogs, version info — and embedded payloads |
| 4 | Certificate (Security) | File offset | Authenticode signature |
| 9 | TLS | RVA | Callbacks that run before the entry point |
| 14 | CLR Runtime Header | RVA | Marks a .NET assembly — switch tools |
Note the odd one out: the certificate directory's "address" is a raw file offset, not an RVA. The signature is never mapped into memory, so an RVA would be meaningless. Tools that blindly convert it through the section table produce garbage.
The resource tree
The resource directory (usually in a section named .rsrc) is a small tree with
three fixed levels:
Resource directory (root)
├── Type e.g. RT_ICON (3), RT_RCDATA (10), RT_VERSION (16), or a name "CONFIG"
│ └── Name numeric ID (#1) or string name ("PAYLOAD")
│ └── Language e.g. 0x0409 (en-US), 0x0000 (neutral)
│ └── Data entry → RVA + size of the raw bytesEach level is an IMAGE_RESOURCE_DIRECTORY followed by entries that are either
an integer ID or an offset to a Unicode name. The leaves are
IMAGE_RESOURCE_DATA_ENTRY records holding the RVA and size of the actual data.
The types you will meet most often in triage:
| Type | ID | Normal use | Seen in malware as… |
|---|---|---|---|
RT_ICON / RT_GROUP_ICON | 3 / 14 | Application icon | Stolen icons (PDF, Word) to trick users |
RT_RCDATA | 10 | Arbitrary binary data | Encrypted config, second-stage payload |
RT_VERSION | 16 | File/product version info | Impersonation of a legitimate vendor |
RT_MANIFEST | 24 | Side-by-side and UAC settings | requireAdministrator requests |
RT_STRING | 6 | Localised strings | Rarely interesting |
| Custom named types | — | App-specific | Anything; names like BIN, DATA, X |
Programs read their own resources at runtime with FindResource,
LoadResource, LockResource and SizeofResource. Seeing that sequence in the
imports of a small binary with one large RT_RCDATA blob is a strong hint that
the blob is the real payload — typically decrypted or decompressed in memory
before use.
Tip: Check the entropy and the first bytes of every large resource. A resource beginning with
MZis an embedded executable; one with entropy near 8.0 is compressed or encrypted. Detect It Easy and PE-bear both show per-resource details.
Version information: useful, but trivially forged
The RT_VERSION resource holds a VS_VERSIONINFO structure: fixed numeric
versions plus a StringFileInfo table with keys such as CompanyName,
FileDescription, InternalName, OriginalFilename and ProductName.
For an analyst it is worth reading for two opposite reasons:
- As an IOC and clustering hint. Builders often reuse the same odd strings
across a campaign — a misspelled company name or a stable
InternalNamemakes a good YARA condition. - As a red flag. A file claiming
CompanyName: Microsoft Corporationthat is unsigned, compiled with MinGW and sitting in%APPDATA%is lying.OriginalFilenamethat differs from the name on disk is also worth noting — it often reveals the name the author used.
Never treat version info as evidence of origin. Anyone can write any string into it with a resource compiler or an editor like Resource Hacker.
Overlays: data after the last section
The loader maps only what the section table describes. Anything appended after the end of the last section's raw data is ignored at load time but is still part of the file on disk. That tail is called the overlay.
Computing where it starts is simple:
overlay_start = max(section.PointerToRawData + section.SizeOfRawData)
overlay_size = file_size - overlay_start (if positive)Legitimate users of overlays are common: installers (NSIS, Inno Setup) append their compressed archives, self-extracting archives do the same, and signed files carry the Authenticode blob there. Malicious uses mirror them: droppers append an encrypted payload, builders append a configuration block, and some samples pad themselves with hundreds of megabytes of junk to exceed the size limits of sandboxes and scanners.
A program finds its own overlay by opening its own file
(GetModuleFileName + CreateFile/ReadFile) and seeking past the sections.
That self-read pattern in a small binary is another dropper hint.
Warning: Two things inflate the apparent overlay. The certificate table of a signed file lives after the sections, so a naive calculation reports it as overlay data. And some toolchains (MinGW without
-s) leave a COFF symbol table after the last section. Always look at what the overlay contains before concluding anything.
Authenticode: what a signature does and does not prove
The certificate directory points at one or more WIN_CERTIFICATE structures
containing a PKCS#7 SignedData blob. The signature covers a hash of the file
computed while skipping the checksum field, the certificate directory entry and
the certificate table itself.
What a valid signature tells you:
- The file has not been modified since it was signed (within the hashed ranges).
- The signer held the private key for that certificate at signing time.
What it does not tell you: that the file is benign. Signed malware is a recurring reality — certificates are stolen from real companies, bought through shell companies, or issued to fraudulent developers. Treat a signature as attribution evidence to check (who is the signer? is the certificate revoked? does the signer make sense for this file?), not as a verdict.
On Windows, sigcheck -a from Sysinternals or the file's Digital Signatures
tab shows the chain; on any platform, osslsigncode verify or a pefile script
can extract the blob for inspection.
Two more directories that change your plan
TLS directory
The TLS directory (index 9) describes thread-local storage — and, crucially, an array of TLS callbacks the loader calls before the entry point runs. A debugger that stops at the entry point has already missed them, which is why malware uses them for anti-debugging and early unpacking. If this directory is present in a small binary, list its callbacks before you do anything else. The TLS callbacks technique page covers the details, and How a Binary Is Loaded and Run shows where they fit in the loader's sequence.
CLR Runtime Header (.NET)
If data directory 14 is non-empty, the file is a .NET assembly. Its "real" code
is CIL bytecode plus metadata, not x86. The native entry point is a tiny stub
that jumps into mscoree.dll. Stop reading disassembly: switch to a .NET
decompiler such as dnSpyEx or ILSpy, which recover near-source C#. Detect It
Easy flags this immediately.
Lab: build, hide and find
This lab builds a harmless Windows program that embeds a small text "config" as
a resource and carries appended data as an overlay — the same structures a
dropper uses, with nothing malicious inside. You need mingw-w64
(brew install mingw-w64 or apt install mingw-w64) and Python with pefile.
-
Create
config.txt:text server=update.example.com interval=3600 mode=demo -
Create
res.rc, which embeds the file asRT_RCDATAand adds version info:c #include <windows.h> CONFIG RCDATA "config.txt" 1 VERSIONINFO FILEVERSION 1,2,3,4 PRODUCTVERSION 1,2,3,4 FILEOS VOS_NT_WINDOWS32 FILETYPE VFT_APP BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904B0" BEGIN VALUE "CompanyName", "Contoso Lab Tools" VALUE "FileDescription", "Resource demo" VALUE "OriginalFilename", "resdemo.exe" VALUE "ProductName", "Lab Resource Demo" END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x0409, 1200 END END -
Create
resdemo.c, which reads its own resource at runtime:c #include <windows.h> #include <stdio.h> int main(void) { HRSRC h = FindResourceA(NULL, "CONFIG", MAKEINTRESOURCEA(10)); /* RT_RCDATA */ if (!h) { puts("resource not found"); return 1; } DWORD size = SizeofResource(NULL, h); const char *data = LockResource(LoadResource(NULL, h)); printf("embedded config (%lu bytes):\n%.*s", size, (int)size, data); return 0; } -
Build it.
-sstrips the COFF symbol table so it does not masquerade as an overlay:bash x86_64-w64-mingw32-windres res.rc -O coff -o res.o x86_64-w64-mingw32-gcc -s -o resdemo.exe resdemo.c res.o -
Create an overlay by appending bytes:
bash cp resdemo.exe resdemo-overlay.exe printf 'OVERLAY-MARKER:this-data-is-never-mapped' >> resdemo-overlay.exe -
Find everything with a short
pefilescript,carve.py:python import sys import pefile pe = pefile.PE(sys.argv[1]) # Resource tree: type -> name -> language for rtype in pe.DIRECTORY_ENTRY_RESOURCE.entries: tname = pefile.RESOURCE_TYPE.get(rtype.id, str(rtype.name)) for rname in rtype.directory.entries: name = str(rname.name) if rname.name else f"#{rname.id}" for rlang in rname.directory.entries: rva, size = rlang.data.struct.OffsetToData, rlang.data.struct.Size print(f"{tname:<12} {name:<8} lang=0x{rlang.id:04x} rva=0x{rva:x} size={size}") if tname == "RT_RCDATA": open(f"rsrc_{name}.bin", "wb").write(pe.get_data(rva, size)) # Version strings for fileinfo in getattr(pe, "FileInfo", []): for entry in fileinfo: if entry.Key == b"StringFileInfo": for table in entry.StringTable: for k, v in table.entries.items(): print(f" {k.decode():<18} {v.decode()}") # Overlay end = max(s.PointerToRawData + s.SizeOfRawData for s in pe.sections) print(f"file size {len(pe.__data__)}, sections end at {end}") if pe.get_overlay_data_start_offset() is not None: open("overlay.bin", "wb").write(pe.get_overlay()) print("overlay written to overlay.bin")bash python3 carve.py resdemo-overlay.exe cat rsrc_CONFIG.bin overlay.bin -
If you have a Windows VM, open
resdemo-overlay.exein Resource Hacker, PE-bear and Detect It Easy and find the same three artefacts in each GUI. Run it and confirm it prints the embedded config — the overlay has no effect on execution.
Questions to answer: Does strings on the executable find the config
text? Would it if the resource were XOR-encoded? Which imports in resdemo.exe
would tip you off that it reads its own resources? Run carve.py on the build
without -s — what does it report as overlay, and why is that misleading?
Key takeaways
- The resource tree has three levels (type, name, language); large
RT_RCDATAor custom-named resources are prime candidates for embedded payloads. - Version info is useful for clustering and spotting impersonation, but it is attacker-controlled text, never proof of origin.
- The overlay starts at the end of the last section's raw data; installers, droppers and signatures all live there, so inspect its contents before judging.
- A valid Authenticode signature proves integrity and key possession, not innocence.
- A TLS directory means code runs before the entry point; a CLR header means you should switch to a .NET decompiler.