When to Use / When Not to Use
- Use: when what you have is a "raw code blob" (no PE/ELF/Mach-O file header) — exploit payloads, second-stage payloads decoded from documents/scripts, code written into memory by droppers
- Use: when the payload has self-decoding (XOR/ROL decoding loops, generated by msfvenom-style encoders) — it looks like garbage statically and needs to be restored before analysis
- Use: when you need to "run it without an environment" to verify payload behavior (architecture mismatch/no host process/don't want to trigger it in a real process) — Unicorn emulation
- Use: to answer "which APIs does this code call, and what is it trying to do" (API hash resolution)
- Don't use: for a complete program with a file format header (that's the [[re-binary-core]] domain)
- Don't use: when you only need initial-survey conclusions (file/hash/entropy, that's [[re-triage]])
- Note: emulated execution counts as dynamic execution and is performed inside a sandbox by default ([[re-analyze/platform-tips]] highest principle); pure static extraction/decoding analysis may skip the sandbox, but when writing decoded output to disk and then actually executing it for verification, you must enter [[re-sandbox]]
Tool Preparation
Verify all tools before use. Static decoding in this skill may skip the sandbox; emulated execution is performed inside a sandbox by default ([[re-analyze/platform-tips]] highest principle).
python3 — runtime environment for decoding/emulation scripts
- Linux:
apt install python3 python3-pip/dnf install python3 python3-pip/pacman -S python python-pip(mostly bundled) - macOS:
brew install python(or bundled with Xcode CLT); Windows: python.org installer (check Add to PATH) orchoco install python; Linux version inside WSL - Verify:
python3 --version
unicorn — the mainstay for emulated execution (pip, Python 3)
- All platforms:
pip install unicorn(Python 3) - Verify:
python3 -c "import unicorn; print(unicorn.__version__)"
capstone — disassembly output (pip, Python 3)
- All platforms:
pip install capstone - Verify:
python3 -c "import capstone; print(capstone.__version__)"
binwalk — scan embedded blobs out of host files
- For installation channels and version forms (v2 legacy / v3 mainline), see [[re-fw-extract]] tool preparation; confirm major there before use
- Verify:
binwalk --version
xxd — hex viewing/signature locating
- Debian/Ubuntu:
apt install xxd(new standalone package; older versions in vim-common); Fedora:dnf install xxd(standalone subpackage since F38; older versions vim-common); Arch:pacman -S xxd - macOS: bundled (shipped with vim); Windows: not bundled — use WSL or PowerShell
Format-Hex - Verify:
xxd -v
objdump (binutils) — raw binary disassembly scanning
- Linux:
apt install binutils/dnf install binutils/pacman -S binutils(preinstalled on most distributions) - macOS: bundled; Windows: use rizin or Ghidra instead
- Verify:
objdump --version
Disassembler — choose according to RE_DECOMPILER or environment
- [[re-radare2]] (rizin, command line/low memory, good Raw Binary support, recommended); [[re-ghidra]] (GUI, Import File → Raw Binary, select architecture)
- For installation and verification, see the corresponding skill's tool preparation
scdbg — optional (shellcode debugger)
- Search GitHub
Nypreo/scdbgreleases to download (Windows executable; on Linux can run under Wine, or simply use the Unicorn approach instead) - Verify:
scdbg.exe -f blob.bincan output disassembly and emulation results
msfvenom — optional (generate comparison samples/encoder signature references)
- Kali: preinstalled (
apt install metasploit-framework); Fedora: after adding the Rapid7 RPM repository,dnf install metasploit-framework;Arch:pacman -S metasploit(extra repository, 6.x) - Verification:
msfvenom -l encoders | head -5(msfvenom can be used standalone and does not depend on msfconsole's PostgreSQL) - Purpose: use known encoders (such as
x86/shikata_ga_nai) to generate samples and compare the instruction forms of the decoding loop
Procedure
Execute in order; archive each step's artifacts (blob/decoding result/emulation output) with sha256 + path (required by the [[re-ioc]] evidence chain).
-
Extract the shellcode blob (three sources: file/memory/network traffic):
# 文件内嵌: 从宿主文件里扫嵌入段 binwalk sample.bin binwalk -e sample.bin # 按签名自动提取 # 定长段提取(已知偏移): dd if=sample.bin of=blob.bin bs=1 skip=<off> count=<len> # ELF .text 段导出(载荷伪装成代码段时): objcopy --dump-section .text=blob.bin sample.elf # 内存: 沙箱内 gcore 转储后按特征找(见 [[re-memdump]]) # 流量: tshark 提取 pcap 中的载荷字节(见 [[re-netcap]]) tshark -r c2.pcap -Y 'http' -T fields -e http.file_data > payload.hex xxd -r -p payload.hex blob.bin- Extraction principle: confirm the boundary before extracting—extracting too much is better than too little (the entry point is inside the blob); when the boundary is uncertain, first expand the range according to the 0xEB short jump/decoding loop span
- Common to the three sources: the payload is often "processed" by the host (encoded/chunked/rearranged)—the extracted blob should first be
file blob.bin+ entropy judgment; if it looks garbled, first assume there is a decoding layer (step 3)
-
Locate the entry point (0xEB short jump / disassembly scan):
xxd blob.bin | head -20 # 头部特征 # 入口候选: 首字节 0xEB(jmp short,跳到解码器)、0xE8(call,自定位取 EIP) objdump -D -b binary -m i386 blob.bin | head -40 # 32 位 x86 裸反汇编 objdump -D -b binary -m i386:x86-64 blob.bin | head -40 # 64 位- Common entry patterns: ①
EB xxshort jump (jump to the decoding loop, with space left before the entry for the decoder) ②E8 00 00 00 00+pop reg(call/pop self-location, get the current EIP) ③ the decoding loop directly at the beginning - rizin side:
rizin -a x86 -b 32 blob.binafter enterings <候选偏移>+pd 20inspect section by section (see [[re-radare2]]); when importing into Ghidra, choose Raw Binary (x86:LE:32:default), and at the candidate entry pointLmark as function - Locating criterion: the entry point should have the structure of "self-location + decoding loop + jump to decoded code"; only a meaningful instruction sequence in disassembly counts as the entry point; if it is all garbled, first go to step 3 to decode and then come back
- Common entry patterns: ①
-
Decoding loop analysis (XOR/ROL self-decoding):
# 特征指令(反汇编中找): # XOR byte ptr [reg+disp], imm 单字节 XOR 解码 # ROL/ROR byte ptr [reg], imm 循环移位解码 # ADD/SUB byte ptr [reg], imm 加减常量解码 # LOOP / JECXZ 循环控制(计数在 ECX/RCX)- Static recovery: extract the decoding parameters from the disassembly (key byte/shift amount/length/starting point) → replicate in python:
data = bytearray(open('blob.bin','rb').read()) key, start, length = 0x55, 0x00, len(data) # 参数来自反汇编 for i in range(start, start + length): data[i] ^= key open('decoded.bin','wb').write(data) - After recovery, return to step 2 to relocate the entry point—the decoded blob is the payload to analyze (the encoder may have a second layer)
- Cross-check verification: use msfvenom with
x86/xor_dynamicand other encoders to generate samples, compare the instruction forms of the decoding loop, and confirm that the identification has not gone astray (see pitfall 2)
- Static recovery: extract the decoding parameters from the disassembly (key byte/shift amount/length/starting point) → replicate in python:
-
Emulation (run without an environment in Unicorn, hook system calls):
from unicorn import * from unicorn.x86_const import * blob = open('blob.bin','rb').read() mu = Uc(UC_ARCH_X86, UC_MODE_32) # 按步骤 2 确认的架构选模式 CODE, STACK = 0x1000, 0x3000 mu.mem_map(CODE, 0x1000); mu.mem_map(STACK, 0x1000) mu.mem_write(CODE, blob) mu.reg_write(UC_X86_REG_ESP, STACK + 0x800) # 设栈(不设必崩) def hook_code(uc, addr, size, user): if addr in API_ADDRS: # 步骤 5 定位的 API 调用点 uc.reg_write(UC_X86_REG_EIP, addr + size) # 跳过 stub print("hit api", hex(addr)) mu.hook_add(UC_HOOK_CODE, hook_code) try: mu.emu_start(CODE, 0x100000) # until 给大上限,非法访问即终止 except UcError as e: print("stop:", e) # 缺 API/越界 = 预期终止点 open('decoded.bin','wb').write(mu.mem_read(CODE, len(blob))) # 解码产物- Letting the decoding loop "run by itself" is faster and more accurate than manual recovery (the countermeasure for pitfall 3);
hook_mem_writecan record where the decoding output lands (see [[re-emulation]] step 4) - System call/API handling: Unicorn has no OS—first disassemble to list all call targets, hook known API call points to return fake values and record parameters (stub them; see step 5 and pitfall 4)
- Letting the decoding loop "run by itself" is faster and more accurate than manual recovery (the countermeasure for pitfall 3);
-
Interaction surface with the host (API resolution: kernel32 traversal, hash lookup):
- The standard technique for Windows shellcode to obtain APIs: PEB → PEB_LDR_DATA → traverse the module linked list to kernel32.dll → look up the export table by name hash (the shellcode equivalent of GetProcAddress)
- Statically locate the API set: disassemble to find the pattern "push 4-byte constant; ...; call"; the 4 bytes are the API name hash; brute-force compare using common hash algorithms:
def djb2(s): h = 5381 for c in s: h = ((h << 5) + h + c) & 0xffffffff return h for name in ['LoadLibraryA','VirtualAlloc','VirtualProtect', 'WriteProcessMemory','CreateThread','WinExec']: print(name, hex(djb2(name.encode()))) - Algorithm comparison: Metasploit-family (generated by msfvenom) API hashes default to ROR13 (not djb2)—when brute-force comparing, calculate both; a hit confirms the generator source
- Recover the calling convention: first determine the bitness—32-bit has no unique convention: Win32 APIs commonly use
WINAPI/__stdcall(callee cleans the stack, ending withRET n), while the program itself defaults to__cdecl, member function__thiscall(only/Gzchanges the default to__stdcall), determined by symbol decoration / caller stack balancing / ECX-EDX usage; 64-bit uses the unified x64 ABI (first four integer/pointer arguments RCX/RDX/R8/R9 + 32-byte shadow space, class fastcall, not__fastcall) — determines the parameter registers/stack order and the return value location (pitfall 3) - API set = behavioral intent: VirtualAlloc/VirtualProtect + WriteProcessMemory → injection; CreateThread/CreateRemoteThread → execution; WinExec/CreateProcess → run program — directly corresponds to the ATT&CK mapping of [[re-behavior]] (T1055 process injection, etc.)
- Simulation stub: add the API call sites to hook_code's API_ADDRS, return fake values according to function (e.g., VirtualAlloc returns the address of an already mapped memory block), and observe the control flow direction
Cross-domain union
- [[re-binary-core]]: this skill is the payload branch of that gateway — specialized analysis of bare code blobs without file format headers; the decompilation foundation reuses [[re-radare2]] / [[re-ghidra]]
- [[re-emulation]]: emulation execution framework and hook techniques (step 4 of this skill directly reuses its steps 2-4)
- [[re-radare2]]: Raw Binary disassembly, entry point scanning ([[re-ghidra]] has the same effect, choose by
RE_DECOMPILER) - [[re-memdump]]: extract shellcode from process memory (default dump first, after gcore find the blob by characteristics)
- [[re-malware]]: malicious sample payload layer — the decoded payload of loaders/fileless samples reaches this skill
- [[re-anti-analysis]]: payload segments extracted from packed/obfuscated samples (unpacking output returns to this skill for analysis)
- [[re-triage]]: preliminary survey before extraction (file/entropy to determine whether there is an encoding layer)
- Reference the highest principle of [[re-analyze/platform-tips]] (sandbox) and the "static first" principle (locate statically first, dynamically as needed)
Common pitfalls and traps
- Position-independent addresses (no concept of a base address): Symptom — Ghidra/objdump disassembly shows jumps/data references all as "garbled" addresses, and analysis by virtual address does not match; Cause — shellcode has no PE/ELF header and no fixed base address, position-independent code uses relative offsets and call/pop self-location, offsets rather than absolute addresses are the truth; Countermeasure — analyze by offset within the blob (rizin
s <偏移>, xref to see relative offsets), import with Ghidra as Raw Binary and mark functions starting from candidate entry points, do not align by absolute addresses - Encoder variants (msfvenom multiple encodings): Symptom — after locating the entry point, the disassembly is all garbage bytes, and no "normal code" can be found; Cause — the payload is generated by an encoder (shikata_ga_nai/xor_dynamic, etc.), and before the decoding loop executes everything is ciphertext; Countermeasure — first find the decoding loop (XOR/ROL characteristic instructions + LOOP), use step 4 emulation execution to directly let it decode itself, or use msfvenom to generate a reference sample with the same encoder to confirm the instruction form; shikata_ga_nai's key is dynamically inserted, so static key recovery fails — emulation execution is the first choice
- Host dependency (requires restoring the calling convention first): Symptom — simulated execution reaches a certain point and behaves oddly/the stack is corrupted/the return value is wrong; Cause — the shellcode is designed to run inside a host process and assumes the host is already initialized (DLL base addresses loaded, stack aligned, calling convention set); Countermeasure — first determine the bitness and calling convention before stubbing (32-bit integer/pointer arguments are mostly on the stack, determined by whether the callee or caller cleans the stack; in 64-bit, the first four integer/pointer arguments are in rcx/rdx/r8/r9), initialize the stack page with a layout approximating the host's, and have APIs return reasonable values according to their signatures
- Missing API (stub) during simulated execution: Symptom — when Unicorn executes to
call kernel32...orsyscall, a UcError terminates execution and control flow cannot continue; Cause — Unicorn has no OS, so neither the kernel nor DLLs exist; Countermeasure — in step 5, first parse the API hashes to recover the real API set, then stub by function (hook the call site to return a fake value and log the arguments), and if necessary use hook_mem to add memory mappings (mem_map the "new pages" returned by VirtualAlloc in advance)