Текст книги "Защита от хакеров корпоративных сетей"
Автор книги: Коллектив Авторов
Жанр: Зарубежная компьютерная литература, Зарубежная литература
сообщить о неприемлемом содержимом
Текущая страница: 22 (всего у книги 69 страниц) [доступный отрывок для чтения: 22 страниц]
Конспект
Опасность непредвиденных входных данных
· Почти все приложения взаимодействуют с пользователем, получая от него данные.
· В приложении должен быть предусмотрен контроль действий пользователя.
· Особое внимание в приложении должно быть обращено на предотвращение переполнения буфера, изменение логики работы и контроль данных, передаваемых системным функциям.
Поиск обусловленных непредвиденными входными данными уязвимостей
· Любое приложение, взаимодействующее с пользователем или другим непроверенным приложением, может стать жертвой непредвиденных данных. С непредвиденными данными чаще всего сталкиваются:
– локальные приложения suid/sgid системы UNIX;
– серверы HTTP и другие Web-технологии построения приложений;
– запросы SQL;
– средства аутентификации приложений;
– методы маскировки данных, используемые при построении программ противодействия системам обнаружения вторжения IDS.
Методы поиска и устранения уязвимостей, обусловленных непредвиденными входными данными
· Просмотр исходного текста программы и метод «черного ящика» помогут обнаружить уязвимости и являются главным средством поиска потенциальных проблем.
· С непредвиденными данными можно бороться правильной фильтрацией данных или пропуском неверных символов. Во многие языки программирования (например, Perl, CFML,ASP, PHP и даже API SQL) встроены подобные возможности.
· Полезным средством борьбы с непредвиденными данными или данными неверного формата являются такие уловки программирования, как подмена символа, централизованные функции фильтрации или удаление неверных данных без выдачи диагностических сообщений.
Использование средств безопасности языков программирования для обработки непредвиденных данных
· Встроенные средства безопасности многих языков программирования позволяют во всеоружии встретить неверные данные.
· Своевременная установка нужных опций конфигурации (установка режимов проверки «дыр» в Perl или безопасной работы «safe_mode» в PHP, настройки «песочницы» (sandbox) в CFML) поможет предотвратить опасные последствия непредвиденных данных пользователя.
· Соответствующие настройки сервера, например отключение опции Enable Parent Paths информационного сервера Интернет IIS, помогут предотвратить доступ приложения к файлу вне корневой Web-директории.
· Установка нужных привилегий пользователь / запрос в MySQL предотвращает вызов из запросов запрещенных функций, например функций доступа к файлам.
Инструментарий обработки непредвиденных данных
· Программа Web Sleuth используется для взаимодействия и работы с Web-приложениями, предоставляя различные механизмы обхода и взлома протокола HTTP. Программа CGIAudit автоматически находит известные уязвимости интерфейса CGI.
· Программы RATS и Flawfinder просматривают исходные тексты программ, выявляя в них потенциально опасные места.
· Коммерческие продукты Retina и Hailstorm предназначены для методического зондирования и провоцирования сетевых приложений для обнаружения уязвимостей и возможности их использования.
· Прокси-HTTP Pudding маскирует запросы HTTP при помощи различных форм кодирования URL, включая сверхдлинное кодирование Unicode/UTF-8.
Часто задаваемые вопросы
Вопрос: Стоит ли заботиться о проверке и фильтрации входных данных?
Ответ: Все поступающие данные должны быть проверены и отфильтрованы. Никаких исключений. Не следует надеяться на то, что входные данные программы не содержат ошибок. Проверено на практике, что проверку данных выполняет небольшой фрагмент программного кода за ничтожно малое время. Поэтому глупо не предусмотреть в программе фильтр данных.
Вопрос: Какой язык программирования самый безопасный?
Ответ: На этот вопрос нельзя ответить однозначно. В языках Perl and PHP реализована прекрасная возможность автоматического распределения памяти, которая позволяет разместить в памяти любое количество входных данных. Но написанные на этих языках программы плохо масштабируются из-за того, что они выполняются в режиме интерпретации. Языки программирования C/C ++ требуют от разработчика принятия дополнительных мер по обеспечению безопасности. Но написанные на языках C/C ++ программы компилируются в выполнимый код, который работает быстрее и лучше масштабируется. Окончательный выбор языка должен быть основан на предъявляемых к приложению требованиях и навыках программиста работы со средствами разработки.
Вопрос: Где можно найти дополнительные сведения об аудите исходного текста программ?
Ответ: В книге «Hack Proofing Your Web Applications» издательства Syngress читатель найдет много намеков, подсказок, хитроумных приемов и указаний по аудиту исходных текстов программ для выявления в них уязвимостей.
Глава 8
Переполнение буфера
В этой главе обсуждаются следующие темы:
• Стек
• Стековый фрейм функции
• Основы переполнения буфера
• Пример программы, уязвимой к переполнению буфера
• Современные способы переполнения буфера
• Новаторские принципы построения программного кода полезной нагрузки
· Резюме
· Конспект
· Часто задаваемые вопросы
Введение
Большинство известных на сегодняшний день уязвимостей основаны на переполнении буфера. Благодаря нему возможна значительная часть удаленного использования программ в злонамеренных целях. Если в результате атаки буфер переполнится, то квалифицированный злоумышленник сможет запустить любую программу с правами атакованного процесса. Атаки на буфер часто используются для внедрения злонамеренной программы с функциями командного процессора.
Переполнение буфера – непредсказуемая ситуация, которая возникает в программах, написанных на разных языках программирования. В этой главе будут подробно рассмотрены причины возникновения переполнения буфера, способы выявления основанных на нем уязвимостей и принципы написания программ, извлекающих из него пользу.
Эта глава разделена на две части: для новичка и для квалифицированного читателя. Если читатель знаком с переполнением буфера и понимает, как им можно воспользоваться, то он может пропустить раздел для новичка. Но в любом случае рекомендуется просмотреть материал, предназначенный для квалифицированного читателя. Некоторые из вошедших в него способов входили в арсенал средств злоумышленников, например использовались при создании червя Code Red.
Стек
Стек – абстрактная структура данных, устроенная по принципу LIFO «последний вошел – первый вышел» (last in, first out – LIFO). Наглядно стек может быть представлен стопкой подносов в кафетерии. Например, если кто-то кладет поднос на вершину стопки, то этот поднос будет первым, который другой возьмет. В поддержке стека важная роль отводится внутренним компонентам процессора, прежде всего регистрам процессора ESP и EBP, которые облегчают его использование.
Стек играет важную роль при взаимодействии программ. При вызове функции программа временно сохраняет в нем адрес возврата, параметры вызываемой функции и ее локальные переменные. Стек позволяет программистам упростить передачу параметров вызываемой функции и доступ к ее локальным переменным. В основном стек подобен буферу, в котором хранится вся необходимая для работы функции информация. Стек создается при вызове функции и разрушается при ее завершении. Стек статичен в том смысле, что после его создания назначение и тип выделенной стеку памяти не меняются, хотя хранящиеся в стеке данные могут изменяться.
Примечание
Все приведенные в главе примеры откомпилированы в операционной системе Windows 2000 компилятором VC++ 6 SP5 (Msdn.microsoft.com), если об этом ничего не сказано. Для большей ясности и простоты при компиляции примеров использовалась возможность построения выполнимой версии программы с отключенными опциями оптимизации программного кода. Примеры дизассемблирования подготовлены с использованием дизассемблера IDA pro 4.18 (www.datarescue.com). Все примеры предполагают использование стандартного чипсета x86.
Рассматриваемые в главе стеки процессора Intel x86 инвертированы в том смысле, что области памяти с меньшими адресами находятся на «вершине» стека. Операция push перемещает указатель вершины стека ниже (проталкивает запись в стек), в то время как операция pop – выше (выталкивает данные из стека). Данные располагаются в области памяти, отведенной под стек, начиная со дна стека, то есть с его максимального адреса, по последовательно уменьшающимся адресам памяти. Отчасти этим объясняется переполнение буфера: при записи в буфер от младших адресов к старшим возможно затирание данных, ранее сохраненных в области памяти со старшими адресами, например подмена сохраненного в стеке содержимого расширенного регистра команд EIP (Extended Instruction Pointer). Адрес доступного верхнего элемента хранится в регистре-указателе стека ESP.
Ошибки и защита
Изучение языка ассемблера
Для того чтобы лучше понять устройство стека, нужно знать ассемблер. Прежде всего использование регистров для работы с данными стека. Как правило, при работе со стеком используются следующие три регистра:
• EIP (Extended Instruction Pointer) – расширенный регистр указателя инструкции. Содержимое регистра указывает на следующую исполняемую машинную команду (текущий программный код). При вызове функций содержимое регистра сохраняется в стеке для дальнейшего использования;
• ESP (Extended Stack Pointer) – расширенный регистр указателя вершины стека. Содержимое регистра указывает на вершину стека (текущее положение в стеке). Добавление данных в стек и их удаление из стека осуществляются командами push и pop или с помощью непосредственных операций над содержимым регистра указателя вершины стека;
• EBP (Extended Base Pointer) – расширенный регистр базового указателя (указателя основной точки стека). Во время работы функции содержимое регистра должно оставаться неизменным. Содержимое регистра и смещение позволяет адресовать хранимые в стеке переменные и данные. Почти всегда регистр указывает на вершину стека выполняющейся функции.
В последующих секциях главы будет рассказано о записи локальных переменных в стек, использовании стека для передачи параметров функции, и показано, каким образом злоумышленник может воспользоваться переполнением буфера, чтобы выполнить злонамеренный код.
Большинство компиляторов в начале функции вставляют служебный программный код, который иногда называют прологом (prologue) функции. Назначение пролога, помимо всего прочего, – подготовить стек для работы функции. Часто именно эта часть программного кода сохраняет старое содержимое регистра EBP и загружает в него указатель текущего положения в стеке. После этих действий регистр EBP содержит указатель на вершину стека выполняющейся функции. Зная содержимое регистра EBP и добавляя к нему смещение, получают ссылку на размещенные в стеке данные. Обычно регистр EBP адресует переменные, хранимые в стеке.
Приведенный ниже пример простой программы с несколькими локальными переменными демонстрирует сказанное. Подробные комментарии в исходном тексте программы позволят читателю лучше понять, что она делает.
Пример программы
Приведенная на рис. 8.1 написанная на языке C программа (C-программа) очень проста. Она присваивает своим переменным некоторые значения.
Рис. 8.1. Пример простой программы, иллюстрирующий работу стека
В программе создаются три локальные переменные, которые будут помещены в стек: 15-байтовый буфер символов buffer и две целые переменные intl и int2. Во время инициализации главной функции программы этим переменным присваиваются значения, а по завершении своей работы программа возвращает 1. Несмотря на простоту, программа полезна для изучения машинного кода оттранслированной функции на языке C вместе с прологом, эпилогом и стеком. Рассмотрим дизассемблерный вид приведенной на рис. 8.1 программы, которая была скомпилирована как консольное приложение Windows в режиме построения окончательной версии Release.
Дизассемблирование
Дизассемблирование приведенной на рис. 8.1 программы показывает, как компилятор решил несложную задачу определения, инициализации локальных переменных и записи их значений в стек. Результаты дизассемблирования приведены на рис. 8.2.
Рис. 8.2. Результаты дизассемблирования простой программы на языке C
Из рисунка 8.2 видно, что в прологе функции _main компилятор сначала сохранил старое значение регистра EBP в стеке, а затем записал в EBP адрес вершины стека функции (текущее положение в стеке). Эти стандартные действия делаются для того, чтобы каждая функция использовала свой собственный стек. Большинство, если не все, функций выполняют подобные операции в начале, а обратные им – в конце, в заключительной части программы – эпилоге.
Дамп стекаДля того чтобы можно было просмотреть область стека, после его инициализации в отладчике была установлена точка прерывания. При просмотре стека видно, что в нем хранится в начале работы функции, и легче понять, что происходит со стеком в процессе ее выполнения. Дамп стека показан на рис. 8.3.
Рис. 8.3. Дамп стека после инициализации
Из рисунка видно следующее. Буфер памяти, инициализированный строкой «Hello World», занимает 16 байт, а каждое целое число занимает область памяти размером по 4 байта. Шестнадцатеричные числа слева от дампа – статические адреса стека, которые определяются компилятором во время трансляции и которые Windows редко использует. Статические адреса стека потребуются для задания точек перехода при изучении возможности использования переполнения буфера в своих целях. Из рисунка 8.3 видно, что в области стека буфер памяти занимает 16 байт, а не 15, как определено в программе. Большинство компиляторов выравнивают область стека и области данных в стеке на границу двойного слова, поэтому области стека и данных начинаются с адреса памяти, кратного четырем. Это является обязательным условием повышения производительности процессора, и многие программы предполагают, что выравнивание обязательно выполняется. Поэтому буфер памяти занимает 16 байт в области стека, а не 15.
Разнообразие стековПосле инициализации стек может измениться по многим причинам. Опции компилятора влияют на размер и выравнивание стека программы, а настройки оптимизации генерируемого компилятором кода могут сильно изменить механизм создания стека и получения к нему доступа.
Некоторые функции в прологе сохраняют в стеке содержимое отдельных регистров. Во-первых, это делать необязательно, а во-вторых, это полностью зависит от компилятора и функции. В генерируемом компилятором коде может быть или несколько команд сохранения регистров, или одна команда pusha, которая сразу сохраняет содержимое всех регистров. К тому же в прологе могут быть изменены размеры стека и некоторые смещения.
Многие современные компиляторы языков С и С++ оптимизируют генерируемый код различными способами, что сказывается на работе со стеком и стековыми переменными. Например, в одном из наиболее часто встречающихся вариантов оптимизации генерируемого программного кода для доступа к данным в стеке вместо регистра EBP используются ESP. Код получается сложным и трудно поддается анализу, но при этом освобождается лишний регистр, который компилятор использует для генерации более быстрого кода. Другим примером влияния оптимизации кода на работу со стеком служит размещение компиляторами временных переменных в области стека. По разным причинам компиляторы размещают временные переменные в области стека, например для сокращения времени выполнения циклов в программе. Всегда подобные действия сопровождаются тщательной настройкой указателя смещения для доступа к данным стека.
Рассматривая проблемы работы со стеками, нельзя не упомянуть о новых способах защиты стеков программным кодом, генерируемым компиляторами. На них основан проект Crispin Cowen's Immunix (www.immunix.com). В проекте используется модифицированный GCC компилятор с языка C для генерации программного кода, практически не позволяющего выполнить злонамеренную программу в результате подмены содержимого регистра EIP при переполнении буфера. Как правило, используется способ, получивший название проверочных величин (canary values). Он основан на записи в прологе дополнительной величины в стек и проверке ее значения в эпилоге функции. При совпадении обоих значений гарантируются целостность стека и неизменность значений сохраненных в стеке регистров EIP и EBP.
Стековый фрейм функции
Как было упомянуто ранее, стек позволяет решить многие задачи. Во-первых, обеспечить локальное хранение переменных и данных функции. Во-вторых, передавать параметры в вызываемую функцию. В этой части главы будет рассказано, как компиляторы передают параметры вызываемым функциям и как это влияет на стек. Кроме того, будет уделено внимание разъяснению вопросов использования стека в командах процессора вызов функции call и возврата из нее ret.
Основные сведенияСтековый фрейм функции (stack frame) – область памяти, выделяемая всякий раз, когда вызывается функция. Она предназначается для временного хранения параметров, содержимого регистра EIP и, возможно, любых других регистров, а также локальных переменных функции. Ранее внимание читателя было заострено на использовании стека при хранении локальных переменных, а теперь будет рассказано о других возможностях его использования.
Для того чтобы понять, как работает стек, следует немного знать о командах процессора Intel call и ret. Команда call – основная команда для существования функции. Команда позволяет выполнить другую часть кода, запомнив при этом адрес точки возврата в стеке. Для этого команда call работает следующим образом:
1) проталкивает в стек адрес следующей команды, который является адресом точки возврата – точки, куда процессор передаст управление (возвратится) после выполнения функции;
2) передает управление по указанному в команде call адресу для выполнения команд функции.
А команда ret делает противоположное. Ее задача состоит в том, чтобы возвратиться из вызываемой функции к команде, следующей за командой call. Для этого команда ret выполняет следующие действия:
1) извлекает из стека сохраненный адрес точки возврата;
2) передает управление по только что извлеченному из стека адресу точки возврата.
Комбинация этих двух команд позволяет легко организовать передачу управления командам функции и вернуться обратно по ее завершении. Кроме того, благодаря сохраненному в стеке содержимому регистра EIP всегда можно прочитать из стека адрес точки перехода. После изучения принципов работы фреймового стека функции об этом будет сказано подробнее.
Передача параметров в функцию. Простой примерВ разделе приведен пример простой программы, иллюстрирующий использование фреймового стека функции для передачи параметров функции. В программе создаются несколько локальных переменных, инициализируется и вызывается функция callex, входными параметрами которой являются только что проинициализированные переменные. Функция callex отображает свои параметры на экране монитора.
На рисунке 8.4 приведена программа, которая поясняет структуру фреймового стека функции и его использование в командах call и ret.
Рис. 8.4. Пример программы, демонстрирующей использование стека в командах вызова и возврата
Дизассемблирование
Приведенная на рис. 8.4 программа была скомпилирована как консольное приложение Windows в режиме построения окончательной версии Release. Результаты дизассемблирования функций callex() и main() приведены на рис. 8.5 и демонстрируют машинный код функций callex() и main() после компиляции. Обратите внимание на передачу по ссылке буфера памяти buffer из функции main() функции callex(). Другими словами, функция callex() получает указатель на буфер buffer, а не копию содержащихся в нем данных. Это означает, что все изменения в буфере buffer, выполненные в функции callex(), тут же отражаются на содержимом буфера buffer в main(), поскольку на самом деле это одна и та же переменная.
Рис. 8.5. Дизассемблированный вид функции callex()
Дампы стека
На рисунках 8.6–8.9 представлен стек в различные моменты выполнения программы. Воспользуемся приведенными на рисунках 8.6–8.9 дампами, исходным текстом программы на языке C и ее дизассемблерным видом, для того чтобы лучше понять происходящие в стеке изменения и их причины. Это поможет понять принципы работы фреймового стека функции и его роль и место в программе.
На рисунке 8.6 показан дамп стека сразу после инициализации переменных, но до операций вызова функции и записи в стек ее входных параметров. Это пример «чистого» стека функции.
Рис. 8.6. Дамп стека после инициализации переменных в функции main()
Далее, перед вызовом функции callex() в стек были помещены ее три параметра (см. рис. 8.7).
Рис. 8.7. Дамп стека до вызова функции callex() из функции main()
Обратите внимание на произошедшие изменения в дампе стека по сравнению с рис. 8.6. После размещения переменных в области стека функции main() в стек были записаны параметры вызываемой функции callex(), но сама функция пока еще не была вызвана. На рисунке 8.8 приведен дамп стека функции callex() после ее вызова.
Рис. 8.8. Дамп стека после вызова функции callex() и выполнения команд пролога, но перед выполнением оператора printf в функции callex()
Показанный на рис. 8.8 стек проинициализирован функцией callex(). Единственное, что осталось выяснить, – это вид стека перед обращением к функции printf(), список параметров которой состоит из четырех элементов.
Наконец, перед обращением в функции callex() к функции вывода значений переменных printf() в стек помещаются четыре параметра. Это видно из дампа стека, представленного на рис. 8.9.
Рис. 8.9. Дамп стека перед обращением к функции printf() в функции callex()
Приведенные дампы стека позволят читателю хорошо понять принципы заполнения стека. Приобретенные знания пригодятся при обсуждении способов переполнения буфера.
Внимание! Это не конец книги.
Если начало книги вам понравилось, то полную версию можно приобрести у нашего партнёра - распространителя легального контента. Поддержите автора!Правообладателям!
Данное произведение размещено по согласованию с ООО "ЛитРес" (20% исходного текста). Если размещение книги нарушает чьи-либо права, то сообщите об этом.Читателям!
Оплатили, но не знаете что делать дальше?