В программах, переполнение буфера стека случается, когда программа пишет по адресам в программном стеке вызова вне предназначенной структурой данных; это обычно буфер фиксированной длины. Баг переполнения буфера стека случается, когда программа пишет больше данных в буфер размещен в стеке, чем было фактически выделено места для буфера. Это почти всегда приводит к порче прилегающих данных в стеке, и в случае если переполнение было сделано по ошибке, часто приводит к краху программы или некорректной работы. Этот тип переполнения является одним из случаев общего класса багов программирования, известных как переполнение буфера.
Если атакованая программа выполняется со специальными привилегиями, или принимает данные из недоверенных хостов сети (напр. вебсервер ), тогда баг является потенциальной уязвимостью безопасности. Если стековый буфер залит данным, поступившим от недоверенных пользователя, тогда этот пользователь может повредить стек таким образом, что в стеке оказывается исполняемый код, инжектированных ним, затем он получает управление процессом. Это один из старейших и надежных методов для злоумышленников получить неавторизованный доступ к компьютеру.
Использование переполнений стековых буферов
Канонический метод эксплуатации переполнения стекового буфера является затирание адреса возврата в функцию, которая вызвала (caller) текущую функцию (callee) указателем на контролируемые хакером данные (обычно на тот же стек). Это иллюстрируется в примере ниже:
- Пример с strcpy:
#include<string.h>
void foo(char* bar){
char c[12];
strcpy(c, bar); // проверки границы массива нет...
}
int main(int argc, char** argv){
foo(argv[1]);
return 0;
}
Этот код берет аргумент из командной строки и копирует его в локальную стековую переменную c. Все работает хорошо для аргументов командной строки, меньше 12 символов (как вы можете видеть из рисунка Б ниже). Любые аргументы больше 11 символов в длину вызывающими порчу стека. (Максимальное число символов, которые будут безопасными являются на один меньше чем размер буфера, поскольку строки в языке C ограничиваются нулевым байтом, который тоже надо учитывать. Строка из двенадцати однобайтных символов действительности требовать тринадцятибайтного вместилища. Нулевой байт тогда потре один байт участка памяти вне концом буфера.)
- Программный стек foo () с различными введениями.
А. - Перед данные
зкопийовано.
|
Б. - "hello" это первый аргумент
командной строки.
|
В. - это первый аргумент командной строки.
|
Примечание. На рисунке указатель char * bar почему лежит поверх адреса возврата и даже сохраненного указателя на кадр стека предыдущей функции (регистр ebp ), но как аргумент функции foo (char *) должен быть как раз перед адресом возврата.
На рисунке 'В' выше, когда аргумент взят из командной строки - больший по 11 байт, foo () (точнее, strcpy (), вызванная foo () ) затирает локальные данные стека, сохраненный указатель на кадр стека ( ebp ), и что самое главное, - адрес возврата. Когда foo () возвращается (исполнение в ней доходит до инструкции ret ), она снимает из стека адрес возврата, и передает на нее управление (команду ret можно трактовать как pop eip ). Как вы можете видеть на рисунке 'В' выше, атакер потер адрес возврата указателю на стековый буфер char c, который теперь содержит вставленные им данные. В настоящем эксплуатации переполнения стекового буфера, строка многих "А" содержал бы шелкод (shellcode) для этой платформы и нужной функции. Если программа имеет особые привилегии (например бит SUID выставлен для выполнения на правах суперпользователя), тогда хакер может использовать уязвимость чтобы получить права суперпользователя на атакован машине.
Хакер также может модифицировать внутренние переменные для эксплуатации некоторых багов. Тот же пример:
#include<stdio.h>
#include<string.h>
void foo(char* bar){
float MyFloat = 10.5f; // Адресс = 0x0023FF4C
char c[12]; // Адресс = 0x0023FF30
// Будет печатать 10.500000
printf("My float value = %f\n",MyFloat);
/* ---------------------------------------
Карта памяти:
@: Память выделена c
#: Память выделена MyFloat
-: Другая память
* C * MyFloat
0x0023FF30 0x0023FF4C
||
@@@@@@@@@@@----------------####
foo ("My string is too long !!!!! XXXX");
memcpy потре последовательностью байтов 0x10 0x10 0xC0 0x42
значение переменной MyFloat. Как плавающий тип, эта последовательность
является числом 96.0313720703125 (байт 0x42 - старший!)
------------------------------------------ */
memcpy(c,bar,strlen(bar)); // не проверяется предел массива...
// Напечатает 96.031372
printf("My Float value = %f\n", MyFloat);
}
int main(int argc, char** argv){
foo("my string is too long !!!!! \x10\x10\xC0\x42");
return 0;
}