reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
---
|
||||
name: malware-dynamic-analysis
|
||||
description: Behavioral analysis of a sample executed in an isolated VM. Use after triage when runtime behavior, C2 traffic, dropped files, persistence, or injection must be observed. Claude produces a tailored VM runbook from triage predictions, then parses the exported text evidence (Procmon CSV, Sysmon JSON/CSV, tshark output, autoruns, strings) on the host to reconstruct behavior and extract IOCs. The analyst runs the VM; Claude never executes the sample.
|
||||
---
|
||||
|
||||
# Malware Dynamic Analysis
|
||||
|
||||
Two halves. **Part A** is a runbook the analyst follows inside the isolated VM (REMnux / FlareVM); you tailor it from triage predictions and it ends with text-format evidence exports. **Part B** is your work: parse that evidence on the host, reconstruct what the sample did, extract behavioral IOCs, and write findings to `analysis_state.md`.
|
||||
|
||||
## Execution Model
|
||||
|
||||
- **You never execute the sample.** The VM is network-isolated and has no Claude in it. Everything you run is on the host against exported evidence files.
|
||||
- **Locate skill files.** Scripts and reference files ship in this skill's directory. Set `R="${CLAUDE_PLUGIN_ROOT:-<dir containing this SKILL.md>}"` once (when installed as a plugin `$CLAUDE_PLUGIN_ROOT` is set; otherwise it is this skill folder). Your working directory is the user's analysis workspace, so prefix every script path below with `$R`, e.g. `python3 "$R"/scripts/ioc_extract.py`.
|
||||
- **Before the VM run:** read the triage findings in `analysis_state.md` and emit a runbook tailored to the predictions (which process names to filter, which Sysmon event IDs matter, expected protocol, expected persistence, expected evasion). Skip the tailoring only if triage is missing — then use the default runbook as-is.
|
||||
- **After the VM run:** the user hands you an evidence directory. Inventory it, reject binary formats with the conversion command (see **Converting Binary Evidence**), and then work through Part B with Bash and the bundled scripts. Do not ask the user to summarize the evidence for you.
|
||||
- **Scripts** (relative to this skill dir): `scripts/procmon_summary.py`, `scripts/sysmon_summary.py`; plus `../scripts/ioc_extract.py` at repo root. All stdlib Python 3.
|
||||
- **Sandbox reports count as evidence.** ANY.RUN / Joe Sandbox / Hybrid Analysis / Threat.Zone (MCP) JSON or text reports go through Part B Step 6.
|
||||
- **Large files:** `wc -l` first. Use the summary scripts and `grep`, never `cat` a 200k-line Procmon CSV.
|
||||
|
||||
---
|
||||
|
||||
## Part A — VM Runbook (analyst executes this)
|
||||
|
||||
Emit this to the user, filled in with the sample name and triage predictions. Keep the safety checklist verbatim.
|
||||
|
||||
### A1. Safety checklist — all boxes or do not execute
|
||||
|
||||
- [ ] Clean VM snapshot taken
|
||||
- [ ] Network: host-only / internal, with INetSim or FakeNet-NG answering (never NAT to the real internet unless the engagement explicitly allows it)
|
||||
- [ ] No shared folders, no clipboard sharing, no drag-and-drop
|
||||
- [ ] Guest time sync disabled
|
||||
- [ ] Monitoring tools started **before** execution (order below)
|
||||
- [ ] Analysis persona active — no real credentials, names, or corporate artifacts on the VM
|
||||
- [ ] Evidence transfer path decided (USB / one-way share) and a `C:\evidence\` directory created
|
||||
|
||||
Full VM hardening and anti-VM-detection countermeasures: `references/sandbox_setup.md`, `references/anti_analysis_bypass.md`.
|
||||
|
||||
### A2. Start monitoring (in this order)
|
||||
|
||||
1. INetSim / FakeNet-NG (network simulation)
|
||||
2. Sysmon — already installed with `olafhartong/sysmon-modular` or SwiftOnSecurity config
|
||||
3. Procmon — clear capture (Ctrl+X), apply filters (below)
|
||||
4. Wireshark — capture on the VM adapter
|
||||
5. System Informer
|
||||
6. Regshot 1st shot (optional) / `autorunsc` baseline (below)
|
||||
|
||||
Tool configuration details: `references/tool_setup.md`.
|
||||
|
||||
**Procmon filters** (tailor from triage — add predicted child processes and LOLBins):
|
||||
```
|
||||
Process Name is <sample.exe> Include
|
||||
Process Name is <predicted child, e.g. powershell.exe, rundll32.exe> Include
|
||||
Operation contains Process Include (keeps Process Create for the tree)
|
||||
Process Name is procmon.exe / SystemInformer.exe / wireshark.exe Exclude
|
||||
```
|
||||
Do **not** filter to the sample alone if injection is predicted — include the predicted target (`explorer.exe`, `svchost.exe`, `RegAsm.exe`, …).
|
||||
|
||||
**Baseline persistence snapshot** (Sysinternals Autoruns, run as admin):
|
||||
```powershell
|
||||
autorunsc64.exe -accepteula -a * -c -h -s -nobanner | Out-File -Encoding utf8 C:\evidence\autoruns_before.csv
|
||||
```
|
||||
|
||||
### A3. Execute
|
||||
|
||||
```powershell
|
||||
# EXE
|
||||
.\sample.exe # add args if triage found a required switch, e.g. /install /silent
|
||||
# DLL — triage lists the exports; try DllMain first, then each named export
|
||||
rundll32.exe sample.dll,DllMain
|
||||
rundll32.exe sample.dll,<Export>
|
||||
# Scripts
|
||||
powershell.exe -ExecutionPolicy Bypass -File sample.ps1
|
||||
cscript.exe //NoLogo sample.vbs
|
||||
wscript.exe sample.js
|
||||
mshta.exe sample.hta
|
||||
```
|
||||
|
||||
Observe **15 minutes minimum**; 60+ if triage found sleep/time checks. Interact where the sample expects it (click the decoy, dismiss dialogs). Note the wall-clock time of execution — it anchors every timeline.
|
||||
|
||||
Watch for: new processes, network attempts (INetSim log), files in `%TEMP%`, `%APPDATA%`, `C:\ProgramData`, `C:\Users\Public`, registry Run keys, new services/tasks, UI changes (ransom note, fake error), CPU spikes.
|
||||
|
||||
If nothing happens: see `references/anti_analysis_bypass.md` (VM checks, sleep patching, required arguments, missing parent process, locale/keyboard checks).
|
||||
|
||||
### A4. Export evidence — text formats only
|
||||
|
||||
Run these before reverting. Every file lands in `C:\evidence\` (or `/evidence` on REMnux) and must be text.
|
||||
|
||||
| Source | Command / action | File |
|
||||
|--------|------------------|------|
|
||||
| Procmon | File → Save → **All events** → **CSV** (or `procmon.exe /OpenLog cap.pml /SaveAs procmon.csv`) | `procmon.csv` |
|
||||
| Sysmon | `Get-WinEvent -FilterHashtable @{LogName='Microsoft-Windows-Sysmon/Operational'} \| Select-Object Id,TimeCreated,Message \| ConvertTo-Json -Depth 3 \| Out-File -Encoding utf8 C:\evidence\sysmon.json` | `sysmon.json` |
|
||||
| Wireshark → tshark | see block below | `dns.txt`, `http.txt`, `conns.txt`, `tls.txt`, `syn.txt` |
|
||||
| Wireshark objects | File → Export Objects → HTTP → save all | `http_objects/` |
|
||||
| INetSim | copy `/var/log/inetsim/service.log` and `report/` | `inetsim_service.log` |
|
||||
| System Informer | File → Save → `processes.txt`; for suspicious process: right-click → Create dump file | `processes.txt`, `<proc>.dmp` |
|
||||
| Memory dump strings | `strings -a -n 8 -e l proc.dmp > proc_strings_wide.txt; strings -a -n 8 proc.dmp > proc_strings.txt` (REMnux `strings`, or FLOSS) | `*_strings*.txt` |
|
||||
| Persistence | `autorunsc64.exe -accepteula -a * -c -h -s -nobanner \| Out-File -Encoding utf8 C:\evidence\autoruns_after.csv` | `autoruns_after.csv` |
|
||||
| Scheduled tasks / services / WMI | PowerShell block below | `persistence.txt` |
|
||||
| Regshot | Compare → output **TXT** | `regshot.txt` |
|
||||
| Dropped files | PowerShell block below; copy the files themselves into `dropped/` (they are new samples) | `dropped_hashes.csv`, `dropped/` |
|
||||
| Screenshots | ransom notes, dialogs, anything visual | `screenshots/*.png` |
|
||||
|
||||
**tshark exports** (run on REMnux against the saved `capture.pcapng`, or in the FlareVM Wireshark install dir):
|
||||
```bash
|
||||
P=capture.pcapng
|
||||
tshark -r $P -Y "dns.flags.response==0" -T fields -e frame.time -e ip.src -e dns.qry.name > dns.txt
|
||||
tshark -r $P -Y "http.request" -T fields -e frame.time -e ip.dst -e http.host -e http.request.method -e http.request.uri -e http.user_agent > http.txt
|
||||
tshark -r $P -Y "tls.handshake.type==1" -T fields -e frame.time -e ip.dst -e tcp.dstport -e tls.handshake.extensions_server_name -e tls.handshake.ja3 > tls.txt
|
||||
tshark -r $P -q -z conv,tcp -z conv,udp > conns.txt
|
||||
tshark -r $P -Y "tcp.flags.syn==1 && tcp.flags.ack==0" -T fields -e frame.time_epoch -e ip.dst -e tcp.dstport > syn.txt
|
||||
tshark -r $P -Y "http.request.method==POST" -T fields -e ip.dst -e http.request.uri -e http.content_length -e http.file_data > http_post.txt
|
||||
```
|
||||
|
||||
**Persistence and dropped files** (PowerShell, after execution):
|
||||
```powershell
|
||||
$E = "C:\evidence"
|
||||
Get-ScheduledTask | ? {$_.TaskPath -notlike "\Microsoft*"} | Select TaskName,TaskPath,State,@{n='Action';e={$_.Actions.Execute + ' ' + $_.Actions.Arguments}} | Format-List | Out-File -Encoding utf8 "$E\persistence.txt"
|
||||
Get-CimInstance Win32_Service | ? {$_.PathName -notlike "C:\Windows\*"} | Select Name,PathName,StartMode,State | Format-List | Out-File -Encoding utf8 "$E\persistence.txt" -Append
|
||||
Get-CimInstance -Namespace root\Subscription -Class __FilterToConsumerBinding | Format-List | Out-File -Encoding utf8 "$E\persistence.txt" -Append
|
||||
foreach ($k in 'HKCU','HKLM') { Get-ItemProperty "${k}:\Software\Microsoft\Windows\CurrentVersion\Run*" | Out-File -Encoding utf8 "$E\persistence.txt" -Append }
|
||||
|
||||
$t = (Get-Date).AddMinutes(-90) # adjust to execution time
|
||||
mkdir "$E\dropped" -Force | Out-Null
|
||||
Get-ChildItem -Path $env:TEMP,$env:APPDATA,$env:LOCALAPPDATA,C:\ProgramData,C:\Users\Public,C:\Windows\Temp -Recurse -File -Force -ErrorAction SilentlyContinue |
|
||||
? {$_.CreationTime -gt $t} | % { Copy-Item $_.FullName "$E\dropped\" -Force -ErrorAction SilentlyContinue; $_ } |
|
||||
Get-FileHash -Algorithm SHA256 | Export-Csv "$E\dropped_hashes.csv" -NoTypeInformation
|
||||
```
|
||||
|
||||
Then: transfer `C:\evidence\` to the host, **revert the snapshot**, verify the revert.
|
||||
|
||||
### Converting Binary Evidence
|
||||
|
||||
If the user brings PML / PCAP(NG) / EVTX instead, reply with the conversion command and wait:
|
||||
|
||||
| Got | Convert with |
|
||||
|-----|--------------|
|
||||
| `.pml` | Windows VM: `procmon.exe /OpenLog x.pml /SaveAs x.csv` |
|
||||
| `.pcap` / `.pcapng` | The tshark block in A4 (tshark exists on REMnux and on any host with Wireshark) — you can run this yourself on the host if tshark is installed: `command -v tshark` |
|
||||
| `.evtx` | Windows: `Get-WinEvent -Path sysmon.evtx \| Select Id,TimeCreated,Message \| ConvertTo-Json -Depth 3 \| Out-File -Encoding utf8 sysmon.json`. Host/REMnux: `evtx_dump --format jsonl sysmon.evtx > sysmon.jsonl` (`pip install evtx`) — `sysmon_summary.py` reads both |
|
||||
| `.dmp` | `strings -a -n 8 x.dmp > x_strings.txt; strings -a -n 8 -e l x.dmp > x_strings_wide.txt` — run on the host, it is not executable |
|
||||
|
||||
PCAP and memory dumps are safe to handle on the host; the PE files in `dropped/` are new samples and go back through triage.
|
||||
|
||||
---
|
||||
|
||||
## Part B — Evidence Analysis (you do this)
|
||||
|
||||
### B0. Inventory
|
||||
|
||||
```bash
|
||||
E=/path/to/evidence
|
||||
ls -la "$E"; file "$E"/* | grep -v ':.*text' ; wc -l "$E"/*.csv "$E"/*.txt "$E"/*.json 2>/dev/null
|
||||
head -2 "$E/procmon.csv" # confirm columns: Time of Day, Process Name, PID, Operation, Path, Result, Detail
|
||||
```
|
||||
|
||||
Anything that is not text → **Converting Binary Evidence**. Note the sample's process name(s) from triage/state; you will scope everything on them.
|
||||
|
||||
### B1. Process behavior — Procmon
|
||||
|
||||
```bash
|
||||
python3 malware-dynamic-analysis/scripts/procmon_summary.py "$E/procmon.csv" --process sample.exe > "$E/procmon_summary.txt"
|
||||
wc -l "$E/procmon_summary.txt"; sed -n 1,80p "$E/procmon_summary.txt"
|
||||
```
|
||||
|
||||
The summary gives: process tree with command lines, files created/written/renamed/deleted, registry writes with persistence keys flagged, network endpoints, DLLs loaded from outside `C:\Windows`, event counts. Read it fully (page with `sed -n`). Then drill into specifics straight from the CSV:
|
||||
|
||||
```bash
|
||||
# everything one child did
|
||||
grep -F '"powershell.exe"' "$E/procmon.csv" | cut -d, -f2,4,5 | sort | uniq -c | sort -rn | head -40
|
||||
# a specific path or key
|
||||
grep -iF 'CurrentVersion\Run' "$E/procmon.csv" | head
|
||||
# timeline of process starts (first-seen order)
|
||||
grep -F '"Process Start"' "$E/procmon.csv" | cut -d, -f1,2,3,7 | head -40
|
||||
```
|
||||
|
||||
Interpret against the triage predictions: confirmed, refuted, or unexpected. Injection shows up as the sample writing to another process's address space only indirectly in Procmon — cross-check with Sysmon EID 8/10 in B2. Self-deletion (`SetDispositionInformationFile` on its own path, or `cmd /c del`), decoy launches, and LOLBin chains (`rundll32`, `regsvr32`, `mshta`, `wscript`, `msiexec`, `certutil`, `bitsadmin`) are the usual tells.
|
||||
|
||||
### B2. Process behavior — Sysmon
|
||||
|
||||
```bash
|
||||
python3 malware-dynamic-analysis/scripts/sysmon_summary.py "$E/sysmon.json" --process sample.exe > "$E/sysmon_summary.txt"
|
||||
sed -n 1,120p "$E/sysmon_summary.txt"
|
||||
```
|
||||
|
||||
Sections and what they prove:
|
||||
|
||||
| Section (EID) | Read it for |
|
||||
|---------------|-------------|
|
||||
| Process tree (1) | Authoritative parent/child + full command lines + hashes of every executed image |
|
||||
| Network (3) | Per-process destinations; `BEACON-LIKE` tag = regular intervals (coefficient of variation < 0.3 over ≥5 connections) |
|
||||
| DNS (22) | Queried names and resolved IPs, even when the connection failed |
|
||||
| Files (11) | Dropped files by process — the source of truth for `dropped/` |
|
||||
| Registry (12/13/14) | `PERSISTENCE` flags: Run/RunOnce, Services, Winlogon, IFEO, TaskCache, AppInit |
|
||||
| Remote threads (8) | `CreateRemoteThread` injection: source → target, start address |
|
||||
| Process access (10) | `OpenProcess` with high rights (`0x1F0FFF`, `0x1FFFFF`, `0x143A`) → injection prep, LSASS access → credential theft |
|
||||
| Image loads (7) | Unsigned DLLs from user-writable paths, sideloading |
|
||||
| Pipes (17/18) | Named pipes = Cobalt Strike / Meterpreter / custom IPC IOCs |
|
||||
| WMI (19/20/21) | WMI event subscription persistence |
|
||||
| Tampering (25) | Process hollowing / herpaderping |
|
||||
|
||||
Correlate B1 and B2: Procmon has more file/registry granularity, Sysmon has hashes, injection, DNS, and command lines. Where they disagree, trust Sysmon for process identity and Procmon for file operations.
|
||||
|
||||
### B3. Network
|
||||
|
||||
```bash
|
||||
cat "$E/dns.txt" | awk '{print $NF}' | sort | uniq -c | sort -rn | head -30
|
||||
cat "$E/http.txt" | head -40
|
||||
cat "$E/tls.txt" | awk -F'\t' '{print $2":"$3, $4, $5}' | sort -u
|
||||
sed -n 1,60p "$E/conns.txt"
|
||||
grep -v '^$' "$E/http_post.txt" | head -20
|
||||
grep -iE 'connect|request|GET|POST' "$E/inetsim_service.log" | head -40
|
||||
```
|
||||
|
||||
Beacon detection from `syn.txt` (interval regularity per destination):
|
||||
|
||||
```bash
|
||||
python3 - "$E/syn.txt" <<'EOF'
|
||||
import sys, statistics, collections
|
||||
by = collections.defaultdict(list)
|
||||
for line in open(sys.argv[1]):
|
||||
p = line.split()
|
||||
if len(p) == 3: by[(p[1], p[2])].append(float(p[0]))
|
||||
for (ip, port), ts in by.items():
|
||||
ts.sort(); gaps = [b-a for a, b in zip(ts, ts[1:])]
|
||||
if len(gaps) >= 4:
|
||||
m = statistics.mean(gaps); cv = statistics.pstdev(gaps)/m if m else 9
|
||||
print(f"{ip}:{port} n={len(ts)} mean={m:.1f}s cv={cv:.2f} {'BEACON' if cv < 0.3 else ''}")
|
||||
EOF
|
||||
```
|
||||
|
||||
What to establish: C2 hosts (DNS name → IP → port → protocol), beacon interval and jitter, User-Agent and URI pattern (Suricata material), TLS SNI/JA3, download URLs and what came back (`http_objects/` — `file` them; PE payloads go back to triage), exfiltration (POST sizes, encoding), DGA signs (many NXDOMAIN, random-looking labels), and whether traffic went to INetSim or the sample gave up (matters for what you can claim).
|
||||
|
||||
### B4. Persistence and system changes
|
||||
|
||||
```bash
|
||||
cat "$E/persistence.txt"
|
||||
# autoruns diff: entries present after but not before
|
||||
python3 - "$E/autoruns_before.csv" "$E/autoruns_after.csv" <<'EOF'
|
||||
import csv, sys
|
||||
key = lambda r: (r.get('Entry Location',''), r.get('Entry',''), r.get('Image Path',''))
|
||||
before = {key(r) for r in csv.DictReader(open(sys.argv[1], encoding='utf-8-sig', errors='replace'))}
|
||||
for r in csv.DictReader(open(sys.argv[2], encoding='utf-8-sig', errors='replace')):
|
||||
if key(r) not in before: print(r.get('Entry Location'), '|', r.get('Entry'), '|', r.get('Image Path'), '|', r.get('Signer',''))
|
||||
EOF
|
||||
grep -iE 'Run|Services|TaskCache|Winlogon' "$E/regshot.txt" | head -40
|
||||
```
|
||||
|
||||
Map each mechanism to ATT&CK (T1547.001 Run keys, T1053.005 scheduled task, T1543.003 service, T1546.003 WMI, T1574.002 sideloading, …) — the detection phase needs the technique IDs.
|
||||
|
||||
### B5. Dropped files and memory strings
|
||||
|
||||
```bash
|
||||
cat "$E/dropped_hashes.csv"
|
||||
file "$E"/dropped/*
|
||||
python3 scripts/ioc_extract.py "$E"/*_strings*.txt # C2, keys, paths decrypted only at runtime
|
||||
grep -iE 'user-agent|/gate|/panel|\.php|password|token|Bearer' "$E"/*_strings*.txt | sort -u | head -40
|
||||
```
|
||||
|
||||
Every PE in `dropped/` is a new sample: add it to `analysis_state.md` and route back through `malware-triage`. Runtime strings that were absent from the static strings dump are the decrypted config — call that out explicitly, it is the strongest YARA/Suricata material.
|
||||
|
||||
### B6. Sandbox reports (if provided)
|
||||
|
||||
ANY.RUN / Joe / Hybrid Analysis / Threat.Zone JSON: `jq` the process tree, network, dropped files, and MITRE sections; treat them as a fourth evidence source and reconcile with B1-B5. Sandbox results are weaker than your own VM run when the sample is VM-aware — say so if behaviors differ.
|
||||
|
||||
```bash
|
||||
jq -r '.. | .processes? // empty' report.json | head; jq -r '.. | .network? // empty' report.json | head
|
||||
```
|
||||
|
||||
### B7. Consolidate
|
||||
|
||||
```bash
|
||||
cat "$E/procmon_summary.txt" "$E/sysmon_summary.txt" "$E"/dns.txt "$E"/http.txt "$E"/tls.txt "$E"/persistence.txt 2>/dev/null | python3 scripts/ioc_extract.py > "$E/iocs_defanged.txt"
|
||||
```
|
||||
|
||||
Remove your own lab artifacts (VM hostname, INetSim IPs, analyst username) and Windows-internal noise from the IOC list before recording it.
|
||||
|
||||
## Output
|
||||
|
||||
Append to `analysis_state.md` under the sample's **Analysis Findings** and **IOCs Identified**, then print the findings summary:
|
||||
|
||||
```markdown
|
||||
## Dynamic Analysis — [sample] (executed [UTC time], observed [n] min, VM: [FlareVM/REMnux], network: [INetSim/FakeNet/none])
|
||||
|
||||
### Execution chain
|
||||
[process tree with command lines; injection targets; LOLBins; self-deletion]
|
||||
|
||||
### Predictions vs observed
|
||||
| Triage prediction | Observed | Evidence |
|
||||
|
||||
### File system
|
||||
Created / modified / deleted, with hashes for dropped PE files
|
||||
|
||||
### Registry & persistence
|
||||
Mechanism → key/task/service → payload path → ATT&CK ID
|
||||
|
||||
### Network
|
||||
C2: host → IP:port, protocol, beacon interval/jitter, UA, URI pattern, TLS SNI/JA3 · Downloads · Exfil · DGA
|
||||
|
||||
### Evasion observed
|
||||
Sleep, VM checks, debugger checks, tampering (EID 25), what was bypassed and how
|
||||
|
||||
### Behavioral IOCs (defanged)
|
||||
Process (names, cmdlines, mutexes, pipes) · File (paths, hashes) · Registry · Network
|
||||
|
||||
### MITRE ATT&CK
|
||||
Technique IDs with the observation that supports each
|
||||
|
||||
### Not observed / limitations
|
||||
[network to real C2 blocked; sample exited after VM check; only 15 min observed …]
|
||||
```
|
||||
|
||||
Recommend `detection-engineer` next (behaviors + network IOCs are ready) unless dropped payloads need triage first.
|
||||
|
||||
## Quality Gate
|
||||
|
||||
Evidence inventoried and all text · process tree with command lines · every prediction marked confirmed/refuted/unexpected · dropped files hashed and routed · persistence mapped to ATT&CK · C2 characterized (or explicitly "no network observed") · IOCs defanged and lab noise removed · limitations stated · state file updated.
|
||||
|
||||
## References
|
||||
|
||||
- `references/tool_setup.md` — Procmon filters/columns, Wireshark filters, System Informer, Sysmon install, Regshot, TCPView, Noriben, INetSim, FakeNet-NG
|
||||
- `references/sandbox_setup.md` — REMnux/FlareVM build, isolation, snapshots, CAPE/Cuckoo
|
||||
- `references/anti_analysis_bypass.md` — VM/debugger/sleep evasion and how to defeat it
|
||||
- `scripts/procmon_summary.py`, `scripts/sysmon_summary.py` — evidence condensers (`--self-test` to verify)
|
||||
- `../scripts/ioc_extract.py` — IOC extraction and defanging
|
||||
@@ -0,0 +1,667 @@
|
||||
# Anti-Analysis Bypass Guide
|
||||
|
||||
Reference for identifying and bypassing malware evasion techniques during dynamic analysis.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Anti-analysis techniques fall into four categories:
|
||||
|
||||
| Category | Goal | MITRE ATT&CK |
|
||||
|----------|------|--------------|
|
||||
| VM/sandbox detection | Avoid executing in automated analysis environments | T1497 |
|
||||
| Debugger detection | Prevent live debugging and breakpoint analysis | T1622 |
|
||||
| Timing-based evasion | Delay execution past sandbox timeout windows | T1497.003 |
|
||||
| Environmental keying | Execute only when specific conditions are met | T1480 |
|
||||
|
||||
---
|
||||
|
||||
## VM and Sandbox Detection
|
||||
|
||||
### How Malware Detects VMs
|
||||
|
||||
**Registry artifact checks (MITRE T1497.001):**
|
||||
|
||||
```
|
||||
HKLM\SOFTWARE\Oracle\VirtualBox Guest Additions
|
||||
HKLM\SOFTWARE\VMware, Inc.\VMware Tools
|
||||
HKLM\SYSTEM\CurrentControlSet\Services\VBoxGuest
|
||||
HKLM\SYSTEM\CurrentControlSet\Services\vmhgfs
|
||||
HKLM\HARDWARE\DEVICEMAP\Scsi\Scsi Port 0\Scsi Bus 0\Target Id 0\Logical Unit Id 0\Identifier
|
||||
→ "VBOX HARDDISK", "VMWARE VIRTUAL IDE HARD DRIVE"
|
||||
```
|
||||
|
||||
**Process enumeration:**
|
||||
|
||||
```
|
||||
vboxservice.exe VirtualBox guest service
|
||||
vboxtray.exe VirtualBox system tray
|
||||
vmtoolsd.exe VMware Tools daemon
|
||||
vmwaretray.exe VMware system tray
|
||||
vmacthlp.exe VMware activation helper
|
||||
vmsrvc.exe VirtualPC service
|
||||
df5serv.exe Parallels service
|
||||
prl_tools.exe Parallels Tools
|
||||
```
|
||||
|
||||
**Driver/device checks:**
|
||||
|
||||
```
|
||||
\\.\VBoxMiniRdrDN
|
||||
\\.\VBoxGuest
|
||||
\\.\vmci
|
||||
\\.\HGFS
|
||||
\Device\VBoxGuest
|
||||
\Device\vmhgfs
|
||||
```
|
||||
|
||||
**CPUID hypervisor bit (EAX=1, ECX bit 31):**
|
||||
|
||||
Malware executes CPUID and checks bit 31 of ECX. If set, a hypervisor is present. Subsequent CPUID with EAX=0x40000000 returns a vendor string: "VMwareVMware", "KVMKVMKVM", "VBoxVBoxVBox", "Microsoft Hv".
|
||||
|
||||
**MAC address OUI checks:**
|
||||
|
||||
```
|
||||
08:00:27 VirtualBox
|
||||
00:0C:29 VMware
|
||||
00:50:56 VMware (vSphere)
|
||||
00:1C:14 VMware
|
||||
00:05:69 VMware
|
||||
00:03:FF Microsoft Hyper-V
|
||||
```
|
||||
|
||||
**Screen resolution / user interaction:**
|
||||
|
||||
```
|
||||
Screen size exactly 1024x768 → sandbox default
|
||||
No mouse movement history
|
||||
No browser history / recent files
|
||||
Uptime < 10 minutes
|
||||
Single CPU core
|
||||
Total RAM < 2 GB
|
||||
```
|
||||
|
||||
**Identifying VM detection in static analysis:**
|
||||
|
||||
Look for these API calls in import tables or dynamic calls:
|
||||
|
||||
```
|
||||
GetSystemInfo → CPU count check
|
||||
GlobalMemoryStatusEx → RAM check
|
||||
EnumProcesses → process list scan
|
||||
CreateFile(\\.\<device>) → device existence check
|
||||
RegOpenKeyEx / RegQueryValueEx → registry artifact checks
|
||||
GetAdaptersInfo → MAC address check
|
||||
GetTickCount / GetSystemTime → uptime / time checks
|
||||
```
|
||||
|
||||
### Bypass Strategies
|
||||
|
||||
**Remove VirtualBox Guest Additions:**
|
||||
|
||||
```powershell
|
||||
# Uninstall via Control Panel or:
|
||||
Start-Process "C:\Program Files\Oracle\VirtualBox Guest Additions\uninstall.exe" -Wait
|
||||
|
||||
# Remove leftover registry keys
|
||||
Remove-Item "HKLM:\SOFTWARE\Oracle\VirtualBox Guest Additions" -Recurse -ErrorAction SilentlyContinue
|
||||
Remove-Item "HKLM:\SYSTEM\CurrentControlSet\Services\VBoxGuest" -Recurse -ErrorAction SilentlyContinue
|
||||
Remove-Item "HKLM:\SYSTEM\CurrentControlSet\Services\VBoxSF" -Recurse -ErrorAction SilentlyContinue
|
||||
Remove-Item "HKLM:\SYSTEM\CurrentControlSet\Services\VBoxMouse" -Recurse -ErrorAction SilentlyContinue
|
||||
|
||||
# Stop and delete VBox services
|
||||
sc.exe stop VBoxGuest
|
||||
sc.exe delete VBoxGuest
|
||||
```
|
||||
|
||||
**Spoof BIOS/DMI strings (VirtualBox, run on host):**
|
||||
|
||||
```bash
|
||||
VBoxManage setextradata "VM-Name" \
|
||||
"VBoxInternal/Devices/pcbios/0/Config/DmiBIOSVendor" "American Megatrends Inc."
|
||||
VBoxManage setextradata "VM-Name" \
|
||||
"VBoxInternal/Devices/pcbios/0/Config/DmiBIOSVersion" "F10"
|
||||
VBoxManage setextradata "VM-Name" \
|
||||
"VBoxInternal/Devices/pcbios/0/Config/DmiSystemProduct" "MS-7B86"
|
||||
VBoxManage setextradata "VM-Name" \
|
||||
"VBoxInternal/Devices/pcbios/0/Config/DmiSystemVendor" "MSI"
|
||||
VBoxManage setextradata "VM-Name" \
|
||||
"VBoxInternal/Devices/pcbios/0/Config/DmiBoardProduct" "MAG Z390 TOMAHAWK"
|
||||
VBoxManage setextradata "VM-Name" \
|
||||
"VBoxInternal/Devices/pcbios/0/Config/DmiChassisVendor" "MSI"
|
||||
```
|
||||
|
||||
**Suppress CPUID hypervisor bit (VirtualBox):**
|
||||
|
||||
```bash
|
||||
VBoxManage modifyvm "VM-Name" --paravirtprovider none
|
||||
```
|
||||
|
||||
**VMware: suppress CPUID and hypervisor detection:**
|
||||
|
||||
Add to the `.vmx` file:
|
||||
|
||||
```
|
||||
hypervisor.cpuid.v0 = "FALSE"
|
||||
SMBIOS.reflectHost = "TRUE"
|
||||
board.vendor = "MSI"
|
||||
board.product = "MAG Z390 TOMAHAWK"
|
||||
```
|
||||
|
||||
**Spoof MAC address:**
|
||||
|
||||
```bash
|
||||
# VirtualBox (use a real vendor OUI)
|
||||
VBoxManage modifyvm "VM-Name" --macaddress1 "8C8D28AABBCC"
|
||||
# Intel OUIs: 8C:8D:28, 00:1B:21, F4:4D:30
|
||||
# Realtek OUI: 00:E0:4C
|
||||
```
|
||||
|
||||
**Add realistic user artifacts:**
|
||||
|
||||
```powershell
|
||||
# Create browser history (requires Chrome installed)
|
||||
# Open URLs from PowerShell
|
||||
Start-Process "chrome.exe" "https://www.gmail.com"
|
||||
Start-Process "chrome.exe" "https://www.stackoverflow.com"
|
||||
|
||||
# Create recent documents
|
||||
$docs = @("budget_2024.xlsx","notes.txt","presentation.pptx")
|
||||
foreach ($d in $docs) {
|
||||
New-Item -Path "$env:USERPROFILE\Documents\$d" -ItemType File -Force
|
||||
}
|
||||
|
||||
# Set realistic uptime by running VM for 30+ minutes before analysis
|
||||
```
|
||||
|
||||
**Use VBoxHardenedLoader (advanced):**
|
||||
|
||||
VBoxHardenedLoader patches the VirtualBox kernel driver to pass CPUID and other checks transparently without removing guest additions.
|
||||
|
||||
```
|
||||
# https://github.com/hfiref0x/VBoxHardenedLoader
|
||||
# Load driver: VBoxHLinstall.cmd
|
||||
# Provides transparent VM hiding while keeping shared folders functional
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugger Detection
|
||||
|
||||
### How Malware Detects Debuggers (MITRE T1622)
|
||||
|
||||
**IsDebuggerPresent / CheckRemoteDebuggerPresent:**
|
||||
|
||||
Most common check. Reads the PEB (Process Environment Block) `BeingDebugged` flag directly.
|
||||
|
||||
```
|
||||
PEB offset 0x2 (32-bit) / 0x2 (64-bit): BeingDebugged byte
|
||||
PEB offset 0x68 (32-bit) / 0xBC (64-bit): NtGlobalFlag (set to 0x70 under debugger)
|
||||
```
|
||||
|
||||
**NtQueryInformationProcess:**
|
||||
|
||||
```
|
||||
ProcessDebugPort (class 7) → non-zero if debugged
|
||||
ProcessDebugObjectHandle (class 30) → valid handle if debugged
|
||||
ProcessDebugFlags (class 31) → 0 if debugged
|
||||
```
|
||||
|
||||
**Heap flags (PEB.ProcessHeap):**
|
||||
|
||||
```
|
||||
Heap.Flags: normally 0x2, under debugger 0x50000062
|
||||
Heap.ForceFlags: normally 0x0, under debugger 0x40000060
|
||||
```
|
||||
|
||||
**Hardware breakpoint detection:**
|
||||
|
||||
Malware reads the debug registers DR0–DR3 via `GetThreadContext`. If any are non-zero, a hardware breakpoint is set.
|
||||
|
||||
**Timing-based debugger detection:**
|
||||
|
||||
```c
|
||||
DWORD t1 = GetTickCount();
|
||||
// execute a few instructions
|
||||
DWORD t2 = GetTickCount();
|
||||
if ((t2 - t1) > threshold) { /* debugger detected */ }
|
||||
```
|
||||
|
||||
When single-stepping in a debugger, even a few instructions take milliseconds vs. nanoseconds normally.
|
||||
|
||||
**Exception-based detection:**
|
||||
|
||||
Malware raises an exception (INT3, divide-by-zero) and checks if the debugger swallows it instead of passing it to the exception handler.
|
||||
|
||||
**Window/process name enumeration:**
|
||||
|
||||
```
|
||||
FindWindow("OLLYDBG", NULL)
|
||||
FindWindow("WinDbgFrameClass", NULL)
|
||||
FindWindow("ID", NULL) → Immunity Debugger
|
||||
FindWindow("Qt5QWindowIcon", NULL) → x64dbg
|
||||
|
||||
EnumProcesses → scan for:
|
||||
ollydbg.exe, x32dbg.exe, x64dbg.exe, windbg.exe,
|
||||
ida.exe, ida64.exe, idag.exe, idaq.exe,
|
||||
immunitydebugger.exe, dnspy.exe, cheatengine.exe
|
||||
```
|
||||
|
||||
### Bypass Strategies
|
||||
|
||||
**ScyllaHide (primary tool):**
|
||||
|
||||
ScyllaHide is an anti-anti-debug plugin for x64dbg, OllyDbg, and IDA Pro. It patches all common detection methods transparently.
|
||||
|
||||
```
|
||||
# Download: https://github.com/x64dbg/ScyllaHide
|
||||
|
||||
# Install in x64dbg:
|
||||
# Copy ScyllaHide.dp32 / ScyllaHide.dp64 to x64dbg\plugins\
|
||||
# Restart x64dbg
|
||||
# Plugins → ScyllaHide → Options
|
||||
|
||||
# Enable all protections for evasive samples:
|
||||
[x] PEB BeingDebugged
|
||||
[x] PEB NtGlobalFlag
|
||||
[x] PEB HeapFlags
|
||||
[x] NtQueryInformationProcess
|
||||
[x] Unhandled Exception Handler
|
||||
[x] Hardware Breakpoint Protection
|
||||
[x] GetTickCount Hook
|
||||
[x] NtSetInformationThread
|
||||
```
|
||||
|
||||
**TitanHide (kernel-level, for advanced evasion):**
|
||||
|
||||
TitanHide is a kernel driver that hides debuggers at the OS level, defeating NtQueryInformationProcess and similar kernel-query-based checks.
|
||||
|
||||
```
|
||||
# Download: https://github.com/mrexodia/TitanHide
|
||||
# Requires: Windows with test signing enabled or TESTSIGNING boot flag
|
||||
# Load driver: TitanHide.sys
|
||||
# Use x64dbg plugin interface for configuration
|
||||
```
|
||||
|
||||
**Manual PEB patching in x64dbg:**
|
||||
|
||||
```
|
||||
# In x64dbg, after attaching:
|
||||
# Open Memory Map → find PEB
|
||||
# Manually zero BeingDebugged byte:
|
||||
|
||||
# 32-bit:
|
||||
# FS:[0x30] → PEB base
|
||||
# PEB+0x2 → BeingDebugged (set to 0x00)
|
||||
# PEB+0x68 → NtGlobalFlag (set to 0x00)
|
||||
|
||||
# Use x64dbg script:
|
||||
mov byte ptr [peb()]:2, 0 # Clear BeingDebugged
|
||||
mov dword ptr [peb()]:0x68, 0 # Clear NtGlobalFlag
|
||||
```
|
||||
|
||||
**Handle timing checks — NOP the sleep/delay:**
|
||||
|
||||
```
|
||||
# In x64dbg, find GetTickCount/QueryPerformanceCounter calls
|
||||
# Identify the comparison instruction after the delta calculation
|
||||
# NOP the conditional jump (JNE, JA, JBE, etc.)
|
||||
# Or patch the threshold to an impossibly large value
|
||||
```
|
||||
|
||||
**Handle exception-based detection:**
|
||||
|
||||
```
|
||||
# In x64dbg: Options → Preferences → Exceptions
|
||||
# Add exception codes to "pass to program":
|
||||
# 0x80000003 (INT3 / STATUS_BREAKPOINT)
|
||||
# 0xC0000094 (divide by zero)
|
||||
# 0xC0000005 (access violation, if used as detection)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Timing-Based Evasion
|
||||
|
||||
### How Malware Uses Timing (MITRE T1497.003)
|
||||
|
||||
**Sleep-based evasion:**
|
||||
|
||||
Malware calls `Sleep(600000)` (10 minutes) or longer. Automated sandboxes that have a 2-5 minute timeout never observe the payload.
|
||||
|
||||
```
|
||||
Sleep(DWORD dwMilliseconds)
|
||||
SleepEx(DWORD dwMilliseconds, BOOL bAlertable)
|
||||
WaitForSingleObject(HANDLE, INFINITE)
|
||||
NtDelayExecution(BOOLEAN Alertable, PLARGE_INTEGER DelayInterval)
|
||||
```
|
||||
|
||||
**Date/time checks:**
|
||||
|
||||
Malware compares the current date against a hardcoded activation date. If before that date, it does nothing (making sandbox captures useless). If after an expiry date, it also does nothing (anti-forensics).
|
||||
|
||||
**Idle/user activity checks:**
|
||||
|
||||
```
|
||||
GetLastInputInfo() → returns milliseconds since last user input
|
||||
GetCursorPos() → called twice, checks if cursor moved
|
||||
GetForegroundWindow() → checks for active user window
|
||||
```
|
||||
|
||||
If no mouse movement or keyboard input for N seconds, the malware assumes it is in a sandbox.
|
||||
|
||||
### Bypass Strategies
|
||||
|
||||
**Patch Sleep calls in x64dbg:**
|
||||
|
||||
```
|
||||
# Find calls to Sleep / SleepEx / NtDelayExecution
|
||||
# Option 1: NOP the entire Sleep call
|
||||
# Option 2: Patch the millisecond argument to 0 or 1
|
||||
|
||||
# In x64dbg, set a breakpoint on Sleep:
|
||||
# Breakpoints → Add → "Sleep"
|
||||
# When hit, change RCX (x64) or stack arg (x86) to 0 before continuing
|
||||
|
||||
# x64dbg script to auto-patch:
|
||||
bp Sleep
|
||||
loopSleep:
|
||||
erun
|
||||
mov rcx, 0 // Zero the sleep duration (x64 first arg)
|
||||
erun
|
||||
goto loopSleep
|
||||
```
|
||||
|
||||
**Accelerate sandbox clock (Cuckoo/CAPE):**
|
||||
|
||||
```yaml
|
||||
# In cuckoo.conf:
|
||||
[timeouts]
|
||||
critical = 60
|
||||
vm_state = 60
|
||||
|
||||
# Enable clock skipping in analysis options:
|
||||
options: "clock=20200101120000"
|
||||
```
|
||||
|
||||
**Manipulate system time on Windows VM:**
|
||||
|
||||
```powershell
|
||||
# Set date forward to trigger time-gated payloads
|
||||
Set-Date -Date "2025-06-15 12:00:00"
|
||||
|
||||
# Use faketime (Linux / Wine)
|
||||
faketime '2025-06-15 12:00:00' wine sample.exe
|
||||
```
|
||||
|
||||
**Simulate user activity:**
|
||||
|
||||
```powershell
|
||||
# Move mouse programmatically
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
while ($true) {
|
||||
$x = Get-Random -Minimum 100 -Maximum 1800
|
||||
$y = Get-Random -Minimum 100 -Maximum 900
|
||||
[System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point($x, $y)
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
```
|
||||
|
||||
**Extend sandbox timeout:**
|
||||
|
||||
For CAPE/Cuckoo, increase the analysis timeout:
|
||||
|
||||
```bash
|
||||
python3 utils/submit.py --file sample.exe --timeout 300
|
||||
```
|
||||
|
||||
For manual analysis, simply wait — set observation window to 60+ minutes for suspected sleep-evasive samples.
|
||||
|
||||
---
|
||||
|
||||
## Environmental Keying
|
||||
|
||||
### How Malware Implements Environmental Checks (MITRE T1480)
|
||||
|
||||
Environmental keying means the malware will only execute its payload if specific environmental conditions are met. This is distinct from VM detection — the malware is looking for a specific victim configuration.
|
||||
|
||||
**Domain/hostname checks:**
|
||||
|
||||
```
|
||||
GetComputerNameEx(ComputerNameDnsDomain, ...)
|
||||
NetGetJoinInformation()
|
||||
LookupAccountSid()
|
||||
→ Malware may require a specific AD domain name or NETBIOS name
|
||||
```
|
||||
|
||||
**Username checks:**
|
||||
|
||||
```
|
||||
GetUserName() → compare against hardcoded victim username
|
||||
WNetGetUser()
|
||||
```
|
||||
|
||||
**Installed software checks:**
|
||||
|
||||
```
|
||||
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\ → enumerate installed apps
|
||||
HKLM\SOFTWARE\ → check for specific vendor keys
|
||||
```
|
||||
|
||||
**Language/locale checks:**
|
||||
|
||||
```
|
||||
GetSystemDefaultLangID()
|
||||
GetUserDefaultLCID()
|
||||
→ Some malware won't run if locale is English or matches researcher's profile
|
||||
→ Others skip execution if locale is Russian/Ukrainian (avoiding domestic prosecution)
|
||||
```
|
||||
|
||||
**Network environment checks:**
|
||||
|
||||
```
|
||||
GetAdaptersInfo() → check for specific IP ranges
|
||||
DnsQuery() → verify domain membership via DNS
|
||||
NetServerGetInfo() → check for domain controller presence
|
||||
```
|
||||
|
||||
### Bypass Strategies
|
||||
|
||||
**Domain/hostname spoofing:**
|
||||
|
||||
```powershell
|
||||
# Change computer name (requires restart)
|
||||
Rename-Computer -NewName "WORKSTATION-01" -Force
|
||||
|
||||
# Join a fake workgroup matching expected domain name
|
||||
# (Does not require actual domain controller)
|
||||
Add-Computer -WorkgroupName "TARGETCORP"
|
||||
```
|
||||
|
||||
**Username spoofing:**
|
||||
|
||||
```powershell
|
||||
# Create a user matching expected victim name
|
||||
New-LocalUser -Name "jsmith" -NoPassword
|
||||
Add-LocalGroupMember -Group "Administrators" -Member "jsmith"
|
||||
# Log in as that user, then execute sample
|
||||
```
|
||||
|
||||
**Locale patching:**
|
||||
|
||||
```
|
||||
# In x64dbg, break on GetSystemDefaultLangID / GetUserDefaultLCID
|
||||
# Patch EAX return value to match expected locale
|
||||
# Russian: 0x0419, Romanian: 0x0418, Ukrainian: 0x0422
|
||||
```
|
||||
|
||||
**Registry keying bypass:**
|
||||
|
||||
```
|
||||
# If malware checks for a specific product registry key:
|
||||
# Identify the expected key via static analysis (strings, IDA)
|
||||
# Create a fake registry entry before execution:
|
||||
New-Item -Path "HKLM:\SOFTWARE\TargetVendor\TargetProduct" -Force
|
||||
New-ItemProperty -Path "HKLM:\SOFTWARE\TargetVendor\TargetProduct" `
|
||||
-Name "Version" -Value "1.0" -PropertyType String
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tools for Anti-Analysis Bypass
|
||||
|
||||
### ScyllaHide
|
||||
|
||||
Purpose: Plugin for x64dbg, OllyDbg, IDA Pro, TitanHide. Patches all common anti-debug checks transparently.
|
||||
|
||||
```
|
||||
Download: https://github.com/x64dbg/ScyllaHide/releases
|
||||
Platforms: x32dbg, x64dbg, OllyDbg 1.10, OllyDbg 2.01, IDA Pro
|
||||
Install: Copy DLL to debugger plugins folder
|
||||
|
||||
Key options:
|
||||
- PEB patch (BeingDebugged, NtGlobalFlag, HeapFlags)
|
||||
- NtQueryInformationProcess hooks
|
||||
- Timing hooks (GetTickCount, QueryPerformanceCounter)
|
||||
- NtSetInformationThread (hide from debugger)
|
||||
- Unhandled exception fix
|
||||
```
|
||||
|
||||
### TitanHide
|
||||
|
||||
Purpose: Kernel-mode driver that hides debugger presence from NtQueryInformationProcess and related syscalls.
|
||||
|
||||
```
|
||||
Download: https://github.com/mrexodia/TitanHide
|
||||
Requires: Windows with test signing or driver certificate
|
||||
Use case: When ScyllaHide user-mode hooks are insufficient
|
||||
(malware uses direct syscalls to bypass user-mode hooks)
|
||||
```
|
||||
|
||||
### al-khaser
|
||||
|
||||
Purpose: Open-source tool that tests over 170 anti-analysis checks. Use to validate VM hardening before analyzing evasive samples.
|
||||
|
||||
```
|
||||
Download: https://github.com/LordNoteworthy/al-khaser
|
||||
Build: Visual Studio solution (.sln)
|
||||
Run: al-khaser.exe
|
||||
Output: Pass/fail for each check category:
|
||||
- Anti-VM
|
||||
- Anti-debug
|
||||
- Anti-sandbox
|
||||
- Anti-disassembly
|
||||
- Timing
|
||||
```
|
||||
|
||||
### pafish
|
||||
|
||||
Purpose: Lightweight VM/sandbox detection tester. Faster than al-khaser for quick pre-analysis validation.
|
||||
|
||||
```
|
||||
Download: https://github.com/a0rtega/pafish/releases
|
||||
Run: pafish.exe
|
||||
Green: Check passed (not detected)
|
||||
Red: Check failed (artifact detected — fix before analyzing evasive samples)
|
||||
```
|
||||
|
||||
### hollows_hunter / pe-sieve
|
||||
|
||||
Purpose: Scan a running process for injected code, hollowed sections, and unpacked payloads in memory. Useful after malware defeats initial analysis controls.
|
||||
|
||||
```
|
||||
Download: https://github.com/hasherezade/hollows_hunter
|
||||
Run: hollows_hunter.exe /pid <malware_pid>
|
||||
Output: Dumps hollowed/injected regions to disk for static analysis
|
||||
```
|
||||
|
||||
### x64dbg Plugins for Anti-Analysis
|
||||
|
||||
```
|
||||
ScyllaHide Anti-debug bypass (essential)
|
||||
xAnalyzer Extended analysis and comment generation
|
||||
xHotConvert Quick conversion utilities
|
||||
OllyDumpEx Process dumper for unpacking
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Identifying Anti-Analysis at a Glance
|
||||
|
||||
During static analysis, flag these patterns for targeted bypass:
|
||||
|
||||
**String indicators:**
|
||||
|
||||
```
|
||||
"IsDebuggerPresent" → debugger detection
|
||||
"CheckRemoteDebugger" → remote debugger detection
|
||||
"NtQueryInformationProcess" → advanced debugger/VM detection
|
||||
"VBOX", "VMWARE", "QEMU", "VIRTUAL" → VM string checks
|
||||
"GetTickCount" → timing-based detection
|
||||
"GetLastInputInfo" → user interaction check
|
||||
"GetSystemDefaultLangID" → locale-based keying
|
||||
"GetComputerName" → hostname keying
|
||||
"EnumProcesses" → process-list scanning
|
||||
```
|
||||
|
||||
**Import table red flags:**
|
||||
|
||||
```
|
||||
kernel32.dll: IsDebuggerPresent, CheckRemoteDebuggerPresent,
|
||||
GetTickCount, QueryPerformanceCounter,
|
||||
GetLastInputInfo, GetCursorPos
|
||||
ntdll.dll: NtQueryInformationProcess, NtSetInformationThread,
|
||||
NtDelayExecution, RtlQueryProcessHeapInformation
|
||||
advapi32.dll: RegOpenKeyEx (combined with SYSTEM\CurrentControlSet\Services)
|
||||
iphlpapi.dll: GetAdaptersInfo (MAC address check)
|
||||
```
|
||||
|
||||
**Behavioral indicators during dynamic analysis:**
|
||||
|
||||
```
|
||||
Process exits immediately (< 2 seconds) → VM or debugger detected
|
||||
No network activity despite expected C2 → sandbox/network detection
|
||||
Sleep calls with long durations (> 30s) → timing evasion
|
||||
Repeated GetTickCount/RDTSC in tight loop → timing check
|
||||
CreateFile on \\.\VBoxGuest or \\.\vmci → device probing
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MITRE ATT&CK Reference
|
||||
|
||||
| Technique | ID | Description |
|
||||
|-----------|-----|-------------|
|
||||
| Virtualization/Sandbox Evasion | T1497 | Parent technique |
|
||||
| System Checks | T1497.001 | Registry, filesystem, hardware artifacts |
|
||||
| User Activity Based Checks | T1497.002 | Mouse movement, input, idle time |
|
||||
| Time Based Evasion | T1497.003 | Sleep calls, date checks, RDTSC |
|
||||
| Debugger Evasion | T1622 | IsDebuggerPresent, NtQueryInformationProcess |
|
||||
| Execution Guardrails | T1480 | Environmental keying, domain/locale checks |
|
||||
| Obfuscated Files or Information | T1027 | Packed/encrypted payloads that unpack in memory |
|
||||
|
||||
---
|
||||
|
||||
## Analysis Decision Tree
|
||||
|
||||
```
|
||||
Sample exits immediately?
|
||||
├─ Yes → Check: is VM artifact present? → Fix VM hardening, retry
|
||||
│ Check: debugger detected? → Enable ScyllaHide, retry
|
||||
│ Check: locale/domain check? → Spoof environment, retry
|
||||
└─ No → Sample is executing
|
||||
|
||||
No network activity?
|
||||
├─ Check: INetSim running and configured?
|
||||
├─ Check: DNS resolving to INetSim IP?
|
||||
├─ Check: malware performing HTTPS certificate pinning?
|
||||
│ → Use Proxifier + mitmproxy to intercept
|
||||
└─ Check: malware waiting for sleep delay?
|
||||
→ Patch Sleep calls, extend timeout
|
||||
|
||||
Behavior stops after initial activity?
|
||||
├─ Check: C2 response expected? → Configure INetSim response templates
|
||||
├─ Check: time-gated payload? → Advance system clock
|
||||
└─ Check: requires second-stage download? → Serve fake C2 response via INetSim
|
||||
```
|
||||
@@ -0,0 +1,465 @@
|
||||
# Sandbox Setup Guide
|
||||
|
||||
Quick reference for building and maintaining isolated malware analysis environments.
|
||||
|
||||
---
|
||||
|
||||
## REMnux - Linux Analysis VM
|
||||
|
||||
REMnux is a curated Ubuntu-based distro pre-loaded with reverse engineering and network simulation tools.
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Option 1: Download pre-built OVA (recommended)
|
||||
# https://remnux.org/#distro
|
||||
# Import via VirtualBox/VMware: File → Import Appliance → remnux-vX.X.ova
|
||||
|
||||
# Option 2: Add REMnux to existing Ubuntu 22.04 LTS
|
||||
wget https://REMnux.org/remnux-cli
|
||||
mv remnux-cli /usr/local/bin/remnux
|
||||
chmod +x /usr/local/bin/remnux
|
||||
sudo remnux install
|
||||
```
|
||||
|
||||
### Key Pre-Installed Tools
|
||||
|
||||
| Category | Tools |
|
||||
|----------|-------|
|
||||
| Network simulation | INetSim, FakeNet-NG |
|
||||
| Traffic analysis | Wireshark, tshark, tcpdump, NetworkMiner |
|
||||
| Static analysis | FLOSS, strings, binwalk, file, ssdeep |
|
||||
| PDF analysis | pdf-parser, pdfid, peepdf |
|
||||
| Office analysis | olevba, mraptor, msodde |
|
||||
| Disassembly | Ghidra, Binary Ninja (trial) |
|
||||
| Scripting | Python 3, Ruby, Node.js |
|
||||
| Memory forensics | Volatility 3, bulk_extractor |
|
||||
|
||||
### Initial Configuration
|
||||
|
||||
```bash
|
||||
# Update REMnux tools
|
||||
sudo remnux upgrade
|
||||
|
||||
# Set timezone to UTC for consistent timestamps
|
||||
sudo timedatectl set-timezone UTC
|
||||
|
||||
# Disable automatic updates (prevent unintended network activity)
|
||||
sudo systemctl disable apt-daily.timer
|
||||
sudo systemctl disable apt-daily-upgrade.timer
|
||||
|
||||
# Verify INetSim is ready
|
||||
sudo systemctl status inetsim
|
||||
```
|
||||
|
||||
### INetSim Configuration on REMnux
|
||||
|
||||
```bash
|
||||
sudo nano /etc/inetsim/inetsim.conf
|
||||
```
|
||||
|
||||
Key settings to configure:
|
||||
|
||||
```
|
||||
# Bind to interface facing Windows analysis VM
|
||||
service_bind_address 192.168.56.1
|
||||
|
||||
# DNS: respond to all queries with INetSim's own IP
|
||||
dns_default_ip 192.168.56.1
|
||||
dns_version "Microsoft DNS 6.1.7601"
|
||||
|
||||
# HTTP/HTTPS
|
||||
http_bind_port 80
|
||||
https_bind_port 443
|
||||
http_fakemode yes
|
||||
|
||||
# SMTP
|
||||
smtp_bind_port 25
|
||||
smtp_banner "220 mail.example.com ESMTP"
|
||||
```
|
||||
|
||||
```bash
|
||||
# Start INetSim
|
||||
sudo inetsim
|
||||
|
||||
# View INetSim logs
|
||||
tail -f /var/log/inetsim/service.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FlareVM - Windows Analysis VM
|
||||
|
||||
FlareVM is a Chocolatey-based installer that transforms Windows into a malware analysis workstation.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Windows 10 (version 1903 or later) — fresh install strongly preferred
|
||||
- At minimum 60 GB disk, 4 GB RAM (8 GB recommended)
|
||||
- PowerShell 5+ with execution policy set to unrestricted
|
||||
- .NET 4+ installed
|
||||
- Disable Windows Defender and Windows Update before installing
|
||||
|
||||
### Disable Defender Before Installation
|
||||
|
||||
```powershell
|
||||
# Run PowerShell as Administrator
|
||||
|
||||
# Disable real-time protection (temporary, for installation)
|
||||
Set-MpPreference -DisableRealtimeMonitoring $true
|
||||
|
||||
# Disable all Defender components
|
||||
Set-MpPreference -DisableBehaviorMonitoring $true
|
||||
Set-MpPreference -DisableIOAVProtection $true
|
||||
Set-MpPreference -DisableScriptScanning $true
|
||||
|
||||
# Disable Windows Update
|
||||
sc.exe config wuauserv start=disabled
|
||||
sc.exe stop wuauserv
|
||||
```
|
||||
|
||||
### Installation
|
||||
|
||||
```powershell
|
||||
# Run PowerShell as Administrator
|
||||
Set-ExecutionPolicy Unrestricted -Force
|
||||
|
||||
# Download and run FlareVM installer
|
||||
(New-Object net.webclient).DownloadFile(
|
||||
'https://raw.githubusercontent.com/mandiant/flare-vm/main/install.ps1',
|
||||
"$env:temp\install.ps1"
|
||||
)
|
||||
Unblock-File "$env:temp\install.ps1"
|
||||
& "$env:temp\install.ps1"
|
||||
```
|
||||
|
||||
The installer opens a GUI for package selection. Installation takes 1-2 hours.
|
||||
|
||||
### Key Installed Tools
|
||||
|
||||
| Category | Tools |
|
||||
|----------|-------|
|
||||
| Debuggers | x64dbg, WinDbg, OllyDbg |
|
||||
| Disassemblers | IDA Free, Ghidra, Binary Ninja (trial) |
|
||||
| Process analysis | System Informer (formerly Process Hacker), ProcDOT, Process Monitor |
|
||||
| Network | Wireshark, FakeNet-NG, TCPView |
|
||||
| Static analysis | PE-bear, CFF Explorer, PEiD, Detect-It-Easy |
|
||||
| String extraction | FLOSS, strings |
|
||||
| .NET | dnSpy, de4dot |
|
||||
| Office | olevba, oletools |
|
||||
| Hex editors | HxD, 010 Editor |
|
||||
| Scripting | Python 3, Ruby |
|
||||
|
||||
### Post-Installation Hardening
|
||||
|
||||
```powershell
|
||||
# Disable Windows Firewall (analysis VM only — never production)
|
||||
netsh advfirewall set allprofiles state off
|
||||
|
||||
# Disable UAC prompts that interrupt malware execution
|
||||
Set-ItemProperty -Path REGISTRY::HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System -Name ConsentPromptBehaviorAdmin -Value 0
|
||||
|
||||
# Show hidden files and extensions
|
||||
Set-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' -Name Hidden -Value 1
|
||||
Set-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced' -Name HideFileExt -Value 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Network Isolation Configuration
|
||||
|
||||
### VirtualBox Host-Only Network Setup
|
||||
|
||||
Host-only networking creates a private network between the host and VMs only — no internet access.
|
||||
|
||||
**Create Host-Only Adapter (VirtualBox):**
|
||||
|
||||
```
|
||||
File → Host Network Manager → Create
|
||||
IPv4 Address: 192.168.56.1
|
||||
IPv4 Network Mask: 255.255.255.0
|
||||
DHCP Server: Disabled (assign static IPs manually)
|
||||
```
|
||||
|
||||
**Configure Windows Analysis VM:**
|
||||
|
||||
```
|
||||
VM Settings → Network
|
||||
Adapter 1: Host-Only Adapter → vboxnet0
|
||||
```
|
||||
|
||||
**Set static IP on Windows VM:**
|
||||
|
||||
```
|
||||
Network and Sharing Center → Change adapter settings
|
||||
→ Right-click adapter → Properties → IPv4 → Manual:
|
||||
IP Address: 192.168.56.10
|
||||
Subnet Mask: 255.255.255.0
|
||||
Gateway: 192.168.56.1 (REMnux INetSim)
|
||||
DNS Server: 192.168.56.1 (REMnux INetSim)
|
||||
```
|
||||
|
||||
**Configure REMnux VM:**
|
||||
|
||||
```
|
||||
VM Settings → Network
|
||||
Adapter 1: Host-Only Adapter → vboxnet0
|
||||
(Static IP: 192.168.56.1 — set during OS install or via netplan)
|
||||
```
|
||||
|
||||
### VMware Host-Only Network Setup
|
||||
|
||||
```
|
||||
Edit → Virtual Network Editor → Add Network → VMnet1 (Host-only)
|
||||
Subnet IP: 192.168.56.0
|
||||
Subnet mask: 255.255.255.0
|
||||
Disable: Use local DHCP service
|
||||
```
|
||||
|
||||
**Assign to analysis VM:**
|
||||
|
||||
```
|
||||
VM Settings → Network Adapter → Custom: VMnet1
|
||||
```
|
||||
|
||||
### Verifying Isolation
|
||||
|
||||
From the Windows analysis VM, confirm no external connectivity:
|
||||
|
||||
```cmd
|
||||
# Should resolve (INetSim responds)
|
||||
nslookup google.com
|
||||
|
||||
# Should fail (no external routing)
|
||||
ping 8.8.8.8
|
||||
|
||||
# Verify gateway is INetSim, not external router
|
||||
tracert google.com
|
||||
```
|
||||
|
||||
From REMnux, confirm INetSim is intercepting:
|
||||
|
||||
```bash
|
||||
# Watch live connections from Windows VM
|
||||
tail -f /var/log/inetsim/service.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Snapshot Management
|
||||
|
||||
Snapshots are the single most important operational practice. A corrupted or infected baseline wastes hours.
|
||||
|
||||
### Snapshot Strategy
|
||||
|
||||
**Baseline snapshot** — taken once after clean VM build, before any analysis:
|
||||
- OS fully patched (final time)
|
||||
- All analysis tools installed and tested
|
||||
- INetSim/FakeNet configured
|
||||
- Sysmon installed with SwiftOnSecurity config
|
||||
- Defender disabled
|
||||
- Named: `BASELINE-CLEAN-YYYY-MM-DD`
|
||||
|
||||
**Pre-execution snapshot** — taken at the start of each analysis session:
|
||||
- All monitoring tools open and ready (Procmon capturing, Wireshark running)
|
||||
- INetSim running on REMnux
|
||||
- Sample copied to VM but not yet executed
|
||||
- Named: `PRE-EXEC-<sample-hash>-YYYY-MM-DD`
|
||||
|
||||
**Mid-analysis snapshots** — optional, taken at significant milestones:
|
||||
- After installation phase completes
|
||||
- Before triggering a specific behavior
|
||||
- Named: `MID-<phase>-<sample-hash>`
|
||||
|
||||
### VirtualBox Snapshot Commands
|
||||
|
||||
```bash
|
||||
# List snapshots
|
||||
VBoxManage snapshot "Windows10-Analysis" list
|
||||
|
||||
# Take snapshot
|
||||
VBoxManage snapshot "Windows10-Analysis" take "PRE-EXEC-abc123-2024-01-15" \
|
||||
--description "Sample abc123 loaded, tools running"
|
||||
|
||||
# Restore snapshot
|
||||
VBoxManage snapshot "Windows10-Analysis" restore "BASELINE-CLEAN-2024-01-01"
|
||||
|
||||
# Delete old snapshots (free disk space)
|
||||
VBoxManage snapshot "Windows10-Analysis" delete "MID-install-abc123"
|
||||
```
|
||||
|
||||
### VMware Snapshot Commands
|
||||
|
||||
```bash
|
||||
# Take snapshot (VMware CLI)
|
||||
vmrun snapshot /path/to/vm.vmx "PRE-EXEC-abc123-2024-01-15"
|
||||
|
||||
# Revert to snapshot
|
||||
vmrun revertToSnapshot /path/to/vm.vmx "BASELINE-CLEAN-2024-01-01"
|
||||
|
||||
# List snapshots
|
||||
vmrun listSnapshots /path/to/vm.vmx
|
||||
```
|
||||
|
||||
### Discipline Rules
|
||||
|
||||
- **Always revert to baseline after each sample** — never analyze two samples in the same session without reverting
|
||||
- **Never take a snapshot while malware is running** — the snapshot captures infected state
|
||||
- **Store baseline OVA/OVF off the analysis machine** — backup in case of host compromise
|
||||
- **Label snapshots with dates** — baselines age; tools need updates
|
||||
|
||||
---
|
||||
|
||||
## VM Detection Countermeasures
|
||||
|
||||
Malware commonly fingerprints VMs to avoid analysis. See `references/anti_analysis_bypass.md` for full bypass techniques. Common baseline hardening steps:
|
||||
|
||||
### Hardware Fingerprint Spoofing
|
||||
|
||||
**VirtualBox — spoof BIOS/hardware strings:**
|
||||
|
||||
```bash
|
||||
# Run on host before starting VM
|
||||
VBoxManage setextradata "Windows10-Analysis" \
|
||||
"VBoxInternal/Devices/pcbios/0/Config/DmiBIOSVendor" "American Megatrends Inc."
|
||||
|
||||
VBoxManage setextradata "Windows10-Analysis" \
|
||||
"VBoxInternal/Devices/pcbios/0/Config/DmiSystemProduct" "MS-7B86"
|
||||
|
||||
VBoxManage setextradata "Windows10-Analysis" \
|
||||
"VBoxInternal/Devices/pcbios/0/Config/DmiSystemVendor" "MSI"
|
||||
|
||||
VBoxManage setextradata "Windows10-Analysis" \
|
||||
"VBoxInternal/Devices/pcbios/0/Config/DmiChassisVendor" "MSI"
|
||||
```
|
||||
|
||||
**Spoof MAC address (remove VirtualBox OUI 08:00:27:xx:xx:xx):**
|
||||
|
||||
```
|
||||
VM Settings → Network → Advanced → MAC Address → Generate new
|
||||
```
|
||||
|
||||
Or set manually to a real vendor OUI (e.g., Intel: `8C:8D:28`):
|
||||
|
||||
```bash
|
||||
VBoxManage modifyvm "Windows10-Analysis" --macaddress1 "8C8D28AABBCC"
|
||||
```
|
||||
|
||||
### Remove VM Artifacts on Windows Guest
|
||||
|
||||
```powershell
|
||||
# Remove VirtualBox Guest Additions (if installed)
|
||||
# Control Panel → Programs → Uninstall VirtualBox Guest Additions
|
||||
|
||||
# Remove VMware Tools
|
||||
# Control Panel → Programs → Uninstall VMware Tools
|
||||
|
||||
# Remove obvious registry keys
|
||||
Remove-Item -Path "HKLM:\SOFTWARE\Oracle\VirtualBox Guest Additions" -Recurse -ErrorAction SilentlyContinue
|
||||
Remove-Item -Path "HKLM:\SOFTWARE\VMware, Inc." -Recurse -ErrorAction SilentlyContinue
|
||||
|
||||
# Remove VBox/VMware processes from autostart
|
||||
# Check: HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
|
||||
```
|
||||
|
||||
### Screen Resolution and User Activity
|
||||
|
||||
```
|
||||
Set VM resolution to common values: 1920x1080 or 1366x768
|
||||
Create desktop shortcuts, browser history, recent documents
|
||||
Place realistic-looking files in Downloads, Documents, Desktop
|
||||
Add a few installed applications (Chrome, 7-Zip, Notepad++)
|
||||
```
|
||||
|
||||
### Testing VM Hardening
|
||||
|
||||
Use pafish (Paranoid Fish) to verify detection resistance:
|
||||
|
||||
```
|
||||
# Download: https://github.com/a0rtega/pafish
|
||||
# Run pafish.exe in the VM
|
||||
# Each red "FAIL" is a detectable artifact
|
||||
# Aim for all green before analyzing evasive samples
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CAPE Sandbox - Local Automated Sandbox
|
||||
|
||||
CAPE (Config And Payload Extraction) automates dynamic analysis at scale.
|
||||
|
||||
### Quick Install (Docker)
|
||||
|
||||
```bash
|
||||
# Clone CAPE
|
||||
git clone https://github.com/kevoreilly/CAPEv2.git
|
||||
cd CAPEv2
|
||||
|
||||
# Use Docker Compose (simplest setup)
|
||||
sudo docker-compose up -d
|
||||
|
||||
# Web interface available at:
|
||||
# http://localhost:8000
|
||||
```
|
||||
|
||||
### Submit Sample via CLI
|
||||
|
||||
```bash
|
||||
# Submit file
|
||||
python3 utils/submit.py --file /path/to/sample.exe
|
||||
|
||||
# Submit with options
|
||||
python3 utils/submit.py \
|
||||
--file /path/to/sample.exe \
|
||||
--timeout 120 \
|
||||
--options "procmemdump=yes,extraction=yes"
|
||||
```
|
||||
|
||||
### Submit via Web UI
|
||||
|
||||
```
|
||||
http://localhost:8000/submit
|
||||
→ Upload file
|
||||
→ Set timeout (60-300 seconds)
|
||||
→ Select package (exe, dll, doc, pdf, etc.)
|
||||
→ Submit
|
||||
```
|
||||
|
||||
### Retrieve Report
|
||||
|
||||
```bash
|
||||
# List tasks
|
||||
python3 utils/process.py -r <task_id>
|
||||
|
||||
# Reports stored at:
|
||||
# /opt/CAPEv2/storage/analyses/<task_id>/reports/report.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Windows VM has internet access (should not):**
|
||||
- Verify adapter is Host-Only, not NAT or Bridged
|
||||
- Check default gateway points to INetSim IP, not router IP
|
||||
- Disable any secondary adapters
|
||||
|
||||
**INetSim not responding to DNS queries:**
|
||||
- Confirm `service_bind_address` matches REMnux's host-only IP
|
||||
- Check INetSim is running: `sudo systemctl status inetsim`
|
||||
- Verify no firewall blocking port 53 on REMnux: `sudo ufw status`
|
||||
|
||||
**Snapshot restore fails (VirtualBox):**
|
||||
- Ensure VM is fully powered off before restoring
|
||||
- Verify sufficient disk space on host
|
||||
- Try: `VBoxManage snapshot <vm> restorecurrent`
|
||||
|
||||
**FlareVM installation fails partway through:**
|
||||
- Re-run installer — Chocolatey packages are idempotent
|
||||
- Check Windows Update is fully disabled
|
||||
- Verify Defender exclusions cover `C:\ProgramData\chocolatey`
|
||||
|
||||
**pafish detects VM even after hardening:**
|
||||
- Check CPUID hypervisor bit: requires CPU passthrough or nested virt disabled
|
||||
- VMware: add `hypervisor.cpuid.v0 = FALSE` to .vmx file
|
||||
- VirtualBox: `VBoxManage modifyvm <vm> --paravirtprovider none`
|
||||
@@ -0,0 +1,397 @@
|
||||
# Dynamic Analysis Tool Setup Guide
|
||||
|
||||
Quick reference for configuring essential dynamic analysis tools.
|
||||
|
||||
## Process Monitor (Procmon) - Sysinternals
|
||||
|
||||
### Initial Setup
|
||||
1. Download from: https://docs.microsoft.com/en-us/sysinternals/downloads/procmon
|
||||
2. Run as Administrator
|
||||
3. Procmon starts capturing immediately
|
||||
|
||||
### Essential Filters (Reduce Noise)
|
||||
|
||||
**Filter by Process:**
|
||||
```
|
||||
Process Name → is → sample.exe → Include
|
||||
Process Name → is → rundll32.exe → Include (if DLL execution)
|
||||
Process Name → is → explorer.exe → Exclude
|
||||
Process Name → is → svchost.exe → Exclude
|
||||
```
|
||||
|
||||
**Filter by Operation Type:**
|
||||
```
|
||||
Operation → contains → Reg → Include (registry operations)
|
||||
Operation → contains → File → Include (file operations)
|
||||
Operation → begins with → Process → Include (process operations)
|
||||
```
|
||||
|
||||
**Filter by Result:**
|
||||
```
|
||||
Result → is → SUCCESS → Include (focus on successful operations)
|
||||
```
|
||||
|
||||
### Columns to Enable
|
||||
- Time
|
||||
- Process Name
|
||||
- PID
|
||||
- Operation
|
||||
- Path
|
||||
- Result
|
||||
- Detail
|
||||
|
||||
### Export Options
|
||||
```
|
||||
File → Save → All Events → CSV
|
||||
File → Save → Filtered Events → CSV (recommended)
|
||||
```
|
||||
|
||||
### Pro Tips
|
||||
- Clear capture (Ctrl+X) before malware execution for clean logs
|
||||
- Use bookmarks (Ctrl+B) to mark important events
|
||||
- Stack duplicate events: Tools → Count Occurrences
|
||||
- Enable boot logging for persistence analysis: Options → Enable Boot Logging
|
||||
|
||||
---
|
||||
|
||||
## Wireshark - Network Analysis
|
||||
|
||||
### Capture Setup
|
||||
1. Download from: https://www.wireshark.org
|
||||
2. Start → Options → Select adapter connected to INetSim
|
||||
3. Start capture before executing malware
|
||||
|
||||
### Display Filters (During Analysis)
|
||||
|
||||
**DNS Traffic:**
|
||||
```
|
||||
dns
|
||||
dns.qry.name contains "malicious"
|
||||
```
|
||||
|
||||
**HTTP Traffic:**
|
||||
```
|
||||
http
|
||||
http.request.method == "POST"
|
||||
http.request.uri contains "/api"
|
||||
```
|
||||
|
||||
**HTTPS/TLS:**
|
||||
```
|
||||
tls
|
||||
tls.handshake.type == 1
|
||||
```
|
||||
|
||||
**Specific IP:**
|
||||
```
|
||||
ip.addr == 192.168.1.100
|
||||
ip.dst == 192.168.1.100
|
||||
```
|
||||
|
||||
**Specific Port:**
|
||||
```
|
||||
tcp.port == 443
|
||||
udp.port == 53
|
||||
```
|
||||
|
||||
### Export Options
|
||||
```
|
||||
File → Export Specified Packets → Save as PCAPNG
|
||||
File → Export Objects → HTTP (extract downloaded files)
|
||||
```
|
||||
|
||||
### Command-Line Analysis (tshark)
|
||||
```bash
|
||||
# Extract DNS queries
|
||||
tshark -r capture.pcapng -Y dns.qry.name -T fields -e dns.qry.name | sort -u
|
||||
|
||||
# Extract HTTP hosts
|
||||
tshark -r capture.pcapng -Y http.request -T fields -e http.host -e http.request.uri
|
||||
|
||||
# Extract all contacted IPs
|
||||
tshark -r capture.pcapng -T fields -e ip.dst | sort -u
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## System Informer (formerly Process Hacker) - Process/Memory Analysis
|
||||
|
||||
### Setup
|
||||
1. Download from: https://systeminformer.sourceforge.io
|
||||
2. Run as Administrator
|
||||
3. Enable: View → Update Speed → Fast
|
||||
|
||||
### Key Features
|
||||
|
||||
**Process Tree View:**
|
||||
```
|
||||
View → Tree View
|
||||
Expand malware process to see child processes
|
||||
```
|
||||
|
||||
**Memory Inspection:**
|
||||
```
|
||||
Double-click process → Memory tab
|
||||
Look for RWX regions (suspicious)
|
||||
Right-click memory region → Read/Write → String scan
|
||||
```
|
||||
|
||||
**Handles:**
|
||||
```
|
||||
Double-click process → Handles tab
|
||||
Filter: Type → Mutant (mutexes)
|
||||
Filter: Type → File (open files)
|
||||
```
|
||||
|
||||
**Network Connections:**
|
||||
```
|
||||
Double-click process → Network tab
|
||||
Shows all connections made by process
|
||||
```
|
||||
|
||||
**Memory Dump:**
|
||||
```
|
||||
Right-click process → Create dump file
|
||||
Save full memory for Volatility analysis
|
||||
```
|
||||
|
||||
### Highlighting Rules
|
||||
```
|
||||
Options → Highlight → New
|
||||
Condition: Process Name → contains → sample.exe
|
||||
Color: Red
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sysmon - Advanced Windows Logging
|
||||
|
||||
### Installation
|
||||
```powershell
|
||||
# Download Sysmon
|
||||
# https://docs.microsoft.com/en-us/sysinternals/downloads/sysmon
|
||||
|
||||
# Install with a community config
|
||||
sysmon64.exe -accepteula -i sysmonconfig.xml
|
||||
|
||||
# Config sources (pick one):
|
||||
# https://github.com/olafhartong/sysmon-modular (actively maintained, MITRE-mapped; use sysmonconfig.xml)
|
||||
# https://github.com/SwiftOnSecurity/sysmon-config (classic, less frequently updated)
|
||||
```
|
||||
|
||||
### View Logs
|
||||
```powershell
|
||||
# Event Viewer
|
||||
eventvwr.msc → Applications and Services Logs → Microsoft → Windows → Sysmon → Operational
|
||||
|
||||
# PowerShell
|
||||
Get-WinEvent -LogName "Microsoft-Windows-Sysmon/Operational" -MaxEvents 100 | Format-List
|
||||
|
||||
# Export logs
|
||||
wevtutil epl Microsoft-Windows-Sysmon/Operational C:\evidence\sysmon.evtx
|
||||
```
|
||||
|
||||
### Key Event IDs
|
||||
- **Event 1**: Process creation (command line, hashes, parent process)
|
||||
- **Event 3**: Network connection (source/dest IP, port, process)
|
||||
- **Event 5**: Process terminated
|
||||
- **Event 7**: Image loaded (DLL loading)
|
||||
- **Event 8**: CreateRemoteThread (injection)
|
||||
- **Event 10**: ProcessAccess (process manipulation)
|
||||
- **Event 11**: FileCreate (file creation time)
|
||||
- **Event 12/13/14**: Registry operations
|
||||
- **Event 22**: DNS query
|
||||
|
||||
---
|
||||
|
||||
## Regshot - Registry Comparison
|
||||
|
||||
### Workflow
|
||||
1. Download from: https://sourceforge.net/projects/regshot/
|
||||
2. Run Regshot
|
||||
3. Click "1st shot" (before malware execution)
|
||||
4. Execute malware
|
||||
5. Wait for all malware activity to complete
|
||||
6. Click "2nd shot" (after malware execution)
|
||||
7. Click "Compare"
|
||||
8. Output saved as HTML (registry_changes.html)
|
||||
|
||||
### Output Interpretation
|
||||
- **Keys added**: New persistence mechanisms
|
||||
- **Keys deleted**: Cleanup activities
|
||||
- **Values modified**: Configuration changes
|
||||
- **Values added**: New settings
|
||||
|
||||
### Pro Tips
|
||||
- Scan selected folders only for faster scans
|
||||
- Use during specific malware phases (install vs. runtime)
|
||||
- Export both shots for archival
|
||||
|
||||
---
|
||||
|
||||
## TCPView - Real-Time Network Connections
|
||||
|
||||
### Setup
|
||||
1. Download from: https://docs.microsoft.com/en-us/sysinternals/downloads/tcpview
|
||||
2. Run as Administrator
|
||||
3. Shows all active TCP/UDP connections
|
||||
|
||||
### Features
|
||||
- Real-time connection monitoring
|
||||
- Color-coded (green = new, red = closed)
|
||||
- Right-click → Close Connection (for testing)
|
||||
- Right-click → Whois (lookup IP)
|
||||
|
||||
### Save Output
|
||||
```
|
||||
File → Save → CSV
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Noriben - Automated Procmon Analysis
|
||||
|
||||
### Setup
|
||||
```bash
|
||||
# Download from: https://github.com/Rurik/Noriben
|
||||
git clone https://github.com/Rurik/Noriben.git
|
||||
cd Noriben
|
||||
```
|
||||
|
||||
### Usage
|
||||
```bash
|
||||
# Automated execution with Procmon
|
||||
python Noriben.py --cmd sample.exe --timeout 300
|
||||
|
||||
# Manual mode (execute malware yourself)
|
||||
python Noriben.py --output noriben_report.txt
|
||||
```
|
||||
|
||||
### Output
|
||||
- Filtered Procmon events
|
||||
- Suspicious behaviors highlighted
|
||||
- Timeline of activities
|
||||
- File, registry, network artifacts
|
||||
|
||||
---
|
||||
|
||||
## INetSim - Fake Internet Simulation
|
||||
|
||||
### Installation (Linux VM)
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install inetsim
|
||||
|
||||
# Configure
|
||||
sudo nano /etc/inetsim/inetsim.conf
|
||||
```
|
||||
|
||||
### Configuration
|
||||
```
|
||||
service_bind_address 0.0.0.0
|
||||
dns_default_ip 192.168.56.1 # INetSim host IP
|
||||
https_bind_port 443
|
||||
http_bind_port 80
|
||||
smtp_bind_port 25
|
||||
```
|
||||
|
||||
### Start INetSim
|
||||
```bash
|
||||
sudo inetsim
|
||||
```
|
||||
|
||||
### Configure Windows VM
|
||||
```
|
||||
Set DNS server to INetSim IP (192.168.56.1)
|
||||
Set gateway to INetSim IP (if using NAT)
|
||||
```
|
||||
|
||||
### Verify
|
||||
```cmd
|
||||
# From Windows VM
|
||||
ping google.com # Should resolve to INetSim IP
|
||||
nslookup microsoft.com # Should resolve to INetSim IP
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FakeNet-NG - Windows Internet Simulation
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
# Download from: https://github.com/mandiant/flare-fakenet-ng
|
||||
pip install flare-fakenet-ng
|
||||
```
|
||||
|
||||
### Usage
|
||||
```cmd
|
||||
# Run as Administrator
|
||||
fakenet.exe
|
||||
|
||||
# Execute malware (FakeNet intercepts all network)
|
||||
```
|
||||
|
||||
### Features
|
||||
- Simulates HTTP, HTTPS, DNS, FTP, SMTP
|
||||
- Captures all network traffic
|
||||
- No external VM needed
|
||||
- Logs all requests/responses
|
||||
|
||||
---
|
||||
|
||||
## Tool Startup Sequence
|
||||
|
||||
**Recommended Order:**
|
||||
1. Take VM snapshot
|
||||
2. Start INetSim (if using)
|
||||
3. Start Sysmon (already installed)
|
||||
4. Start Procmon (clear capture first)
|
||||
5. Start Wireshark
|
||||
6. Start System Informer
|
||||
7. Start Regshot → 1st shot (if using)
|
||||
8. Execute malware
|
||||
9. Monitor for 15+ minutes
|
||||
10. Regshot → 2nd shot (if using)
|
||||
11. Stop all captures
|
||||
12. Export all evidence
|
||||
13. Revert to snapshot
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference Card
|
||||
|
||||
| Tool | Purpose | When to Use |
|
||||
|------|---------|-------------|
|
||||
| **Procmon** | File/Registry/Process activity | Always |
|
||||
| **Wireshark** | Network traffic | Always |
|
||||
| **System Informer** | Process/Memory analysis | Always |
|
||||
| **Sysmon** | Windows event logging | Always (background) |
|
||||
| **Regshot** | Registry comparison | When need before/after snapshot |
|
||||
| **TCPView** | Real-time connections | Quick network checks |
|
||||
| **Noriben** | Automated Procmon | Time-constrained analysis |
|
||||
| **INetSim** | Fake internet | Always (network isolation) |
|
||||
| **FakeNet-NG** | Windows fake internet | Alternative to INetSim |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Procmon not capturing:**
|
||||
- Run as Administrator
|
||||
- Clear filters (Ctrl+R)
|
||||
- Check "Capture Events" is enabled
|
||||
|
||||
**Wireshark shows no traffic:**
|
||||
- Verify correct network adapter selected
|
||||
- Check malware actually made network requests
|
||||
- Verify VM network settings
|
||||
|
||||
**System Informer can't dump process:**
|
||||
- Run as Administrator
|
||||
- Process may be protected
|
||||
- Try x64dbg or WinDbg instead
|
||||
|
||||
**Sysmon not logging:**
|
||||
- Verify installation: `sc query sysmon64`
|
||||
- Check config file is valid XML
|
||||
- View logs: Event Viewer → Sysmon/Operational
|
||||
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
procmon_summary.py - condense a Procmon CSV export into a behavior summary.
|
||||
|
||||
Runs on the host against evidence exported from the analysis VM
|
||||
(Procmon: File -> Save -> All events -> CSV). Stdlib only.
|
||||
|
||||
Usage:
|
||||
python3 procmon_summary.py procmon.csv --process sample.exe
|
||||
python3 procmon_summary.py procmon.csv --all # no process filter
|
||||
python3 procmon_summary.py --self-test
|
||||
|
||||
--process takes a comma list of image names (case-insensitive). The summary
|
||||
covers those processes plus every descendant found via "Process Create".
|
||||
Without --process (or with --all) every process in the log is included.
|
||||
|
||||
Output sections: process tree, files written/renamed/deleted, registry writes
|
||||
(persistence keys flagged), network endpoints, non-system image loads.
|
||||
Pipe the output through scripts/ioc_extract.py to get defanged IOCs.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import io
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict, OrderedDict
|
||||
|
||||
PERSISTENCE_RE = re.compile(
|
||||
r"CurrentVersion\\(Run|RunOnce|RunServices|Explorer\\(Shell Folders|User Shell Folders))"
|
||||
r"|CurrentControlSet\\Services\\"
|
||||
r"|Winlogon\\(Shell|Userinit|Notify)"
|
||||
r"|Image File Execution Options"
|
||||
r"|Windows\\CurrentVersion\\Policies\\Explorer\\Run"
|
||||
r"|Schedule\\TaskCache"
|
||||
r"|Office\\.*\\Security"
|
||||
r"|AppInit_DLLs"
|
||||
r"|Start Menu\\Programs\\Startup",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
SYSTEM_DIR_RE = re.compile(r"^[A-Z]:\\Windows\\", re.IGNORECASE)
|
||||
PID_RE = re.compile(r"PID:\s*(\d+)")
|
||||
CMD_RE = re.compile(r"Command line:\s*(.*?)(?:,\s*Current directory:|$)", re.DOTALL)
|
||||
NEWFILE_RE = re.compile(r"OpenResult:\s*(Created|Overwritten|Superseded)")
|
||||
|
||||
|
||||
def read_text(path):
|
||||
"""Read exported evidence regardless of PowerShell's default encoding (UTF-16LE with BOM)."""
|
||||
raw = open(path, "rb").read()
|
||||
if raw.startswith((b"\xff\xfe", b"\xfe\xff")):
|
||||
return raw.decode("utf-16", errors="replace")
|
||||
return raw.decode("utf-8-sig", errors="replace")
|
||||
|
||||
|
||||
def load(path):
|
||||
return list(csv.DictReader(io.StringIO(read_text(path))))
|
||||
|
||||
|
||||
def field(row, *names):
|
||||
for n in names:
|
||||
if n in row and row[n] is not None:
|
||||
return row[n]
|
||||
return ""
|
||||
|
||||
|
||||
def build_tree(rows):
|
||||
"""Return (children: pid -> [pid], info: pid -> (name, cmdline))."""
|
||||
children = defaultdict(list)
|
||||
info = {}
|
||||
for r in rows:
|
||||
op = field(r, "Operation")
|
||||
if op == "Process Create":
|
||||
m = PID_RE.search(field(r, "Detail"))
|
||||
if not m:
|
||||
continue
|
||||
child = m.group(1)
|
||||
parent = field(r, "PID")
|
||||
children[parent].append(child)
|
||||
cmd = CMD_RE.search(field(r, "Detail"))
|
||||
# name the child from the created image path (the actor row's name is the parent)
|
||||
created = field(r, "Path")
|
||||
child_name = created.rsplit("\\", 1)[-1] if created else "?"
|
||||
info.setdefault(child, [child_name or "?", cmd.group(1).strip() if cmd else ""])
|
||||
elif op == "Process Start":
|
||||
pid = field(r, "PID")
|
||||
cmd = CMD_RE.search(field(r, "Detail"))
|
||||
info[pid] = [field(r, "Process Name"), cmd.group(1).strip() if cmd else ""]
|
||||
for r in rows: # fill names for processes seen only as actors
|
||||
pid = field(r, "PID")
|
||||
if pid and pid not in info:
|
||||
info[pid] = [field(r, "Process Name"), ""]
|
||||
elif pid in info and info[pid][0] == "?":
|
||||
info[pid][0] = field(r, "Process Name")
|
||||
return children, info
|
||||
|
||||
|
||||
def descendants(seeds, children):
|
||||
seen, stack = set(), list(seeds)
|
||||
while stack:
|
||||
p = stack.pop()
|
||||
if p in seen:
|
||||
continue
|
||||
seen.add(p)
|
||||
stack.extend(children.get(p, []))
|
||||
return seen
|
||||
|
||||
|
||||
def print_tree(pid, children, info, depth, out):
|
||||
name, cmd = info.get(pid, ("?", ""))
|
||||
out.append(f"{' ' * depth}{name} (PID {pid})" + (f" {cmd}" if cmd else ""))
|
||||
for c in children.get(pid, []):
|
||||
print_tree(c, children, info, depth + 1, out)
|
||||
|
||||
|
||||
def summarize(rows, process_names=None):
|
||||
children, info = build_tree(rows)
|
||||
if process_names:
|
||||
wanted = {n.lower() for n in process_names}
|
||||
seeds = [pid for pid, (name, _) in info.items() if name.lower() in wanted]
|
||||
scope = descendants(seeds, children)
|
||||
roots = seeds
|
||||
else:
|
||||
scope = set(info)
|
||||
all_children = {c for cs in children.values() for c in cs}
|
||||
roots = [p for p in info if p not in all_children]
|
||||
|
||||
files = OrderedDict()
|
||||
renames, deletes = OrderedDict(), OrderedDict()
|
||||
reg = OrderedDict()
|
||||
net = OrderedDict()
|
||||
loads = OrderedDict()
|
||||
counts = defaultdict(int)
|
||||
|
||||
for r in rows:
|
||||
pid = field(r, "PID")
|
||||
if pid not in scope:
|
||||
continue
|
||||
op, path, detail = field(r, "Operation"), field(r, "Path"), field(r, "Detail")
|
||||
who = f"{field(r, 'Process Name')}:{pid}"
|
||||
counts[who] += 1
|
||||
if op == "CreateFile" and NEWFILE_RE.search(detail):
|
||||
files.setdefault(path, who)
|
||||
elif op == "WriteFile":
|
||||
files.setdefault(path, who)
|
||||
elif op == "SetRenameInformationFile":
|
||||
m = re.search(r"FileName:\s*(.*)$", detail)
|
||||
renames.setdefault(f"{path} -> {m.group(1) if m else '?'}", who)
|
||||
elif op in ("SetDispositionInformationFile", "SetDispositionInformationEx") and "True" in detail:
|
||||
deletes.setdefault(path, who)
|
||||
elif op in ("RegSetValue", "RegDeleteValue", "RegDeleteKey") or (
|
||||
op == "RegCreateKey" and "REG_CREATED_NEW_KEY" in detail
|
||||
):
|
||||
data = ""
|
||||
m = re.search(r"Data:\s*(.*)$", detail)
|
||||
if m:
|
||||
data = m.group(1)
|
||||
reg.setdefault(f"{op} {path}" + (f" = {data}" if data else ""), who)
|
||||
elif op.startswith(("TCP", "UDP")) and "->" in path:
|
||||
dst = path.split("->", 1)[1].strip()
|
||||
net.setdefault(f"{op.split()[0]} {dst}", who)
|
||||
elif op == "Load Image" and not SYSTEM_DIR_RE.match(path):
|
||||
loads.setdefault(path, who)
|
||||
|
||||
out = [f"# Procmon summary ({len(rows)} events, {len(scope)} processes in scope)", "", "## Process tree"]
|
||||
for root in roots:
|
||||
print_tree(root, children, info, 0, out)
|
||||
|
||||
def section(title, items, limit=200):
|
||||
out.extend(["", f"## {title} ({len(items)})"])
|
||||
for i, (k, who) in enumerate(items.items()):
|
||||
if i >= limit:
|
||||
out.append(f"... {len(items) - limit} more")
|
||||
break
|
||||
out.append(f"- {k} [{who}]")
|
||||
|
||||
section("Files created/written", files)
|
||||
section("Files renamed", renames)
|
||||
section("Files deleted", deletes)
|
||||
out.extend(["", f"## Registry writes ({len(reg)})"])
|
||||
for k, who in reg.items():
|
||||
flag = " <-- PERSISTENCE" if PERSISTENCE_RE.search(k) else ""
|
||||
out.append(f"- {k} [{who}]{flag}")
|
||||
section("Network endpoints", net)
|
||||
section("Image loads outside C:\\Windows", loads)
|
||||
out.extend(["", "## Event count per process"])
|
||||
for who, n in sorted(counts.items(), key=lambda x: -x[1])[:20]:
|
||||
out.append(f"- {who}: {n}")
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
SELF_TEST_CSV = '''"Time of Day","Process Name","PID","Operation","Path","Result","Detail"
|
||||
"1","explorer.exe","100","Process Create","C:\\Users\\Public\\sample.exe","SUCCESS","PID: 200, Command line: ""C:\\Users\\Public\\sample.exe"" /install"
|
||||
"2","sample.exe","200","Process Start","","SUCCESS","Parent PID: 100, Command line: ""C:\\Users\\Public\\sample.exe"" /install, Current directory: C:\\Users\\Public\\"
|
||||
"3","sample.exe","200","CreateFile","C:\\Users\\Bob\\AppData\\Roaming\\svc.exe","SUCCESS","Desired Access: Generic Write, OpenResult: Created"
|
||||
"4","sample.exe","200","RegSetValue","HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\svc","SUCCESS","Type: REG_SZ, Length: 60, Data: C:\\Users\\Bob\\AppData\\Roaming\\svc.exe"
|
||||
"5","sample.exe","200","TCP Connect","host.local:49700 -> 203.0.113.5:443","SUCCESS","Length: 0"
|
||||
"6","sample.exe","200","Process Create","C:\\Windows\\System32\\cmd.exe","SUCCESS","PID: 300, Command line: cmd.exe /c del sample.exe"
|
||||
"7","cmd.exe","300","SetDispositionInformationFile","C:\\Users\\Public\\sample.exe","SUCCESS","Delete: True"
|
||||
"8","chrome.exe","999","WriteFile","C:\\Users\\Bob\\cache.dat","SUCCESS","Offset: 0"
|
||||
'''
|
||||
|
||||
|
||||
def self_test():
|
||||
rows = list(csv.DictReader(io.StringIO(SELF_TEST_CSV)))
|
||||
out = summarize(rows, ["sample.exe"])
|
||||
assert "cmd.exe (PID 300)" in out and " cmd.exe" in out, "child not nested under sample"
|
||||
assert "svc.exe" in out and "PERSISTENCE" in out, "persistence run key not flagged"
|
||||
assert "203.0.113.5:443" in out, "network endpoint missing"
|
||||
assert "cache.dat" not in out, "unrelated process leaked into scope"
|
||||
assert "Files deleted (1)" in out, "deletion missing"
|
||||
print("self-test OK")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("csv", nargs="?")
|
||||
ap.add_argument("--process", help="comma list of image names to scope on (plus descendants)")
|
||||
ap.add_argument("--all", action="store_true", help="include every process")
|
||||
ap.add_argument("--self-test", action="store_true")
|
||||
a = ap.parse_args()
|
||||
if a.self_test:
|
||||
return self_test()
|
||||
if not a.csv:
|
||||
ap.error("csv path required")
|
||||
names = None if a.all or not a.process else [n.strip() for n in a.process.split(",")]
|
||||
print(summarize(load(a.csv), names))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
sysmon_summary.py - condense exported Sysmon events into a behavior summary.
|
||||
|
||||
Runs on the host against evidence exported from the analysis VM. Stdlib only.
|
||||
|
||||
Accepted inputs (auto-detected):
|
||||
* PowerShell Get-WinEvent ... | Select Id,TimeCreated,Message | ConvertTo-Json (array or single object)
|
||||
* PowerShell Get-WinEvent ... | Export-Csv (CSV with Id + Message)
|
||||
* evtx_dump --format jsonl (one {"Event":{"System":..,"EventData":..}} per line)
|
||||
|
||||
Usage:
|
||||
python3 sysmon_summary.py sysmon.json --process sample.exe
|
||||
python3 sysmon_summary.py sysmon.csv --all
|
||||
python3 sysmon_summary.py --self-test
|
||||
|
||||
Sections: process tree (EID 1), network (EID 3, beacon interval check), DNS (22),
|
||||
files created (11), registry writes (12/13/14, persistence flagged), remote
|
||||
threads (8), process access (10), non-Windows image loads (7), pipes (17/18),
|
||||
WMI (19/20/21), tampering (25). Pipe output through scripts/ioc_extract.py.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import statistics
|
||||
import sys
|
||||
from collections import defaultdict, OrderedDict
|
||||
from datetime import datetime
|
||||
|
||||
PERSISTENCE_RE = re.compile(
|
||||
r"CurrentVersion\\(Run|RunOnce|RunServices|Explorer\\(Shell Folders|User Shell Folders))"
|
||||
r"|CurrentControlSet\\Services\\|Winlogon\\(Shell|Userinit|Notify)|Image File Execution Options"
|
||||
r"|Schedule\\TaskCache|AppInit_DLLs|Start Menu\\Programs\\Startup",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
SYSTEM_DIR_RE = re.compile(r"^[A-Z]:\\Windows\\", re.IGNORECASE)
|
||||
|
||||
|
||||
def parse_message(msg):
|
||||
d = {}
|
||||
for line in str(msg).replace("\r", "").split("\n"):
|
||||
k, sep, v = line.partition(": ")
|
||||
if sep:
|
||||
d[k.strip()] = v.strip()
|
||||
return d
|
||||
|
||||
|
||||
def read_text(path):
|
||||
"""Read exported evidence regardless of PowerShell's default encoding (UTF-16LE with BOM)."""
|
||||
raw = open(path, "rb").read()
|
||||
if raw.startswith((b"\xff\xfe", b"\xfe\xff")):
|
||||
return raw.decode("utf-16", errors="replace")
|
||||
return raw.decode("utf-8-sig", errors="replace")
|
||||
|
||||
|
||||
def load(path):
|
||||
"""Yield (event_id:int, data:dict) regardless of export format."""
|
||||
text = read_text(path).lstrip("\ufeff")
|
||||
stripped = text.lstrip()
|
||||
if stripped.startswith(("[", "{")):
|
||||
try:
|
||||
obj = json.loads(text)
|
||||
records = obj if isinstance(obj, list) else [obj]
|
||||
except json.JSONDecodeError: # jsonl
|
||||
records = [json.loads(l) for l in text.splitlines() if l.strip()]
|
||||
for r in records:
|
||||
if "Event" in r: # evtx_dump
|
||||
sysm = r["Event"].get("System", {})
|
||||
eid = sysm.get("EventID")
|
||||
eid = eid.get("#text", eid) if isinstance(eid, dict) else eid
|
||||
data = dict(r["Event"].get("EventData") or {})
|
||||
yield int(eid), data
|
||||
else:
|
||||
yield int(r.get("Id", 0)), parse_message(r.get("Message", ""))
|
||||
else:
|
||||
for r in csv.DictReader(io.StringIO(text)):
|
||||
yield int(r.get("Id") or 0), parse_message(r.get("Message", ""))
|
||||
|
||||
|
||||
def beacon_stats(times):
|
||||
ts = sorted(t for t in times if t)
|
||||
if len(ts) < 5:
|
||||
return ""
|
||||
gaps = [(b - a).total_seconds() for a, b in zip(ts, ts[1:])]
|
||||
mean = statistics.mean(gaps)
|
||||
if mean <= 0:
|
||||
return ""
|
||||
cv = statistics.pstdev(gaps) / mean
|
||||
tag = " <-- BEACON-LIKE" if cv < 0.3 else ""
|
||||
return f" [{len(ts)} conns, mean gap {mean:.0f}s, cv {cv:.2f}]{tag}"
|
||||
|
||||
|
||||
def parse_time(s):
|
||||
try:
|
||||
return datetime.strptime(s[:19], "%Y-%m-%d %H:%M:%S")
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def summarize(events, process_names=None):
|
||||
events = list(events)
|
||||
children, info = defaultdict(list), {}
|
||||
for eid, d in events:
|
||||
if eid == 1:
|
||||
pid, ppid = d.get("ProcessId", "?"), d.get("ParentProcessId", "?")
|
||||
info[pid] = (d.get("Image", "?"), d.get("CommandLine", ""))
|
||||
info.setdefault(ppid, (d.get("ParentImage", "?"), d.get("ParentCommandLine", "")))
|
||||
children[ppid].append(pid)
|
||||
if process_names:
|
||||
wanted = {n.lower() for n in process_names}
|
||||
seeds = [p for p, (img, _) in info.items() if img.split("\\")[-1].lower() in wanted]
|
||||
scope, stack = set(), list(seeds)
|
||||
while stack:
|
||||
p = stack.pop()
|
||||
if p not in scope:
|
||||
scope.add(p)
|
||||
stack.extend(children.get(p, []))
|
||||
roots = seeds
|
||||
else:
|
||||
scope = None
|
||||
all_children = {c for cs in children.values() for c in cs}
|
||||
roots = [p for p in info if p not in all_children]
|
||||
|
||||
def in_scope(d):
|
||||
return scope is None or d.get("ProcessId") in scope or d.get("SourceProcessId") in scope
|
||||
|
||||
net, net_times = OrderedDict(), defaultdict(list)
|
||||
dns, files, reg, threads, access, loads, pipes, wmi, tamper = (OrderedDict() for _ in range(9))
|
||||
for eid, d in events:
|
||||
if not in_scope(d):
|
||||
continue
|
||||
img = d.get("Image", d.get("SourceImage", "?")).split("\\")[-1]
|
||||
if eid == 3:
|
||||
key = f"{d.get('Protocol', '?')} {d.get('DestinationIp', '?')}:{d.get('DestinationPort', '?')}" + (
|
||||
f" ({d['DestinationHostname']})" if d.get("DestinationHostname") not in (None, "", "-") else ""
|
||||
)
|
||||
net.setdefault(key, img)
|
||||
net_times[key].append(parse_time(d.get("UtcTime", "")))
|
||||
elif eid == 22:
|
||||
dns.setdefault(f"{d.get('QueryName', '?')} -> {d.get('QueryResults', '')}", img)
|
||||
elif eid == 11:
|
||||
files.setdefault(d.get("TargetFilename", "?"), img)
|
||||
elif eid in (12, 13, 14):
|
||||
key = f"{d.get('EventType', '')} {d.get('TargetObject', '?')}" + (
|
||||
f" = {d['Details']}" if d.get("Details") else ""
|
||||
)
|
||||
reg.setdefault(key, img)
|
||||
elif eid == 8:
|
||||
threads.setdefault(f"{d.get('SourceImage')} -> {d.get('TargetImage')} @ {d.get('StartAddress')}", img)
|
||||
elif eid == 10:
|
||||
access.setdefault(f"{d.get('SourceImage')} -> {d.get('TargetImage')} access {d.get('GrantedAccess')}", img)
|
||||
elif eid == 7 and not SYSTEM_DIR_RE.match(d.get("ImageLoaded", "")):
|
||||
loads.setdefault(f"{d.get('ImageLoaded')} signed={d.get('Signed', '?')}", img)
|
||||
elif eid in (17, 18):
|
||||
pipes.setdefault(f"{'created' if eid == 17 else 'connected'} {d.get('PipeName')}", img)
|
||||
elif eid in (19, 20, 21):
|
||||
wmi.setdefault(f"EID{eid} {d.get('Name', '')} {d.get('Query', '')} {d.get('Destination', '')}".strip(), img)
|
||||
elif eid == 25:
|
||||
tamper.setdefault(f"{d.get('Image')} {d.get('Type', '')}", img)
|
||||
|
||||
out = [f"# Sysmon summary ({len(events)} events)", "", "## Process tree (EID 1)"]
|
||||
|
||||
def tree(pid, depth):
|
||||
img, cmd = info.get(pid, ("?", ""))
|
||||
out.append(f"{' ' * depth}{img} (PID {pid})" + (f" {cmd}" if cmd else ""))
|
||||
for c in children.get(pid, []):
|
||||
tree(c, depth + 1)
|
||||
|
||||
for r in roots:
|
||||
tree(r, 0)
|
||||
|
||||
def section(title, items, extra=None):
|
||||
out.extend(["", f"## {title} ({len(items)})"])
|
||||
for k, who in items.items():
|
||||
out.append(f"- {k} [{who}]" + (extra(k) if extra else ""))
|
||||
|
||||
section("Network connections (EID 3)", net, lambda k: beacon_stats(net_times[k]))
|
||||
section("DNS queries (EID 22)", dns)
|
||||
section("Files created (EID 11)", files)
|
||||
section("Registry writes (EID 12/13/14)", reg, lambda k: " <-- PERSISTENCE" if PERSISTENCE_RE.search(k) else "")
|
||||
section("Remote threads (EID 8)", threads)
|
||||
section("Process access (EID 10)", access)
|
||||
section("Image loads outside C:\\Windows (EID 7)", loads)
|
||||
section("Named pipes (EID 17/18)", pipes)
|
||||
section("WMI activity (EID 19/20/21)", wmi)
|
||||
section("Process tampering (EID 25)", tamper)
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
SELF_TEST = [
|
||||
{"Id": 1, "Message": "Process Create:\nUtcTime: 2026-01-01 10:00:00.000\nProcessId: 200\nImage: C:\\Users\\Public\\sample.exe\nCommandLine: sample.exe /install\nParentProcessId: 100\nParentImage: C:\\Windows\\explorer.exe\nParentCommandLine: explorer.exe"},
|
||||
{"Id": 1, "Message": "Process Create:\nProcessId: 300\nImage: C:\\Windows\\System32\\cmd.exe\nCommandLine: cmd /c whoami\nParentProcessId: 200\nParentImage: C:\\Users\\Public\\sample.exe"},
|
||||
{"Id": 13, "Message": "Registry value set:\nEventType: SetValue\nProcessId: 200\nImage: C:\\Users\\Public\\sample.exe\nTargetObject: HKU\\S-1\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\svc\nDetails: C:\\Users\\Bob\\AppData\\Roaming\\svc.exe"},
|
||||
{"Id": 11, "Message": "File created:\nProcessId: 999\nImage: C:\\Program Files\\Chrome\\chrome.exe\nTargetFilename: C:\\Users\\Bob\\cache.dat"},
|
||||
] + [
|
||||
{"Id": 3, "Message": f"Network connection detected:\nUtcTime: 2026-01-01 10:0{i}:00.000\nProcessId: 200\nImage: C:\\Users\\Public\\sample.exe\nProtocol: tcp\nDestinationIp: 203.0.113.5\nDestinationPort: 443\nDestinationHostname: -"}
|
||||
for i in range(6)
|
||||
]
|
||||
|
||||
|
||||
def self_test():
|
||||
import tempfile, os
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as f:
|
||||
json.dump(SELF_TEST, f)
|
||||
try:
|
||||
out = summarize(load(f.name), ["sample.exe"])
|
||||
finally:
|
||||
os.unlink(f.name)
|
||||
assert " C:\\Windows\\System32\\cmd.exe (PID 300)" in out, "child not nested"
|
||||
assert "PERSISTENCE" in out, "run key not flagged"
|
||||
assert "BEACON-LIKE" in out, "regular 60s connections not flagged"
|
||||
assert "cache.dat" not in out, "out-of-scope process leaked"
|
||||
print("self-test OK")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("path", nargs="?")
|
||||
ap.add_argument("--process", help="comma list of image names to scope on (plus descendants)")
|
||||
ap.add_argument("--all", action="store_true")
|
||||
ap.add_argument("--self-test", action="store_true")
|
||||
a = ap.parse_args()
|
||||
if a.self_test:
|
||||
return self_test()
|
||||
if not a.path:
|
||||
ap.error("path required")
|
||||
names = None if a.all or not a.process else [n.strip() for n in a.process.split(",")]
|
||||
print(summarize(load(a.path), names))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user