reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
---
|
||||
name: malware-triage
|
||||
description: Rapid static assessment, classification, and prioritization of malware samples. Use for the first look at any unknown file — hashes, file type, packing, imports, strings, initial IOCs, threat level, and the decision on which deep-analysis phase comes next. Claude runs the static tooling on the host itself; the sample is never executed.
|
||||
---
|
||||
|
||||
# Malware Triage
|
||||
|
||||
First phase for every sample. You run static tools on the host, interpret the output, classify the sample, and decide the next phase. Target: 5-15 minutes per sample.
|
||||
|
||||
## Execution Model
|
||||
|
||||
- **You run the commands.** You have Bash. Execute every command in this skill yourself and interpret the output. Do not ask the user to run tools or paste results unless a tool is missing and cannot be installed.
|
||||
- **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`.
|
||||
- **Never execute the sample.** Only static tools touch it: `file`, `strings`, `pe_info.py`, `upx -d`, `7z l`, hashing. No `chmod +x`, no `wine`, no `./sample`.
|
||||
- **Never upload the sample** to any service without explicit consent. Hash lookups are fine; file uploads are not.
|
||||
- **Script paths** are relative to this skill directory. From the repo/skill root: `malware-triage/scripts/pe_info.py`, `malware-triage/scripts/hash_calculator.py`, `scripts/ioc_extract.py`. Resolve them from wherever this SKILL.md lives.
|
||||
- **Tool check first.** Run once per session; note what is missing and degrade rather than stop:
|
||||
```bash
|
||||
command -v file strings upx 7z yara; python3 -c "import pefile" 2>&1 | tail -1
|
||||
# pefile missing -> pip install pefile (needed by pe_info.py)
|
||||
```
|
||||
- **Big outputs:** never `cat` a strings dump. Save to a file, `wc -l`, then `grep`/`head`.
|
||||
- **Ask the user only for:** the sample path, context (where it came from, incident ID, anything already known), and reputation results if no MCP server is available.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1 — Identify and hash
|
||||
|
||||
```bash
|
||||
S=/path/to/sample
|
||||
file "$S"; ls -l "$S"
|
||||
python3 malware-triage/scripts/hash_calculator.py "$S"
|
||||
```
|
||||
|
||||
Record MD5/SHA1/SHA256, size, `file` output. Then route on the `file` output:
|
||||
|
||||
| `file` says | Action |
|
||||
|-------------|--------|
|
||||
| `Mono/.Net assembly` | Record hashes, hand back to orchestrator → `specialized-file-analyzer` (check this **before** PE) |
|
||||
| `Microsoft Office`, `PDF document`, `ELF`, `MS Windows shortcut`, `Zip/RAR/7-zip`, `ISO 9660`, `Microsoft Disk Image`, `HTML document`, script/text | Record hashes, hand back to orchestrator → `specialized-file-analyzer` |
|
||||
| `PE32` / `PE32+` (not .NET) | Continue below |
|
||||
| `data` / unrecognized | Continue at **Unknown Files** |
|
||||
|
||||
### Step 2 — Reputation
|
||||
|
||||
- **MCP available** (VirusTotal, Threat Intel / MalwareBazaar): look up the SHA256. Record detection ratio, family labels, first-seen date, tags. Look up any imphash and section hashes too.
|
||||
- **No MCP:** give the user the SHA256 and ask them to check VirusTotal / MalwareBazaar while you continue. Do not block on it. Zero detections on a fresh hash means nothing; 0 detections on an old hash is informative.
|
||||
- Reputation is a hint, not the verdict. Static findings override AV labels.
|
||||
|
||||
### Step 3 — PE static summary
|
||||
|
||||
```bash
|
||||
python3 malware-triage/scripts/pe_info.py "$S" # add --json for structured output
|
||||
```
|
||||
|
||||
Read every section of the output. What matters:
|
||||
|
||||
| Output | Interpretation |
|
||||
|--------|----------------|
|
||||
| `.NET: YES` | Stop, route to specialized-file-analyzer |
|
||||
| Compile timestamp in the future / before 2000 | Forged; ignore for dating. Rich header linker info is harder to fake |
|
||||
| Overall entropy > 7.0, or executable section > 6.8, or `!` markers | Packed/encrypted — see **Packed Samples** |
|
||||
| Entry point outside `.text`/`CODE`, or in last section | Packer stub or appended code |
|
||||
| Import count < 10, or only `LoadLibrary`/`GetProcAddress`/`VirtualAlloc` | Runtime API resolution — packed or deliberately hiding imports |
|
||||
| Suspicious import categories (injection, keylogging, network, anti-debug, crypto, persistence) | Map each to a capability; cross-check with `references/indicators.md` |
|
||||
| Overlay present | Appended data: config, second-stage payload, or installer archive. Carve it: `python3 -c "..."` using the reported offset, then `file` the result |
|
||||
| Signature present | Verify claimed signer; revoked/self-signed/mismatched = red flag. Unsigned is neutral |
|
||||
| PDB path | Project/user names, build environment — high-value attribution string |
|
||||
| Resources with high entropy or large size | Embedded payload. Note type/size; extract in deep analysis |
|
||||
| TLS callbacks | Code runs before entry point — anti-debug or early unpacking |
|
||||
| Version info mimicking Microsoft/Adobe/etc. while unsigned | Masquerading |
|
||||
|
||||
### Step 4 — Strings and IOCs
|
||||
|
||||
```bash
|
||||
W=triage_$(basename "$S") # work dir
|
||||
mkdir -p "$W"
|
||||
strings -a -n 6 "$S" > "$W/strings_ascii.txt"
|
||||
strings -a -n 6 -e l "$S" > "$W/strings_wide.txt" # UTF-16LE, essential for Windows binaries
|
||||
wc -l "$W"/strings_*.txt
|
||||
python3 scripts/ioc_extract.py "$W"/strings_*.txt # defanged IPs, domains, URLs, emails, hashes, reg keys, paths
|
||||
```
|
||||
|
||||
Then targeted greps (adjust after reading the ioc_extract output):
|
||||
|
||||
```bash
|
||||
grep -ihE 'HKEY_|HKLM|HKCU|CurrentVersion\\Run|schtasks|sc create|netsh|vssadmin|bcdedit|wmic' "$W"/strings_*.txt | sort -u | head -40
|
||||
grep -ihE 'user-agent|mozilla/|POST |GET |Content-Type|\.php|\.onion|/gate|/panel' "$W"/strings_*.txt | sort -u | head -40
|
||||
grep -ihE 'mutex|Global\\|Local\\|\\pipe\\' "$W"/strings_*.txt | sort -u | head -20
|
||||
grep -ihE 'IsDebuggerPresent|VMware|VBox|vbox|QEMU|Xen|sandbox|SbieDll|wireshark|procmon|ollydbg|x64dbg' "$W"/strings_*.txt | sort -u
|
||||
grep -ihE 'bitcoin|monero|\.onion|decrypt|ransom|your files|README' "$W"/strings_*.txt | sort -u | head -20
|
||||
grep -ihE '\.exe|\.dll|\.bat|\.ps1|\.vbs|\.tmp' "$W"/strings_*.txt | grep -viE 'kernel32|ntdll|user32|advapi32|msvcr|api-ms-win' | sort -u | head -40
|
||||
```
|
||||
|
||||
Note what is **absent** too: a binary with network imports but no URL/IP strings is decrypting its config at runtime.
|
||||
|
||||
Suspicious-API and string categories: `references/indicators.md`.
|
||||
|
||||
### Packed Samples
|
||||
|
||||
Triggered by Step 3 entropy/import findings.
|
||||
|
||||
| Packer | Indicators | Unpack on host? |
|
||||
|--------|------------|-----------------|
|
||||
| UPX | `UPX0`/`UPX1` sections, `UPX!` in overlay | Yes: `upx -d "$S" -o "$W/unpacked.exe"` |
|
||||
| Themida / WinLicense | `.themida`, `.winlicense` sections, huge protected section | No — dynamic analysis |
|
||||
| VMProtect | `.vmp0`/`.vmp1` | No — dynamic analysis |
|
||||
| ASPack / MPRESS / Petite / Enigma / Upack | named sections (`.aspack`, `.MPRESS1`, `.petite`, `.enigma1`, `.Upack`) | Rarely — dynamic analysis |
|
||||
| Custom / unknown | high entropy, few imports, no known section names | No — dynamic analysis |
|
||||
|
||||
If UPX unpacking succeeds: hash the unpacked file, re-run Steps 3-4 on it, record both hashes (packed = delivery artifact, unpacked = what matches families). If `upx -d` fails, the UPX header was tampered; treat as custom.
|
||||
|
||||
Set sophistication: known packer → at least Moderate; commercial protector → Advanced; custom packer → Advanced/APT. Add "requires dynamic analysis to reveal imports/strings" to next steps.
|
||||
|
||||
### Step 5 — Classify
|
||||
|
||||
Combine imports, strings, reputation, and context. Consult `references/indicators.md` for the patterns.
|
||||
|
||||
| Type | Typical evidence |
|
||||
|------|------------------|
|
||||
| Trojan / RAT / Backdoor | socket or WinHTTP + command dispatch strings, screen/keyboard APIs, persistence |
|
||||
| Ransomware | Crypt* APIs, file enumeration, `vssadmin delete shadows`, ransom-note strings, wallet addresses |
|
||||
| Infostealer | browser profile paths (`Login Data`, `cookies.sqlite`), wallet dirs, `CryptUnprotectData`, exfil URL |
|
||||
| Dropper / Loader | small binary, high-entropy resource or overlay, `URLDownloadToFile`, `WriteFile` + `CreateProcess`, few other capabilities |
|
||||
| Cryptominer | pool URLs (`stratum+tcp://`), `xmrig`, high CPU-related config strings |
|
||||
| Worm | SMB/RPC APIs, network share enumeration, removable-drive checks |
|
||||
|
||||
**Threat level:** Critical (destructive, ransomware, APT) / High (data theft, full compromise) / Medium (limited capability, targeted) / Low (adware, PUP, commodity).
|
||||
**Sophistication:** Simple / Moderate / Advanced / APT-level, driven by protection, evasion, and custom code.
|
||||
**Verdict:** Malicious / Suspicious / Benign. Benign requires positive evidence (valid signature from a plausible vendor, expected imports for its stated purpose, reputation clean over time), not just absence of red flags.
|
||||
|
||||
### Step 6 — Predict behavior
|
||||
|
||||
Turn static evidence into a watch list for the dynamic phase. Each prediction names the evidence:
|
||||
|
||||
- **Process:** child processes or injection expected? Which targets (`CreateRemoteThread` + `OpenProcess` → injection; `CreateProcess` + `cmd`/`powershell` strings → LOLBin chain)?
|
||||
- **Files:** paths from strings; temp/AppData drops if `GetTempPath`/`SHGetFolderPath` imported.
|
||||
- **Registry:** Run keys, service creation, IFEO from strings/imports.
|
||||
- **Network:** protocol (WinHTTP/WinINet → HTTP(S); `socket`/`connect` → raw TCP; `DnsQuery` only → DNS tunnelling/DGA), hard-coded C2 vs runtime-decrypted, likely beacon.
|
||||
- **Persistence:** mechanism and location.
|
||||
- **Evasion:** anti-VM/anti-debug strings → plan for bypass (`malware-dynamic-analysis/references/anti_analysis_bypass.md`).
|
||||
|
||||
These predictions become the Procmon/Sysmon filters and the observation plan for `malware-dynamic-analysis`.
|
||||
|
||||
### Step 7 — Priority and next phase
|
||||
|
||||
| Priority | When |
|
||||
|----------|------|
|
||||
| **Immediate** | Unknown/undetected, incident-related, destructive capability, APT indicators, matches current threat intel |
|
||||
| **Standard** | Known family variant, commodity malware with existing coverage |
|
||||
| **Low** | Well-documented sample, adware/PUP, old, likely false positive |
|
||||
|
||||
Next phase: `malware-dynamic-analysis` for PE files that are packed, have runtime-resolved config, or need behavioral confirmation; `detection-engineer` + `malware-report-writer` directly when the sample is a known variant and static IOCs suffice; `Benign` closes the sample.
|
||||
|
||||
## Unknown Files
|
||||
|
||||
`file` returned `data` or something unhelpful:
|
||||
|
||||
```bash
|
||||
xxd "$S" | head -20 # magic bytes, structure
|
||||
strings -a -n 8 "$S" | head -50
|
||||
python3 - "$S" <<'EOF' # single-byte XOR brute force for a hidden PE
|
||||
import sys; b=open(sys.argv[1],'rb').read()
|
||||
for k in range(1,256):
|
||||
d=bytes(x^k for x in b[:4096])
|
||||
if d[:2]==b'MZ' or b'This program cannot' in d: print(f"XOR key 0x{k:02x} reveals PE header")
|
||||
EOF
|
||||
```
|
||||
|
||||
Also check: base64 blob (`[A-Za-z0-9+/]{100,}`), gzip/zlib magic (`1f 8b`, `78 9c`), reversed PE (`ZM` at end), shellcode (starts with `fc e8` / `e8 00 00 00 00` / `55 8b ec`). Decode what you find, `file` the result, and re-route.
|
||||
|
||||
## Batch Triage
|
||||
|
||||
For N samples, do a quick pass on all before deep-diving any:
|
||||
|
||||
```bash
|
||||
for f in samples/*; do
|
||||
echo "== $f"; file "$f"; sha256sum "$f"
|
||||
python3 malware-triage/scripts/pe_info.py "$f" 2>/dev/null | grep -E '^\.NET:|packed|imported functions'
|
||||
done
|
||||
```
|
||||
|
||||
Produce a ranking table (sample, type, packed, detections, priority, one-line reason), then run the full workflow on Immediate samples first.
|
||||
|
||||
## Output
|
||||
|
||||
1. **Print the triage report** (template below, filled in — no placeholders left).
|
||||
2. **Append to `analysis_state.md`** in the user's working directory using the orchestrator's structure: hashes, type, priority, classification, threat level, triage findings, IOCs (defanged), next-phase recommendation.
|
||||
3. **Recommend the next phase** with reasoning, then wait for the user.
|
||||
|
||||
```markdown
|
||||
## Triage Report — [filename]
|
||||
|
||||
**Hashes:** MD5 … / SHA1 … / SHA256 … **Size:** … bytes **Type:** … **Packed:** No | Yes (packer)
|
||||
**Reputation:** VT x/y, family label(s), first seen … | not checked
|
||||
|
||||
### Static Findings
|
||||
- Compile time / linker / signature / PDB
|
||||
- Sections + entropy (flag anomalies)
|
||||
- Suspicious imports by category → capability
|
||||
- Notable strings: URLs, IPs, paths, registry, mutex, UA, commands
|
||||
- Resources / overlay / TLS callbacks
|
||||
|
||||
### Classification
|
||||
**Verdict:** Malicious | Suspicious | Benign **Type:** … **Threat:** Critical | High | Medium | Low **Sophistication:** …
|
||||
**Capabilities:** …
|
||||
|
||||
### Predicted Behavior
|
||||
Process / Files / Registry / Network / Persistence / Evasion — each with the evidence behind it
|
||||
|
||||
### Initial IOCs (defanged)
|
||||
File: hashes, imphash, dropped names · Network: … · Host: registry, paths, mutex
|
||||
|
||||
### Recommendation
|
||||
**Priority:** … **Next phase:** … **Why:** …
|
||||
```
|
||||
|
||||
## Quality Gate
|
||||
|
||||
Before handing back: all three hashes; `file` type; packing status with evidence; imports categorized; strings run in both ASCII and UTF-16; IOCs extracted and defanged; classification with reasoning; predictions tied to evidence; state file updated.
|
||||
|
||||
## References
|
||||
|
||||
- `references/indicators.md` — suspicious API imports, string patterns, behavioral indicators, packer signatures, classification patterns
|
||||
- `references/triage_checklist.md` — full checklist, decision tree, dynamic go/no-go criteria, tool reference
|
||||
- `scripts/pe_info.py` — PE header/section/import/resource/overlay/signature summary (needs `pefile`)
|
||||
- `scripts/hash_calculator.py` — MD5/SHA1/SHA256
|
||||
- `../scripts/ioc_extract.py` — IOC extraction and defanging from any text
|
||||
@@ -0,0 +1,325 @@
|
||||
# Common Malware Indicators Reference
|
||||
|
||||
## Suspicious Import Functions
|
||||
|
||||
### Process Injection/Manipulation
|
||||
- `CreateRemoteThread` - Inject code into another process
|
||||
- `WriteProcessMemory` - Write to another process's memory
|
||||
- `VirtualAllocEx` - Allocate memory in another process
|
||||
- `OpenProcess` - Access another process
|
||||
- `NtUnmapViewOfSection` - Process hollowing technique
|
||||
- `SetThreadContext` - Modify thread execution
|
||||
- `ResumeThread` - Resume suspended thread (often after injection)
|
||||
- `QueueUserAPC` - APC injection technique
|
||||
|
||||
### Keylogging
|
||||
- `SetWindowsHookEx` - Install keyboard hooks
|
||||
- `GetAsyncKeyState` - Poll keyboard state
|
||||
- `GetForegroundWindow` - Get active window title
|
||||
- `GetKeyState` - Check key status
|
||||
|
||||
### Network Activity
|
||||
- `InternetOpen` / `InternetOpenUrl` - WinINet HTTP requests
|
||||
- `HttpSendRequest` - Send HTTP request
|
||||
- `send` / `recv` - Raw socket communication
|
||||
- `WSAStartup` - Initialize Winsock
|
||||
- `connect` / `socket` - Network connections
|
||||
- `URLDownloadToFile` - Download files from URLs
|
||||
|
||||
### Persistence Mechanisms
|
||||
- `RegSetValueEx` / `RegCreateKeyEx` - Registry manipulation
|
||||
- `CreateService` - Install Windows service
|
||||
- `StartService` - Start service
|
||||
- `ITaskService` / `ITaskFolder::RegisterTaskDefinition` - COM interfaces for scheduled task creation; `schtasks.exe /create` is the command-line equivalent
|
||||
|
||||
### Stealth/Anti-Analysis
|
||||
- `IsDebuggerPresent` - Debugger detection
|
||||
- `CheckRemoteDebuggerPresent` - Remote debugger check
|
||||
- `NtQueryInformationProcess` - Process information (anti-debug)
|
||||
- `GetTickCount` - Timing checks (anti-sandbox)
|
||||
- `Sleep` - Delay execution (sandbox evasion)
|
||||
- `FindWindow` - Look for analysis tools
|
||||
|
||||
### EDR/Telemetry Evasion
|
||||
- `EtwEventWrite` / `NtTraceEvent` - ETW patching target; writing 0xC3 (ret) to prologue blinds EDR telemetry
|
||||
- `AmsiScanBuffer` / `AmsiScanString` / `AmsiInitialize` - AMSI bypass targets; patched to return S_OK (0) with AMSI_RESULT_CLEAN (0), or to return E_INVALIDARG so the caller treats the result as no threat
|
||||
- `UpdateProcThreadAttribute` - PPID spoofing when called with PROC_THREAD_ATTRIBUTE_PARENT_PROCESS attribute
|
||||
- `VirtualProtect` - Required before patching; look for calls targeting ntdll.dll or amsi.dll memory ranges
|
||||
- `GetProcAddress` + `LoadLibrary` - Runtime API resolution to avoid static import evidence (common in syscall stubs)
|
||||
|
||||
### Callback-based Shellcode Execution
|
||||
- `EnumChildWindows` / `EnumDesktopWindows` - Callback used as execution primitive instead of CreateThread
|
||||
- `CreateTimerQueueTimer` - Timer callback used to invoke shellcode
|
||||
- `TpAllocWork` / `TpPostWork` / `TpReleaseWork` - Threadpool callbacks for shellcode dispatch
|
||||
- `SetTimer` / `EnumSystemLocales` / `EnumTimeFormatsEx` - Less common callback execution primitives
|
||||
|
||||
### File Manipulation
|
||||
- `CreateFile` / `WriteFile` / `ReadFile` - File I/O
|
||||
- `DeleteFile` - Delete files
|
||||
- `MoveFile` / `CopyFile` - File operations
|
||||
- `FindFirstFile` / `FindNextFile` - Directory enumeration
|
||||
|
||||
### Encryption/Encoding
|
||||
- `CryptEncrypt` / `CryptDecrypt` - Data encryption
|
||||
- `CryptAcquireContext` - Crypto provider setup
|
||||
- `CryptGenKey` - Generate encryption keys
|
||||
|
||||
## Suspicious String Patterns
|
||||
|
||||
### Network Indicators
|
||||
- Hardcoded IP addresses (especially RFC1918 or suspicious IPs)
|
||||
- Domain names (especially newly registered or suspicious TLDs)
|
||||
- URL patterns: `http://`, `https://`, `ftp://`
|
||||
- Port numbers in strings (especially non-standard ports)
|
||||
- User-Agent strings (especially custom/unusual ones)
|
||||
- Email addresses (especially for data exfiltration)
|
||||
|
||||
### File Paths
|
||||
- `%APPDATA%`, `%TEMP%`, `%LOCALAPPDATA%` - Common malware locations
|
||||
- `\Microsoft\Windows\Start Menu\Programs\Startup\` - Persistence
|
||||
- `C:\ProgramData\` - Hidden location
|
||||
- Hidden folders: `$Recycle.Bin`, `System Volume Information`
|
||||
|
||||
### Registry Keys
|
||||
**Persistence (Run Keys):**
|
||||
- `Software\Microsoft\Windows\CurrentVersion\Run`
|
||||
- `Software\Microsoft\Windows\CurrentVersion\RunOnce`
|
||||
- `SOFTWARE\Microsoft\Windows\CurrentVersion\RunServices`
|
||||
|
||||
**System Modification:**
|
||||
- `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon` (especially Userinit, Shell)
|
||||
- `SYSTEM\CurrentControlSet\Services` - Service installation
|
||||
- `SOFTWARE\Microsoft\Windows\CurrentVersion\policies` - Policy changes
|
||||
|
||||
### Commands and Scripts
|
||||
- PowerShell indicators:
|
||||
- `-EncodedCommand` - Base64 encoded commands
|
||||
- `-ExecutionPolicy Bypass` - Bypass script restrictions
|
||||
- `-WindowStyle Hidden` - Hide execution
|
||||
- `DownloadString`, `DownloadFile` - Download payloads
|
||||
- `IEX` (Invoke-Expression) - Execute code
|
||||
|
||||
- CMD indicators:
|
||||
- `cmd.exe /c` - Execute command
|
||||
- `schtasks` - Task scheduling
|
||||
- `reg add` - Registry manipulation
|
||||
- `netsh` - Network/firewall changes
|
||||
- `wmic` - WMI commands
|
||||
- `rundll32` - Execute DLL functions
|
||||
|
||||
### Encoding/Obfuscation
|
||||
- Base64 encoded strings (check for padding: `==`)
|
||||
- Hex encoded data
|
||||
- XOR key patterns
|
||||
- Reversed strings
|
||||
- Unicode/wide character strings
|
||||
|
||||
### Cryptocurrency
|
||||
- Bitcoin addresses (start with 1, 3, or bc1)
|
||||
- Ethereum addresses (0x followed by 40 hex chars)
|
||||
- Monero addresses
|
||||
- Wallet-related terms: "wallet.dat", "cryptocurrency"
|
||||
|
||||
### Ransomware Indicators
|
||||
- File extension strings: `.encrypted`, `.locked`, `.crypto`, etc.
|
||||
- Ransom note filenames: `README.txt`, `HOW_TO_DECRYPT.txt`
|
||||
- Payment terms: "bitcoin", "decrypt", "ransom", "payment"
|
||||
- Encryption terms: "AES", "RSA", "encrypted"
|
||||
|
||||
## Suspicious File Characteristics
|
||||
|
||||
### PE Header Anomalies
|
||||
- Compilation timestamp in the future
|
||||
- Compilation timestamp too old for file features
|
||||
- Mismatched architecture in headers
|
||||
- Unusual section names (normal: `.text`, `.data`, `.rdata`, `.rsrc`)
|
||||
- High entropy sections (>7.0 suggests encryption/packing)
|
||||
- Overlapping sections
|
||||
- Large overlay data
|
||||
- Missing or invalid digital signatures
|
||||
|
||||
### Entropy Analysis
|
||||
- **Low entropy (0-3):** Likely unencrypted, readable data
|
||||
- **Medium entropy (3-5):** Typical compiled code
|
||||
- **High entropy (7-8):** Packed, encrypted, or compressed
|
||||
- Check individual PE sections for high entropy
|
||||
|
||||
### Packer Signatures
|
||||
**Common packers:**
|
||||
- UPX (common, legitimate and malicious)
|
||||
- ASPack
|
||||
- PECompact
|
||||
- Themida/WinLicense (strong protection)
|
||||
- Armadillo
|
||||
- VMProtect
|
||||
- Enigma Protector
|
||||
- Custom/unknown packers (more suspicious)
|
||||
|
||||
### File Size Oddities
|
||||
- Very small for claimed functionality (<10KB for complex malware)
|
||||
- Unusually large resources section
|
||||
- Significant overlay data
|
||||
- File size doesn't match claimed type
|
||||
|
||||
## Behavioral Indicators
|
||||
|
||||
### Anti-Analysis Behaviors
|
||||
- Checks for VM artifacts (VMware, VirtualBox, QEMU)
|
||||
- Looks for debugger presence
|
||||
- Timing checks (execution delays)
|
||||
- Checks for analysis tools (Process Monitor, Wireshark, IDA)
|
||||
- Sandbox detection (limited resources, username checks)
|
||||
- Parent process verification
|
||||
- Mouse movement monitoring
|
||||
- Checks for user interaction
|
||||
|
||||
### Modern EDR/AV Evasion Techniques
|
||||
|
||||
**ETW Patching:**
|
||||
- Overwrites the prologue of `EtwEventWrite` or `NtTraceEvent` with a `ret` instruction (0xC3) to suppress Event Tracing for Windows telemetry
|
||||
- Look for: `VirtualProtect` → `WriteProcessMemory`/`memcpy` targeting ntdll address space, followed by restoration of original page protections
|
||||
- Effect: EDR and ETW-based logging goes blind for the process lifetime
|
||||
|
||||
**AMSI Bypass:**
|
||||
- Patches `AmsiScanBuffer` or `AmsiScanString` in amsi.dll to return S_OK (0) with `AMSI_RESULT_CLEAN` (0), or to return E_INVALIDARG so the caller treats the result as no threat; disables script content scanning
|
||||
- Look for: `LoadLibrary("amsi.dll")` + `GetProcAddress` + `VirtualProtect` + small write (1-8 bytes); or `AmsiInitialize` never called while PowerShell/script engine is used
|
||||
- Effect: Malicious PowerShell, VBScript, or JScript passes AMSI unchecked
|
||||
|
||||
**Direct Syscalls / SysWhispers:**
|
||||
- Bypasses EDR hooks placed on ntdll.dll exports by issuing raw syscall/sysenter instructions with the correct System Service Number (SSN)
|
||||
- Look for: `syscall` or `sysenter` instructions in non-system modules (use `objdump`/`dumpbin`); runtime SSN resolution via ntdll stub scanning (Hell's Gate, Halo's Gate, Tartarus Gate patterns); absence of ntdll imports for expected operations (no `NtAllocateVirtualMemory` import but memory is allocated)
|
||||
- Common SSNs targeted: NtAllocateVirtualMemory, NtWriteVirtualMemory, NtCreateThreadEx, NtProtectVirtualMemory
|
||||
|
||||
**PPID Spoofing:**
|
||||
- Creates a child process with a forged parent PID by passing a handle to the target parent via `UpdateProcThreadAttribute(PROC_THREAD_ATTRIBUTE_PARENT_PROCESS)` before calling `CreateProcess`
|
||||
- Look for: `OpenProcess` on an unrelated high-privilege process (e.g., explorer.exe, svchost.exe) immediately before `CreateProcess`; process tree in logs showing implausible parent-child relationships
|
||||
- Effect: Evades detections that trigger on suspicious parent-child pairs (e.g., Word spawning cmd.exe)
|
||||
|
||||
**Module Stomping / DLL Hollowing:**
|
||||
- Loads a legitimate, signed DLL into memory then overwrites its `.text` section with malicious shellcode, so execution appears to originate from a trusted module
|
||||
- Look for: `LoadLibrary` on an unexpected DLL followed by `VirtualProtect(PAGE_EXECUTE_READWRITE)` and `WriteProcessMemory`/`memcpy` into that DLL's base address; signed DLL mapped but exports resolve to unexpected code
|
||||
- Distinguish from process hollowing: the host process is not suspended — the stomped DLL simply runs attacker code under a legitimate module name
|
||||
|
||||
### Persistence Techniques
|
||||
- Registry Run key modification
|
||||
- Scheduled task creation
|
||||
- Service installation
|
||||
- WMI event subscriptions
|
||||
- DLL hijacking/search order hijacking
|
||||
- AppInit_DLLs registry key
|
||||
- Startup folder files
|
||||
- Browser extension installation
|
||||
|
||||
### Network Patterns
|
||||
- Beaconing (regular C2 communication)
|
||||
- HTTP with suspicious User-Agents
|
||||
- Raw socket connections
|
||||
- IRC protocols
|
||||
- DNS tunneling (unusual DNS queries)
|
||||
- TOR usage
|
||||
- Fast flux domains
|
||||
|
||||
### Data Exfiltration
|
||||
- Unusual outbound data volumes
|
||||
- Screenshots being taken
|
||||
- Credential harvesting
|
||||
- Browser data collection
|
||||
- Keylogger output transmission
|
||||
- File upload to external servers
|
||||
|
||||
## Red Flags by File Type
|
||||
|
||||
### Executable Files (.exe, .dll, .scr)
|
||||
- No digital signature or invalid signature
|
||||
- Packed/protected
|
||||
- Very few imports (possibly resolved at runtime)
|
||||
- Unusual compilation timestamp
|
||||
- Suspicious filename mimicking system files
|
||||
|
||||
### Script Files (.ps1, .vbs, .js, .bat)
|
||||
- Heavy obfuscation
|
||||
- Base64 encoded sections
|
||||
- Download and execute patterns
|
||||
- WMI usage for execution
|
||||
- Registry manipulation
|
||||
|
||||
### Office Documents (.doc, .xls, .docx, .xlsx)
|
||||
- Macros enabled
|
||||
- Auto-execute macro names (AutoOpen, Auto_Open, Workbook_Open)
|
||||
- External template references
|
||||
- Embedded objects
|
||||
- Suspicious OLE streams
|
||||
|
||||
### Archive Files (.zip, .rar, .7z)
|
||||
- Executable files with misleading extensions
|
||||
- Multi-layer nested archives
|
||||
- Password-protected archives (especially with generic passwords)
|
||||
- Suspicious filenames inside
|
||||
|
||||
## Quick Classification Patterns
|
||||
|
||||
### Ransomware
|
||||
- File encryption APIs
|
||||
- Many file I/O operations
|
||||
- File extension changes
|
||||
- Ransom note creation
|
||||
- Cryptocurrency addresses
|
||||
- File deletion after encryption
|
||||
|
||||
### Trojan/RAT
|
||||
- Network communication APIs
|
||||
- Remote control capabilities
|
||||
- Screen capture
|
||||
- File transfer
|
||||
- Command execution
|
||||
- Persistence mechanisms
|
||||
|
||||
### Infostealer
|
||||
- Browser data access
|
||||
- Credential harvesting
|
||||
- Clipboard monitoring
|
||||
- Form grabbing
|
||||
- Cookie stealing
|
||||
- Cryptocurrency wallet targeting
|
||||
|
||||
### Dropper/Loader
|
||||
- Embedded resources
|
||||
- Creates and executes files
|
||||
- Downloads additional payloads
|
||||
- Process injection
|
||||
- Minimal visible functionality
|
||||
|
||||
### Cryptominer
|
||||
- High CPU usage
|
||||
- Mining pool connections
|
||||
- Cryptocurrency-related strings
|
||||
- GPU access APIs
|
||||
- Persistence with hidden processes
|
||||
|
||||
## Tool Detection Strings
|
||||
|
||||
Malware often checks for these analysis tools:
|
||||
|
||||
**Debuggers:**
|
||||
- OllyDbg, x64dbg, IDA Pro, WinDbg
|
||||
- Immunity Debugger
|
||||
|
||||
**Monitors:**
|
||||
- Process Monitor (procmon), Process Explorer
|
||||
- Wireshark, Fiddler
|
||||
- TCPView, RegShot
|
||||
|
||||
**Sandboxes:**
|
||||
- Cuckoo, Joe Sandbox
|
||||
- Any.Run, Hybrid Analysis
|
||||
- VMRay, FireEye
|
||||
|
||||
**VM Artifacts:**
|
||||
- "VMware", "VirtualBox", "QEMU", "Hyper-V"
|
||||
- "vmmouse", "vmhgfs", "vboxguest"
|
||||
- Registry keys related to VMs
|
||||
|
||||
**Analysis Artifacts:**
|
||||
- Username: "sandbox", "malware", "virus", "sample"
|
||||
- Hostname: "VIRUS-PC", "MALWARE-ANALYSIS"
|
||||
- Limited resources (low RAM, CPU count)
|
||||
@@ -0,0 +1,252 @@
|
||||
# Malware Triage Checklist
|
||||
|
||||
## Initial Assessment Phase
|
||||
|
||||
### File Information Collection
|
||||
- [ ] Calculate all hashes (MD5, SHA1, SHA256)
|
||||
- [ ] Document original filename
|
||||
- [ ] Record file size
|
||||
- [ ] Identify file type (PE32/PE64/Script/Document/Archive)
|
||||
- [ ] Note file creation/modification timestamps
|
||||
- [ ] Check for file signatures (magic bytes)
|
||||
|
||||
### Reputation and Context
|
||||
- [ ] Search hash on VirusTotal
|
||||
- [ ] Search hash on MalwareBazaar
|
||||
- [ ] Search hash on Hybrid Analysis
|
||||
- [ ] Check if file is known good/bad
|
||||
- [ ] Review AV detection rates
|
||||
- [ ] Check submission dates and prevalence
|
||||
- [ ] Review any available sandbox reports
|
||||
|
||||
### Quick Static Indicators
|
||||
|
||||
#### PE Files
|
||||
- [ ] Check if file is packed/compressed (high entropy)
|
||||
- [ ] Identify packer if detected (UPX, ASPack, Themida, etc.)
|
||||
- [ ] Review PE sections (unusual names, high entropy sections)
|
||||
- [ ] Check compilation timestamp (realistic or fake)
|
||||
- [ ] Examine digital signature (signed/unsigned, valid/invalid)
|
||||
- [ ] Review import table for suspicious APIs
|
||||
- [ ] Check for overlay data
|
||||
- [ ] Examine resources (icons, version info, embedded files)
|
||||
|
||||
#### Script Files (PowerShell, JavaScript, VBS, etc.)
|
||||
- [ ] Check for obfuscation
|
||||
- [ ] Identify encoding methods (Base64, hex, etc.)
|
||||
- [ ] Look for download/execution commands
|
||||
- [ ] Check for embedded URLs or IPs
|
||||
- [ ] Review variable/function names (random = suspicious)
|
||||
|
||||
#### Office Documents
|
||||
- [ ] Check for macros
|
||||
- [ ] Examine embedded objects/OLE streams
|
||||
- [ ] Look for external references (templates, links)
|
||||
- [ ] Check for DDE/equation editor exploits
|
||||
- [ ] Review document metadata
|
||||
|
||||
### Strings Analysis
|
||||
- [ ] Extract readable strings
|
||||
- [ ] Look for URLs and IP addresses
|
||||
- [ ] Identify file paths
|
||||
- [ ] Find registry keys
|
||||
- [ ] Check for email addresses
|
||||
- [ ] Look for error messages or debug strings
|
||||
- [ ] Identify cryptocurrency wallet addresses
|
||||
- [ ] Find mutex names
|
||||
- [ ] Locate service/process names
|
||||
|
||||
### Behavior Prediction
|
||||
Based on initial findings, predict likely behaviors:
|
||||
- [ ] Network communication expected?
|
||||
- [ ] File creation/modification expected?
|
||||
- [ ] Registry changes expected?
|
||||
- [ ] Process injection expected?
|
||||
- [ ] Persistence mechanism type?
|
||||
|
||||
## Classification Phase
|
||||
|
||||
### Malware Type Determination
|
||||
- [ ] Trojan (general backdoor/RAT)
|
||||
- [ ] Ransomware (file encryption)
|
||||
- [ ] Worm (self-propagating)
|
||||
- [ ] Rootkit (stealth/hiding)
|
||||
- [ ] Botnet client
|
||||
- [ ] Infostealer (credentials, browser data)
|
||||
- [ ] Keylogger
|
||||
- [ ] Backdoor/RAT
|
||||
- [ ] Dropper/Loader
|
||||
- [ ] Cryptominer
|
||||
- [ ] Adware/PUP
|
||||
- [ ] Banking trojan
|
||||
- [ ] Unknown/Hybrid
|
||||
|
||||
### Sophistication Assessment
|
||||
- [ ] Simple (basic functionality, no obfuscation)
|
||||
- [ ] Moderate (some obfuscation, standard techniques)
|
||||
- [ ] Advanced (heavy obfuscation, anti-analysis)
|
||||
- [ ] APT-level (custom, targeted, advanced evasion)
|
||||
|
||||
### Threat Level
|
||||
Based on capabilities and target:
|
||||
- [ ] Critical (destructive, wide impact)
|
||||
- [ ] High (data theft, system compromise)
|
||||
- [ ] Medium (limited functionality, targeted)
|
||||
- [ ] Low (minimal impact, easily detected)
|
||||
|
||||
## Dynamic Analysis Decision
|
||||
|
||||
### Should I run dynamic analysis?
|
||||
**YES, if:**
|
||||
- Static analysis insufficient for understanding
|
||||
- Need to confirm predicted behaviors
|
||||
- Unknown/new sample requiring behavior observation
|
||||
- Investigating specific functionality
|
||||
- Creating behavioral signatures
|
||||
|
||||
**NO, if:**
|
||||
- Clearly identified as known malware
|
||||
- Destructive capabilities too risky
|
||||
- Time constraints for quick triage
|
||||
- Static analysis provides sufficient information
|
||||
|
||||
### Safe Execution Requirements
|
||||
If proceeding with dynamic analysis:
|
||||
- [ ] Isolated VM environment confirmed
|
||||
- [ ] Network isolation configured (or monitoring ready)
|
||||
- [ ] Snapshots taken
|
||||
- [ ] Monitoring tools prepared (Process Monitor, Wireshark, etc.)
|
||||
- [ ] Host system protected
|
||||
- [ ] Backup and recovery plan ready
|
||||
|
||||
## Priority Assessment
|
||||
|
||||
### Immediate Priority (Analyze Now)
|
||||
- Unknown samples with unclear threat level
|
||||
- Samples matching recent threat intelligence
|
||||
- Active incident-related samples
|
||||
- Samples with destructive capabilities
|
||||
- Targeted/APT-style malware
|
||||
|
||||
### Standard Priority (Normal Queue)
|
||||
- Known malware variants
|
||||
- Common commodity malware
|
||||
- Samples with clear signatures
|
||||
- Historical samples for research
|
||||
|
||||
### Low Priority (Defer if Needed)
|
||||
- Clearly identified samples
|
||||
- Adware/PUP with minimal impact
|
||||
- Old/outdated malware
|
||||
- Test files or false positives
|
||||
|
||||
## Initial Findings Summary Template
|
||||
|
||||
Use this template to document triage results:
|
||||
|
||||
```
|
||||
## Triage Summary
|
||||
|
||||
**Sample:** [filename]
|
||||
**Hashes:**
|
||||
- MD5: [hash]
|
||||
- SHA1: [hash]
|
||||
- SHA256: [hash]
|
||||
|
||||
**File Type:** [type]
|
||||
**Size:** [size]
|
||||
**Packed:** [Yes/No - Packer name]
|
||||
|
||||
**Initial Classification:**
|
||||
- Type: [Malware type]
|
||||
- Threat Level: [Critical/High/Medium/Low]
|
||||
- Sophistication: [Simple/Moderate/Advanced]
|
||||
|
||||
**Key Indicators:**
|
||||
- [Notable finding 1]
|
||||
- [Notable finding 2]
|
||||
- [Notable finding 3]
|
||||
|
||||
**Predicted Behaviors:**
|
||||
- [Expected behavior 1]
|
||||
- [Expected behavior 2]
|
||||
|
||||
**Recommendation:**
|
||||
[Proceed with full analysis / Quick report / Defer]
|
||||
|
||||
**Priority:** [Immediate/Standard/Low]
|
||||
|
||||
**Next Steps:**
|
||||
1. [Action 1]
|
||||
2. [Action 2]
|
||||
```
|
||||
|
||||
## Common Triage Pitfalls
|
||||
|
||||
### Don't:
|
||||
- Rush through hash calculation (always get all three)
|
||||
- Skip online searches (leverage community intelligence)
|
||||
- Assume packing means malicious (many legitimate software is packed)
|
||||
- Execute without proper isolation
|
||||
- Trust timestamps (easily forged)
|
||||
- Overlook string encoding (Base64, hex, unicode)
|
||||
- Ignore resource sections (often contain payloads)
|
||||
|
||||
### Do:
|
||||
- Document everything immediately
|
||||
- Use multiple analysis tools
|
||||
- Verify findings with different methods
|
||||
- Consider false positive possibilities
|
||||
- Check for packer signatures before deep analysis
|
||||
- Review import table carefully
|
||||
- Look for anti-analysis techniques
|
||||
- Consider file context (how obtained, where found)
|
||||
|
||||
## Time-Efficient Triage
|
||||
|
||||
For efficient time management:
|
||||
|
||||
**5-minute triage:**
|
||||
- Calculate hashes
|
||||
- Check online databases
|
||||
- Run strings
|
||||
- Check packer
|
||||
|
||||
**15-minute triage:**
|
||||
- Above +
|
||||
- PE header analysis
|
||||
- Import table review
|
||||
- Resource examination
|
||||
- Quick prediction
|
||||
|
||||
**30-minute triage:**
|
||||
- Above +
|
||||
- Detailed strings analysis
|
||||
- Preliminary YARA rule concepts
|
||||
- Behavior prediction writeup
|
||||
- Classification and prioritization
|
||||
|
||||
## Tools Quick Reference
|
||||
|
||||
**Hash Calculation:**
|
||||
- `scripts/hash_calculator.py`
|
||||
- CertUtil (Windows): `certutil -hashfile file.exe SHA256`
|
||||
- sha256sum (Linux): `sha256sum file.exe`
|
||||
|
||||
**Online Databases:**
|
||||
- VirusTotal: virustotal.com
|
||||
- MalwareBazaar: bazaar.abuse.ch
|
||||
- Hybrid Analysis: hybrid-analysis.com
|
||||
- Any.Run: any.run
|
||||
|
||||
**Static Analysis:**
|
||||
- PE analysis: pestudio, PEview, CFF Explorer
|
||||
- Strings: strings.exe, FLOSS
|
||||
- Hex editor: HxD, 010 Editor
|
||||
- Packer detection: Detect It Easy (DIE), Exeinfo PE
|
||||
|
||||
**Quick Sandboxing:**
|
||||
- Any.Run (interactive)
|
||||
- Hybrid Analysis
|
||||
- Joe Sandbox
|
||||
- Cuckoo (self-hosted)
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Quick hash calculator for malware samples.
|
||||
Calculates MD5, SHA1, and SHA256 hashes simultaneously.
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def calculate_hashes(file_path):
|
||||
"""
|
||||
Calculate MD5, SHA1, and SHA256 hashes for a file.
|
||||
|
||||
Args:
|
||||
file_path: Path to the file
|
||||
|
||||
Returns:
|
||||
dict: Dictionary containing all three hashes
|
||||
"""
|
||||
md5 = hashlib.md5()
|
||||
sha1 = hashlib.sha1()
|
||||
sha256 = hashlib.sha256()
|
||||
|
||||
try:
|
||||
with open(file_path, 'rb') as f:
|
||||
# Read in chunks for memory efficiency
|
||||
while chunk := f.read(8192):
|
||||
md5.update(chunk)
|
||||
sha1.update(chunk)
|
||||
sha256.update(chunk)
|
||||
|
||||
return {
|
||||
'md5': md5.hexdigest(),
|
||||
'sha1': sha1.hexdigest(),
|
||||
'sha256': sha256.hexdigest()
|
||||
}
|
||||
except (FileNotFoundError, PermissionError, IsADirectoryError, OSError) as e:
|
||||
return {'error': str(e)}
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python hash_calculator.py <file_path>")
|
||||
sys.exit(1)
|
||||
|
||||
file_path = Path(sys.argv[1])
|
||||
|
||||
if not file_path.exists():
|
||||
print(f"Error: File '{file_path}' not found")
|
||||
sys.exit(1)
|
||||
|
||||
if not file_path.is_file():
|
||||
print(f"Error: '{file_path}' is not a file")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Calculating hashes for: {file_path}")
|
||||
print(f"File size: {file_path.stat().st_size} bytes")
|
||||
print("-" * 70)
|
||||
|
||||
hashes = calculate_hashes(file_path)
|
||||
|
||||
if 'error' in hashes:
|
||||
print(f"Error calculating hashes: {hashes['error']}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"MD5: {hashes['md5']}")
|
||||
print(f"SHA1: {hashes['sha1']}")
|
||||
print(f"SHA256: {hashes['sha256']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,684 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
pe_info.py - one-shot static PE triage summary.
|
||||
|
||||
Runs entirely on the host against an exported/copied sample file. Never
|
||||
executes the sample. Prints a plain-text triage report (or JSON with
|
||||
--json) covering headers, sections, packing heuristics, imports/exports,
|
||||
signature, resources, overlay, PDB path, version info, TLS callbacks and
|
||||
rich header.
|
||||
|
||||
Usage:
|
||||
python3 pe_info.py <path-to-pe> [--json]
|
||||
|
||||
Requires: pefile (pip install pefile)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import hashlib
|
||||
import math
|
||||
import json
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
|
||||
try:
|
||||
import pefile
|
||||
except ImportError:
|
||||
print("pefile is required: pip install pefile", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
PACKER_SECTION_NAMES = {
|
||||
"UPX0", "UPX1", ".THEMIDA", ".VMP0", ".VMP1", ".ASPACK",
|
||||
".MPRESS1", ".ENIGMA1", ".PETITE", ".NSP0", ".UPACK",
|
||||
}
|
||||
|
||||
SUBSYSTEM_NAMES = {
|
||||
1: "NATIVE", 2: "GUI", 3: "CUI", 5: "OS2 CUI", 7: "POSIX CUI",
|
||||
9: "WINDOWS CE GUI", 10: "EFI APPLICATION", 11: "EFI BOOT SERVICE DRIVER",
|
||||
12: "EFI RUNTIME DRIVER", 13: "EFI ROM", 14: "XBOX",
|
||||
16: "WINDOWS BOOT APPLICATION",
|
||||
}
|
||||
|
||||
MACHINE_NAMES = {
|
||||
0x14c: "x86", 0x8664: "x64", 0xaa64: "ARM64",
|
||||
0x1c0: "ARM", 0x1c4: "ARM Thumb-2", 0x200: "IA64",
|
||||
}
|
||||
|
||||
SUSPICIOUS_CATEGORIES = {
|
||||
"injection": ["CreateRemoteThread", "WriteProcessMemory", "VirtualAllocEx",
|
||||
"OpenProcess", "NtUnmapViewOfSection", "SetThreadContext",
|
||||
"ResumeThread", "QueueUserAPC", "NtCreateThreadEx",
|
||||
"RtlCreateUserThread"],
|
||||
"keylogging": ["SetWindowsHookEx", "GetAsyncKeyState", "GetKeyState",
|
||||
"GetForegroundWindow"],
|
||||
"network": ["InternetOpen", "InternetOpenUrl", "InternetReadFile",
|
||||
"HttpOpenRequest", "HttpSendRequest", "URLDownloadToFile",
|
||||
"WSAStartup", "socket", "connect", "send", "recv",
|
||||
"WinHttpOpen", "WinHttpConnect", "DnsQuery", "getaddrinfo",
|
||||
"gethostbyname"],
|
||||
"persistence": ["RegSetValueEx", "RegCreateKeyEx", "CreateService",
|
||||
"StartService", "ChangeServiceConfig", "CoCreateInstance"],
|
||||
"anti-analysis": ["IsDebuggerPresent", "CheckRemoteDebuggerPresent",
|
||||
"NtQueryInformationProcess", "OutputDebugString",
|
||||
"GetTickCount", "QueryPerformanceCounter", "FindWindow",
|
||||
"GetLastInputInfo", "GetCursorPos", "GlobalMemoryStatusEx",
|
||||
"GetSystemInfo", "EnumProcesses", "CreateToolhelp32Snapshot",
|
||||
"Process32First", "Process32Next"],
|
||||
"evasion": ["UpdateProcThreadAttribute", "VirtualProtect", "EtwEventWrite",
|
||||
"AmsiScanBuffer", "AmsiScanString", "NtTraceEvent",
|
||||
"AddVectoredExceptionHandler"],
|
||||
"crypto": ["CryptEncrypt", "CryptDecrypt", "CryptAcquireContext",
|
||||
"CryptGenKey", "CryptHashData", "BCryptEncrypt", "BCryptDecrypt",
|
||||
"CryptImportKey"],
|
||||
"file-ops": ["CreateFile", "WriteFile", "DeleteFile", "MoveFile",
|
||||
"CopyFile", "FindFirstFile", "FindNextFile", "SetFileAttributes"],
|
||||
"execution": ["CreateProcess", "ShellExecute", "WinExec", "system",
|
||||
"CreateProcessAsUser", "CreateProcessWithLogon"],
|
||||
"callback-exec": ["EnumChildWindows", "EnumDesktopWindows",
|
||||
"CreateTimerQueueTimer", "EnumSystemLocales",
|
||||
"EnumTimeFormatsEx", "SetTimer", "TpAllocWork"],
|
||||
"dynamic-loading": ["LoadLibrary", "LoadLibraryEx", "GetProcAddress",
|
||||
"LdrLoadDll"],
|
||||
}
|
||||
|
||||
INJECTION_APIS = {n.lower() for n in SUSPICIOUS_CATEGORIES["injection"]}
|
||||
DYNLOAD_APIS = {n.lower() for n in SUSPICIOUS_CATEGORIES["dynamic-loading"]}
|
||||
|
||||
|
||||
def norm_api_name(name: str) -> str:
|
||||
"""Normalize a WinAPI import name for matching: strip stdcall decoration
|
||||
(@N / leading _), strip a trailing Ansi/Wide 'A'/'W' suffix, lowercase."""
|
||||
n = name.strip()
|
||||
if n.startswith("_"):
|
||||
n = n[1:]
|
||||
if "@" in n:
|
||||
n = n.split("@")[0]
|
||||
if len(n) > 1 and n[-1] in ("A", "W") and n[-2].isalpha():
|
||||
n = n[:-1]
|
||||
return n.lower()
|
||||
|
||||
|
||||
CATEGORY_LOOKUP = {}
|
||||
for _cat, _names in SUSPICIOUS_CATEGORIES.items():
|
||||
for _n in _names:
|
||||
CATEGORY_LOOKUP.setdefault(norm_api_name(_n), []).append(_cat)
|
||||
|
||||
|
||||
def shannon_entropy(data: bytes) -> float:
|
||||
if not data:
|
||||
return 0.0
|
||||
freq = [0] * 256
|
||||
for b in data:
|
||||
freq[b] += 1
|
||||
length = len(data)
|
||||
entropy = 0.0
|
||||
for f in freq:
|
||||
if f:
|
||||
p = f / length
|
||||
entropy -= p * math.log2(p)
|
||||
return entropy
|
||||
|
||||
|
||||
def hash_file(path: str):
|
||||
md5, sha1, sha256 = hashlib.md5(), hashlib.sha1(), hashlib.sha256()
|
||||
with open(path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(1 << 20), b""):
|
||||
md5.update(chunk)
|
||||
sha1.update(chunk)
|
||||
sha256.update(chunk)
|
||||
return md5.hexdigest(), sha1.hexdigest(), sha256.hexdigest()
|
||||
|
||||
|
||||
def decode(b):
|
||||
if b is None:
|
||||
return None
|
||||
if isinstance(b, bytes):
|
||||
return b.rstrip(b"\x00").decode(errors="replace")
|
||||
return str(b)
|
||||
|
||||
|
||||
class Report:
|
||||
"""Collects text lines + a parallel structured dict for --json."""
|
||||
|
||||
def __init__(self):
|
||||
self.lines = []
|
||||
self.data = {}
|
||||
|
||||
def h(self, title):
|
||||
self.lines.append("")
|
||||
self.lines.append(f"=== {title} ===")
|
||||
|
||||
def p(self, text=""):
|
||||
self.lines.append(text)
|
||||
|
||||
def fail(self, section, err):
|
||||
self.lines.append(f"section failed: {err}")
|
||||
self.data.setdefault("errors", {})[section] = str(err)
|
||||
|
||||
|
||||
def section_file_info(pe, path, rpt: Report):
|
||||
rpt.h("File")
|
||||
size = os.path.getsize(path)
|
||||
md5, sha1, sha256 = hash_file(path)
|
||||
with open(path, "rb") as f:
|
||||
data = f.read()
|
||||
entropy = shannon_entropy(data)
|
||||
try:
|
||||
imphash = pe.get_imphash()
|
||||
except Exception as e:
|
||||
imphash = f"n/a ({e})"
|
||||
|
||||
magic = pe.OPTIONAL_HEADER.Magic
|
||||
pe_type = "PE32+" if magic == 0x20B else "PE32" if magic == 0x10B else f"unknown (0x{magic:x})"
|
||||
machine_val = pe.FILE_HEADER.Machine
|
||||
machine = MACHINE_NAMES.get(machine_val, f"0x{machine_val:x}")
|
||||
subsys_val = pe.OPTIONAL_HEADER.Subsystem
|
||||
subsystem = SUBSYSTEM_NAMES.get(subsys_val, f"0x{subsys_val:x}")
|
||||
is_dll = pe.is_dll()
|
||||
kind = "DLL" if is_dll else "EXE"
|
||||
|
||||
com_dir = pe.OPTIONAL_HEADER.DATA_DIRECTORY[
|
||||
pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR"]
|
||||
]
|
||||
is_dotnet = com_dir.VirtualAddress != 0
|
||||
|
||||
ts = pe.FILE_HEADER.TimeDateStamp
|
||||
ts_flag = ""
|
||||
try:
|
||||
ts_dt = datetime.fromtimestamp(ts, tz=timezone.utc)
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
if ts_dt > now:
|
||||
ts_flag = " [FLAG: timestamp is in the future]"
|
||||
if is_dotnet:
|
||||
ts_flag += " (normal for .NET deterministic builds: field holds a hash, not a date)"
|
||||
elif ts_dt.year < 2000:
|
||||
ts_flag = " [FLAG: timestamp before year 2000]"
|
||||
ts_str = ts_dt.isoformat()
|
||||
except (OverflowError, OSError, ValueError):
|
||||
ts_str = f"invalid raw value 0x{ts:x}"
|
||||
ts_flag = " [FLAG: unparsable timestamp]"
|
||||
|
||||
rpt.p(f"Path: {path}")
|
||||
rpt.p(f"Size: {size} bytes")
|
||||
rpt.p(f"MD5: {md5}")
|
||||
rpt.p(f"SHA1: {sha1}")
|
||||
rpt.p(f"SHA256: {sha256}")
|
||||
rpt.p(f"Entropy: {entropy:.2f} (whole file)")
|
||||
rpt.p(f"Imphash: {imphash}")
|
||||
rpt.p(f"Format: {pe_type}, machine={machine}, subsystem={subsystem}, {kind}")
|
||||
rpt.p(f".NET: {'YES - route to specialized-file-analyzer' if is_dotnet else 'no'}")
|
||||
rpt.p(f"Compiled: {ts_str}{ts_flag}")
|
||||
rpt.p(f"Linker: {pe.OPTIONAL_HEADER.MajorLinkerVersion}.{pe.OPTIONAL_HEADER.MinorLinkerVersion}")
|
||||
|
||||
rpt.data["file"] = {
|
||||
"path": path, "size": size, "md5": md5, "sha1": sha1, "sha256": sha256,
|
||||
"entropy": round(entropy, 2), "imphash": imphash, "pe_type": pe_type,
|
||||
"machine": machine, "subsystem": subsystem, "is_dll": is_dll,
|
||||
"is_dotnet": is_dotnet, "compile_time_utc": ts_str,
|
||||
"linker_version": f"{pe.OPTIONAL_HEADER.MajorLinkerVersion}.{pe.OPTIONAL_HEADER.MinorLinkerVersion}",
|
||||
}
|
||||
return is_dotnet
|
||||
|
||||
|
||||
def section_sections(pe, rpt: Report):
|
||||
rpt.h("Sections")
|
||||
ep = pe.OPTIONAL_HEADER.AddressOfEntryPoint
|
||||
ep_section = pe.get_section_by_rva(ep)
|
||||
ep_name = decode(ep_section.Name) if ep_section else None
|
||||
|
||||
rows = []
|
||||
header = f"{'Name':<10} {'VirtSize':>10} {'RawSize':>10} {'Entropy':>8} {'Flags':>6} Notes"
|
||||
rpt.p(header)
|
||||
rpt.p("-" * len(header))
|
||||
for i, sec in enumerate(pe.sections):
|
||||
name = decode(sec.Name)
|
||||
vsize = sec.Misc_VirtualSize
|
||||
rsize = sec.SizeOfRawData
|
||||
try:
|
||||
ent = sec.get_entropy()
|
||||
except Exception:
|
||||
ent = 0.0
|
||||
chars = sec.Characteristics
|
||||
flags = ""
|
||||
flags += "R" if chars & 0x40000000 else "-"
|
||||
flags += "W" if chars & 0x80000000 else "-"
|
||||
flags += "X" if chars & 0x20000000 else "-"
|
||||
|
||||
notes = []
|
||||
packer_hit = name.strip().upper() in PACKER_SECTION_NAMES
|
||||
high_entropy = ent > 7.0
|
||||
if packer_hit or high_entropy:
|
||||
notes.append("!")
|
||||
if name == ep_name and ep_section is not None:
|
||||
notes.append("[ENTRY POINT]")
|
||||
rows.append({
|
||||
"name": name, "virtual_size": vsize, "raw_size": rsize,
|
||||
"entropy": round(ent, 2), "flags": flags,
|
||||
"packer_name_match": packer_hit, "high_entropy": high_entropy,
|
||||
})
|
||||
rpt.p(f"{name:<10} {vsize:>10} {rsize:>10} {ent:>8.2f} {flags:>6} {' '.join(notes)}")
|
||||
|
||||
if ep_section is None:
|
||||
rpt.p("WARNING: entry point RVA does not resolve to any section")
|
||||
else:
|
||||
if ep_name not in (".text", "CODE"):
|
||||
rpt.p(f"WARNING: entry point is in section '{ep_name}', not .text/CODE")
|
||||
if pe.sections and ep_section is pe.sections[-1]:
|
||||
rpt.p(f"WARNING: entry point is in the LAST section ('{ep_name}') - common packer stub pattern")
|
||||
|
||||
rpt.data["sections"] = rows
|
||||
return rows
|
||||
|
||||
|
||||
def section_packing_verdict(pe, rpt: Report, overall_entropy, sections, total_imports, kernel32_only_loader_pattern, is_dotnet=False):
|
||||
rpt.h("Packing verdict")
|
||||
reasons = []
|
||||
if overall_entropy > 7.0:
|
||||
reasons.append(f"overall file entropy {overall_entropy:.2f} > 7.0")
|
||||
exec_high = [s for s in sections if "X" in s["flags"] and s["entropy"] > 6.8]
|
||||
if exec_high:
|
||||
names = ", ".join(s["name"] for s in exec_high)
|
||||
reasons.append(f"executable section entropy > 6.8 in: {names}")
|
||||
if is_dotnet:
|
||||
rpt.p("Note: .NET assembly - import-table heuristics skipped (managed code resolves calls via CLR metadata).")
|
||||
else:
|
||||
if total_imports < 10:
|
||||
reasons.append(f"only {total_imports} imported functions total")
|
||||
if kernel32_only_loader_pattern:
|
||||
reasons.append("imports are only from KERNEL32 with a LoadLibrary/GetProcAddress/VirtualAlloc/VirtualProtect pattern")
|
||||
|
||||
if reasons:
|
||||
rpt.p("Likely packed. Reasons:")
|
||||
for r in reasons:
|
||||
rpt.p(f" - {r}")
|
||||
else:
|
||||
rpt.p("No strong packing indicators.")
|
||||
rpt.data["packing_verdict"] = {"likely_packed": bool(reasons), "reasons": reasons}
|
||||
|
||||
|
||||
def section_imports(pe, rpt: Report):
|
||||
rpt.h("Imports")
|
||||
total = 0
|
||||
category_hits = {}
|
||||
dll_names = set()
|
||||
kernel32_functions = set()
|
||||
all_dll_only_kernel32 = True
|
||||
|
||||
if hasattr(pe, "DIRECTORY_ENTRY_IMPORT"):
|
||||
for entry in pe.DIRECTORY_ENTRY_IMPORT:
|
||||
dll = decode(entry.dll) or "?"
|
||||
dll_names.add(dll.lower())
|
||||
if dll.lower() != "kernel32.dll":
|
||||
all_dll_only_kernel32 = False
|
||||
names = []
|
||||
for imp in entry.imports:
|
||||
total += 1
|
||||
fname = decode(imp.name) if imp.name else f"ordinal#{imp.ordinal}"
|
||||
names.append(fname)
|
||||
if dll.lower() == "kernel32.dll" and imp.name:
|
||||
kernel32_functions.add(norm_api_name(fname))
|
||||
if imp.name:
|
||||
cats = CATEGORY_LOOKUP.get(norm_api_name(fname))
|
||||
if cats:
|
||||
for c in cats:
|
||||
category_hits.setdefault(c, set()).add(fname)
|
||||
rpt.p(f"{dll} ({len(names)} functions):")
|
||||
rpt.p(" " + ", ".join(names))
|
||||
else:
|
||||
rpt.p("No import table.")
|
||||
|
||||
rpt.p("")
|
||||
rpt.p("Suspicious imports by category:")
|
||||
if category_hits:
|
||||
for cat in sorted(category_hits):
|
||||
names = ", ".join(sorted(category_hits[cat]))
|
||||
rpt.p(f" {cat}: {names}")
|
||||
else:
|
||||
rpt.p(" none matched")
|
||||
|
||||
loader_pattern = {"loadlibrary", "getprocaddress", "virtualalloc", "virtualprotect"}
|
||||
kernel32_loader_pattern = (
|
||||
dll_names == {"kernel32.dll"} and loader_pattern.issubset(kernel32_functions)
|
||||
)
|
||||
|
||||
# delay-load imports
|
||||
rpt.p("")
|
||||
rpt.p("Delay-load imports:")
|
||||
if hasattr(pe, "DIRECTORY_ENTRY_DELAY_IMPORT") and pe.DIRECTORY_ENTRY_DELAY_IMPORT:
|
||||
for entry in pe.DIRECTORY_ENTRY_DELAY_IMPORT:
|
||||
dll = decode(entry.dll) or "?"
|
||||
names = [decode(i.name) if i.name else f"ordinal#{i.ordinal}" for i in entry.imports]
|
||||
rpt.p(f" {dll} ({len(names)}): {', '.join(names)}")
|
||||
else:
|
||||
rpt.p(" none")
|
||||
|
||||
# exports
|
||||
rpt.p("")
|
||||
rpt.p("Exports:")
|
||||
exports = []
|
||||
if hasattr(pe, "DIRECTORY_ENTRY_EXPORT"):
|
||||
for sym in pe.DIRECTORY_ENTRY_EXPORT.symbols[:50]:
|
||||
exports.append(decode(sym.name) if sym.name else f"ordinal#{sym.ordinal}")
|
||||
rpt.p(" " + ", ".join(exports) if exports else " (export directory present, no symbols)")
|
||||
total_exports = len(pe.DIRECTORY_ENTRY_EXPORT.symbols)
|
||||
if total_exports > 50:
|
||||
rpt.p(f" ... ({total_exports} total, showing first 50)")
|
||||
else:
|
||||
rpt.p(" none")
|
||||
|
||||
rpt.data["imports"] = {
|
||||
"total_functions": total,
|
||||
"categories": {k: sorted(v) for k, v in category_hits.items()},
|
||||
"exports_count": len(exports),
|
||||
}
|
||||
return total, kernel32_loader_pattern
|
||||
|
||||
|
||||
def section_signature(pe, rpt: Report):
|
||||
rpt.h("Signature")
|
||||
sec_dir = pe.OPTIONAL_HEADER.DATA_DIRECTORY[
|
||||
pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_SECURITY"]
|
||||
]
|
||||
present = sec_dir.VirtualAddress != 0 and sec_dir.Size != 0
|
||||
rpt.p(f"Authenticode directory present: {'yes' if present else 'no'} (size={sec_dir.Size})")
|
||||
rpt.p("Not verifying signature here - verify with `osslsigncode verify` or `signtool verify`.")
|
||||
rpt.data["signature"] = {"present": present, "size": sec_dir.Size}
|
||||
|
||||
|
||||
def section_resources(pe, rpt: Report):
|
||||
rpt.h("Resources")
|
||||
if not hasattr(pe, "DIRECTORY_ENTRY_RESOURCE"):
|
||||
rpt.p("No resource directory.")
|
||||
rpt.data["resources"] = {"count": 0}
|
||||
return
|
||||
|
||||
leaves = []
|
||||
has_manifest = False
|
||||
|
||||
def type_name(entry):
|
||||
if entry.name is not None:
|
||||
return decode(entry.name.string) if hasattr(entry.name, "string") else str(entry.name)
|
||||
rid = entry.struct.Id
|
||||
name = None
|
||||
try:
|
||||
name = pefile.RESOURCE_TYPE.get(rid)
|
||||
except Exception:
|
||||
name = None
|
||||
return name or f"RT_UNKNOWN_{rid}"
|
||||
|
||||
for res_type in pe.DIRECTORY_ENTRY_RESOURCE.entries:
|
||||
tname = type_name(res_type)
|
||||
if tname == "RT_MANIFEST":
|
||||
has_manifest = True
|
||||
if not hasattr(res_type, "directory"):
|
||||
continue
|
||||
for res_id in res_type.directory.entries:
|
||||
if not hasattr(res_id, "directory"):
|
||||
continue
|
||||
for res_lang in res_id.directory.entries:
|
||||
if not hasattr(res_lang, "data"):
|
||||
continue
|
||||
rva = res_lang.data.struct.OffsetToData
|
||||
size = res_lang.data.struct.Size
|
||||
leaves.append((tname, rva, size))
|
||||
|
||||
rpt.p(f"Total resource entries: {len(leaves)}")
|
||||
rpt.p(f"MANIFEST present: {'yes' if has_manifest else 'no'}")
|
||||
type_counts = {}
|
||||
for tname, _, _ in leaves:
|
||||
type_counts[tname] = type_counts.get(tname, 0) + 1
|
||||
for tname, count in sorted(type_counts.items()):
|
||||
rpt.p(f" {tname}: {count}")
|
||||
|
||||
rpt.p("Entries larger than 4KB:")
|
||||
large_found = False
|
||||
large_entries = []
|
||||
for tname, rva, size in leaves:
|
||||
if size > 4096:
|
||||
large_found = True
|
||||
try:
|
||||
data = pe.get_data(rva, size)
|
||||
ent = shannon_entropy(data)
|
||||
except Exception as e:
|
||||
ent = None
|
||||
flag = " [FLAG: possible embedded payload]" if (ent is not None and ent > 7.0) else ""
|
||||
ent_str = f"{ent:.2f}" if ent is not None else "n/a"
|
||||
rpt.p(f" {tname}: {size} bytes, entropy={ent_str}{flag}")
|
||||
large_entries.append({"type": tname, "size": size, "entropy": ent})
|
||||
if not large_found:
|
||||
rpt.p(" none")
|
||||
|
||||
rpt.data["resources"] = {
|
||||
"count": len(leaves), "has_manifest": has_manifest,
|
||||
"type_counts": type_counts, "large_entries": large_entries,
|
||||
}
|
||||
|
||||
|
||||
def section_overlay(pe, rpt: Report, path):
|
||||
rpt.h("Overlay")
|
||||
try:
|
||||
overlay_offset = pe.get_overlay_data_start_offset()
|
||||
except Exception:
|
||||
overlay_offset = None
|
||||
if overlay_offset is None:
|
||||
rpt.p("No overlay.")
|
||||
rpt.data["overlay"] = {"size": 0}
|
||||
return
|
||||
|
||||
file_size = os.path.getsize(path)
|
||||
size = file_size - overlay_offset
|
||||
rpt.p(f"Overlay size: {size} bytes (starts at offset 0x{overlay_offset:x})")
|
||||
guess = "unknown"
|
||||
head = b""
|
||||
if size > 0:
|
||||
with open(path, "rb") as f:
|
||||
f.seek(overlay_offset)
|
||||
head = f.read(8)
|
||||
hex_head = head.hex()
|
||||
rpt.p(f"First 8 bytes: {hex_head}")
|
||||
if head[:2] == b"PK":
|
||||
guess = "PK zip (possible appended archive/installer payload)"
|
||||
elif head[:2] == b"MZ":
|
||||
guess = "MZ (possible embedded PE)"
|
||||
elif head[:6] == b"7z\xbc\xaf\x27\x1c":
|
||||
guess = "7z archive"
|
||||
elif head[:4] == b"UPX!":
|
||||
guess = "UPX marker"
|
||||
elif b"NullsoftInst" in head:
|
||||
guess = "NSIS installer marker"
|
||||
elif b"Inno Setup" in head:
|
||||
guess = "Inno Setup marker"
|
||||
else:
|
||||
sec_dir = pe.OPTIONAL_HEADER.DATA_DIRECTORY[
|
||||
pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_SECURITY"]
|
||||
]
|
||||
if sec_dir.VirtualAddress == overlay_offset:
|
||||
guess = "Authenticode certificate table (not a separate overlay payload)"
|
||||
rpt.p(f"Guess: {guess}")
|
||||
rpt.data["overlay"] = {"size": size, "offset": overlay_offset, "first_bytes": head.hex(), "guess": guess}
|
||||
|
||||
|
||||
def section_pdb(pe, rpt: Report):
|
||||
rpt.h("PDB")
|
||||
pdb_path = None
|
||||
if hasattr(pe, "DIRECTORY_ENTRY_DEBUG"):
|
||||
for dbg in pe.DIRECTORY_ENTRY_DEBUG:
|
||||
entry = getattr(dbg, "entry", None)
|
||||
if entry is not None and hasattr(entry, "PdbFileName"):
|
||||
pdb_path = decode(entry.PdbFileName)
|
||||
break
|
||||
rpt.p(f"PDB path: {pdb_path if pdb_path else 'none found'}")
|
||||
rpt.data["pdb_path"] = pdb_path
|
||||
|
||||
|
||||
def section_version_info(pe, rpt: Report, path):
|
||||
rpt.h("Version info")
|
||||
fields = {}
|
||||
if hasattr(pe, "FileInfo"):
|
||||
for fileinfo_list in pe.FileInfo:
|
||||
for entry in fileinfo_list:
|
||||
if getattr(entry, "Key", None) == b"StringFileInfo":
|
||||
for st in entry.StringTable:
|
||||
for k, v in st.entries.items():
|
||||
fields[decode(k)] = decode(v)
|
||||
wanted = ["CompanyName", "ProductName", "OriginalFilename", "FileDescription", "FileVersion"]
|
||||
any_found = False
|
||||
for w in wanted:
|
||||
if w in fields:
|
||||
any_found = True
|
||||
rpt.p(f"{w}: {fields[w]}")
|
||||
if not any_found:
|
||||
rpt.p("No version info resource found.")
|
||||
|
||||
orig = fields.get("OriginalFilename")
|
||||
if orig and orig.lower() != os.path.basename(path).lower():
|
||||
rpt.p(f"WARNING: OriginalFilename ('{orig}') does not match actual filename ('{os.path.basename(path)}')")
|
||||
|
||||
rpt.data["version_info"] = fields
|
||||
|
||||
|
||||
def section_tls(pe, rpt: Report):
|
||||
rpt.h("TLS callbacks")
|
||||
if not hasattr(pe, "DIRECTORY_ENTRY_TLS") or pe.DIRECTORY_ENTRY_TLS is None:
|
||||
rpt.p("No TLS directory.")
|
||||
rpt.data["tls_callbacks"] = 0
|
||||
return
|
||||
cb_rva = pe.DIRECTORY_ENTRY_TLS.struct.AddressOfCallBacks
|
||||
if not cb_rva:
|
||||
rpt.p("TLS directory present but no callbacks.")
|
||||
rpt.data["tls_callbacks"] = 0
|
||||
return
|
||||
|
||||
image_base = pe.OPTIONAL_HEADER.ImageBase
|
||||
is_64 = pe.OPTIONAL_HEADER.Magic == 0x20B
|
||||
read_ptr = pe.get_qword_at_rva if is_64 else pe.get_dword_at_rva
|
||||
cb_va = cb_rva - image_base if cb_rva > image_base else cb_rva
|
||||
count = 0
|
||||
try:
|
||||
for i in range(100):
|
||||
ptr = read_ptr(cb_va + i * (8 if is_64 else 4))
|
||||
if not ptr:
|
||||
break
|
||||
count += 1
|
||||
except Exception:
|
||||
pass
|
||||
rpt.p(f"TLS callback count: {count}{' [FLAG: possible anti-debug via TLS callback]' if count else ''}")
|
||||
rpt.data["tls_callbacks"] = count
|
||||
|
||||
|
||||
def section_rich_header(pe, rpt: Report):
|
||||
rpt.h("Rich header")
|
||||
rich = getattr(pe, "RICH_HEADER", None)
|
||||
if rich is None:
|
||||
rpt.p("No Rich header.")
|
||||
rpt.data["rich_header"] = {"present": False}
|
||||
return
|
||||
values = getattr(rich, "values", []) or []
|
||||
compiler_ids = len(values) // 2
|
||||
rpt.p(f"Rich header present: yes ({compiler_ids} compiler-id entries)")
|
||||
rpt.data["rich_header"] = {"present": True, "compiler_id_count": compiler_ids}
|
||||
|
||||
|
||||
def analyze(path, as_json):
|
||||
rpt = Report()
|
||||
try:
|
||||
pe = pefile.PE(path, fast_load=True)
|
||||
except pefile.PEFormatError as e:
|
||||
print(f"Not a valid PE file: {e}", file=sys.stderr)
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"Failed to open file: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
pe.parse_data_directories()
|
||||
except Exception as e:
|
||||
rpt.p(f"warning: parse_data_directories partially failed: {e}")
|
||||
|
||||
with open(path, "rb") as f:
|
||||
overall_entropy = shannon_entropy(f.read())
|
||||
|
||||
is_dotnet = False
|
||||
try:
|
||||
is_dotnet = bool(section_file_info(pe, path, rpt))
|
||||
except Exception as e:
|
||||
rpt.fail("file", e)
|
||||
|
||||
sections = []
|
||||
try:
|
||||
sections = section_sections(pe, rpt)
|
||||
except Exception as e:
|
||||
rpt.fail("sections", e)
|
||||
|
||||
total_imports = 0
|
||||
kernel32_loader_pattern = False
|
||||
try:
|
||||
total_imports, kernel32_loader_pattern = section_imports(pe, rpt)
|
||||
except Exception as e:
|
||||
rpt.fail("imports", e)
|
||||
|
||||
try:
|
||||
section_packing_verdict(pe, rpt, overall_entropy, sections, total_imports, kernel32_loader_pattern, is_dotnet)
|
||||
except Exception as e:
|
||||
rpt.fail("packing_verdict", e)
|
||||
|
||||
try:
|
||||
section_signature(pe, rpt)
|
||||
except Exception as e:
|
||||
rpt.fail("signature", e)
|
||||
|
||||
try:
|
||||
section_resources(pe, rpt)
|
||||
except Exception as e:
|
||||
rpt.fail("resources", e)
|
||||
|
||||
try:
|
||||
section_overlay(pe, rpt, path)
|
||||
except Exception as e:
|
||||
rpt.fail("overlay", e)
|
||||
|
||||
try:
|
||||
section_pdb(pe, rpt)
|
||||
except Exception as e:
|
||||
rpt.fail("pdb", e)
|
||||
|
||||
try:
|
||||
section_version_info(pe, rpt, path)
|
||||
except Exception as e:
|
||||
rpt.fail("version_info", e)
|
||||
|
||||
try:
|
||||
section_tls(pe, rpt)
|
||||
except Exception as e:
|
||||
rpt.fail("tls", e)
|
||||
|
||||
try:
|
||||
section_rich_header(pe, rpt)
|
||||
except Exception as e:
|
||||
rpt.fail("rich_header", e)
|
||||
|
||||
if as_json:
|
||||
print(json.dumps(rpt.data, indent=2, default=str))
|
||||
else:
|
||||
print("\n".join(rpt.lines))
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Static PE triage summary (never executes the sample).")
|
||||
parser.add_argument("path", help="Path to the PE file to analyze")
|
||||
parser.add_argument("--json", action="store_true", help="Output JSON instead of plain text")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not os.path.isfile(args.path):
|
||||
print(f"No such file: {args.path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
sys.exit(analyze(args.path, args.json))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user