На главную | Содержание | Назад | Вперёд
Наши друзья

 

 

Переполнение буфера в стеке

Использование переменных в стеке для создания переполнения буфера называют переполнением буфера в стеке (buffer overflow, или smashing the stack). Атаки на переполнение буфера в стеке были первым типом атак на переполнение буфера, которые получили широкое распространение. Известны тысячи уязвимых мест для атак на переполнение буфера в стеке в коммерческом программном обеспечении, работающем практически на всех платформах. Ошибки переполнения буфера в стеке связаны в основном с уязвимостью процедур по обработке строки, которые присутствуют в стандартных библиотеках языка С.
Мы рассмотрим только основные принципы атак на переполнение буфера в стеке, поскольку эта тема уже давно обсуждается во многих материалах по обеспечению безопасности. Читателям, абсолютно незнакомым с атаками этого типа, советуем обратиться к книге Building Secure Software (Viega, McGraw, 2001). В этом разделе мы обратим основное внимание на менее известные проблемы при обработке строк и расскажем подробно о том, что часто упускают при стандартном изложении этой проблемы.
Буферы фиксированного размера
Признаком классической ошибки с переполнением буфера в стеке является буфер для строки данных с жестко заданным размером, который находится в стеке и "дополняется" процедурой обработки строки, зависимой от буфера, конец которого обозначается символом NULL. В качестве примеров таких процедур можно назвать вызовы функций strep у () nstrcat()B буферах фиксированного размера, а также вызовы функций sprintf () () и vsprintf () в буферах фиксированного размера с использованием строки форматирования %s. Существуют и другие варианты, включая вызов функции scanf ()в буферах фиксированного размера с использованием строки форматирования %s. Ниже приведен неполный перечень процедур обработки строки, которые приводят к возникновению ситуаций переполнения буфера в стеке8.
Поскольку все эти функции уже широко известны и теперь считаются "легкой добычей" для хакеров, то классические атаки на переполнение буфера в стеке постепенно уходят в прошлое. Насколько быстро предается огласке информация о возможности проведения атаки на переполнение буфера в стеке, настолько же быстро и устраняется ошибка. Однако есть множество других проблем, которые приводят к искажению данных в памяти и переполнению буфера.
Функции, для которых не требуется наличие завершающего символа NULL
Управление буфером является намного более сложной проблемой, чем думают многие люди. Это не просто проблема нескольких вызовов функций API, которые работают с буферами, оканчивающимися символом NULL Зачастую с целью избежать стандартных проблем переполнения буфера используются арифметические операции по вычислению длины строки в буфере. Однако некоторые из призванных быть полезными функций API достаточно сложны в использовании, что приводит к путанице.
Одним из таких вызовов API, при использовании которых легко ошибиться, является вызов функции strncpyQ. Это весьма любопытный вызов, поскольку он предназначен в основном для предотвращения возможности переполнения буфера. Проблема в том, что в этом вызове один крайне важный и крайне опасный момент, о котором часто забывают: эта функция не устанавливает завершающий символ NULL в конце строки, если эта строка слишком большая, чтобы уместиться в предназначенный для нее буфер. Это может привести к тому, что "чужая" область памяти будет "присоединена" к предназначенному буферу. Здесь нет переполнения буфера в классическом смысле, но строка оказывается незавершенной.
Проблема в том, что теперь любой вызов функции strlen () завершится возвращением некорректного (т.е. неверного) значения. Не забывайте, что функция strlen () работает со строками, завершающимися символом NULL. Таким образом, она будет возвращать длину оригинальной строки плюс столько байтов, сколько будет до появления символа NULL в памяти, т.е. возвращаемое значение обычно будет намного больше, чем действительная длина строки. Любые арифметические операции, выполняемые на основе этой информации, будут неверными (и станут целью атаки).
Рассмотрим пример на основе следующего кода.
strncpy(цель/ источник, sizeof^anB)) ;
Если цель составляет 10символов, а источник— 11 символов (или более), включая символ NULL, то 10 символов не являются корректно завершенной строкой с символом NULL!
Рассмотрим дистрибутив UNIX-подобной операционной системы FreeBSD. BSD часто считают одной из наиболее безопасных UNIX-сред, однако даже в ней регулярно обнаруживаются трудные для выявления ошибки наподобие той, что была описана чуть выше. В реализации функции syslog есть программный код, с помощью которого выполняется проверка на предмет того, имеет ли удаленный хост права на подключение к демону syslogd. Этот программный код во FreeBSD 3.2 выглядит следующим образом.
strncpy(name, hname, sizeof name); if (strchrjmaee, '.")) == NULL)) {
strncat(name, sizeof name - strlen(name) - 1);
strncat(name, LocalDomain, sizeof name - strlen(name) - 1);
1

В данном случае, если переменная hname достаточно велика, чтобы целиком "заполнить" переменную name, то завершающий символ NULL не будет размещен в конце значения переменной name. В этом и заключается стандартная проблема использования функции strncpyC. При последующих арифметических операциях вычисление выражения sizeof name - strlen(name) приводит к получению отрицательного результата. Функция strncat принимает беззнаковое значение переменной, при этом негативное значение будет интерпретировано программой как очень большое положительное число. Таким образом функция strncat перезаписывает память после окончания буфера, выделенного для функции name. Для демона syslogd игра проиграна.
Ниже приведен список функций, в которых автоматически не устанавливается завершающий символ NULL в буфере.
fread() read () readv() pread() memcpy() memccpy() bcopy () gethostname () strncat ()
Уязвимые места, связанные с некорректным использованием функции strncpy (и ей подобных), можно назвать неисследованным источником будущих атак. Когда закончатся возможности проведения атак на более доступные цели, хакеры наверняка обратят свой взор на ошибки, подобные той, о которой мы только что рассказали.
Проблема завершающего символа NULL
В некоторых строковых функциях завершающий символ NULL всегда размещается в конце строки. Вероятно, это гораздо лучше, чем оставлять знакоместо для символа NULL для заполнения программистом, но проблемы все равно возникают. Арифметические операции, встроенные в некоторые из этих функций, могут выполняться с ошибками, в результате чего иногда символ NULL размещается после окон­чания буфера. Это так называемая ситуация "одного лишнего", когда происходит перезапись одного байта памяти. Эта на первый взгляд незначительная проблема порой приводит к полной компрометации программы.
Удачным примером можно назвать вызов функции strncat (), которая всегда размещает символ NULL после последнего байта переданной строки и поэтому может быть использована для перезаписи указателя в стековом фрейме. Следующая извлекаемая (pulled) из стека функция перемещает содержимое регистра ЕВР в ESP — указатель стека (6).
После выполнения строки 4, содержимое стека будет выглядеть следующим образом.
0012FEC8 0012FECC 0012FED0 0012FED4 74 65 73 74 00 СС СС СС СС СС СС СС 2С FF 12 00 В2 10 40 00 test <-
.ш <-
массив символов массив символов IIiI <- массив символов у.. <- содержимое ebp содержимое eip
Обратите внимание, что для массива символов [12] в памяти выделяется 12 байт.
Если мы предоставим в качестве значения р короткую строку ххх, то стек будет выглядеть следующим образом.
0012FEC8 0012FECC 0012FED0 0012FED4 0012FED8 74 65 73 74 test 78 78 78 00 ххх. <CC CC CC CC 1111
добавленное значение "ххх"
2C FF 12 00 B2 10 40 00
Обратите иннмаиие на добавленную строку ххх и что завершающим символ теперь установлен в самом конце буфера.
Что же произойдет, если мы предоставим чересчур длинную строку, наподобие ххххххххххх? Стек приобретет следующий вид.
При возвращении значения функции выполняются следующие машинные команды.
00401078 0040107А 0040107В
mov pop esp,ebp ebp
Можно проследить, что содержимое ESP восстанавливается из ЕВР. Тут все нормально. Затем мы видим, что сохраненное значение ЕВР обновляется из стека,

Переполнение буфера
273
но значением ЕВР в стеке является измененное нами значение. При возврате значения следующей функции из стека повторяются те же команды.
004010С2 mov 0040ЮС4 pop 004010C5 ret
esp,ebp ebp
Теперь у нас есть ЕВР с искаженным содержимым, которое в конечном итоге преобразуется в указатель стека.
Рассмотрим более сложную атаку на стек, при которой происходит управление данными в нескольких местах. В приведенном далее стеке содержится строка символов f f f f, которые были там размещены злоумышленником при предыдущем вызове функции. Правильным значением ЕВР должно было стать 0xl2FF28, но, как видим, нам удалось затереть это значение значением 0xl2FF00. Здесь основной мо­мент заключается в том, что значение 0xl2FF00 относится к строке символов f f f f, которыми мы можем управлять в стеке. Таким образом, мы можем заставить программу вернуться к месту, которое мы контролируем, а значит, провести успешную атаку на переполнение буфера. Обратите внимание, что хакер разместил значение FFFF в строке, следующей сразу после нового адреса, сохраненного в указателе ЕВР. Поскольку в коде эпилога как раз перед возвращением значения функции выполняется команда pop ebp, то значение, сохраненное по адресу, на который указывает новый ЕВР, выходит за пределы стека. Значение ESP увеличивается на 4 байт до адреса 0xl2FF04. Если мы разместим значение нашего EIP по адресу 0xl2FF04, то новым значением EIP станет 0x46464646. Атака завершилась полным успехом.
Перезапись фреймов обработчика исключений
В стеке также обычно хранятся указатели на обработчики исключений, а значит, вполне реально использовать переполнение буфера для перезаписи указателя на обработчик исключения. Используя очень большое переполнение буфера, мы можем затереть данные после окончания стека и специально вызвать исключение. Затем, поскольку мы уже переписали указатель обработчика исключений, исключение приведет к исполнению нашей полезной нагрузки (7). На следующем рисунке изображен внедренный буфер, который затирает данные после окончания стека. Хакер перезаписывает запись для обработчика исключений, которая хранится в стеке. Новая запись указывает на полезную нагрузку (атакующий код), поэтому при вызове исключения SEGV процессор переходит к исполнению атакующего кода.

 

На главную | Содержание | Назад | Вперёд
 
Яндекс.Метрика