关于 analyzing-shellcode
面向恶意软件与逆向分析人员的技能,用于分析不带 PE、ELF 等文件头的原始 shellcode。涵盖解码、反汇编、模拟执行和提取 IOC,并强调隔离执行。
主要功能
- 01规范化 shellcode 字节
- 02识别架构与解码器特征
- 03反汇编原始代码
- 04模拟执行并解析 API
- 05识别载荷阶段与家族
- 06提取 C2 和其他 IOC
适用场景
- 01分析从利用载荷中提取的代码
- 02解码混淆或自解码载荷
- 03调查 Metasploit 或 Cobalt Strike 阶段
- 04从 shellcode 提取网络指标
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ trilwu/ secskills --skill analyzing-shellcode安装后在 Claude Code 中输入/analyzing-shellcode确认已加载。
详细介绍
分析 Shellcode
Shellcode 是没有容器的代码——没有 PE/ELF 头、没有导入表,也没有固定加载地址——因此必须在运行时自行查找所需的一切。正是这种自给自足暴露了它的身份:PEB 遍历、API 哈希、与位置无关的 GetPC 技巧,都是结构指纹,即使在了解载荷功能之前也能识别。
适用场景
- 没有 PE/ELF/Mach-O 头的原始位置无关代码块
- 从漏洞利用、宏、加载器或 JS/HTA 投放器中提取的载荷
\x转义、带0x前缀、Base64 或经过 XOR 混淆的字节缓冲区- 疑似 Metasploit stager(reverse_tcp/https)或 Cobalt Strike 信标阶段
- 在较大二进制文件分流分析时发现的 egg-hunter 或 GetPC/解码器存根
- 从网络捕获、RWX 内存或文档流中恢复的字节
不适用场景
- 样本是带头部的常规 PE/ELF/Mach-O 文件——使用
analyzing-binaries执行由加载器支持的反汇编工作流 - 需要进行更广泛的样本分流分析或家族识别——使用
analyzing-malware; 仅在其中包含原始载荷阶段时再回到这里 - 代码块是打包后的可执行文件,而非原始 Shellcode——先使用
unpacking-protected-binaries解包 - 需要先从内存转储中提取 Shellcode——使用
analyzing-memory-images定位并提取内存区域,然后再回到这里 - 解码后的深度静态逆向分析——
reverse-engineering不是一项 技能;持续进行反汇编工作时,请转到analyzing-binaries
先获取字节
Shellcode 通常经过封装。首先将其规范化为扁平二进制文件。
# \x-escaped or 0x C-array pasted from an exploit → raw bytes
python3 -c "import sys,re; d=open('sc.txt').read(); \
b=bytes(int(x,16) for x in re.findall(r'(?:0x|\\\\x)([0-9a-fA-F]{2})',d)); \
open('sc.bin','wb').write(b); sys.stderr.write(f'{len(b)} bytes\n')"
# base64 buffer
base64 -d payload.b64 > sc.bin
# single-byte XOR brute (find the key that yields printable / low entropy)
python3 -c "import sys; d=open('sc.bin','rb').read(); \
[print(k, bytes(c^k for c in d[:16]).hex()) for k in range(256)]"
然后在反汇编之前先识别内容:
file sc.bin && wc -c < sc.bin # size: stagers ~200-800B, stages tens of KB
ent sc.bin 2>/dev/null || binwalk -E sc.bin # high entropy → encoded/self-decoder
对于来自文档和捕获文件的 Shellcode,请先提取:Office 宏使用 olevba --deobf
(analyzing-malware 涵盖此内容);使用
tshark -r c.pcap --export-objects http,out/ 或跟踪 tcp.stream 从 PCAP 中提取,
然后按上述方法处理字节。
初步分流分析
xxd sc.bin | head -40
查看开头以判断结构,而非含义:
- 架构推测。 x86 会出现
55 8B EC序言、64 A1 30 00/64 8B(fs:[0x30])PEB 访问,以及E8 00000000 5x调用/弹出指令。x64 会出现65 48 8B(gs:[0x60])、带 REX 前缀的48 ...,以及 RSP 对齐指令48 83 EC。 ARM/AArch64 使用统一的 4 字节字,没有 x86 序言特征。 - GetPC 技巧。
E8 00 00 00 00 58/59/5A/5B/5E(call $+5 然后 pop),或fnstenv/D9 EE D9 74 24 F4 5B这种 FPU 技巧,可恢复 EIP 以实现位置无关。 出现这些指令意味着代码会自行重定位,后面通常会跟着解码器。 - Egg-hunter 标记。 重复出现的 8 字节标记,例如
w00tw00t(77 30 30 74×2),表示 egg-hunter 正在内存中搜索真正的载荷。 - 无空字节 / 字母数字编码。 不含
00字节,或仅含可打印字符,意味着编码器存根 (Metasploitalpha_mixed、shikata_ga_nai)封装了真正的代码——你看到的是解码器,而非载荷。
在正确的基址进行静态反汇编
最常见的错误是在错误的架构下或从指令中间开始反汇编。尝试两种位宽,判断哪种能产生合理的序言。
# objdump on a raw blob — set the arch explicitly, Intel syntax
objdump -D -b binary -m i386 -M intel sc.bin # 32-bit
objdump -D -b binary -m i386:x86-64 -M intel sc.bin # 64-bit
# ndisasm — quick, and -e skips a leading offset if the entry is not at 0
ndisasm -b 32 sc.bin
ndisasm -b 64 sc.bin
# rizin / radare2: assemble/disassemble a blob directly
rasm2 -a x86 -b 32 -d "$(xxd -p sc.bin | tr -d '\n')"
r2 -a x86 -b 32 -m 0x400000 sc.bin # load raw at a base
# e asm.arch=x86 ; e asm.bits=32 ; s 0 ; pd 60 disassemble entry
# pdf @ 0 treat entry as a function
要在 Ghidra/IDA/Cutter 中加载原始 Shellcode:作为原始二进制导入,设置处理器 (x86:LE:32 或 x86:LE:64),选择基址,并定义入口点 / 在真正的第一条指令处反汇编—— 如果前面有长度前缀或无用填充指令序列,不要从偏移 0 开始。若未定义入口,自动分析器会将数据 反汇编为代码,生成的清单也就毫无意义。
解码自解码器
编码后的 shellcode 由一个小型解码器桩和其后的加密主体组成。不要尝试静态读取主体——只反汇编桩,然后让它运行。
- 识别解码器桩。 一个短循环使用
xor/add/sub/rol,操作对象包括运行指针、长度计数器以及密钥或密钥调度。循环末尾会跳回,直到计数器归零,然后顺流执行已解码的主体。 - shikata_ga_nai。 Metasploit 的多态反馈 XOR 解码器:先是 GetPC 代码块,然后是 XOR 循环,其中每个密钥字都会根据前一个明文进行修改(加法反馈)。每次构建时都会重新编码,因此字节特征无法匹配——应通过执行来解码,而不是依靠特征匹配。
- 让它自行解码。 模拟或单步执行解码器桩,在循环退出后转储内存;它写入的区域就是真正的阶段。然后对其进行反汇编。
# scdbg dumps the decoded buffer and unpacks the second stage for x86 Windows
scdbg -f sc.bin -s -1 # -s -1 lifts the step limit (default 2M)
scdbg -f sc.bin -d # dump self-modified/decoded memory
通过哈希解析 Windows API
Shellcode 没有导入表,因此会遍历已加载模块列表,并根据 API 名称的哈希解析 API。识别出这一点,往往就完成了大部分分析。
- 遍历 PEB。
fs:[0x30](x86)或gs:[0x60](x64)会加载 PEB,然后遍历PEB->Ldr->InMemoryOrderModuleList,查找kernel32.dll(或ntdll.dll),无需硬编码地址。 - 导出项哈希。 对每个导出名称,解码器桩都会计算一个哈希——通常是
ror13加法算法(经典的 Metasploitblock_api)或djb2——并与一个 4 字节常量进行比较。只要反向还原该常量,它就是 API 名称。 - 将哈希映射回名称。 预先计算系统 DLL 中每个导出项的哈希,然后查找你看到的常量:
# Reproduce Metasploit block_api ror13 — the constant is hash(module)+hash(name)
python3 - <<'PY'
def ror13(b):
h=0
for c in b:
h=((h>>13)|(h<<19))&0xffffffff; h=(h+c)&0xffffffff
return h
mod=ror13("KERNEL32.DLL\0".encode('utf-16le')) # module: UTF-16LE, uppercased
for api in ("LoadLibraryA","GetProcAddress","WinExec","VirtualAlloc"):
print(hex((ror13((api+"\0").encode('ascii'))+mod)&0xffffffff), api)
PY
工具:scdbg 会在模拟时打印已解析的 API 调用及其参数;在调试器下使用 jmp2it/blob_runner,可以在解析器处设置断点;导出表可通过 dllexp 或 rabin2 -E kernel32.dll 获取。哈希算法为自定义算法时,将其移植到 Python 脚本中,并对导出项列表进行暴力匹配。
模拟是最快的途径
对于 Windows x86 shellcode,模拟可在几秒内解析 API、解码各个阶段并打印行为——应优先尝试,而不是手动跟踪。
# scdbg — emulates x86 Windows shellcode, reports API calls, C2, and stages
scdbg -f sc.bin # default trace of resolved API calls
scdbg -f sc.bin /findsc # brute-force the correct start offset
scdbg -f sc.bin -foff 0x1a # force entry at a known offset
# speakeasy — emulate x86/x64 shellcode or a raw module
speakeasy -t sc.bin --raw --arch x86 # --raw shellcode, --arch x86|amd64
speakeasy -t sc.bin --raw --arch amd64 -o report.json
对于自定义逻辑或非 Windows 目标,构建一个脚本环境:
- Unicorn — 映射内存、设置寄存器、挂钩
code/mem事件、运行,然后读取解码后的区域。最适合需要编写脚本进行解码的场景。 - Qiling — 更高层的工具;它会模拟操作系统层,使基于哈希的 API 解析以及
VirtualAlloc/WinExec能正常运行,并提供 rootfs 和系统调用拦截。 - 在调试器下运行。
blob_runner、shellcode2exe或jmp2it可将 blob 封装成可加载的桩;在 x32dbg/x64dbg 中打开,将 EIP/RIP 设置为入口,然后单步跟踪解码器,直到到达正在运行的阶段。
识别阶段和家族
- Metasploit 分阶段载荷。
reverse_tcp会创建套接字,然后通过recv将第二阶段接收到 RWX 内存中并跳转到该阶段;reverse_https使用WinInet(InternetOpen/HttpOpenRequest)。scdbg/speakeasy 会直接显示连接地址和端口。 - Cobalt Strike。 Beacon 阶段包含编码后的配置;解码后,解析其中的 C2 主机、端口、URI、用户代理、休眠时间/抖动值以及可塑配置文件。社区解析器(例如 beacon 配置提取工具)可以读取配置块;确认是 Beacon 后,将其交给
analyzing-malware。 - 提取端点。 从模拟日志或解码后的配置中提取 C2 主机/端口和请求 URI——这些是需要立即封锁的 IOC。
- 与已知解码器对比。 shikata、
call4_dword_xor、alpha_mixed和block_api解析器都有可识别的结构;匹配其中一种即可确定所用工具包,并判断后续字节应是什么内容。
安全执行规范
模拟执行是安全的;实际运行 shellcode 就是引爆。请采用与 analyzing-malware 相同的隔离措施。
- 使用隔离的 VM,在执行前创建快照,执行后还原。
- 断开网络,或使用伪造的 C2(INetSim/FakeNet-NG),让加载器的回连请求指向你的监听器,而非真实操作者。
- 在正在处置的事件中,绝不要让加载器连接真实 C2——回连会告知操作者你正在调查,而下载第二阶段载荷则会泄露你的分析。
提取 IOC 并移交
解码时收集稳定的指标:C2 主机/IP、端口、请求 URI 和 user-agent、互斥体名称、解码密钥或哈希常量,以及 egg 标签。像 analyzing-malware 一样对它们排序——行为和配置比不断轮换的主机更持久。
然后移交工作:
- 解码器或解析器的字节/结构特征 →
writing-yara-rules(编写规则),然后交给engineering-detections(部署规则) - 落地或下载的第二阶段载荷 →
analyzing-malware - 对大型解码阶段进行持续反汇编 →
analyzing-binaries - 撰写分析报告 →
reporting-security-findings
应当否决的自我安慰
- “objdump 显示的是乱码,所以它经过了加密。” 更可能是反汇编时使用了错误的架构、错误的偏移,或直接反汇编了整个解码器桩代码。先尝试两种位宽,并找到具有合理函数序言的偏移。
- “没有导入表,所以我无法判断它调用了什么。” Shellcode 会通过哈希解析 API。找到 PEB 遍历代码,并还原哈希常量。
- “我直接运行看看它会做什么。” 不行,除非使用隔离且网络受控的测试环境。应改用 scdbg/speakeasy 进行模拟。
- “字节特征可以捕获这个加载器。” 对多态加载器无效——shikata_ga_nai 会在每次构建时重新编码。应针对解码后的阶段或结构性解析器制作特征,而不是针对编码器。
- “字符串里没有明显的 C2。” 地址是在运行时构造的,配置也经过编码。先解码桩代码,再通过模拟读取 connect 调用。
- “它从偏移 0 开始。” 通常并非如此——入口之前可能有长度前缀、对齐滑道或 GetPC 代码块。使用
/findsc,或读取代码以确定真正的第一条指令。 - “反汇编器自动分析过了,所以列表是正确的。” 对于没有文件头的二进制块,分析器会猜测入口,并把数据当作代码处理。应指定真实入口和处理器,否则整个反汇编列表都会错位。
ATT&CK 覆盖范围
根据 secskills-core/ttp-index.json 生成——编辑该文件,然后运行
python3 scripts/sync_attack.py --write。在报告中引用 ID 前,请对照当前 ATT&CK 版本再次核实。
防御规避(TA0005)
- T1027 混淆文件或信息——另请参阅
analyzing-malware、analyzing-binaries - T1140 对文件或信息进行反混淆/解码——另请参阅
analyzing-malware、analyzing-binaries - T1620 反射式代码加载——另请参阅
analyzing-malware
针对以上任一项编写检测内容:engineering-detections。主动搜寻:hunting-threats。入侵后处置:responding-to-incidents。
参考资料
analyzing-binaries— 解码阶段的反汇编工作流analyzing-malware— 样本分流、隔离、家族识别和落地阶段分析analyzing-memory-images— 先从内存转储中提取 shellcodewriting-yara-rules— 为解码器或解码阶段编写持久有效的特征engineering-detections— 将解码器/解析器特征转化为已部署的规则reporting-security-findings— 撰写分析报告- 使用 scdbg、speakeasy、Unicorn、Qiling 进行模拟
- 使用 radare2/rizin(
rasm2/pd)、Cutter 对原始二进制块进行静态反汇编 - 使用 x64dbg/x32dbg 搭配 blob_runner / shellcode2exe / jmp2it 在调试器下运行