Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Методические указания для АЗ-ИБ.doc
Скачиваний:
65
Добавлен:
07.05.2019
Размер:
1.3 Mб
Скачать

Переполнение буфера

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

void overflow_func(char *str)

{

char buf[20];

strcpy(buf, str); // Функция, копирующая str в buf

}

 

int main()

{

char big_string[128];

int i;

 

for(i=0; i < 128; i++) // Повторить цикл 128 раз

{

big_string[i] = 'A'; // Заполнить big_string символами 'A'

}

overflow_func(big_string);

 

return 0;

}

Функция overflow_func попытается втиснуть 128 байт данных в буфер, которому выделено всего 20 байт. Оставшиеся 108 байт данных просто покроют всё, что находится в памяти за буфером.

$ gcc -o overflow overflow.c

$ ./overflow

Segmentation fault

$

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

Переполнение в стеке

В рассмотренном примере при вызове функции overflow_func() в стек помещается кадр стека. При первом вызове функции кадр стека может выглядеть примерно так:

buf

указатель кадра стека (sfp)

адрес возврата (ret)

*str (аргумент функции)

остальной стек


Однако когда функция пытается записать 128 байт данных в buf длиной 20 байт, лишние 108 байт запишутся за пределы буфера, затирая указатель кадра стека, адрес возврата и аргумент функции указатель str. Затем, когда функция завершает свою работу, порграмма пытается перейти по адресу возврата, который теперь заполнен буквами "А" (0х41 в шестнадцатеричном виде). Программа пытается выполнить возврат по этому адресу, заставляя EIP перейти на 0x41414141 некоторый случайный адрес, который либо относится к недопустимому пространству памяти, либо содержит недопустимые команды, что приводит к аварийному завершению программы. Это называется переполнением в стеке, потому что оно происходит в стековом сегменте памяти.

Переполнения могут происходить и в других сегментах памяти (об этом позже), но переполнения в стеке более разнообразны и интересны, поскольку могут изменять адрес возврата. Аварийное завершение программы при переполнении в стеке не столь интересно, как причина, по которой оно происходит. Если адрес возврата можно было бы контролировать и записать в него какой-нибудь адрес, где находится реально исполняемый код, то тогда программа "вернулась" бы и выполнила этот код, а не завершилась аварийно. А если данные, переписывающие адрес возврата, зависят от данных, введённых пользователем, например от текста в поле для ввода имени пользователя, то он сможет управлять адресом возврата и последующим выполнением программы.

Если можно модифицировать адрес возврата и изменить порядок выполнения путём переполнения буфера, то всё, что требуется, это какой-нибудь полезный код, который хотелось бы выполнить. Здесь мы сталкиваемся с инжекцией байт-кода. Байт-код это искусно написанный на ассемблере код, являющийся законченной программой, которую можно вредить в буфер. На байт-код накладываются некоторые ограничения: он должен быть законченной программой и в нём не должно быть некоторых специальных символов, потому что он должен иметь вид обычных данных, помещаемых в буфер.

Самый распространённый пример байт-кода это шеллкод. Это код, который запускает оболочку. Если suid-программу (Set UID — программа, у которой установлен этот бит, выполняется с правами хозяина файла программы. Имеет смысл только при установке на исполняемые файлы) с правами root удастся заставить выполнить шеллкод, то атакующий получит пользовательскую оболочку с правами root, при этом система будет считать, что suid-программа продолжает делать то, что ей положено.