| name | mcp-malware-analysis |
| description | Use when analyzing any malware sample with IDA Pro MCP and x64dbg MCP connected. Covers the full static-to-dynamic workflow: binary fingerprinting, packer detection, execution graph mapping, anti-sandbox bypass, breakpoint strategy, API resolution tracing, payload extraction, and report generation. Use when a sample is open in IDA and x64dbg is running in a sandbox VM. |
MCP Malware Analysis
Overview
Two-phase workflow: static first, dynamic second. IDA produces the execution graph, exact addresses, decryption keys, and breakpoint targets before a single instruction executes. x64dbg validates those findings and captures runtime-only data — resolved APIs, decrypted payloads, network IOCs.
Core rule: Never run the sample blind. Map the execution graph statically, then instrument only the points that matter.
Phase 1 — Static (IDA Pro MCP)
1.1 Fingerprint
mcp__ida-pro-mcp__get_metadata → hashes, base, size, path
mcp__ida-pro-mcp__get_entry_points → entry point address
mcp__ida-pro-mcp__list_imports → visible IAT (may be decoy)
mcp__ida-pro-mcp__list_strings → offset=0, count=0 (get all)
Packer signals:
| String / Import | Packer |
|---|
MpVmp32Entry | VMProtect |
UPX0 / UPX1 section names | UPX |
MPRESS1 / MPRESS2 | MPRESS |
ASPack | ASPack |
| Tiny IAT (≤3 DLLs) + imports used only in small stub functions | Custom packer / decoy IAT |
1.2 Map the execution graph
mcp__ida-pro-mcp__decompile_function → entry point
mcp__ida-pro-mcp__get_callees → each function in the chain
What to look for:
- Gate function called first — checks environment before proceeding
- Decoy path — small functions consuming the visible IAT; always exits cleanly
- Real path — large sequential functions (
>0x3000 bytes) with empty callees (VMP/obfuscated)
- Shellcode loader — last real function; calls
VirtualAlloc + a decrypt sub + executes result
If get_callees returns empty, the function is VM-obfuscated. Switch to get_xrefs_to on each import address to map which module actually uses which API.
1.3 Anti-sandbox gate patterns
Decompile the gate function and identify which checks it performs:
| Check | API | Detection logic |
|---|
| RAM size | GlobalMemoryStatusEx | ullTotalPhys above threshold |
| CPU count | GetSystemInfo | dwNumberOfProcessors >= N |
| Timing | Sleep(N) + GetTickCount | Delta must be ≥ N (defeats accelerated sandboxes) |
| Debugger presence | IsDebuggerPresent | Non-zero return |
| Kernel debugger | NtQuerySystemInformation class 35 | KdDebuggerEnabled flag |
| PEB fields | NtCurrentPeb() | BeingDebugged, NtGlobalFlag, heap flags |
| Process list | CreateToolhelp32Snapshot | Known analysis tool names |
| Window titles | FindWindowW / EnumWindows | Sandbox/AV window class names |
| CPUID | inline CPUID | Hypervisor bit (ECX bit 31) |
| Registry | RegOpenKeyExW | VM artifact keys (VMware, VirtualBox) |
| Disk size | DeviceIoControl IOCTL_DISK_GET_DRIVE_GEOMETRY | Below threshold |
1.4 Find the decryption routine
Look for the last function before execution transfers to dynamic code. Signs:
- Calls
VirtualAlloc or NtAllocateVirtualMemory with PAGE_READWRITE (0x04)
- Immediately followed by a loop copying bytes from a large static blob
- Calls a sub that takes
(buffer, size, key, key_len) — the cipher
mcp__ida-pro-mcp__decompile_function → shellcode loader and cipher sub
mcp__ida-pro-mcp__read_memory_bytes → key bytes (address=key_global, size=key_length)
If get_global_variable_value_at_address fails on untyped data (e.g. xmmword), use read_memory_bytes directly.
Common cipher patterns:
| Pattern | Cipher |
|---|
S[i] = (S[i] + S[j] + key[i % keylen]) % 256 loop | RC4 KSA (or variant) |
Output byte = S[(S[i]+S[j]) % 256] | RC4 PRGA (or variant) |
| Single byte XOR of buffer against repeating key | XOR cipher |
| 4-byte DWORD XOR with rolling key | DWORD XOR / custom |
RtlDecompressBuffer / LZnt1 | Compression (not encryption) |
AES constants (0x63636363, S-box patterns) | AES |
1.5 Verify the IAT is a decoy
mcp__ida-pro-mcp__get_xrefs_to → address of each import thunk
If every xref points only to the decoy stub functions → IAT is entirely fake. Real API resolution happens at runtime via PEB walk or LoadLibrary+GetProcAddress by hash.
1.6 Static output checklist
Before moving to dynamic:
Phase 2 — Dynamic (x64dbg MCP)
2.1 Connect
x64dbg MCP v1.0.4+ exposes a tools/call JSON-RPC wrapper. Always probe with initialize first — method names differ from older reference docs:
curl -s -X POST http://<VM_IP>:12345/ \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2024-11-05",
"capabilities":{},
"clientInfo":{"name":"claude","version":"1.0"}}}'
The response lists all available tools. All subsequent calls use:
curl -s -X POST http://<VM_IP>:12345/ \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"<tool_name>","arguments":{...}}}'
Essential tools:
| Task | Tool name |
|---|
| Debugger state + EIP | debug_get_state |
| Load / restart | debug_init, debug_restart |
| Run / step | debug_run, debug_step_into, debug_step_over, debug_step_out |
| Breakpoints | breakpoint_set (type: software|hardware|memory) |
| Registers | register_get_batch, register_set |
| Memory read | memory_read (encoding: hex|ascii|base64) |
| Memory dump | dump_memory_region (as_raw_binary: true) |
| Expression eval | eval_expression (supports [ebp-0x24], symbol names) |
| Symbol lookup | symbol_from_address |
| Module info | module_get_main, module_list, module_get_exports |
| Disassembly | disassembly_at, disassembly_range |
| Thread list | thread_list |
2.2 Compute ASLR-adjusted addresses
runtime_addr = static_addr - static_base + runtime_base
Get runtime_base from module_get_main → .base immediately after loading. Recalculate every restart — ASLR re-randomizes each run.
2.3 Breakpoint strategy
Rule: Use software BPs on the outer loader (pre-shellcode). Switch to hardware BPs (type: hardware) the moment you step into shellcode or any code that may scan for 0xCC bytes. Hardware BPs use CPU debug registers (DR0–DR3) and leave code bytes untouched.
Limit: 4 hardware BPs active at a time. Disable earlier ones as analysis advances.
General BP progression:
1. Gate entry → confirm correct execution path (EAX on return)
2. Shellcode call site → capture entry point address in register before call
3. Inside shellcode:
a. After module resolution → confirm target DLL base in register
b. After API resolution → read resolved addresses from stack frame
c. After VirtualAlloc → note allocated buffer address
d. After decrypt completes → dump buffer NOW (before execution)
e. Payload entry point → step in to payload
4. Inside payload:
a. Network init → capture C2 address before connect
b. Crypto init → capture key material
2.4 Handle the anti-sandbox gate
Step out of the gate function (wait for any Sleep to complete — poll debug_get_state until state = paused).
Check return value:
- Gate returns 0 / false → real path ✓ — continue
- Gate returns non-zero / true → sandbox detected → patch:
register_set name=eax value=0x0, then run
If the gate uses multiple checks in sequence, patch each failing register/flag at the check site rather than patching the final return — this avoids side-effects from skipped checks.
2.5 Trace API resolution by hash
Shellcodes commonly resolve APIs without importing them. Pattern:
push <hash_constant> ; 32-bit hash of function name
push <module_handle> ; base address of target DLL
call <resolver_function> ; internal GetProcByHash
mov [ebp-<offset>], eax ; store result
To identify resolved addresses:
- Set a BP after all hash calls in the resolver init block
- Read each stack slot:
eval_expression → "[ebp-<offset>]"
- Identify each address:
symbol_from_address — works if symbols are loaded
- If not:
module_get_exports → save to file → grep -ob "<VA_hex>" file → dd if=file bs=1 skip=<offset-200> count=250 to read context around the match
Common hash algorithms:
| Algorithm | Recognizable pattern |
|---|
| ROR13 (Metasploit/Meterpreter) | ror eax, 0xD in loop over name chars |
| djb2 | hash = hash * 33 + char |
| Bernstein XOR | hash = hash * 33 XOR char |
| Custom XOR-shift | Varies — decompile resolver to identify |
2.6 Dump decrypted payloads
Immediately after the decrypt routine returns and before execution transfers:
dump_memory_region → {
"address": "<buffer_address>",
"size": <alloc_size>,
"output_path": "C:/Users/<user>/Desktop/stage<N>.bin",
"as_raw_binary": true
}
Check first two bytes:
MZ (4D 5A) → embedded PE — parse headers
- Not
MZ → position-independent shellcode — disassemble from offset 0
2.7 Parse an embedded PE from memory
memory_read → {"address": "<pe_base>", "size": 256, "encoding": "ascii"}
Walk the structure:
e_lfanew at offset 0x3C → PE header offset
- PE header →
NumberOfSections, SizeOfOptionalHeader
- Section table at
pe_base + 0x18 + SizeOfOptionalHeader
- Read
.rdata section: import name strings, C2 strings, config data
- Read import directory (DataDir[1]) → DLL names reveal capabilities
- Read
.data section for encrypted config if plaintext C2 not found in .rdata
Capability signals in imports:
| DLL | Functions | Capability |
|---|
wininet.dll | InternetOpen, HttpSendRequest | HTTP C2 |
ws2_32.dll | connect, send, recv | Raw socket C2 |
winhttp.dll | WinHttpOpen, WinHttpConnect | HTTP/S C2 |
GDI32.dll | BitBlt, GetDIBits | Screen capture |
USER32.dll | GetClipboardData, GetAsyncKeyState | Clipboard / keylogger |
SHELL32.dll | SHGetSpecialFolderPath | File system recon |
ole32.dll | CoCreateInstance, CoSetProxyBlanket | WMI / COM recon |
ADVAPI32.dll | CryptEncrypt, BCryptEncrypt | Data encryption |
ADVAPI32.dll | RegSetValueEx, CreateServiceW | Persistence |
2.8 Find C2 at runtime
If the C2 config is encrypted in .data, let the payload run with FakeNet-NG active:
- Start FakeNet-NG before running the sample — C2 connects at payload init, not loader entry
- Set a BP on the network API (e.g.
kernel32.WinHttpConnect, ws2_32.connect)
- When hit, read the address/hostname argument from the stack
- Cross-reference with FakeNet-NG DNS log
2.9 Crash recovery
debug_restart — reloads sample fresh
- Re-fetch
module_get_main — ASLR gives a new base every run
- Recalculate all addresses
- Replace any software BPs with hardware BPs
- Reduce active hardware BPs to ≤4 — disable earlier ones as you advance
Phase 3 — Report
Structure every report identically for consistent IOC extraction:
- Sample identification — filename, MD5, SHA256, size, architecture, packer
- Executive summary — malware type, capabilities, evasion techniques (2–4 bullets)
- Static analysis — execution flow diagram, gate checks, decryption algorithm, key bytes
- Dynamic analysis — ASLR base, resolved APIs with hashes, payload PE fingerprint, capabilities confirmed, victim env captured, C2 status
- Artifacts — paths to all dumped files on VM desktop
- Detection
- YARA rule (key bytes, unique strings, or PE timestamp)
- Host IOCs (mutex names, registry keys, drop paths)
- Network IOCs (C2 domains, IPs, URIs, User-Agent strings)
- Classification — malware family (if known), type, capabilities confirmed vs suspected, compile timestamp
Common Mistakes
| Mistake | Fix |
|---|
| Software BPs inside anti-debug shellcode | Switch to hardware BPs before stepping in |
| Using stale addresses after restart | Re-fetch module_get_main.base — ASLR changes every run |
get_callees empty on obfuscated functions | Use get_xrefs_to on import addresses instead |
get_global_variable_value_at_address fails on untyped data | Use read_memory_bytes with explicit size |
| Dumping payload before decryption finishes | BP after the decrypt call returns, not before it's called |
| Hitting the 4-hardware-BP limit | Disable earlier BPs as analysis advances |
module_get_exports failing with module name string | Pass hex base address instead: "module": "0x7FFE0000" |
| Exports grep returning the entire file | Use grep -ob to find byte offset, then dd skip=<offset-200> |
| FakeNet-NG started after sample launches | Start FakeNet-NG first — C2 connect fires at payload init |
| Patching gate return without knowing all checks | Patch each failing check in-place, not just the final return |