最短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 | push 0xb |
64位
1 | xor rsi, rsi |
生成纯可见字符的shellcode
某些场景(如被 strcpy 类函数或输入校验过滤)要求 shellcode 的每个字节都落在可打印 ASCII(0x20 ~ 0x7E)范围内。思路是:只用操作码本身可打印的指令写一个”解码头”,运行时在栈上反向构造出原始 shellcode,再跳进去执行。
32 位下可打印的常用指令:
| 指令 | 操作码 | 对应字符 |
|---|---|---|
push/pop r32 |
0x50 ~ 0x5F | P ~ Z、X ~ _ |
inc/dec r32 |
0x40 ~ 0x4F | @、A ~ O |
push imm32 |
0x68 + 4 字节 | h.... |
sub al/eax, imm |
0x2c / 0x2d | ,、- |
xor al/eax, imm |
0x34 / 0x35 | 4、5 |
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 | # msfvenom:x86/alpha_mixed 输出纯可见字符(需指定基址寄存器) |
生成纯字母和数字的shellcode
约束进一步收紧到 [0-9A-Za-z](0x300x39、0x410x5A、0x61~0x7A),-(sub)、, 等字符不再可用,可用指令缩减为:
| 指令 | 操作码 | 对应字符 |
|---|---|---|
push/pop r32 |
0x50 ~ 0x5A、0x61 不可(popad 除外) | P ~ Z、a |
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 ~ y、z |
jno/jo 等 |
0x71 / 0x70 | q / p |
失去了 sub,置零和构造值主要靠 push imm32 + xor eax, imm32 组合。例如置零 eax:
1 | ; 字节序列:hAAAA X 5AAAA => "hAAAAX5AAAA",纯字母数字 |
之后用 xor 链逐位构造目标值(rix 在 Phrack 57《Writing ia32 alphanumeric shellcodes》中给出完整推导),编码器把这套逻辑自动化:
1 | # msfvenom:alpha_upper 输出大写字母+数字 |
注意事项:
- 编码器需要一个寄存器指向 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_fd → out_fd 的数据拷贝,一次系统调用顶 read+write 两步。orw(open-read-write)题目中用它把 flag 直接送到标准输出,省去一块缓冲区和对 rsi/rdx 的布置:
1 | ; open("/flag", 0) 拿到 fd=3 后: |
openat / openat2
1 | int openat(int dirfd, const char *pathname, int flags, ... /* mode_t mode */); |
openat 以 dirfd 指向的目录为基准解析相对路径,dirfd 取 AT_FDCWD(-100)时行为与 open 等价——seccomp 只 ban 了 open 时,openat 是最常用的替代品(rax=257)。
openat2(内核 5.6+,rax=437)把参数收进 struct open_how,并新增 resolve 标志位(如 RESOLVE_NO_SYMLINKS、RESOLVE_IN_ROOT),既能用于防御路径逃逸,在攻击面题目里也是 open/openat 双 ban 时的下一线选择。
execveat
1 | int execveat(int dirfd, const char *pathname, |
execve 的 dirfd 版本(rax=322)。flags 置 AT_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 |