19 смертных грехов, угрожающих безопасности программ
Шрифт:
Мы не станем приводить исчерпывающий список языков, подверженных ошибкам из–за переполнения буфера, скажем лишь, что к их числу относится большинство старых языков.
Как происходит грехопадение
Классическое проявление переполнения буфера – это затирание стека. В откомпилированной программе стек используется для хранения управляющей информации (например, аргументов). Здесь находится также адрес возврата из функции и, поскольку число регистров в процессорах семейства х86 невелико, сюда же перед входом в функцию помещаются регистры для временного хранения. Увы, в стеке же выделяется память для локальных переменных. Иногда их неправильно называют статически распределенными в противоположность динамической памяти, выделенной из кучи. Когда кто–то говорит о переполнении статического буфера, он чаще всего имеет в виду переполнение
Возникает вопрос: почему мы продолжаем пользоваться столь очевидно опасной системой? Избежать проблемы, по крайней мере частично, можно было бы, перейдя на 64–разрядный процессор Intel Itanium, где адрес возврата хранится в регистре. Но тогда пришлось бы смириться с утратой обратной совместимости, хотя на момент работы над этой книгой представляется, что процессор х64 в конце концов станет популярным.
Можно также спросить, почему мы не переходим на языки, осуществляющие строгий контроль массивов и запрещающие прямую работу с памятью. Дело в том, что для многих приложений производительность высокоуровневых языков недостаточно высока. Возможен компромисс: писать интерфейсные части программ, с которыми взаимодействуют пользователи, на языке высокого уровня, а основную часть кода – на низкоуровневом языке. Другое решение–в полной мере задействовать возможности С++ и пользоваться написанными для него библиотеками для работы со строками и контейнерными классами. Например, в Web–сервере Internet Information Server (IIS) 6.0 обработка всех входных данных переписана с использованием строковых классов; один отважный разработчик даже заявил, что даст отрезать себе мизинец, если в его коде отыщется хотя бы одно переполнение буфера. Пока что мизинец остался при нем, и за два года после выхода этого сервера не было опубликовано ни одного сообщения о проблемах с его безопасностью. Поскольку современные компиляторы умеют работать с шаблонными классами, на С++ теперь можно создавать очень эффективный код.
Но довольно теории, рассмотрим пример.
tinclude <stdio.h>
void DontDoIhis (char* input)
{
char buf[16];
strcpy(buf, input);
printf("%s\n» , buf);
}
int main(int argc, char* argv[])
{
// мы не проверяем аргументы
// а чего еще ожидать от программы, в которой используется
// функция strcpy?
DontDoThis(argv[l]);
return 0;
}
Откомпилируем эту программу и посмотрим, что произойдет. Для демонстрации автор собрал приложение, включив отладочные символы и отключив контроль стека. Хороший компилятор предпочел бы встроить такую короткую функцию, как DontDoThis, особенно если она вызывается только один раз, поэтому оптимизация также была отключена. Вот как выглядит стек непосредственно перед вызовом strcpy:
0x0012FEC0 с8 fe 12 00 .. <– адрес аргумента buf
0x0012FEC4 с4 18 32 00 .2. <– адрес аргумента input
0x0012FEC8 d0 fe 12 00 .. <– начало буфера buf
0x0012FECC 04 80 40 00 .<<Unicode: 80>>@.
0x0012FED0 el 02 3f 4f .?0
0x0012FED4 66 00 00 00 f… <– конец buf
0x0012FED8 e4 fe 12 00 .. <– содержимое регистра EBP
0x0012FEDC 3f 10 40 00 ?.@. <– адрес возврата
0x0012FEE0 c4 18 32 00 .2. <– адрес аргумента DontDoThis
0x0012FEE4 cO ff 12 00 ..
0x0012FEE8 10 13 40 00 ..@. <– адрес, куда вернется main
Напомним, что стек растет сверху вниз (от старших адресов к младшим). Этот пример выполнялся на процессоре Intel со схемой адресации «little–endian». Это означает, что младший байт хранится в памяти первым, так что адрес возврата «3f104000» на самом деле означает 0x0040103f.
А теперь посмотрим, что происходит, когда буфер buf переполняется. Сразу вслед за buf находится сохраненное значение регистра EBP (Extended Base Pointer – расширенный указатель на базу). ЕВР содержит указатель кадра стека; при ошибке на одну позицию его значение будет затерто. Если противник сможет получить контроль над областью памяти, начинающейся с адреса 0x0012fe00 (последний байт вследствие ошибки обнулен), то программа перейдет по этому адресу и выполнит помещенный туда противником код.
Если не ограничиваться переполнением на один байт, то следующим будет затерт адрес возврата. Коль скоро противник
После того как адрес возврата переписан, в распоряжении противника оказываются аргументы атакуемой функции. Если функция перед возвратом каким–то образом модифицирует переданные ей аргументы, то открываются новые соблазнительные возможности. Это следует иметь в виду, оценивая эффективность таких средств борьбы с переполнением стека, как программа Stackguard Криспина Коуэена (Crispin Cowan), программа ProPolice, распространяемая IBM, и флаг /GS в компиляторе Microsoft.
Как видите, мы предоставили противнику как минимум три возможности получить контроль над нашим приложением, а это ведь была очень простая функция. Если в стеке объявлен объект класса С++ с виртуальными функциями, то станет доступна таблица указателей на виртуальные функции; такая ошибка тоже легко эксплуатируется. Если одним из аргументов функции является указатель на функцию, что часто бывает в оконных системах (например, в X Window System или Microsoft Windows), то перезапись этого указателя перед использованием–очевидный способ получить контроль над приложением.
Есть множество хитроумных способов перехватить управление программой, гораздо больше, чем способен измыслить наш слабый ум. Существует несоответствие между возможностями и ресурами, доступными разработчику и хакеру. В своей работе вы ограничены сроками, тогда как противник может тратить все свое свободное время на то, чтобы придумать, как заставить вашу программу делать то, что нужно ему. Ваша программа может защищать ресурс, достаточно ценный, чтобы потратить на ее взлом несколько месяцев. Хакер тратит массу времени на то, чтобы быть в курсе последних достижений в области взлома. К его услугам – такие ресурсы, как www.metasploit.com, позволяющие в несколько «кликов» создать shell–код, который будет делать что угодно и при этом включать только символы из ограниченного набора.
Если вы попытаетесь выяснить, можно ли создать эксплойт для какой–то программы, то, скорее всего, полученный ответ будет неполным. В большинстве случае можно лишь доказать, что программа либо уязвима, либо вы недостаточно хитроумны (или потратили на поиск решения недостаточно времени), чтобы написать для нее эксплойт. Очень редко можно с уверенностью утверждать, что для некоторого переполнения эксплойт невозможен.
Мораль, стало быть, в том, что самое правильное – исправить ошибки! Сколько раз случалось, что модификации с целью «повысить качество кода» заодно приводили и к исправлению ошибок, связанных с безопасностью. Автор как–то битых три часа убеждал команду разработчиков исправить некую ошибку. В переписке приняло участие восемь человек, и мы потратили 20 человекочасов (половина рабочей недели одного программиста), споря, нужно ли исправлять ошибку, поскольку разработчики жаждали получить доказательства того, что для нее можно написать эксплойт. Когда эксперты по безопасности доказали, что проблема действительно есть, для исправления потребовался час работы программиста и еще четыре часа на Тестирование. Сколько же времени ушло впустую!
Заниматься анализом надо непосредственно перед поставкой программы. На завершающих стадиях разработки хорошо бы иметь обоснованное предположение о том, достаточно ли велика опасность написания эксплойта для ошибки, чтобы оправдать риск, связанный с переделками и, как следствие, нестабильностью продукта.
Распространено заблуждение, будто переполнение буфера в куче не так опасно, как буфера в стеке. Это совершенно неправильно. Большинство реализаций кучи страдают тем же фундаментальным пороком, что и стек, – пользовательские и управляющие данные хранятся вместе. Часто можно заставить менеджер кучи поместить четыре указанных противником байта по выбранному им же адресу. Детали атаки на кучу довольно сложны. Недавно Matthew «shok» Conover и Oded Horovitz подготовили очень ясную презентацию на эту тему под названием «Re–liable Windows Heap Exploits» («Надежный эксплойт переполнения кучи в Win–dows»), которую можно найти на страницеДаже если сам менеджер кучи не поддается взломщику, в соседних участках памяти могут находиться указатели на функции или на переменные, в которые записывается информация. Когда–то эксплуатация переполнений кучи считалась экзотическим и трудным делом, теперь же это одна из самых распространенных атакуемых ошибок.