Lesson 2 of 9 · 25m
Static Triage: The First Ten Minutes
Hashes that matter, file identity, strings done properly, and reading imports and capa output to decide what a file can do before anything executes.
Most tickets that land on a malware analyst's desk are one question in disguise: is this the thing we saw last week, and do I need to worry? You can answer a surprising amount without executing a single instruction. Do the same steps in the same order every time and write down what you find — these findings become your YARA strings later.
Identity: which hashes, and why
sha256sum invoice_0412.exe # identity — this is what goes in the IOC list
md5sum invoice_0412.exe # only because old intel feeds still key on it
ssdeep -b invoice_0412.exe # fuzzy hash: survives small edits, clusters variants
python3 -c "import pefile; print(pefile.PE('invoice_0412.exe').get_imphash())"SHA-256 changes when one byte changes — crypters exploit exactly that. imphash hashes the import table in order, so builds from the same source and toolchain cluster even when every SHA-256 differs. Search hashes on VirusTotal or MalwareBazaar first: a hash lookup is passive; uploading a file is not.
What is it, actually?
Extensions lie; magic bytes don't. MZ is a PE, PK a zip (and every .docx/.jar), D0 CF 11 E0 an OLE compound file (.doc/.xls/.msi), %PDF a PDF. file reads the magic; Detect It Easy (diec) goes further and reports compiler and packer — Packer: UPX(3.96)[NRV,brute] is a signature worth noticing.
Strings, done properly
Plain strings misses half of Windows malware because Windows APIs love UTF-16LE. strings -el gets wide strings; FLOSS goes further and recovers stack strings and simply-encoded strings by emulating the decode loops.
floss -n 6 invoice_0412.exe | grep -iE 'https?://|\.php|CurrentVersion|powershell|vssadmin|mutex'// ASCII + UTF-16LE string extraction — the core of any strings tool
function strings(bytes, min = 6) {
const out = []; let a = "", w = "";
for (let i = 0; i < bytes.length; i++) {
const b = bytes[i], pr = b >= 0x20 && b < 0x7f;
if (pr && bytes[i+1] === 0 && (w || (bytes[i+2] >= 0x20 && bytes[i+3] === 0))) {
w += String.fromCharCode(b); i++; // UTF-16LE pair; plain strings sees 1-char runs and drops them
} else {
if (w.length >= min) out.push("W: " + w); w = "";
a = pr ? a + String.fromCharCode(b) : (a.length >= min && out.push("A: " + a), "");
}
}
return a.length >= min ? out.concat("A: " + a) : out;
}
const enc = new TextEncoder(), wide = s => new Uint8Array([...s].flatMap(c => [c.charCodeAt(0), 0]));
const blob = new Uint8Array([0x4d,0x5a,0x90,0, ...enc.encode("http://203.0.113.47/gate.php"), 0, 0xff, ...wide("cmd.exe /c vssadmin delete shadows /all /quiet"), 0, 0]);
console.log(strings(blob));Capabilities: imports and capa
Imports are a capability sketch: VirtualAllocEx + WriteProcessMemory + CreateRemoteThread is injection; CryptEncrypt + vssadmin smells like ransomware; an import table of only LoadLibraryA/GetProcAddress means the real imports resolve at runtime — usually packing. Then let capa do the pattern-matching:
$ capa invoice_0412.exe
| inject thread | host-interaction/process/inject |
| persist via Run registry key | persistence/registry/run |Field notes
- A missing signal is a signal: a 400 KB executable with 30 readable strings is packed or encrypted — lesson 4 picks that up.
TimeDateStampis trivially forged; Delphi binaries famously show 1992-06-19. Zeroed or future dates are deliberate.- "Signed" isn't "safe" — stolen code-signing certs are routine. Check that the signature validates and who it chains to.
- Run capa and FLOSS again on the unpacked payload; on a packed file they mostly describe the packer.