Cheat sheet — Static Analysis: Strings & PE¶
Companion to Module 03 — Static Analysis — Strings & PE · CC BY 4.0 — print it, pin it, share it.
Last reviewed: 2026-07
Only handle live malware in an isolated, disposable analysis VM with no bridged network. Static analysis never runs the sample — but the sample still lives on disk.
Strings — the fast first read¶
strings sample.exe # printable runs, min length 4 (default)
strings -n 8 sample.exe # raise the floor — cut ASCII noise
strings -e l sample.exe # 16-bit little-endian (UTF-16LE / Windows wide strings)
strings -a sample.exe # scan the whole file, not just loaded sections
strings sample.exe | grep -iE 'http|\.exe|\\\\|reg|mutex' # C2, paths, registry, mutexes
- High-signal categories: URLs / C2 domains, file paths, registry keys, error messages (often the clearest intent), and mutex names — a mutex like
Global\{a37f...}is more unique and searchable than a hash. - Default
stringsmisses UTF-16 — always add-e lon Windows binaries or you'll skip half the payload.
FLOSS — obfuscated & stacked strings¶
floss sample.exe # static + decoded + stacked + tight strings
floss --only static sample.exe # just what plain strings would find
floss -n 6 sample.exe # raise minimum length
floss --json sample.exe > strings.json # machine-readable for the metadata report
- FLOSS emulates the sample's own decode routines to recover strings that malware only builds at runtime — the ones plain
stringsnever sees. Read the output categories (static / decoded / stacked) so you know what each block means.
pefile — the PE metadata report¶
import pefile
pe = pefile.PE("sample.exe")
# COFF compile timestamp (T1027.005 — often zeroed or faked)
import datetime
ts = pe.FILE_HEADER.TimeDateStamp
print("compiled:", datetime.datetime.utcfromtimestamp(ts))
# Import Address Table — the capability declaration
for entry in pe.DIRECTORY_ENTRY_IMPORT:
print(entry.dll.decode())
for imp in entry.imports:
if imp.name:
print(" ", imp.name.decode())
# Per-section entropy (packed vs unpacked)
for s in pe.sections:
print(s.Name.rstrip(b"\x00").decode(), f"{s.get_entropy():.2f}")
- The IAT is the highest-signal static data — the APIs the binary told the OS it intends to call. Look up unknowns on MalAPI.io; combos declare behaviour:
CreateRemoteThread+VirtualAllocEx+WriteProcessMemory= process injection.SetWindowsHookEx/GetAsyncKeyState= keylogging (Agent Tesla).WSAStartup/connect/send/recv= socket C2 client.
YARA — author, then prove it¶
rule agent_tesla_keylogger {
meta:
author = "you"
hash = "<sha256>"
attck = "T1056.001"
strings:
$mutex = "Global\\{a37f78d2-3b1c-4e5a}" ascii wide
$api1 = "SetWindowsHookEx"
$api2 = "GetAsyncKeyState"
condition:
uint16(0) == 0x5A4D and $mutex and ($api1 and $api2)
}
yara -s rule.yar sample.exe # -s: print matching strings + offsets
yara rule.yar ./benign_corpus/ # MUST stay quiet — the true-negative half
uint16(0) == 0x5A4Danchors on theMZmagic. Modifiers:nocase,ascii,wide,fullword.
Gotchas worth remembering¶
- Static analysis only sees what's unpacked.
strings/IAT nearly empty ≠ benign — it usually means packed, unpack first (Module 09). The stub's strings still often name the packer. - A YARA rule isn't done when it compiles or even when it matches the sample — it's done when it also stays quiet on a benign control. Match without a true-negative is half a rule.
- The COFF timestamp catches lazy forgers, not careful ones. A future date is a near-certain forgery; a plausible fake tells you nothing.
- Strings lie after packing — you're reading the unpacker stub, not the payload. Confirm the entropy read from triage before trusting a "quiet" dump.
- AI maps imports → ATT&CK fast but hallucinates API names — treat its output as leads to confirm on MalAPI.io, never as the finding.
Comments
Sign in with GitHub to comment. Choose the type: Feedback (errors or suggestions on this page) · Hints (help for fellow learners — no spoilers) · General (anything else).