Malware Triage: Static-First Analysis of an Unknown Binary
- Explain why static triage comes before running anything — and the risk of executing an unknown sample
- Identify a file with file/hashing and pull embedded IOCs with strings
- Read an ELF's structure with readelf/objdump: its declared libraries and any overlay appended past its end
- Recognise obfuscation and add a decode step (base64/xor) to recover hidden config
- Infer a binary's capabilities from its imported symbols without executing it
- Record indicators and a hash so the sample can be tracked and shared with intel
Static first: never run what you don't understand
Frame triage as safe, static analysis before any execution.
A binary lands on your desk: pulled off a host that was beaconing, labeled something innocent like
storagesyncd. The one thing you must not do first is run it. Executing an unknown sample on your own box hands the attacker code execution on your machine — it can beacon, encrypt, or wipe before you learn anything.Static analysis — reading the file without executing it — is the safe first pass, and it's astonishingly productive. Malware has to carry its configuration somewhere: C2 addresses, install paths, campaign tags, second-stage URLs. Those are sitting in the file as data. You can read them off with tools that never run a single instruction of the sample.
The triage ladder, cheapest first: identify (what is this, and how is it built?), strings (what plaintext does it carry?), deobfuscate (what's hidden?), capabilities (what can it do?), hash + record (track and share it). Each rung tells you more, and you climb only as far as you need.
Identify the file
Use file (and a hash) to establish what you're holding.
Start by asking what the thing is. Extensions lie;
filereads the actual bytes — the magic number and header — and tells you the real type. That decides which tools apply next (an ELF getsstrings/nm; a script you just read; a zip you unpack).Runnable in the re-strings-triage room — that room’s setup plants the files it reads
file /opt/backup/storagesyncd/opt/backup/storagesyncd: ELF 64-bit LSB executable, x86-64, dynamically linked, stripped
- (bare arg)
- file inspects the bytes/magic of the path and names the real format, ignoring the extension.
A 64-bit ELF, dynamically linked (it imports functions from shared libraries — useful later), and stripped (symbol names removed, a common way to slow analysis). Now you know it's a native executable, so strings and symbol tools are the right next moves. Runnable in the re-strings-triage room — that room’s setup plants the files it reads
sha256sum /opt/backup/storagesyncd9f2b1c... (64 hex chars) /opt/backup/storagesyncd
- sha256sum
- Emit the SHA-256 hash — the standard sample identifier for lookups and sharing.
Take the hash NOW, before you touch anything. A SHA-256 is the sample's fingerprint: you can look it up in threat-intel feeds (VirusTotal, MalwareBazaar) to see if it's known, and every report you write references it so others analyse the exact same bytes. readelf and objdump: read the ELF's own structure
Parse the ELF header and dynamic section, and find an overlay appended past the ELF's end.
filegives you one line. `readelf` (from binutils) parses the whole ELF structure, still without running a single instruction. Two views matter in triage. `readelf -h` prints the ELF header: the class (32- or 64-bit), the machine it targets, the entry point, and where the section header table sits, how big each entry is and how many there are. `readelf -d` prints the dynamic section, whose `NEEDED` entries are the shared libraries the binary declares it must load.`objdump` is readelf's binutils sibling and overlaps it:
objdump -pshows the sameNEEDEDdeclarations among its private headers,objdump -Tlists the imported symbols (the next step's capability read), andobjdump -ddisassembles, which is past triage. Learn readelf for structure and reach for objdump when you want symbols or code.readelf -h /usr/bin/true | grep -E 'Class|Machine'Class: ELF64 Machine: Advanced Micro Devices X86-64
- readelf -h
- Print the ELF file header: class, data encoding, type, machine, entry point, and the offset, entry size and count of the program and section header tables.
The header on a binary you trust, so you know what a normal read looks like: a 64-bit ELF built for x86-64. On a sample this rules out the odd surprises (a 32-bit or ARM build dropped on an x86 server says the attacker brought tooling for a different fleet). Run it without the grep to see every field, including the three section headerslines the overlay check below uses.readelf -d /usr/bin/true | grep NEEDED objdump -p /usr/bin/true | grep NEEDED0x0000000000000001 (NEEDED) Shared library: [libc.so.6] NEEDED libc.so.6
- readelf -d
- Print the dynamic section; its NEEDED entries are the shared libraries the binary declares.
- objdump -p
- Print the private (format-specific) headers, including the same NEEDED list.
The same fact from both tools: truedeclares exactly one library, libc. TheseNEEDEDentries (theDT_NEEDEDtags) are the binary's direct dependencies, read straight from the file, and they hint at capability before you look at a single symbol: a sample that declareslibsslspeaks TLS, one that declareslibpcapsniffs traffic. Counting them withgrep -c NEEDEDgives the direct-dependency count.lddshows the larger picture, every library the loader would pull in transitively, but it works by invoking the dynamic loader on the file, so never pointlddat an untrusted sample: theNEEDEDlist is the static, safe read.The header also tells you where the ELF ends. On a normally linked binary the section header table is the last structure in the file, so the ELF's last byte is at
Start of section headers + Number of section headers × Size of section headers. The loader only maps what the headers describe, so bytes appended after that point never execute and never disturb the program, which makes them a favourite hiding place: packers and trojanized samples carry their payload or config as an overlay glued onto a genuine binary. If the file is longer than the ELF the header describes, carve off the tail and read it on its own.cp /usr/bin/true sample && printf 'C2_URL=http://198.51.100.50/cfg\n' >> sample end=$(readelf -h sample | awk '/Start of section headers/{o=$5} /Size of section headers/{s=$5} /Number of section headers/{n=$5} END{print o+n*s}') tail -c +$((end + 1)) sampleC2_URL=http://198.51.100.50/cfg
- tail -c +N
- Output the file starting at byte N (1-based), so +$((end + 1)) starts at the first byte past the ELF.
Build a stand-in sample (a real ELF with a config line appended), compute where its ELF structure ends from three header fields, and print only the bytes after that offset. What comes back is the overlay and nothing else: the donor binary's own strings are excluded, so every line you see is something the attacker added. On a real sample, stat -c %slarger than that computed end is the overlay tell, andstringson the carved tail is a far cleaner IOC read thanstringson the whole file.Check yourself
A sample's
readelf -hputs the section header table at offset 24000, with 30 entries of 64 bytes each, andstatsays the file is 31000 bytes. At what byte offset does the ELF structure end?strings: read the plaintext IOCs
Pull embedded indicators out of the binary with strings + grep.
`strings` prints runs of printable characters from a file. Because malware bakes its config in as text,
stringsoften hands you the indicators directly — hard-coded IPs, URLs, file paths, registry keys, campaign tags. The output is noisy (a binary is full of incidental strings), so you filter withgrepfor the patterns you care about.Runnable in the re-strings-triage room — that room’s setup plants the files it reads
strings /opt/backup/storagesyncd | grep -E 'SGCFG:|http|/etc/|([0-9]{1,3}\.){3}[0-9]{1,3}'SGCFG:c2=203.0.113.42:44100 SGCFG:persist=/etc/cron.d/.sync-a1b2c3 SGCFG:campaign=storm-9f2b1c
- grep -E
- Extended regex — filter strings output to just the lines that look like indicators.
Three IOCs fall straight out: the C2 endpoint, a persistence path (a dot-prefixed cron file — hiding), and a campaign id. The grep keeps only lines that look like config markers, URLs, system paths, or IPv4 addresses, cutting the binary's own noise. This is the highest value-per-second step in all of triage. Deobfuscate: strings isn't always enough
Recognise encoded config and add a decode step.
Authors know you'll run
strings, so they obfuscate. The plaintext C2 never appears; instead you see one opaque blob — base64, hex, or an xor'd chunk. The IOC is still there, just encoded. Triage becomes strings plus a decode step.Learn the three common encodings by their fingerprints:
- base64 — a long run of
[A-Za-z0-9+/], often ending in=or==padding. Decode withbase64 -d. - hex — only
[0-9a-f], usually an even length (two chars per byte). Decode withxxd -r -p. - xor — looks like random bytes, but a fixed single-byte key often leaves tell-tale repetition; if you spot the key nearby in the strings, xor it back.
Start with the cheapest guess (base64) and escalate. Pull the blob out and pipe it through the decoder — if the output is readable config, you guessed right; if it's garbage, try the next encoding.
Runnable in the re-decode-config room — that room’s setup plants the files it reads
strings /opt/telemetry/telemetryd | sed -n 's/^SGCFG:b64=//p'YzI9MjAzLjAuMTEzLjk5OjQ0MTAwCnN0YWdlMj1odHRwOi8vMTk4LjUxLjEwMC4zL3VwZGF0ZS9kcmlmdC5iaW4K
- sed -n 's/^SGCFG:b64=//p'
- Print only lines matching the marker, with the marker prefix removed — isolating the blob.
stringsfinds only this — an opaque base64 line.sed -n 's/^SGCFG:b64=//p'strips the marker and prints just the blob. On its own it tells you nothing; that's the point of obfuscation.Runnable in the re-decode-config room — that room’s setup plants the files it reads
strings /opt/telemetry/telemetryd | sed -n 's/^SGCFG:b64=//p' | base64 -dc2=203.0.113.99:44100 stage2=http://198.51.100.3/update/drift.bin
- base64 -d
- Decode base64 on stdin back to the original bytes.
One pipe stage — base64 -d— and the real config appears: the C2 endpoint and a stage-2 download URL that plainstringswould never have shown. Decoding is a routine, expected step, not an exotic one. If base64 -d produces garbage, try hex (xxd -r -p) or an xor with a key you spotted nearby.Check yourself
strings on a sample shows only a long line of [A-Za-z0-9+/] characters ending in '==', and no readable C2. In one word, what step recovers the real config?
- base64 — a long run of
Capabilities: what can it do?
Infer behaviour from imported symbols without running the sample — and read a null result honestly.
IOCs tell you who it talks to; imported symbols tell you what it can do. A dynamically-linked ELF must declare the library functions it calls, and those names are readable statically. See
socket/connectand it networks;fork/execve/systemand it spawns processes or shells;unlink/chmodand it tampers with files. You infer capability from the imports without executing a thing.nm -D /usr/bin/wget | grep -E ' U (socket|connect)'U connect@GLIBC_2.2.5 U socket@GLIBC_2.2.5- nm -D
- List dynamic (imported/exported) symbols of a shared object or dynamically-linked executable.
- U
- In nm output, an undefined symbol — resolved from a shared library, i.e. a function the binary imports and calls.
The technique first, on a binary whose job you already know. nm -Dlists dynamic symbols; theUmarks them as undefined — resolved from a shared library, i.e. functions this binary imports and calls.socket+connectis the signature of something that opens network connections, andwgetself-evidently does. That is the reading you carry to an unknown sample: the imports bound what the code can do, without running it. (objdump -T <file>is the fallback wherenmis unavailable.)Runnable in the re-strings-triage room — that room’s setup plants the files it reads
nm -D /opt/backup/storagesyncd | wc -l nm -D /opt/backup/storagesyncd | grep -cE 'socket|connect|exec|system|fork|unlink'62 0
Now the same read on the sample you are triaging — and the capability grep comes back empty. Sixty-two dynamic symbols, not one of them a networking or exec call. That is not something you did wrong: the sample handed to you in this room is an inert training artifact (a genuine ELF body with the intruder's config appended), so its import table belongs to the donor binary rather than to an implant. Report it that way. An import scan that finds nothing is a result — the honest line is "imports do not corroborate", not a capability you wished into the evidence. Record and try it live
Write the IOC list, and practise the ladder on the sandbox.
The deliverable is an IOC list plus the sample hash — everything another responder needs to hunt for this sample across the fleet and to pull intel on it. Then practise the ladder yourself on the box to your right. Make a tiny fake 'sample' so nothing here is remotely dangerous, and run the exact moves: identify → strings → decode.
printf 'c2=203.0.113.7:8080\n' | base64 -w0Encodes a line the way a sample would hide it → you get a base64 blob. This stands in for the obfuscated config you'd find with strings. printf 'c2=203.0.113.7:8080\n' | base64 -w0 | base64 -dRound-trips it back: base64 -ddecodes the blob to the originalc2=203.0.113.7:8080. That decode pipe is the exact move you use on a real obfuscated sample.printf '%s\n' 'c2=203.0.113.7:8080' | sed -n 's/^c2=\([0-9.]*\):.*/\1/p'Extracts just the IP (203.0.113.7) from the c2 line, dropping the port — the normalised form you'd record in c2.txt for the fleet-wide hunt.
- Triage statically first — reading the file, never executing it; running an unknown sample gives the attacker code execution on your box.
- Identify with file and take a SHA-256 immediately (the sample's fingerprint for intel lookups and sharing).
- readelf -h/-d (or objdump -p) read the ELF's structure statically: its NEEDED libraries, and where it ends, so bytes past that point are an overlay to carve and read.
- strings + grep pulls plaintext IOCs (C2, paths, campaign tags); extract each with a re-runnable command, don't eyeball it.
- Obfuscation is normal — recognise base64/hex/xor and add a decode step; the C2 is encoded, not absent.
- Imported symbols (nm -D / objdump -T) reveal capability — socket/connect/execve mean network + execution — corroborating the IOCs without running anything.
| Rung | Tool | Yields |
|---|---|---|
| Identify | file, sha256sum | Real type + trackable fingerprint |
| Structure | readelf -h/-d, objdump -p | Declared libraries + where an appended overlay starts |
| Plaintext IOCs | strings | grep | C2, paths, campaign, URLs |
| Deobfuscate | base64 -d, xxd -r | Hidden config the strings step missed |
| Capabilities | nm -D, objdump -T | Network / exec / file-tamper imports |
| Record | IOC list + hash | What responders hunt and share |
