Основы переполнения буфера

Основы переполнения буфера

Буфер переполняется, когда в него пытаются записать слишком много данных. Предположим, что буфер – это стакан воды. Можно наполнять стакан, пока он не станет полным, но потом вода начнет переливаться через край. Буферы похожи на стакан воды, а язык С (и производные от него, подобно С++) предлагает множество способов записи в буфер лишних, невмещающихся данных.

В этом случае при записи в стек данных появляются проблемы. Ранее были приведены примеры размещения в стеке локальных переменных (см. 16-байтный буфер данных в программах на рис. 8.1 и 8.4). Эти примеры показывают, что буфер фиксированного размера может быть размещен в стеке где угодно. Что произойдет при записи в буфер важной информации большего размера, чем может вместить размещенный в стеке буфер? Подобно стакану с водой, буфер переполнится!

При записи 16 байт в область буфера программы, представленной на рис. 8.1, он становится полным. При записи 17 байт один байт записывается в область стека, предназначенную для хранения переменной int2. Произошло искажение, или порча, данных (data corruption). При всех последующих обращениях к переменной int2 будет получено ее неверное значение. Продолжая в том же духе, при записи в буфер 28 байт будет затерто ранее сохраненное в стеке значение регистра EBP, а при записи 32 байт – значение регистра EIP. В результате при выполнении команды возврата из функции ret в регистр EIP будет записано значение из стека (затертое при записи в буфер) и, интерпретируя записанное значение как адрес следующей выполняемой команды, будет передано управление по содержимому регистра EIP. Если в регистр EIP будет помещен указатель на программу, то она будет выполнена.

Языку С может быть приписано следующее высказывание: «Мы предлагаем достаточно веревки, чтобы повеситься». Другими словами, язык С предлагает мощные средства управления компьютером, которыми программист должен разумно пользоваться, избегая потенциальных проблем. C – язык, в котором чрезмерно строго не контролируются типы обрабатываемых данных (loosely typed language), поэтому в нем не предусмотрено каких-либо мер безопасности при обработке разнотипных данных. Часто в написанных на языке C программах происходит переполнение буфера из-за допущенных ошибок при обработке строк. В таблице 8.1 приведены некоторые из небезопасных функций обработки строк языка C. Эта таблица ни в коем случае не претендует на полное освещение всех проблематичных функций, но дает хорошее представление относительно наиболее часто используемых.

Таблица 8.1.

Примеры проблематичных функций языка С

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

Простое неуправляемое переполнение: программа-пример

На рисунке 8.10 приведен простой пример неуправляемого переполнения. Он не годится для использования на практике, но полезен для изучения. Программа демонстрирует наиболее типичную ошибку программирования, которая отражается на работоспособности программы. В программе вызывается специально написанная для примера функция bof(), которая записывает двадцатисимвольную строку в буфер, предназначенный для хранения 8 байт. В результате происходит переполнение буфера. Заметьте, что в главной функции main функция printf() никогда не будет вызвана, поскольку в результате переполнения буфера при завершении функции bof () управление будет передано по неверному адресу возврата. Приведенная на рис. 8.10 программа была скомпилирована как консольное приложение Windows в режиме построения окончательной версии Release.

Рис. 8.10. Простое неуправляемое переполнение в стеке

Дизассемблирование

Хорошо раскрывают суть предыдущей программы результаты ее дизассемблирования, приведенные на рис. 8.11. Обратите внимание на то, что в функции main() стековые переменные не создаются, а переменная буфера buffer в функции bof() используется без предварительной инициализации. Определение переменной без инициализации уже может стать источником проблем и потенциальной возможности переполнения буфера. Это целиком зависит от состояния стека в момент создания переменной и ее использования в дальнейшем. Для инициализации переменных рекомендуется использовать функции memset() или bzero().

Рис. 8.11. Дизассемблированный вид программы-примера неуправляемого переполнения

Дампы стека

Приведенные дампы стека прослеживают изменения в стеке программы вплоть до возникновения переполнения буфера. Хотя в этой секции не рассматривается вопрос использования содержимого регистра EIP в личных целях, представленной на рис. 8.12 информации достаточно для того, чтобы позднее выполнить его.

Рис. 8.12. Дамп стека до вызова функции bof() в программе main()

Перед началом работы программы main() в стеке сохранены только значения регистров EBP и EIP, поскольку в программе main() нет локальных переменных.

На рисунке 8.13 показан дамп стека после начала работы функции bof(), но до инициализации переменной buffer функцией strcpy(). Поскольку буфер еще не проинициализирован, то в отведенной для него области памяти находятся случайные значения, которые ранее хранились в стеке. Дамп стека функции bof() после обращения к функции strcpy(), но до инициализации переменной buffer показан на рис. 8.14.

Рис. 8.13. Дамп стека после вызова функции bof(), но до выполнения функции strcpy()

Рис. 8.14. Дамп стека функции bof() после обращения к функции strcpy(), но до инициализации переменной buffer

Теперь в дампе стека видны два параметра функции strcpy. Первый параметр указывает на область буфера, размещенного в стеке, а второй – на статический буфер, вмещающий 20 символов «А».

Рис. 8.15. Дамп стека функции bof() после инициализации переменной buffer функцией strcpy() (сравните с рис. 8.13)

Из дампа стека видно, что функция strcpy(), проинициализировав буфер, уничтожила ранее записанные в стеке данные. В эпилоге функции bof() программа, попытавшись восстановить из стека содержимое регистра EBP, загрузит в регистр значение 0x414141. После этого команда ret восстановит из стека содержимое регистра EIP и попытается передать управление по восстановленному адресу. В результате возникнет ошибка нарушения доступа при попытке выполнить неразрешенную операцию с памятью, поскольку команда ret загрузит в регистр EIP значение 0x41414141, указывающее на недействительную область памяти (см. рис. 8.16).

Рис. 8.16. Диагностика аварийного завершения программы из-за неверного содержимого регистров EIP и EBP

Данный текст является ознакомительным фрагментом.