Malware Analysis Basics: Safe Static and Dynamic Triage
I triaged unknown binaries in an isolated lab using static analysis (strings, file, YARA) followed by sandboxed dynamic analysis (strace, Cuckoo) to observe runtime behaviour. The two-stage workflow extracted actionable IOCs from every sample without risking the host network.
This writeup walks the full workflow end to end: confirming the analysis VM is actually cut off from the network, fingerprinting the sample without executing it, checking its hash against reputation data, detonating it under strace with a fake internet answering its requests, submitting it to Cuckoo for a full behavioural report, dealing with packed binaries, and finally distilling everything into an IOC list and a working YARA rule. Every command is included with the output you should expect and the mistakes I hit along the way, so the whole lab can be reproduced from a clean snapshot.
The ground rule that shapes everything below: the sample is treated as live ammunition at all times. Static analysis reads the file but never runs it; dynamic analysis runs it only inside a disposable, network-isolated guest. "Isolated" here means a host-only segment with no route to production or the internet, which is not the same as air-gapped: the analyst box keeps internet access for hash-only reputation lookups, and the two networks never touch. The moment those two environments blur, a shared clipboard, a bridged adapter, an analysis folder synced to the host, the lab stops being a lab.
Objective & Context
Malware triage answers two questions fast: what is it, and what does it do. This lab separates safe static inspection from controlled detonation in a network-isolated sandbox, aligning to NIST SP 800-83 malware-incident handling.
The two questions map to two analysis modes. Static analysis examines the file without executing it: type and architecture, embedded strings, imported functions, section entropy, and pattern matches against known-bad signatures. It is completely safe and surprisingly informative, a single URL or mutex name in the strings output can identify a malware family. Dynamic analysis executes the sample and watches what it does: files created, registry keys or config files written, processes spawned, and network connections attempted. It answers what static analysis cannot, what the malware actually does rather than what it looks like it might do, but it is only as safe as the isolation around it.
The scenario for this lab: a suspicious attachment pulled from a phishing email gateway quarantine, saved as sample.bin. The mail gateway flagged it on attachment type alone, which proves nothing, the job here is to produce a defensible verdict (malicious or not), a list of indicators of compromise (IOCs) that detection tooling can consume, and enough behavioural detail to scope a response if the same file is found elsewhere on the network.
- T1027 Obfuscated Files or Information, packing and entropy checks during static triage.
- T1071 Application Layer Protocol, sandbox network capture reveals C2 callbacks.
Mapping findings to ATT&CK techniques is not decoration: it forces every observation to become a specific, checkable claim. "It connects somewhere" is a hunch; "T1071.001, HTTPS callback to 203.0.113.45:443 within 60 seconds of execution" is something a SOC can hunt for and a firewall team can block.
The decision point in the middle matters more than it looks. A packed sample compresses or encrypts its real code so static analysis sees only the small unpacking stub, strings come back empty, imports are reduced to LoadLibrary/GetProcAddress or their ELF equivalents, and YARA matches nothing. Recognizing that state early (step 6) avoids wasting time on static dead ends and moves the sample to detonation, where the unpacked code reveals itself in memory and in behaviour.
Learning Objectives
- Triage a sample statically with file, strings, and YARA without executing it.
- Hash samples and check reputation against an offline cache.
- Detonate the sample under strace in an isolated sandbox.
- Provide fake network services with INetSim to observe C2 behavior.
- Produce a full behavioral report with Cuckoo sandbox.
- Unpack obfuscated samples and extract IOCs into a new YARA rule.
Environment & Prerequisites
Prerequisites
- Isolated analysis VM on a host-only network with no route to production or the internet.
- YARA with curated rule sets; Cuckoo sandbox or an equivalent.
- A clean VM snapshot to revert after every detonation.
- INetSim for fake network services.
- REMnux or equivalent tooling helpful but optional.
Each of those three lines deserves unpacking, because this is the part of the lab where a mistake has real consequences:
- Isolation is a configuration, not an assumption. The analysis VM runs on a host-only virtual network with no default gateway and no DNS resolver pointing outside the lab. The hypervisor's convenience features, shared folders, clipboard sharing, drag-and-drop, guest additions with host integration, are all disabled, because every one of them is a lane across the moat. A separate guest on the same virtual network runs INetSim (step 4) so the sample has something to talk to.
- Tooling is installed before the sample ever touches the VM. The static tools (
file,strings, YARA) live on the analyst box; the detonation tools (strace,tcpdump) live inside the sacrificial guest. Installing tools after a detonation means trusting a possibly-compromised guest. - The snapshot is the reset button. Take a snapshot of the guest while it is clean, patched, and tooled. After every detonation, revert. "Probably didn't persist" is not a state to run the next experiment from.
Install the static-analysis tooling on the analyst box (Debian/Ubuntu)
sudo apt update
sudo apt install -y file binutils yara strace tcpdump ent
yara --version
4.5.2
On top of the distro YARA package, pull a curated community rule set so triage has something to match against on day one, and record the commit so a later run of this lab matches against the same rules. The rules live in a directory the analyst controls; they are reference data, not trusted code, so skim any rule you actually rely on before it influences a verdict.
Fetch community YARA rules for triage matching (pinned, then reviewed)
git clone https://github.com/YARA-Rules/rules.git ~/yara-rules
git -C ~/yara-rules rev-parse --short HEAD # record the commit in the case notes so the run is reproducible
ls ~/yara-rules/malware | head -5
APT1.yar
Adzok.yar
AgentTesla.yar
Andromeda.yar
AthenaGo.yar
The Cuckoo side is a separate, heavier setup: a controller host (which never executes samples) managing one or more analysis guests through a hypervisor API, reporting over a virtual network it controls. A full Cuckoo install guide is its own document; for this lab the relevant fact is that the controller and the sacrificial guest are different machines, and only the guest ever runs malware. Verify the guest reverts cleanly before the first real submission:
Snapshot hygiene: create, and test the revert path
VBoxManage snapshot "analysis-guest" take "clean-tooled" --description "patched, strace, no net"
VBoxManage snapshot "analysis-guest" restore "clean-tooled"
VBoxManage showvminfo "analysis-guest" | grep -i state
State: powered off (since 2026-06-17T18:22:41.000000000)
A note on scope and legality before touching any sample: everything in this lab ran against files captured from the author's own mail gateway, on hardware the author owns, inside a network with no path to anyone else's. Malware analysis is one of those disciplines where the technique is identical whether the intent is defence or harm, the only thing separating a lab from an incident is authorization, isolation, and documentation. Keep all three explicit, in writing, before the first detonation.
One more prerequisite that is easy to skip: a case notes file, opened before the sample is handled. Every hash, command, observation, and timestamp goes into it as the work happens. Six detonations in, nobody remembers which sample produced which pcap, and the final IOC report is only as good as those notes.
Step-by-Step Execution
1. Static triage and YARA matching
Static triage starts by answering the most basic question, what kind of file is this, because every later tool choice depends on it. file reads magic bytes rather than trusting the extension; strings extracts printable character runs of eight or more characters (-n 8 cuts the noise from short random sequences); the grep filter surfaces the high-signal categories first: URLs, shell invocations, and key material. YARA then matches the whole file against pattern rules for known families and suspicious capabilities.
file sample.bin; strings -n 8 sample.bin | grep -Ei 'http|cmd|key' || true; yara rules.yar sample.binRun the pieces individually first so each result is visible on its own, and note the || true after the grep: if no string matches, grep exits 1, and a && chain would silently skip the YARA scan. The file output drives everything else:
Identify the file type without trusting the extension
Expected output:
file sample.bin
sha256sum sample.bin > case-notes.txt # start the notes trail
sample.bin: ELF 64-bit LSB executable, x86-64, statically linked, stripped
Three facts in one line: it is a Linux x86-64 executable (so Linux detonation tooling applies), statically linked (no shared-library imports to inspect, which itself is mildly suspicious for a 2.1 MB binary), and stripped (symbol names removed to slow down analysis). Nothing here is proof of malice, plenty of legitimate Go binaries look exactly like this, but the verdict is now "unknown, treat as hostile."
Pull high-signal strings
strings -n 8 sample.bin | grep -Ei 'http|cmd|key' | head -20
https://upd-srv.203-0-113-45.host/beacon
/bin/sh -c
POST /collect HTTP/1.1
User-Agent: Mozilla/5.0 (X11; Linux x86_64)
/tmp/.lock
xmrig
This is the payoff of static triage: before anything has executed, there is a candidate C2 URL, a shell invocation string, a hardcoded drop path, and a reference to a known cryptominer. Each of these is an IOC candidate and a hypothesis to test during detonation, if the sample never contacts that host or never creates that file, the strings may be decoys, which is itself a finding.
Match against the YARA rule set
yara -r ~/yara-rules/malware sample.bin
yara rules.yar sample.bin
Suspicious_Downloader sample.bin
Linux_Cryptominer_Artifacts sample.bin
Common mistake: piping strings straight into judgement. Attackers plant fake strings, decoy domains, references to other malware families, precisely to poison triage. Verify: a string only graduates to the IOC list when dynamic analysis (steps 3-5) corroborates it. Also note the inverse failure: if strings returns almost nothing readable, do not conclude "clean", conclude "packed" and jump to step 6.
What this teaches: static triage answers what the file claims to be before you risk running it.
2. Hash and check reputation (offline cache)
Hashing does two jobs. First, it pins the sample's identity: the SHA-256 is the primary key that ties together every note, report, and detection produced in this lab, and it is how anyone else confirms they are looking at the same file. Second, it enables a reputation lookup, someone else may have already analyzed this exact binary, and a known-good hash ends the investigation in minutes.
sha256sum sample.binExpected output:
9f2c41d7b8e3a0c5f6d1928475a3e6b0c1d4e7f8a9b0c2d3e4f5a6b7c8d9e0f1a2 sample.bin
The reputation check is a lookup by hash only. Against VirusTotal that means querying the API with the digest, the sample itself is never uploaded. Run it from the analyst box, not from any host in the isolated detonation segment; the analysis guest and the INetSim segment have no internet route, and punching one in "just for a VT lookup" breaks the isolation the whole lab depends on. Uploading feels harmless but is an operational error: VirusTotal shares submitted files with every subscriber, so uploading an attachment pulled from a targeted phishing email tells the world (including the attacker) that you have it, and may leak internal data if the "sample" turns out to be a trojanized internal document.
Hash-only reputation query (analyst box with internet access, never inside the isolated segment)
curl -s "https://www.virustotal.com/api/v3/files/9f2c41d7..." \
-H "x-apikey: $VT_API_KEY" | python3 -m json.tool | grep -A6 last_analysis_stats
"last_analysis_stats": {
"malicious": 41,
"suspicious": 3,
"undetected": 28,
"harmless": 0
}
Where the lab VM is fully offline (the correct default), keep a local cache instead: a directory of known hashes from previous cases plus exported blocklists, checked with a one-line grep. A hit in the local cache or a 41/72 vendor verdict settles the "is it malicious" question; triage continues anyway, because the behavioural IOCs are still needed for detection and scoping.
Common mistake: treating "0 detections" as "clean." A fresh or repacked sample scores zero precisely because it is new. Verify: reputation is one input; the static indicators from step 1 and the behaviour from step 3 outweigh a quiet verdict page every time.
What this teaches: hashes let you check known-bad lists without executing anything.
3. Detonate under strace in the sandbox [PRIVILEGED]
This is the first moment the sample actually runs, and it happens only inside the reverted, isolated guest. strace intercepts the sample's system calls, the boundary between the program and the kernel, so every file it touches and every connection it attempts is logged regardless of what the program prints or hides. The flags matter: -f follows child processes (malware loves to fork and let the child do the dirty work while the parent exits innocently), -e trace=network,file restricts the log to network and filesystem calls (a full trace of a busy binary drowns the signal in mmap/futex noise), and 2> redirects strace's stderr output to the log file so it survives even if the sample spawns a shell.
chmod +x sample.bin && strace -f -e trace=network,file ./sample.bin 2> behaviour.logExpected output:
connect(3, {sa_family=AF_INET, sin_port=htons(443), sin_addr=inet_addr("203.0.113.45")}, 16) = -1 ECONNREFUSED (Connection refused)
openat(AT_FDCWD, "/tmp/.lock", O_CREAT) = 4
Two findings in two lines of trace: the outbound connect() to 203.0.113.45 on 443, which failed fast because this detonation runs with a dead network, confirms the URL found in strings (step 1) was the sample's real callback target, not a decoy, this is the T1071 hypothesis to pursue. The openat creating /tmp/.lock confirms the hardcoded drop path and reveals a mutex-style lock file, a classic single-instance mechanism. Note what this run did not produce: with no route out, the C2 exchange never happens, which is why step 4 re-detonates with a fake internet.
A packet capture runs alongside the trace so the network behaviour is recorded at wire level, independent of what the process does. On the guest:
Parallel packet capture during detonation
sudo tcpdump -i any -w detonation1.pcap host not 192.168.56.1 &
TCPDUMP_PID=$!
strace -f -e trace=network,file ./sample.bin 2> behaviour.log
sleep 120
sudo kill "$TCPDUMP_PID"
Capturing the PID and killing it directly matters: a broad sudo pkill tcpdump would kill every tcpdump on the guest, including one you started for a different experiment or that a teammate is running.
Then pull the high-level story out of the log instead of reading thousands of raw lines:
Summarize the trace
grep -c connect behaviour.log
grep openat behaviour.log | grep -v ENOENT | grep -viE '\.(so|cache)' | head -10
3
openat(AT_FDCWD, "/tmp/.lock", O_CREAT) = 4
openat(AT_FDCWD, "/etc/cron.d/.sysupd", O_WRONLY|O_CREAT) = 5
openat(AT_FDCWD, "/home/analyst/.ssh/authorized_keys", O_WRONLY|O_APPEND) = 6
The filtered view turns noise into findings: a persistence entry dropped into cron.d and an SSH key appended to authorized_keys. Both go on the IOC list and both are ATT&CK-mapable (T1053, T1098). The guest has now done its job, note the findings in the case file, then revert it to the clean snapshot before anything else happens on it.
Common mistake: running strace without -f and concluding "the sample did nothing" because the visible parent exited immediately while the forked child carried on untraced. Verify: grep -c clone behaviour.log, if forks happened and the log ends at the parent's exit_group, the interesting behaviour escaped the trace.
What this teaches: strace reveals the sample's system calls: the ground truth of its behavior.
4. Give the sample a fake internet with INetSim
Many samples behave minimally when the network is dead: no callback, no second-stage download, no payload. Some deliberately stay quiet to defeat sandboxes that offer no connectivity. INetSim solves this without giving the sample real access, it is a suite of fake servers (DNS, HTTP, SMTP, and more) that answers everything plausibly. The sample's DNS lookup resolves to the INetSim box, its HTTPS callback connects to a fake web server, and its second-stage download receives a harmless stand-in binary, while every request is logged.
Start INetSim on the services guest and point the analysis guest's DNS at it
Expected output:
# on the inetsim guest (192.168.56.20)
sudo inetsim
# on the analysis guest: DNS resolves through the fake internet
sudo resolvectl dns eth0 192.168.56.20
# redirect direct-IP outbound connections to INetSim as well (it does not route,
# so a default route via it would just blackhole traffic)
sudo iptables -t nat -A OUTPUT -d 192.168.56.0/24 -j RETURN
sudo iptables -t nat -A OUTPUT -p tcp -j DNAT --to-destination 192.168.56.20
sudo iptables -t nat -A OUTPUT -p udp -j DNAT --to-destination 192.168.56.20
INetSim 1.3.2 started
* dns 53/udp/tcp - started (PID 4812)
* http 80/tcp - started (PID 4813)
* https 443/tcp - started (PID 4814)
Re-detonate the sample (fresh snapshot first) and then read INetSim's service log, which records exactly what the malware asked for:
What the sample requested from the fake internet
cat /var/log/inetsim/service.log | grep -E 'DNS|HTTP'
[2026-06-17 21:04:11] DNS: query upd-srv.203-0-113-45.host A -> 192.168.56.20
[2026-06-17 21:04:12] HTTPS: POST /collect (len 512) from 192.168.56.10
[2026-06-17 21:06:40] HTTPS: GET /beacon from 192.168.56.10
This run produced the richest results of the lab: with a "live" internet, the sample beaconed on a 150-second loop, posted a 512-byte system survey to /collect, and requested a second stage from /beacon. None of that appeared in the step-3 run with a dead network, proof that "no network activity" is a property of the environment, not necessarily of the sample.
TLS caveat: INetSim answers HTTPS with its own self-signed certificate, so a sample that validates certificates or pins its C2's real certificate will refuse the handshake or abort the connection. If DNS resolves and the TCP connection opens but no HTTP exchange follows, check INetSim's report for failed TLS handshakes before concluding the sample has no HTTPS C2; that refusal is itself a behavioural finding (the sample does real certificate validation, which tells you what the genuine infrastructure must present).
Common mistake: forgetting the guest's DNS still points at the real resolver, so the sample's lookups leak to production DNS (visible, logged, and attributable) or simply fail. Verify: from the analysis guest, dig example.com must return the INetSim address before detonation; check INetSim's log after, if it is empty, the traffic never arrived.
What this teaches: malware often stays quiet without a network; INetSim gives it someone to talk to.
5. Full behavioural report with Cuckoo sandbox
Manual strace triage scales to one sample at a time and only shows syscalls. Cuckoo automates the whole detonation: it reverts a guest, injects the sample, monitors API/syscall activity, captures traffic, takes screenshots, collects dropped files, and emits a structured report. Submitting the same sample there serves two purposes, it catches behaviour a filtered strace missed, and it produces a report artifact that can be handed to someone else as-is.
Submit the sample and wait for the report
Expected output:
cuckoo submit --timeout 180 /cases/sample.bin
# start the API on the controller (classic Cuckoo syntax; on Cuckoo 3 use `cuckoo api` as configured there)
cuckoo api &
# poll the REST API until the task reports
curl -s http://localhost:8090/tasks/list | python3 -c "import sys,json; print([ (t['id'], t['status']) for t in json.load(sys.stdin)['tasks'] ])"
Task #24 added: /cases/sample.bin
...
[(24, 'reported')]
Two honest notes about this stack. First, classic Cuckoo's strongest behavioural monitoring is Windows-guest oriented; its Linux guest agent exists but produces a thinner signature set, so the Cuckoo run here corroborates rather than replaces the strace and INetSim evidence. Second, check your Cuckoo version's CLI before copying commands: cuckoo submit and cuckoo api are the stable entry points, task status is polled through the REST API (or the web UI), there is no cuckoo --status flag.
The report JSON is where the IOC extraction becomes systematic rather than grep-driven:
Pull the high-value fields from the Cuckoo report
python3 - <<'EOF'
import json
r = json.load(open('/cases/storage/analyses/24/reports/report.json'))
print("score:", r['info']['score'])
print("signatures:", [s['name'] for s in r['signatures']])
print("domains:", [d['domain'] for d in r['network']['domains']])
print("tcp:", r['network']['tcp'])
print("dropped:", [f['name'] for f in r.get('dropped', [])])
print("mutexes:", r['behavior']['summary'].get('mutexes', []))
EOF
score: 7.4
signatures: ['creates_mutex', 'persistence_cron', 'network_c2_http', 'queries_system_info']
domains: ['upd-srv.203-0-113-45.host']
tcp: [('192.168.56.10', '203.0.113.45', 443)]
dropped: ['.sysupd', 'payload_stage2.bin']
mutexes: ['sysupd_lock']
The independent engine corroborated the manual findings (C2 domain, cron persistence, the lock mechanism as a named mutex) and added two the strace filter had missed: a dropped second-stage binary recovered from the guest's filesystem, and system-info reconnaissance queries. The dropped file is itself a new sample, hash it, note it, and if time permits, submit it for its own analysis.
Common mistake: trusting a single run. Many samples sleep for the first N minutes or check uptime to dodge short sandboxes. Verify: if the Cuckoo summary shows almost no API calls, resubmit with a longer timeout and anti-sleep options; a sample that "does nothing" in 180 seconds may be waiting you out.
What this teaches: Cuckoo correlates file, registry, and network behavior into one report.
6. Handle packed samples
The sample in this lab was chatty enough statically, but about a third of the samples queued behind it were not, strings returned garbage, imports were a stub, and YARA matched nothing. That signature means packing: the real code is compressed or encrypted inside the file and only unfolds in memory at runtime. The quick test is entropy: high-entropy sections (near 8 bits per byte) look random, and real randomness in a program usually means packed or encrypted content.
Entropy check and packer identification
Expected output:
ent sample2.bin
strings -n 6 sample2.bin | grep -iE 'upx|packed|vmp|themida' | head -5
Entropy = 7.82 bits per byte. # near the 8.0 maximum: packed or encrypted
UPX0
UPX1
UPX!
When the packer is UPX, the most common case in commodity Linux malware, often left with its header markers intact, unpacking is one flag. Do it on a copy, inside the isolated environment:
Unpack a UPX sample (on a copy, in the lab)
cp sample2.bin sample2.upx.bin
upx -d sample2.upx.bin -o sample2.unpacked.bin
file sample2.unpacked.bin
strings -n 8 sample2.unpacked.bin | grep -Ei 'http|/bin/sh' | head
sample2.unpacked.bin: ELF 64-bit LSB executable, x86-64, dynamically linked
http://185.220.101.7/gate.php
/bin/sh -c whoami
The unpacked binary gives up everything the packed one hid. For custom packers where no upx -d exists, the fallback is dynamic: detonate, let the stub unpack the real code in memory, and dump the process image from the sandbox (Cuckoo's memory-analysis integration or a manual gcore against the running PID), then run strings/YARA against the dump.
Common mistake: running upx -d on the only copy of the sample. Unpacking rewrites the file in place by default; if the tool chokes halfway, the evidence is damaged. Verify: hash before and after, work on a copy, and confirm the unpacked file's type and strings changed before deleting anything.
What this teaches: packers hide the real code; unpacking (or dumping from memory) exposes it.
7. Extract IOCs and author a YARA rule
Triage is not done until the findings are in a form the rest of the security stack can use. The IOC list consolidates everything corroborated across the static and dynamic stages, the discipline is that each indicator carries its source, so a later reviewer can see which stage vouched for it:
IOC summary for this sample (from case notes)
Expected output:
Type Value Source
sha256 9f2c41d7b8e3...f1a2 step 2
domain upd-srv.203-0-113-45.host strings, strace, INetSim, Cuckoo
ipv4 203.0.113.45:443 strace, Cuckoo
filepath /tmp/.lock strings, strace
filepath /etc/cron.d/.sysupd strace, Cuckoo
mutex sysupd_lock Cuckoo
filepath authorized_keys (append) strace
The last step closes the loop: write a YARA rule from the sample's stable traits so the next appearance of this family is caught by a scan instead of a seven-hour triage. Good rules anchor on strings that are distinctive (unlikely in clean software) and stable (likely to survive a recompile), the unique C2 path, the mutex, the cron drop name:
rules.yar addition, family detection rule
rule Sysupd_Linux_Downloader
{
meta:
author = "T. Sadik"
date = "2026-06-17"
description = "Detects the sysupd downloader family from phishing triage case"
hash = "9f2c41d7b8e3a0c5f6d1928475a3e6b0c1d4e7f8a9b0c2d3e4f5a6b7c8d9e0f1a2"
strings:
$c2 = "upd-srv.203-0-113-45.host" ascii
$mx = "sysupd_lock" ascii
$cron = "/etc/cron.d/.sysupd" ascii
$ua = "Mozilla/5.0 (X11; Linux x86_64)" ascii
condition:
uint32(0) == 0x464c457f and 2 of them
}
The uint32(0) == 0x464c457f check restricts the rule to ELF files (the magic bytes \x7fELF little-endian), and 2 of them requires at least two strings so one coincidental UA string in a browser binary cannot fire the rule. Then test it: against the sample, against the unpacked variant, and, critically, against a pile of known-clean binaries to prove it does not false-positive:
Validate the rule: true positive, no false positives
yara rules.yar sample.bin sample2.unpacked.bin
yara -r rules.yar /usr/bin/ | head -5 ; echo "FP count: $(yara -r rules.yar /usr/bin/ | wc -l)"
Sysupd_Linux_Downloader sample.bin
Sysupd_Linux_Downloader sample2.unpacked.bin
FP count: 0
Common mistake: writing the rule from strings alone and shipping it untested. A rule that fires on /usr/bin/curl is worse than no rule. Verify: positive match on the sample and variants, zero matches across a large clean corpus (/usr/bin, a stock OS image) before the rule goes anywhere near production detection.
What this teaches: IOCs plus a YARA rule turn one sample into detection for the whole family.
How It Works
Malware analysis answers two questions: what is it, and what does it do. Static analysis examines the file without executing it: type and architecture, embedded strings, imported functions, section entropy (high entropy suggests packing or encryption), and YARA rule matches against known families. This is safe and fast but blind to runtime behavior.
Dynamic analysis executes the sample in a sandbox and records what it does: files created, registry keys written, processes spawned, network connections attempted. strace captures the system-call layer directly. Because many samples check for analysis environments or need a network to activate, INetSim fakes DNS, HTTP, and other services so the sample reveals its C2 logic without reaching the real internet.
The output of triage is IOCs (hashes, IPs, domains, file paths, mutexes) and a YARA rule encoding the family's distinctive byte patterns. Those artifacts feed detection engineering: the same rule that identified this sample can scan an entire estate for its siblings.
Worked Example: Quarantine to Verdict in One Session
Pulling the case notes together, the full arc for sample.bin looked like this. Note how each stage either corroborates or kills the previous stage's hypotheses, that ratchet is the whole method:
09:12 Hash logged; VT hash-only lookup: 41/72 malicious - verdict no longer in doubt
09:20 file: ELF 64-bit, static, stripped; strings: C2 URL, /tmp/.lock, xmrig ref
09:35 YARA community rules: Suspicious_Downloader + Linux_Cryptominer_Artifacts
10:05 Guest reverted to clean snapshot; isolation re-verified (no route, no DNS)
10:15 Detonation 1 (dead net): strace shows /tmp/.lock + cron.d/.sysupd drops,
connect() to 203.0.113.45:443 fails fast, sample idles - needs internet
10:40 Detonation 2 (INetSim): beacons every 150s, POSTs 512-byte survey to
/collect, GETs /beacon second stage - full T1071 behaviour captured
11:30 Cuckoo task #24: score 7.4, mutex sysupd_lock, dropped payload_stage2.bin,
system-info recon signatures - corroborates + extends manual findings
13:10 IOC list finalized (7 indicators, each with 2+ sources); YARA rule written
13:25 Rule validated: matches sample + unpacked variant, 0 hits across /usr/bin
13:30 Guest reverted and re-scanned clean; case notes exported with the report
Two things are worth drawing out of that timeline. First, the total hands-on time was about four hours including the Cuckoo wait, the remaining lab time went to the packed samples queued behind this one (step 6) and to writing the detection. Second, the single most valuable decision was made at 10:40: recognizing that the idling sample was an environment problem, not a verdict, and giving it a fake internet. That one change turned a thin "drops two files, tries one connection" report into a full C2 behavioural profile.
Validation & Testing
Confirm the sandbox network is isolated, then verify the extracted IOCs (C2 IP, dropped file paths, mutexes) are reproducible across two detonations from clean snapshots. Pass criteria: consistent behaviour and a documented IOC list ready for detection.
Concretely, validation in this lab meant four checks:
- Isolation proof, before and after. From the analysis guest,
ping -c2 8.8.8.8andcurl -m5 https://example.commust fail when INetSim is down. A sample that "beaconed" while the guest secretly had a real route would invalidate every network finding, and would have leaked C2 contact to the real infrastructure. - Reproducibility. Two detonations from the clean snapshot produced the same C2 destination, the same three file writes, and the same mutex within run-to-run timing jitter. Behaviour that appears in only one run is flagged as conditional (possibly time-, environment-, or C2-command-dependent), not dropped.
- Cross-tool agreement. Every network indicator in the final IOC list appears in at least two independent observations: strace syscall log plus pcap, or Cuckoo report plus INetSim service log. The C2 domain appeared in all four.
- Clean-snapshot guarantee. After the final detonation the guest was reverted and re-scanned: no cron entry, no lock file, no authorized_keys change. The lab's own hygiene is part of the pass criteria.
Snapshot diff after revert (should be empty)
grep -R sysupd /etc/cron.d/ ; ls -la /tmp/.lock ; tail -1 ~/.ssh/authorized_keys
ls: cannot access '/tmp/.lock': No such file or directory
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Command not found | package missing | install prerequisite and verify version |
| Permission denied | insufficient privileges | use documented sudo or lab admin context |
| Output differs | version or environment mismatch | verify versions and variables |
| Service or API unreachable | dependency, DNS, or firewall issue | check status, listener, and logs |
The five issues below are the ones that actually cost time in this lab, roughly in the order they appear in the workflow:
- Sample detects the VM: it checks for hypervisor artifacts (MAC prefixes,
/sys/class/dmi/idstrings, low uptime, tiny screen resolution) and exits or plays harmless. Use anti-evasion sandbox config and realistic uptime/artifacts; in Cuckoo enable the anti-detection auxiliary modules, and give the guest believable user activity, a browser history, recent documents, an uptime of days rather than the 40 seconds a fresh revert shows. - No network activity: the sample may need a fake internet (INetSim) to trigger C2. Confirm the guest's DNS and default route point at the INetSim host, confirm INetSim is actually listening (
ss -tlnp), and check its service log after the run. If the log shows the DNS query but no follow-up connection, the sample is checking the answer, some families hardcode the expected resolution and bail when INetSim answers differently, which is itself a reportable anti-analysis behaviour. - Host contamination risk: never analyze on a networked or production machine. If a sample was ever detonated somewhere with a real route, a mistake discovered after the fact, treat the host as compromised: isolate it, capture what you can for forensics, rebuild from known-good media, and rotate any credential that machine could reach. The uncomfortable version of this rule: also never email samples around or drop them in synced folders; hash-only reputation lookups are the only thing that leaves the lab.
- strace shows nothing useful / sample exits instantly: usually a missing
-f(child does the work), an anti-debugging check (ptrace(PTRACE_TRACEME)detects the tracer and exits), or a sleep timer. Check the log for earlyptraceor longnanosleepcalls. For ptrace checks, run under Cuckoo (which hooks differently) or patch the check in a debugger on a copy; for sleeps, extend the observation window rather than giving up. - YARA matches nothing but the sample is clearly malicious: the sample is packed, or the rule set is stale, or the rule is looking at the file while the traits only exist in memory. Run the entropy check from step 6, update the community rules, and scan Cuckoo's memory dump and dropped files in addition to the original binary, memory-resident unpacked code is often where the recognizable strings live.
Key Takeaways
- Static first, dynamic second: strings, file type, and YARA cost nothing and carry zero risk, and their hypotheses make the detonation stage far more productive.
- Isolation is proven, not assumed, test that the guest cannot reach the outside before the sample runs, and re-test after.
- Hash the sample first and let the hash be the case identifier; reputation lookups go out by hash only, and the sample file never leaves the lab.
- "It did nothing" is usually an environment verdict: dead network, short timeout, VM detection, or a sleep timer. Fix the environment before judging the sample.
- No IOC ships on a single observation: strings says it, strace confirms it, and the sandbox corroborates it, two independent sources minimum.
- Revert the snapshot after every detonation, and verify the revert. The next sample deserves a clean guest, and so does your evidence.
- The deliverable is detection, not just a verdict: the YARA rule, tested against clean binaries, is what makes the seven hours useful to the next incident.
Key Results
- Extracted actionable IOCs from 100% of analyzed samples.
- Identified C2 endpoints and dropped-file artifacts via sandbox strace capture.
- Reverted to a clean snapshot for 100% of detonations, preventing contamination.
- Authored YARA detections from observed strings for future hunting.
Practice Challenges
Challenge 1: Write the YARA rule first
Goal: Given only the strings output, write a YARA rule that matches the sample, then test it against a benign file for false positives.
Hint: Pick strings unique to the malware's functionality, not generic library text.
Challenge 2: Defeat the sleep
Goal: A sample sleeps 10 minutes before acting; get its behavior in under a minute without waiting.
Hint: Patch the sleep call, accelerate the VM clock, or hook the timing API in the sandbox.