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

Переполнения буфера появились вместе с появление архитектуры Фон-Неймана 1. Впервые широкую известность они получили в 1988 году вместе с Интернет-червем Мурса (Moorse). К сожалению, точно такая же атака повторилась и в наши дни. Из 17 бюллетеней безопасности CERT за 1999 год, 10 были непосредственно вызваны ошибкам в программном обеспечении, связанным с переполнениями буфера. Самые распространенные типы атак с использованием переполнения буфера основаны на разрушении стека.

Самые современные вычислительные системы используют стек для передачи аргументов процедурам и сохранения локальных переменных. Стек является буфером типа LIFO (последним вошел первым вышел) в верхней части области памяти процесса. Когда программа вызывает функцию, создается новая "граница стека". Эта граница состоит из аргументов, переданных в функцию, а также динамического количества пространства локальных переменных. "Указатель стека" является регистром, хранящим текущее положение вершины стека. Так как это значение постоянно меняется вместе с помещением новых значений на вершину стека, многие реализации также предусматривают "указатель границы", который расположен около начала стека, так что локальные переменные можно легко адресовать относительно этого значения. 1 Адрес возврата из функции также сохраняется в стеке, и это является причиной нарушений безопасности, связанных с переполнением стека, так как перезаписывание локальной переменной в функции может изменить адрес возврата из этой функции, потенциально позволяя злоумышленнику выполнить любой код.

Хотя атаки с переполнением стека являются замыми распространенными, стек можно также перезаписать при помощи атаки, основанной на выделении памяти (malloc/free) из "кучи".

Как и во многих других языках программирования, в C не выполняется автоматической проверки границ в массивах или указателях. Кроме того, стандартная библиотека C полна очень опасных функций.

strcpy(char *dest, const char *src)

Может переполнить целевой буфер

strcat(char *dest, const char *src)

Может переполнить целевой буфер

getwd(char *buf)

Может переполнить буфер buf

gets(char *s)

Может переполнить буфер s

[vf]scanf(const char *format, ...)

Может переполнить свои аргументы.

realpath(char *path, char resolved_path[])

Может переполнить буфер path

[v]sprintf(char *str, const char *format, ...)

Может переполнить буфер str.

3.3.1. Пример переполнения буфера

В следующем примере кода имеется ошибка переполнения буфера, предназначенная для перезаписи адреса возврата и обхода инструкции, следующей непосредственно за вызовом функции. (По мотивам 4)

    #include <stdio.h>
    
    void manipulate(char *buffer) {
      char newbuffer[80];
      strcpy(newbuffer,buffer);
    }
    
    int main() {
      char ch,buffer[4096];
      int i=0;
    
      while ((buffer[i++] = getchar()) != '\n') {};
    
      i=1;
      manipulate(buffer);
      i=2;
      printf("The value of i is : %d\n",i);
      return 0;
    }
         

Давайте посмотрим, как будет выглядеть образ процесса, если в нашу маленькую программу мы введем 160 пробелов.

[XXX figure here!]

Очевидно, что для выполнения реальных инструкций (таких, как exec(/bin/sh)), может быть придуман более вредоносный ввод.

3.3.2. Как избежать переполнений буфера

Самым прямолинейным решением проблемы переполнения стека является использование только памяти фиксированного размера и функций копирования строк. Функции strncpy и strncat являются частью стандартной библиотеки C. Эти функции будут копировать не более указанного количества байт из исходной строки в целевую. Однако у этих функций есть несколько проблем. Ни одна из них не гарантирует наличие символа NUL, если размер входного буфера больше, чем целевого. Параметр длины также по-разному используется в strncpy и strncat, так что для программистов легко запутаться в правильном использовании. Есть также и значительная потеря производительности по сравнению с strcpy при копировании короткой строки в большой буфер, потому что strncpy заполняет символами NUL пространство до указанной длины.

Для избежания этих проблем в OpenBSD была сделана другая реализация копирования памяти. Функции strlcpy и strlcat гарантируют, что они они всегда терминируют целевую строку нулевым символом, если им будет передан аргумент ненулевой длины. Более подробная информация об этом находится здесь 6. Инструкции OpenBSD strlcpy и strlcat были во FreeBSD начиная с 3.5.

3.3.2.1. Вкомпилированная проверка границ во время выполнения

К несчастью, все еще широко используется очень большой объем кода, который слепо копирует память без использования только что рассмотренных функций с проверкой границ. Однако есть другое решение. Существует несколько расширений к компилятору и библиотекам C/C++ для выполнения контроля границ во время выполнения.

Одним из таких добавлений является StackGuard, который реализован как маленький патч к генератору кода gcc. Согласно сайту StackGuard, http://immunix.org/stackguard.html:

"StackGuard распознает и защищает стек от атак, не позволяя изменять адрес возврата в стеке. При вызове функции StackGuard помещает вслед за адресом возврата сигнальное слово. Если после возврата из функции оно оказывается измененным, то была попытка выполнить атаку на стек, и программа отвечает на это генерацией сообщения о злоумышленнике в системном журнале, а затем прекращает работу."

"StackGuard реализован в виде маленького патча к генератору кода gcc, а именно процедур function_prolog() и function_epilog(). function_prolog() усовершенствована для создания пометок в стеке при начале работы функции, а function_epilog() проверяет челостность пометки при возврате из функции. Таким образом, любые попытки изменения адреса возврата определяются до возврата из функции."



Перекомпиляция вашего приложения со StackGuard является эффективным способом остановить большинство атак переполнений буфера, но все же полностью это проблемы не решает.

3.3.2.2. Проверка границ во время выполнения с использованием библиотек.

Механизмы на основе компилятора полностью бесполезны для программного обеспечения, поставляемого в двоичном виде, которое вы не можете перекомпилировать. В этих ситуациях имеется некоторое количество библиотек, в которых реализованы небезопасные функции библиотеки C (strcpy, fscanf, getwd, и так далее..), обеспечивающие невозможность записи после указателя стека.

  • libsafe

  • libverify

  • libparnoia

К сожалению, эти защиты имеют некоторое количество недостатков. Эти библиотеки могут защитить только против малого количества проблем, и не могут исправить реальные проблемы. Эти защиты могут не сработать, если приложение скомпилировано с параметром -fomit-frame-pointer. К тому же переменные окружения LD_PRELOAD и LD_LIBRARY_PATH могут быть переопределены/сняты пользователем.