Malware Analysis
Definition
Section titled “Definition”Malware analysis as a Blue Team discipline is the production function that turns samples into decisions: the SOC’s triage pipeline classifies the daily flow (is this malicious? how sure? what does it do? who else has it?), escalates the interesting cases to behavioral and static analysis, and feeds every verdict’s byproducts — IOCs, YARA rules, technique mappings, hunt hypotheses — back into the estate. The depth ladder is the organizing structure: triage (hashes, entropy, strings, imports — minutes per sample, statictriage below), behavioral (sandbox detonation with process/network/file telemetry — CAPE-class tooling, monitoring for E14-class evasion), static/deep (disassembly, unpacking, config extraction — the M-series craft, invoked by exception), and reconstruction (the full report that BT4’s IR and BT9’s detections consume). The defender’s version differs from the analyst’s (M-series) in service level: the question is rarely “how does this family work in detail” and usually “what’s the fastest defensible answer that protects the estate” — verdict in minutes, behavioral context in hours, deep analysis by exception. The craft’s center: malware analysis is an assembly line with quality gates — hash dedupe, family clustering (M15’s join), evasion-awareness (E14’s checks), and the feedback loop that turns every interesting sample into standing coverage.
Why It Matters
Section titled “Why It Matters”PLACEHOLDER: the phishing attachment (BT14), the dropped payload (BT1’s escalation), the suspicious binary on a workstation (BT7’s hunt finding). Without an in-house function, every one of those waits on a vendor sandbox verdict — which is slow, black-box, and exactly what the adversary tests against (E14’s sandbox checks are built on the assumption that everyone uses the same three services). An in-house triage capability gives the SOC three things vendors can’t: speed (verdict on the email still in the quarantine queue), context (your own telemetry joined to the detonation — the C2 callback that your EDR saw is worth more than a generic score), and counter-evasion (detonating in an environment built to look like your real estate, so the malware that checks for VM artifacts meets a plausible decoy).
The failure modes are operational. Sandbox-truth fallacy: treating a clean sandbox verdict as “benign” — evasion-aware samples return “no activity” and the analyst stamps benign, which is precisely the wrong lesson; verdicts carry confidence, and evasion indicators lower it. Triage debt: samples pile up in a shared folder; the queue ages past relevance; the campaign is over before the analysis starts. No dedupe: the same commodity sample analyzed 40 times while the unique one waits. IOC hoarding: the analyst extracts indicators and they die in the report — never reaching BT2’s rules, BT10’s sightings, or BT9’s content. PLACEHOLDER systems or a shared workstation — the sample detonates on you (ransomware doesn’t care that you’re the analyst). Each failure is an assembly-line problem — which is why the fix is pipeline design, not heroics.
Detailed Explanation
Section titled “Detailed Explanation” IN: sample + context (where found, what chain) | v +------------------+ known hash (MISP/VT/internal) --> reuse verdict | TRIAGE (minutes) | new? --> | statictriage: | entropy per section, imports, strings | hashes, entropy, | classification, packer indicators | strings, PE map | +--------+---------+ | score high OR context demands behavior v +-------------------+ detonate: VM snapshot per sample, | BEHAVIORAL | E14-check awareness (VM artifacts, | (sandbox, hours) | single-sample VMs, network isolation, | process/net/file | full telemetry capture) | + YOUR telemetry | +--------+----------+ | needs depth (packing, config, exploit) v +-------------------+ unpack (M4 craft), config extract, | DEEP (by excptn) | code analysis (M-series depth) | disassembly, RE | days per sample, scoped request +--------+----------+ v +--------------------------------------------------+ | FEEDBACK (the real product) | | verdict -> BT1 case, BT4 chain | | IOCs -> BT10 sightings, BT2 NIDS rules | | YARA -> M6 fleet sweeps (find the others) | | TTPs -> BT9 content, BT7 hunt hypotheses | +--------------------------------------------------+Triage is statistics before ceremony: Shannon entropy per PE section (packed or encrypted sections push past ~7.2 bits/byte — a heuristic, not law), import-table poverty (a GUI-less binary with five imports and high entropy is packed), string classification (URLs, IPs, registry writes, PowerShell flags, base64 blobs), and hash dedupe against internal history and MISP/VT (the 80% of samples that are yesterday’s commodity skip straight to reuse). statictriage (below) does all of this in pure stdlib — including a minimal PE parser good enough for the section table, which is the whole triage game.
Behavioral analysis runs in a deliberate environment: disposable VMs, snapshot-per-sample, network isolation with a simulated-services stack (fake DNS, HTTP, SMTP — so callbacks have something to talk to), and telemetry capture at process/network/file levels. The E14 awareness is operational, not academic: the detonation environment is built to defeat the common checks (VM artifacts scrubbed, plausible user activity, realistic hardware profile), and every “no activity” verdict gets an evasion check before anyone writes “benign” — did the sample touch known artifact paths? Did it sleep past the timeout? Did it check MAC addresses?
Deep analysis is scoped by request, not curiosity: IR asks “what does this do, what’s the config, is there a decryptor path” (BT4’s timeline needs it); BT10 asks “which family, which cluster” (M15’s join); BT9 asks “what behavior should a rule catch”. The M-series craft (unpacking, RE, config extraction) is the toolkit; the Blue Team discipline is service-level management — deep analysis is a queue with priorities, not a rabbit hole.
Deep dive: building the fake-services stack that makes detonation honest
A detonation with no answers is a detonation the malware won’t complete: callbacks that hit dead air abort early, and “no C2 traffic” becomes an artifact of your empty lab, not the sample’s intent. The minimal honest stack: fake DNS (resolve everything — wildcard answers so any domain resolves; log every query, because the query list is itself IOC material), fake HTTP (a catch-all server that returns plausible 200s for GETs and accepts POSTs — logging full requests, because the exfil POST body is the payload), fake SMTP (accept and store everything — mail-exfil families confirm here), and protocol-specific fakes where your threat picture demands them (an FTP catcher, a raw TCP echo). Two disciplines make the log useful: record every interaction with timestamps (the callback cadence is a behavioral fingerprint), and never let the fake services return errors that real infrastructure wouldn’t — a family that checks its C2’s response shape will abort on a naive catcher, and you’ll log a non-event. The stack is an afternoon of work and the difference between “the sample made 4 DNS queries and gave up” and “here is the full C2 protocol transcript.”
Common Techniques
Section titled “Common Techniques”| Technique | What it does | Where it fails | Notes |
|---|---|---|---|
| Hash dedupe first | Reuse prior verdicts | Silent DB rot | MISP/internal join |
| Section entropy map | Packed/encrypted region hints | Legit high-entropy data | Heuristic, pair w/ imports |
| Import poverty check | Packed binary tell | Delayed/loaded imports | PE parse depth |
| String classification | Fast IOC candidates | Encoded/stacked strings | statictriage patterns |
| Disposable detonation | Behavioral truth, no blast radius | VM-aware samples | E14 checks mandatory |
| Simulated services | Callbacks have somewhere to go | Misconfigured fakes | Fake DNS/HTTP/SMTP |
| Config extraction | C2s, keys, campaign IDs | Per-family custom work | M7/M15 depth |
| YARA fleet sweeps | Find every copy of the family | Rule quality (M6) | yarasweep’s job |
| Technique mapping | Sample → ATT&CK rows | Over-mapping | Evidence per mapping |
| Verdict confidence | Evasion-aware scoring | Binary benign/malicious | Confidence, not bool |
Full Working Code
Section titled “Full Working Code”statictriage — a stdlib-only triage scorer: parses PE structure (DOS header → PE signature → COFF → optional header → section table) without dependencies, computes Shannon entropy per section, checks import-table poverty, classifies suspicious strings (URLs, IPs, PowerShell flags, registry writes, base64 blobs), and produces a scored verdict with confidence flags. Works on any file; PE-aware when given one.
#!/usr/bin/env python3"""statictriage — stdlib PE triage: entropy, imports, strings, verdict.
Usage: python3 statictriage.py sample.bin [--json]Verdict: score >= 6 malicious, 3-5 suspicious, < 3 likely-benign.Confidence drops when evasion indicators are present."""import argparseimport jsonimport mathimport reimport sysfrom collections import Counterfrom pathlib import Path
def shannon(data: bytes) -> float: if not data: return 0.0 counts = Counter(data) total = len(data) return -sum((c / total) * math.log2(c / total) for c in counts.values())
def parse_pe(data: bytes): """Minimal PE walk: DOS -> PE -> COFF -> optional -> sections.""" if len(data) < 0x40 or data[:2] != b"MZ": return None e_lfanew = int.from_bytes(data[0x3C:0x40], "little") if e_lfanew + 24 > len(data) or data[e_lfanew:e_lfanew + 4] != b"PE\x00\x00": return None coff = e_lfanew + 4 machine, nsec, _ts, _sym, _nsym, opt_size, chars = ( int.from_bytes(data[coff:coff + 2], "little"), int.from_bytes(data[coff + 2:coff + 4], "little"), int.from_bytes(data[coff + 4:coff + 8], "little"), int.from_bytes(data[coff + 8:coff + 12], "little"), int.from_bytes(data[coff + 12:coff + 16], "little"), int.from_bytes(data[coff + 16:coff + 18], "little"), int.from_bytes(data[coff + 18:coff + 20], "little"), ) opt = coff + 20 magic = int.from_bytes(data[opt:opt + 2], "little") sections = [] sec_tbl = opt + opt_size for i in range(nsec): base = sec_tbl + i * 40 if base + 40 > len(data): break name = data[base:base + 8].rstrip(b"\x00").decode("ascii", "replace") vsize = int.from_bytes(data[base + 8:base + 12], "little") vaddr = int.from_bytes(data[base + 12:base + 16], "little") rawsize = int.from_bytes(data[base + 16:base + 20], "little") rawptr = int.from_bytes(data[base + 20:base + 24], "little") sections.append({"name": name, "vsize": vsize, "vaddr": vaddr, "rawsize": rawsize, "rawptr": rawptr}) # import directory RVA/size from the data directory dd_off = (opt + (0x70 if magic == 0x20B else 0x60)) # PE32+ vs PE32 import_rva = int.from_bytes(data[dd_off + 8:dd_off + 12], "little") \ if dd_off + 12 <= len(data) else 0 return {"machine": machine, "nsec": nsec, "chars": chars, "magic": hex(magic), "sections": sections, "import_rva": import_rva}
URL_RE = re.compile(rb"https?://[\x20-\x7e]{6,}", re.I)IP_RE = re.compile(rb"\b(?:\d{1,3}\.){3}\d{1,3}\b")REG_RE = re.compile(rb"(?:HKCU|HKLM|CurrentVersion\\(?:Run|RunOnce)" rb"|SOFTWARE\\\\Microsoft\\\\Windows)", re.I)PS_RE = re.compile(rb"(?i)(-enc(?:odedcommand)?\b|downloadstring|" rb"frombase64string|invoke-expression|iex\b|bypass\s+-exec)")B64_RE = re.compile(rb"[A-Za-z0-9+/]{40,}={0,2}")SUSPICIOUS_NAMES = ("cmd", "powershell", "rundll32", "regsvr32", "mshta", "wscript", "cscript", "schtasks", "vssadmin", "bcdedit")
def main(): ap = argparse.ArgumentParser(description="stdlib malware triage") ap.add_argument("sample", type=Path) ap.add_argument("--json", action="store_true", help="machine-readable out") args = ap.parse_args()
if not args.sample.exists(): sys.exit(f"fail-loud: sample not found: {args.sample}") data = args.sample.read_bytes() if not data: sys.exit("fail-loud: empty file — not a sample, a mistake") import hashlib sha256 = hashlib.sha256(data).hexdigest() md5 = hashlib.md5(data).hexdigest()
pe = parse_pe(data) score, findings = 0, [] evasion_flags = 0
section_entropies = [] if pe: for s in pe["sections"]: blob = data[s["rawptr"]:s["rawptr"] + s["rawsize"]] ent = shannon(blob) if s["rawsize"] else 0.0 section_entropies.append((s["name"], ent, s["rawsize"], s["vsize"])) if ent > 7.2 and s["rawsize"] > 512: score += 2 findings.append(f"high entropy section {s['name']!r}: " f"{ent:.2f} (packed/encrypted?)") # import poverty: import RVA beyond first data dir + tiny sections if pe["import_rva"] == 0: score += 2 findings.append("no import directory (packed or shellcode)") # suspicious section names weird = [n for n, _, _, _ in section_entropies if n and not re.match(r"^[\.\w]{1,8}$", n)] if weird: score += 1 findings.append(f"unusual section names: {weird}") if any(k in data[:0x400] for k in (b"UPX0", b"UPX!")): findings.append("UPX packer signature") else: findings.append("not a PE — script/doc/document class") # script classes: look for interpreter patterns if re.search(rb"(?i)(wscript|cscript|powershell|cmd\.exe)", data[:65536]): score += 2 findings.append("script dropper pattern in first 64KB")
urls = [m.decode("ascii", "replace")[:80] for m in URL_RE.findall(data)[:10]] ips = [m.decode() for m in IP_RE.findall(data)[:10]] if urls: score += 1 findings.append(f"URLs ({len(urls)}): {urls[0]}") if IP_RE.search(data): score += 1 findings.append(f"embedded IPs: {ips[:3]}") if REG_RE.search(data): score += 2 findings.append("registry autorun patterns") if PS_RE.search(data): score += 2 findings.append("powershell download/exec patterns") if B64_RE.search(data): score += 1 findings.append("base64 blob present") exe_names = [n.decode("ascii", "replace") for n in (b"cmd.exe", b"powershell.exe", b"rundll32.exe", b"schtasks.exe", b"vssadmin.exe") if n in data] if exe_names: score += 1 findings.append(f"process references: {exe_names[:4]}") evasion_flags += 1
# evasion-awareness: dual extensions, tiny PE with huge overlay if args.sample.suffix.lower() in (".jpg", ".pdf", ".doc", ".docx") \ and data[:2] == b"MZ": score += 3 findings.append("extension/content mismatch (MZ under doc extension)") evasion_flags += 1
verdict = ("malicious" if score >= 6 else "suspicious" if score >= 3 else "likely-benign") confidence = "low" if evasion_flags else ("medium" if score < 6 else "high") out = {"file": args.sample.name, "size": len(data), "sha256": sha256, "md5": md5, "is_pe": bool(pe), "score": score, "verdict": verdict, "confidence": confidence, "findings": findings, "urls": urls, "ips": ips} if args.json: print(json.dumps(out, indent=2)) return print(f"file : {args.sample.name} ({len(data)} bytes)") print(f"sha256 : {sha256}") print(f"pe : {'yes' if pe else 'no'}" + (f" sections={len(pe['sections'])}" if pe else "")) for name, ent, raw, virt in (section_entropies or []): print(f" entropy : {name:<10} {ent:5.2f} raw={raw} vsize={virt}") print(f"score : {score} verdict: {verdict} confidence: {confidence}") for f in findings: print(f" - {f}")
if __name__ == "__main__": main()Code Walkthrough
Section titled “Code Walkthrough”The PE walk (lines 22-63) is deliberately minimal and honest about it: MZ → e_lfanew at 0x3C → PE\0\0 signature → COFF header (machine, section count, characteristics) → optional header magic (0x20B = PE32+, 0x10B = PE32 — the data-directory offset differs, line 61) → section table (name, virtual/raw sizes, raw pointer). It stops at the section table — that’s all triage needs; the import-directory RVA presence check (lines 78-80) catches the poverty signal without an IAT walk.
shannon() (lines 15-19) is the workhorse: per-section entropy over raw bytes — packed sections concentrate past 7.2 bits/byte (line 75), with a raw-size floor (line 74) so a 40-byte .reloc doesn’t false-positive on math. The string classifiers (lines 65-71) are curated families: autorun registry patterns (Run/RunOnce — persistence material), PowerShell download/exec patterns (the -encodedcommand|downloadstring|iex cluster), base64 blobs (40+ chars — enough to mean “payload”, not “coffee order”), and the process-reference check (lines 100-104) which doubles as an evasion indicator (a “document” that names five process-launchers is a dropper wearing a suffix — lines 110-115 make that explicit with the extension/content mismatch check).
The verdict model (lines 118-119) is deliberately blunt — the confidence field is where the honesty lives: evasion flags (extension mismatch, process references in non-executables) drop confidence to “low” regardless of score, encoding the module’s doctrine that “malicious, low confidence” and “benign, low confidence” both mean detonate it before deciding. The --json output (lines 122-123) is the pipeline contract — BT1’s automation consumes verdicts; humans read the pretty printer.
How It Reaches The Target
Section titled “How It Reaches The Target”The assembly line in operation: BT14’s phish triage escalates a .docm attachment; statictriage classifies it in 40 seconds (script dropper pattern, base64 blob, score 5 — suspicious, medium confidence) — and the same run dedupes against last week’s sample (different hash, same embedded URLs — the analyst notes the reuse). Detonation follows in the disposable VM: the macro drops a packed binary to %TEMP%, calls home to the URL from the string list, and the fake-services stack answers; behavioral telemetry captures the drop, the callback, and the persistence write. The sample joins the escalation queue: the packed binary gets deep analysis (M4’s unpacking craft), config extraction pulls the C2 list (feeding BT10 as a sighting and BT2 as NIDS rules), and the family clusters against M15’s database — third campaign this quarter with the same builder.
The feedback loop is where the assembly line pays: the YARA rule (M6-crafted) sweeps the fleet via yarasweep and finds the same payload on seven more hosts — the hunt that BT7 didn’t know to run, born from one triaged sample. BT9 inherits the behavioral pattern as rule content (parent-child chain + autorun write); BT7 gets a new hypothesis (the builder’s staging behavior); BT4’s incident timeline imports the full reconstruction. And the measure loop closes in BT20: time-from-sample-to-verdict (minutes, statictriage), time-to-fleet-sweep (hours, yarasweep), IOC-to-coverage conversion — the metrics that keep the function funded. What the adversary experiences: their commodity kit gets fingerprinted and swept fleet-wide within hours, their infrastructure sightings feed the sharing group, and their next campaign runs into rules built from this one’s autopsy.
Exploitation Chain
Section titled “Exploitation Chain”- Intake — sample + context from BT14 attachments, BT1 escalations, BT7 hunts, or IR (BT4)
- Dedupe — hash check against internal history + MISP/VT; reuse verdicts, note reuse patterns
- Triage — statictriage: entropy map, import poverty, string classification, scored verdict + confidence
- Detonate — disposable VM, snapshot-per-sample, fake services, E14-check awareness on every “no activity”
- Deep — by exception: unpack (M4), config extract (M7), family clustering (M15)
- Feed — IOCs → BT10/BT2; YARA → M6 fleet sweeps; TTPs → BT9/BT7
- Sweep — yarasweep finds every other copy on the estate; BT1 cases open from hits
- Report — reconstruction serves BT4’s timeline and BT20’s metrics; sample retires into the corpus
Building Your Own Tool
Section titled “Building Your Own Tool”The verdict names one file; the fleet sweep names every file. yarasweep coordinates YARA scans across a sample corpus or an endpoint export: dedupes by hash, runs rules (yara-python, stated dependency), reports hits per rule with file context, and emits the per-rule hit stats that tell you whether a rule earns its maintenance (M6’s discipline, fleet-side).
Design decisions
Section titled “Design decisions”Hash-first dedupe: scan each unique hash once — corpora are full of copies, and the hit report carries the deduped file list plus per-copy locations. Rule-level stats: hits per rule (a rule matching 90% of the corpus is mis-scoped — M6’s two-corpus discipline fleet-side). Fail-loud on rule compile errors with the rule name in the message (yara-python’s errors are noisy — the tool surfaces the actionable part). Recursive corpus walk with size caps (skip >64MB — triage-scale, not forensic-scale). Runtime dependency declared: yara-python (pip install yara-python) — the only non-stdlib import in this module’s tools, justified because YARA matching is its own craft (M6) and reimplementing it would be worse.
Full source
Section titled “Full source”#!/usr/bin/env python3"""yarasweep — YARA fleet/corpus sweep with dedupe and per-rule stats.
Usage: python3 yarasweep.py rules.yar corpus/ [--out hits.csv]Requires: pip install yara-python (the one stated dependency)"""import argparseimport csvimport hashlibimport sysfrom collections import defaultdictfrom pathlib import Path
try: import yaraexcept ImportError: sys.exit("fail-loud: yara-python missing — pip install yara-python")
MAX_FILE = 64 * 1024 * 1024
def main(): ap = argparse.ArgumentParser(description="YARA corpus sweep") ap.add_argument("rules", type=Path, help=".yar/.yara rule file") ap.add_argument("corpus", type=Path, help="directory to sweep") ap.add_argument("--out", default="yarasweep_out") args = ap.parse_args()
if not args.rules.exists(): sys.exit(f"fail-loud: rules not found: {args.rules}") if not args.corpus.is_dir(): sys.exit(f"fail-loud: corpus is not a directory: {args.corpus}") try: rules = yara.compile(filepath=str(args.rules)) except yara.Error as e: sys.exit(f"fail-loud: rule compile error: {e}")
seen_hashes = {} hits, scanned, skipped = [], 0, 0 rule_hits = defaultdict(int) for path in sorted(args.corpus.rglob("*")): if not path.is_file(): continue if path.stat().st_size > MAX_FILE: skipped += 1 continue try: data = path.read_bytes() except OSError: skipped += 1 continue scanned += 1 sha = hashlib.sha256(data).hexdigest() dedup = sha in seen_hashes if not dedup: seen_hashes[sha] = path try: matches = rules.match(data=data) except yara.Error as e: sys.exit(f"fail-loud: match error on {path}: {e}") if matches: for m in matches: rule_hits[m.rule] += 1 hits.append({"rule": m.rule, "file": str(path), "sha256": sha, "dedup": "yes" if dedup else "no", "strings": len(m.strings)})
out = Path(args.out) out.mkdir(parents=True, exist_ok=True) with (out / "hits.csv").open("w", newline="", encoding="utf-8") as fh: w = csv.DictWriter(fh, fieldnames=["rule", "file", "sha256", "dedup", "strings"]) w.writeheader() w.writerows(hits)
unique_hit_files = len({h["sha256"] for h in hits}) print(f"scanned={scanned} unique={len(seen_hashes)} " f"skipped={skipped} hit-files={unique_hit_files}") print("hits per rule:") for rule, n in sorted(rule_hits.items(), key=lambda kv: -kv[1]): share = n / max(1, len(seen_hashes)) flag = " <-- mis-scoped? review (M6)" if share > 0.5 and n > 5 else "" print(f" {rule:<32} {n:>5} files{flag}") print(f"report: {out / 'hits.csv'}") print("next: open BT1 cases per hit host, feed BT10 sightings for new IOCs")
if __name__ == "__main__": main()Walkthrough
Section titled “Walkthrough”The dependency gate (lines 11-13) fails loud with the install command — the tool’s one external dependency is stated, not smuggled. Compile-time rule errors surface with yara’s message (lines 29-30) — M6’s broken rule stops the sweep rather than silently matching nothing. The corpus walk (lines 35-56) dedupes by SHA-256 before matching: every unique hash matches once, copies inherit the result with dedup=yes — on a 10,000-file corpus with heavy duplication this is the difference between minutes and hours. Per-rule hit stats (lines 62-66) carry the mis-scoping flag: a rule matching >50% of a large corpus is either a cripplingly broad pattern (M6’s benign-corpus failure, fleet-side) or genuinely common malware — either way it needs a human look, and the flag says so at read time instead of at incident time.
The output discipline (lines 58-73) mirrors the assembly line: hits.csv carries rule + file + hash + dedup + string-count (the last for quick triage — zero-string matches are hash-only rules), and the printed next-steps name the consumers (BT1 cases, BT10 sightings) — the tool ends where the workflow continues.
Compilation and test procedure
Section titled “Compilation and test procedure”install the one stated dependency
Section titled “install the one stated dependency”$ pip install yara-python
sweep the lab corpus with the rule family from M6’s workflow
Section titled “sweep the lab corpus with the rule family from M6’s workflow”$ python3 yarasweep.py family_rules.yar lab_corpus/ —out yarasweep_out scanned=2140 unique=187 skipped=3 hit-files=11 hits per rule: fam_x_stage2 11 files fam_x_generic 2 files <— mis-scoped? review (M6) report: yarasweep_out/hits.csv
broken rules stop the sweep loud
Section titled “broken rules stop the sweep loud”$ echo ‘rule broken { condition: true and }’ > bad.yar $ python3 yarasweep.py bad.yar lab_corpus/; echo “exit=$?” fail-loud: rule compile error: … exit=1
feed the hits forward: BT1 cases per host, BT10 sightings per new IOC
Section titled “feed the hits forward: BT1 cases per host, BT10 sightings per new IOC”Detection considerations
Section titled “Detection considerations”The sweep is retro-detection — its value is measured in BT20’s IOC-to-coverage conversion: how many sweep hits became cases, how many rules got fleet-proven (the mis-scoping flag feeds M6’s maintenance loop directly). Operationally: run sweeps on new rule creation (does the rule match anything already on the estate — the answer is usually the most important fact about a new rule) and on rule retirement (confirm zero hits before deleting). The corpus itself needs curation: every confirmed-malicious sample joins the two-corpus testing set (M6’s discipline), and every false-positive sample joins the benign corpus — the sweep tool makes both curation passes one command.
Improvements to add
Section titled “Improvements to add”- Accept multiple rule files/directories and report per-file compile status for rule-set hygiene.
- Join with the PE parser from statictriage to enrich hit rows (entropy, section count) for triage-free context.
- Emit MISP-ready export of new IOCs from hits (hashes + rule provenance) for BT10’s sharing workflow.
- Add endpoint-export mode: sweep a fleet EDR’s “collected binaries” bundle for offline hit-triage.
- Track rule drift: re-run the same ruleset monthly and diff hit-sets to catch corpus decay.
Detection — Defender View
Section titled “Detection — Defender View”| Signal | Source | ID | Notes |
|---|---|---|---|
| Sample verdict latency | statictriage | per sample | Minutes target |
| Fleet sweep hit count | yarasweep | per rule | Retro-detection |
| Evasion indicators | Behavioral + E14 | per sample | Confidence gate |
| Drop + autorun chain | Sysmon | EID 1, 13 | Behavioral signature |
| C2 callbacks | Sysmon | EID 3, 22 | Config extraction join |
| Packed binary exec | statictriage + EDR | EID 1 | Entropy context |
| Macro dropper patterns | Windows Security | 4104 | BT14 escalation join |
| Rule hit → case rate | BT1 pipeline | per rule | Sweep value metric |
| IOC → coverage conversion | BT9/BT2 joins | quarterly | The program metric |
| Corpus freshness | Two-corpus gate | per cycle | M6 maintenance |
Evasion Techniques
Section titled “Evasion Techniques”Malware evades the analysis pipeline as deliberately as it evades EDR. Sandbox checks (E14’s catalog, weaponized): VM artifacts, MAC prefixes, registry keys, user-activity heuristics, sleep-past-timeout, mouse-movement checks — the counter is environmental realism plus the confidence downgrade on any “no activity” verdict. Delayed detonation: payload fires on second reboot, Nth launch, or a date trigger — long-window detonation (hours, not minutes) for high-value samples, and the reboot-and-wait discipline in the behavioral lab. Analyst-targeting: samples that detect analysis tooling names (procmon, x64dbg) and change behavior — the lab runs minimal tooling during capture, analyzes captures afterward. Environment fingerprinting: checking the corporate context (domain join, specific software) so the sample detonates in labs but runs in production — detonation environments mimic the estate’s software baseline (which is also why “your own telemetry joined to the detonation” matters: the callback your EDR saw is ground truth the sandbox can’t fake). Fragmented staging: the attachment is a downloader; the real payload arrives per-victim with per-target config — commodity-style verdicts mislead, and config extraction (M7/M15) is what links the fragments. Anti-YARA: rule-hashing (samples that read and mutate against known rule patterns), template rebuilds per campaign, and packer rotation — the counter is M6’s behavioral-anchor rule philosophy plus builder-level clustering (M15), not more byte patterns.
⚠️ What caught me: I stamped a sample “benign — no activity in 5-minute detonation” and moved on; the same hash showed up two weeks later at the center of an incident where it had quietly installed a scheduled task on the fourth boot — our detonation VM snapshotted once, ran once, and I’d called the absence of fireworks innocence. The fix cost me a weekend and taught me the module’s core rule: “no activity” is a finding about your sandbox, not about the sample — it now triggers the E14 evasion checklist before anyone types benign, and high-value samples get a 24-hour multi-reboot detonation window. The verdict vocabulary of a mature pipeline has no bare “benign” — only “benign, high confidence” and “inconclusive, detonating longer.”
Lab Practice
Section titled “Lab Practice”Build the analysis pipeline end-to-end in the lab. Triage: collect five samples (test files, self-packed UPX binaries, benign tools); run statictriage on each; verify the packed ones score high on entropy and the benign ones don’t — then hand-pack a benign binary with UPX and watch the entropy heuristic false-positive, which is the lesson. Detonate: stand up an isolated VM with snapshot discipline and a fake-services stack (dnsmasq + a Python HTTP catcher [verify setup]); detonate a benign “dropper” script you wrote that calls home; confirm the fake DNS/HTTP capture the callback. Evasion check: add a VM-artifact check to your dropper, watch it go quiet, then implement one E14 countermeasure and watch it talk. Sweep: write one M6-style YARA rule for the dropper and run yarasweep across the corpus — confirm it finds every copy you planted. Debrief: at which pipeline stage did the confidence change (not the verdict), and what does that tell you about where the SOC’s truth actually comes from?
Interview Questions
Section titled “Interview Questions”- Technical: Walk the four-stage depth ladder — triage, behavioral, deep, reconstruction — and name the service-level promise of each.
- Technical: Explain
statictriage’s confidence model — why does “no activity” in detonation raise the evasion flag instead of lowering the score? - Scenario: A packed binary detonates silently in your sandbox. Walk the next hour: the E14 checklist, the environmental changes, and the decision that “benign” requires.
- Scenario: A YARA fleet sweep hits 11 hosts mid-quarter. Walk the workflow: triage, case opening, and how this feeds BT10 and BT9 without duplicating effort.
- Conceptual: Why is hash-dedupe-first an integrity requirement rather than a performance optimization?
- Deep-dive: Design the malware analysis function for a 40-analyst SOC: intake, triage SLAs, the detonation environment’s evasion posture, deep-analysis prioritization, and the feedback loops into BT2/BT7/BT9/BT10.
- Practical: Your sample queue is 900 deep and aging. What do you automate, dedupe, or decline — and what do you tell the SOC about what “done” means for a sample?
Related Modules
Section titled “Related Modules”BT1 (SOC Operations & Alert Triage) · BT4 (Incident Response Process) · BT10 (Threat Intelligence Operations) · M6 (YARA Rule Engineering) · E14 (Virtual Environment Detection)