SuperAPI AI Directory

re-shellcode

Data & analytics

View repository

Updated 10-16 05:06

About this skill

Shellcode Analysis

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) or choco 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/scdbg releases to download (Windows executable; on Linux can run under Wine, or simply use the Unicorn approach instead)
  • Verify: scdbg.exe -f blob.bin can 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).

  1. 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)
  2. 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 xx short 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.bin after entering s <候选偏移> + pd 20 inspect section by section (see [[re-radare2]]); when importing into Ghidra, choose Raw Binary (x86:LE:32:default), and at the candidate entry point L mark 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
  3. 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_dynamic and 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)
  4. 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_write can 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)
  5. 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 with RET n), while the program itself defaults to __cdecl, member function __thiscall(only /Gz changes 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... or syscall , 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)