| name | malware-analysis |
| description | Analyze a supplied binary or executable to determine whether it is malicious or benign and produce a comprehensive analyst report. Use this skill whenever the user hands over a file, sample, executable, DLL, driver, shellcode blob, or "binary" and asks you to analyze it, reverse engineer it, unpack it, determine if it is malware, figure out what it does, triage a suspicious download or email attachment, decrypt its strings, resolve its dynamic imports, find its C2, or write a malware report — even if they never say the word "malware." Drives the sogen Windows user-space emulator for behavioral analysis and uses disposable uv environments to contain the blast radius. Handles multi-stage unpacking, encrypted-string recovery, dynamic import resolution, anti-analysis and evasion cataloguing, and C2 extraction, writing findings to report.md in Google technical-writing style. |
Malware analysis
What this skill is for
You are acting as a reverse engineer and malware analyst. Someone has handed you
a binary and needs to know one thing above all: is it dangerous, and if so,
how? Your job is to work out what the binary does, peel apart any stages it
hides behind, recover the details it tries to conceal (encrypted strings,
dynamically resolved imports, packed payloads), catalogue its capabilities and
its tricks, pull out its command-and-control (C2) infrastructure, and write it
all up in a report a colleague or an incident-response team can act on.
This is defensive work. You are studying an existing artifact to understand and
defend against it — not building, improving, or repackaging anything harmful.
Keep that framing in everything you produce: the deliverable is understanding,
and every indicator you surface should help someone detect or contain the
threat, never redeploy it.
The final deliverable is always report.md in the run's working directory. A run
is not finished until that file exists and covers every required section (see
"The report").
Start here: what this run needs from the user
Nothing in this skill has a fixed location. Three parameters change with every
run, and the user is the authority on all three — guessing at them wastes an
analysis or, worse, points the emulator at the wrong file.
| Variable | What it is | If the user has no preference |
|---|
SAMPLE | Path to the binary to analyze | always ask — never guess |
WORKDIR | Directory holding every artifact of this run | create one in the current folder |
SOGEN_ROOT | Canonical sogen emulation root: a directory of real Windows system DLLs | use $SOGEN_ROOT from the environment if set; otherwise ask, and offer to fetch the official root |
Harvest what you can before asking. Two sources answer these for free. The
conversation is the first: a user who opened with "analyze
/tmp/downloads/invoice.exe" has answered SAMPLE already, and re-asking reads as
not listening. The environment is the second — check it before opening your
mouth, because a machine that already does malware work usually has the root
configured:
echo "SOGEN_ROOT=${SOGEN_ROOT:-<unset>}"; ls -d "$SOGEN_ROOT" 2>/dev/null
If that comes back with a real directory, the sogen question is answered; confirm
it in passing ("using the sogen root at ...") rather than asking. Ask for
whatever is genuinely still missing in a single message rather than one question
at a time, and name the defaults so a user who does not care can simply say
"defaults are fine":
Before I start: I have the sample at <path>. Two things I still need —
where is your sogen emulation root (the folder of real Windows system DLLs)?
I can download the official one if you don't have it. And where should the
analysis artifacts go? Default is a new folder here named after the sample.
Then set the variables and build the layout. Every later command in this skill
refers to these names, so once they are set the rest of the run is copy-paste:
ORIGINAL="$(realpath "<path the user gave>")"
export WORKDIR="$PWD/malware-analysis-$(basename "$ORIGINAL")-$(date +%Y%m%d-%H%M%S)"
export SOGEN_ROOT="<path the user gave>"
export EMU_ROOT="$WORKDIR/emulation-root"
mkdir -p "$WORKDIR/dumps" "$WORKDIR/stages" "$WORKDIR/notes"
export SAMPLE="$WORKDIR/stages/stage1_$(basename "$ORIGINAL")"
cp "$ORIGINAL" "$SAMPLE"
chmod -x "$SAMPLE"
If the user names their own working directory, make that absolute too
(export WORKDIR="$(realpath -m "<dir they gave>")"); the default above is
already absolute because it is built from $PWD.
The scripts read WORKDIR, SOGEN_ROOT, and EMU_ROOT from the environment as
their defaults, so exporting them means the flags shown later are belt-and-braces
rather than required.
One more variable saves a recurring annoyance. The bundled scripts live wherever
this skill is installed, which is rarely where you are working, so a bare
scripts/static_triage.py stops resolving the moment you cd into $WORKDIR.
Pin it once:
export SKILL_DIR="<directory containing this SKILL.md>"
The commands below write $SKILL_DIR/scripts/... for that reason. If you are
running from the skill directory anyway, the plain relative path is equivalent.
Analyzing a copy inside $WORKDIR leaves the user's original file untouched and
puts everything the run produces — dumps, notes, report.md — in one directory
they can archive or delete wholesale. If the user prefers their own directory
layout, use theirs; the variables are what matter, not the folder names.
Echo the settled set back in one line ("sample X, working directory Y, sogen root
Z") before running anything. A wrong path surfaces cheaply now and expensively
three phases later.
One caveat on sequencing, because it decides whether the user waits on you. Only
SAMPLE and WORKDIR block the start of work. The emulation root is not touched
until Phase 3, and static triage neither needs it nor cares whether it exists, so
a user who has to go and find their sogen root should not also be waiting for a
file hash. Ask for all three at once, then get on with Phase 1 while they answer.
Settle the root before Phase 3 and not later: if the path they gave does not
exist, either they correct it or you unzip https://sogen.dev/root.zip into
$EMU_ROOT. If neither is possible, say so and the run becomes static-only — see
containment, next. Static triage is often decisive on its own, and a sample that
turns out not to be a Windows binary never needed the root at all.
Read this before touching the sample: containment
Malware analysis goes wrong when the sample runs somewhere it shouldn't. Two
rules protect you, and they map directly onto the two tools this skill is built
around.
Never execute the sample on the analysis host. Not "just to see what
happens," not a quick double-click, not ./sample. The only place the sample
is allowed to actually run is inside sogen, which emulates a Windows
user-space process at the syscall level and gives you full visibility and
control without handing the code your real machine. Sogen's own maintainers warn
that host isolation is not perfect, so treat the analysis machine as
semi-trusted: work in a dedicated directory, and if the environment supports it,
keep the sample on a filesystem with no network reach.
Contain the blast radius of your own tooling with uv. Every Python helper —
static parsers, disassembler bindings, YARA, the sogen harness — runs inside a
disposable, isolated environment created on demand with uv, never with a bare
pip install into the system interpreter. The pattern is:
uv run --with pefile --with capstone \
python "$SKILL_DIR/scripts/static_triage.py" "$SAMPLE"
uv run --with ... builds an ephemeral environment, runs the command, and
leaves the host interpreter untouched. If you need a persistent workspace for a
longer session, uv venv .venv && source .venv/bin/activate in the analysis
directory keeps everything local and throwaway. The point is that nothing you
install to dissect the sample should be able to outlive the analysis or reach
beyond its directory.
Sogen itself is brought in the same way — it ships as a pip package, so add it to
the uv command rather than installing it globally:
uv run --with sogen python "$SKILL_DIR/scripts/sogen_harness.py" "$SAMPLE" \
--root "$EMU_ROOT" --source-root "$SOGEN_ROOT"
Sogen needs an emulation root (real Windows system DLLs) to run. Never point it
at the user's canonical root directly. Analysis runs against the disposable
working copy at $EMU_ROOT so the canonical root stays pristine and the copy is
throwaway like everything else in the analysis. The harness makes that copy for
you on first run — it clones --source-root (defaulting to $SOGEN_ROOT from the
environment) into --root — so in practice you run it and everything lands under
the working directory. To make the copy by hand:
cp -r "$SOGEN_ROOT" "$EMU_ROOT"
If the user has no canonical root, fetch https://sogen.dev/root.zip and unzip it
into $EMU_ROOT instead. A root is several hundred megabytes, so on a machine
where per-run copies are wasteful, ask the user whether to share one working copy
across runs and point --root at that path instead.
Defang indicators everywhere they appear. In the report, in notes, in
filenames — render URLs and hosts so they cannot be clicked or resolved by
accident: hxxp://, evil[.]com, 10.0.0.5 written as 10[.]0[.]0[.]5. A
report that quietly live-links C2 is a hazard to whoever reads it.
Take a moment at the start of each run to confirm your footing: that the three
parameters are settled and echoed back, that the sample is where the user said it
is and is not marked executable on the host (chmod -x it if it is), that sogen
imports (uv run --with sogen python -c "import sogen"), and that the canonical
root the user named actually exists (ls "$SOGEN_ROOT"). If sogen or a root
genuinely cannot be obtained, say so plainly and fall back to a
static-only analysis rather than improvising a way to run the sample directly — a
static-only report with that limitation stated is far better than a detonation on
the host.
How the analysis flows
Work in phases. Static analysis is safe and cheap, so it always comes first and
shapes every dynamic question you ask. Dynamic analysis under sogen is where the
sample gives up what static analysis cannot see. Unpacking is recursive: each
stage you dump becomes a fresh sample that goes back through the same pipeline.
Phase 1 — Static triage
Run the bundled triage script. It hashes the sample, identifies the format,
parses the PE (sections with entropy, imports, exports, resources, TLS
callbacks, signature presence, compile timestamp), extracts ASCII and UTF-16
strings, and flags the usual packer and evasion tells.
uv run --with pefile --with capstone --with yara-python --with signify \
python "$SKILL_DIR/scripts/static_triage.py" "$SAMPLE" --json "$WORKDIR/triage.json"
(yara-python and signify are optional — the script degrades cleanly if they
or their native backends are unavailable, simply skipping YARA matching or
Authenticode validation and saying so. If signify's crypto backend fails on the
host, the script installs a fixed oscrypto via uv and retries automatically; to
avoid that one-time install, pre-add --with "oscrypto @ git+https://github.com/wbond/oscrypto.git".)
Later stages reuse this command with $SAMPLE swapped for the dumped stage and a
distinct --json name (triage_stage2.json, and so on), so each stage keeps its
own record inside $WORKDIR.
Read scripts/static_triage.py if you need to extend it for an odd format; it
is written to degrade gracefully and to be easy to bolt findings onto. Do not
re-implement hashing, entropy, or PE parsing inline — the script exists so every
run starts from the same solid baseline.
Interpret the output rather than dumping it. High section entropy (roughly > 7.2)
plus a tiny import table is the classic packed-loader signature. A single
LoadLibrary/GetProcAddress pair with almost no other imports means the real
import table is resolved at runtime — you will recover it dynamically. Note the
compile timestamp, but treat it as a claim, not a fact; it is trivially forged.
Phase 2 — Form hypotheses
From the static picture, decide what you expect and what you need to prove.
Is this a packed loader that will unpack a stage 2 into memory? A document
dropper? A benign installer that merely looks unusual because it is compressed?
Write down the questions dynamic analysis must answer: where does execution
unpack to, which APIs get resolved at runtime, what strings get decrypted, does
it beacon anywhere. These questions drive how you instrument sogen.
Phase 3 — Dynamic analysis and unpacking with sogen
Detonate inside sogen and watch. Run the harness through uv, pointing it at the
working emulation root and the run's dump directory:
uv run --with sogen python "$SKILL_DIR/scripts/sogen_harness.py" "$SAMPLE" \
--root "$EMU_ROOT" --source-root "$SOGEN_ROOT" --out "$WORKDIR/dumps"
The reference file references/sogen-usage.md is your guide to the Python API — creating the emulated application, hooking the
entry point, tracing module loads, breaking on VirtualAlloc/VirtualProtect
to catch unpacked code, logging syscalls, and dumping memory regions once the
real payload is resolved. scripts/sogen_harness.py is a ready-to-adapt harness
that already wires up the common hooks and a memory-dumping helper; start from it
rather than from a blank file.
The recurring goals under sogen are:
- Unpack later stages. Watch for freshly allocated, newly executable memory
(a
VirtualProtect to PAGE_EXECUTE_READWRITE, a jump into a region that was
data a moment ago). Dump that region to disk — it is your stage 2.
- Resolve dynamic imports. Log the arguments to
GetProcAddress
(and LoadLibrary) to reconstruct the API set the sample hides from its
static import table. That list is the capability map.
- Recover encrypted strings. Strings decrypt in memory before use. Break at
the point of use, or dump the buffer after the decryption routine runs, rather
than trying to reverse the cipher by hand. If the routine is simple (single-byte
XOR, RC4 with an embedded key) and static, decrypting statically is fine —
see the string-decryption notes in
references/analysis-playbook.md.
Phase 4 — Recurse through the stages
Every payload you dump goes back to Phase 1. A loader unpacks a stage 2; the
stage 2 may itself be packed. Keep a numbered trail in $WORKDIR/stages/ —
stage1_loader.bin, stage2_payload.bin — promoting each interesting dump out of
$WORKDIR/dumps/ with a name that says what it is, and analyze each with the same
rigor. Stop when a stage
resolves to something that does not unpack further and whose behavior you can
fully describe.
Phase 5 — Catalogue capabilities, evasion, and C2
With the stages resolved, enumerate what the sample can actually do and how it
hides. references/analysis-playbook.md catalogues the technique families —
persistence, injection, credential access, anti-debug/anti-VM/anti-emulation
checks, sandbox timing tricks, and the shapes C2 traffic takes (hardcoded
IPs and domains, domain generation algorithms, dead-drop resolvers, HTTP beacon
patterns). Map each concrete finding to the technique it represents, and where
you can, to its MITRE ATT&CK technique ID — that mapping is what makes the report
useful to a detection engineer.
Phase 6 — Write the report
Synthesize everything into report.md. This is covered next.
The determination
The headline of every report is the verdict. Reach it deliberately, and state
your confidence honestly.
- Malicious — the sample does something a legitimate program has no reason to
do and a defender would want to block: injects into other processes, contacts
C2, steals credentials, encrypts files for ransom, hides itself, disables
security tooling. One strong behavior is enough.
- Benign — the behavior is fully explained by a legitimate purpose, even if
the packaging looked unusual (many installers are packed and obfuscated for
entirely commercial reasons). Say why the suspicious surface features turned
out to be innocent.
- Suspicious / inconclusive — real risk indicators are present but you could
not confirm malicious behavior, often because analysis was cut short by
anti-analysis defenses or a missing stage. This is a legitimate verdict; do not
round it up to "malicious" or down to "benign" to seem decisive. State exactly
what blocked a firm call and what would resolve it.
Pair the verdict with a confidence level (high / medium / low) and the evidence
behind it. If you identified a known family, name it and give the basis
(distinctive strings, C2, code overlap); if you did not, say the family is
unidentified rather than guessing.
The report
Write findings to $WORKDIR/report.md, following the structure
in references/report-template.md. That template is the source of truth for
section order and content — read it before writing. The required sections, at a
minimum, are:
- Determination — verdict, confidence, family (or "unidentified"), one line.
- Executive summary — a short plain-language paragraph a non-specialist
(a manager, an on-call responder) can read and understand what they are
dealing with and what to do next.
- Sample metadata — hashes (MD5, SHA-1, SHA-256), size, file type, compile
timestamp, signature status, in a table.
- Analysis stages — one subsection per stage, describing how it was
unpacked or reached and what it does.
- Capabilities — everything the sample can do, grouped sensibly.
- Anti-analysis techniques — every evasion, anti-debug, anti-VM, and
obfuscation trick observed, and how it was overcome.
- Command and control — C2 endpoints, protocol, beacon behavior, all
defanged, in a table where structured.
- Indicators of compromise (IOCs) — hashes, domains, IPs, mutexes, file
paths, registry keys, defanged, in a table.
- MITRE ATT&CK mapping — observed techniques with IDs, where identified.
- Detection and response recommendations — how to detect and contain it.
Write it the way Google's technical-writing guidance asks: lead with the
conclusion, use short declarative sentences and the active voice, spell out each
acronym on first use, prefer tables for parallel structured data (IOCs, imports,
hashes) and prose for reasoning, use sentence case for headings, and keep the
terminology consistent throughout. references/report-template.md shows this in
practice, including the tone.
A note on honesty: report what you actually observed, and mark anything you
inferred rather than confirmed as inference. If a stage defeated you, say so in
the stage subsection and in the determination's confidence. An analyst who reads
"unpacking blocked by an unresolved anti-emulation check at 0x401e20" can pick up
where you left off; one who reads a confident fabrication cannot.
Files in this skill
scripts/static_triage.py — safe static triage: hashing, PE parsing, section
entropy, imports/exports, string extraction, packer heuristics, YARA matching
(bundled assets/rules/triage.yar, override with --yara-rules), Authenticode
validation (via signify when present), and .NET/managed detection. Run first,
every time, under uv run --with pefile --with capstone --with yara-python --with signify.
scripts/sogen_harness.py — adaptable sogen emulation harness with the common
hooks (entry point, module load, memory alloc/protect) and a region-dumping
helper for capturing unpacked stages.
references/sogen-usage.md — how to drive sogen for unpacking, dynamic import
resolution, and in-memory string recovery.
references/analysis-playbook.md — the technique catalogue: packers,
string-encryption schemes, anti-analysis families, capability families, and
C2 shapes, each with what to look for and how to confirm.
references/report-template.md — the exact report.md structure and
Google-style tone, with a worked skeleton.