Автор: (c)Крис Касперски ака мыщъх
Ядро - это фундамент всей системы. На багистом ядре хорошего линуха не построишь. Разработчики не отходят от клавиатуры, выявляя все новые и новые ошибки, но баги размножаются быстрее! Далеко не все ошибки "заразны" и лишь немногие из них допускают удаленное проникновение в систему. Найти такой баг - большая удача! Как хакеры исследуют ядро? Какие инструменты используют? Вот об этом мы сейчас и поговорим!
Линуховое ядро - это довольно сложное инженерное сооружение, исходные тексты которого занимают свыше сотни мегабайт. Чего тут только нет! Драйвера, TCP/IP-стек, менеджер виртуальной памяти, планировщик потоков, загрузчик ELF-файлов и прочее барахло. Все это хозяйство не свободно от ошибок, над поиском которых работают десятки хакерских групп и тысячи независимых кодокопателей по всему миру. Хотите к ним приобщиться? Что за вопрос! Кто же этого не хочет! Правда, не у всех получается, особенно - с первого раза, но "лиха беда начало"!
Существует, по меньшей мере, две методики поиска багов, но обе они порочные и неправильные. Одни хакеры предпочитают просматривать исходные коды ядра, анализируя строку за строкой, другие - дизассемблируют готовое ядро. Вот неполный перечень недостатков первого способа:
а) Вместо фактического значения переменной в Си сплошь и рядом используются макросы, определяемые неизвестно где, причем макрос может переопределяться многократно или, что еще хуже - различные включаемые файлы содержат несколько независимых макросов с одинаковым именем, так что глобальный контекстный поиск, практикуемый многими исследователями, не помогает (можно, правда, прогнать исходный текст через препроцессор - cpp имя_файла.c , но от этого его объем, а значит и время анализа только возрастет);
б) Ни одна известная мне IDE не способна отображать перекрестные ссылки на функции/данные, трассировать поток управления и делать множество других полезных вещей, с которыми легко справляется любой приличный дизассемблер;
в) В процессе компиляции могут "маскироваться" одни ошибки и добавляться другие, к тому же никогда нельзя сказать наперед, по каким адресам и в каком порядке компилятор расположит переменные и буфера в памяти, а для написания shell-кода это критично!
С другой стороны, дизассемблерный листинг ядра не просто велик. Он огромен! Это миллионы строк ассемблерного кода, и даже если на каждую команду потратить всего несколько секунд, даже поверхностный анализ растянется как минимум на сезон. Но ведь нам и не нужно дизассемблировать все ядро целиком! Ошибки не размазаны тонким слоем по машинному коду, а гнездятся во вполне предсказуемых местах. Никто не говорит, что ловля багов - это просто. Зато интересно! Сознайтесь, разве вам никогда не хотелось заглянуть в ядро, потрогать машинный код руками и посмотреть, как все это выглядит вживую (то есть "на самом деле"), а не в исходных текстах, которые любой "чиста хакер" может скачать из сети? И эта возможность сейчас представится!
Для штурма ядра нам, во-первых, понадобится само ядро, которое мы собрались штурмовать. Какой дистрибутив выбрать? Лучше взять тот, что поновее, хотя особой разницы между ними нет, ведь ядро разрабатывается независимо от остальной "начинки". Главное, чтобы оно было широко распространено, иначе - какой прок от дырки, которая есть только на одной-двух машинах во всем мире?
Ядро будет лежать в директории /boot под именем vmlinuz. В действительности, это еще не ядро, а только символическая ссылка на него. Само же ядро лежит рядом под именем vmlinuz.x.y.z, где xyz - версия ядра. Мы покажем, как распотрошить ядра с 2.4.27 и 2.6.7, входящие в мой любимый дистрибутив KNOPPIX 3.7. Остальные потрошатся аналогично, только смещения, естественно, будут другими.
Кроме самого двоичного файла нам также потребуются его исходные тексты, с которыми мы будет сверяться, в случае чего. Если они не входят в дистрибутив (а большинство популярных дистрибутивов занимают всего один CD и распространяются без исходных текстов) их можно скачать с сервера: http://www.kernel.org/pub/linux/kernel/. Нам придется принять от 25 до 45 мегабайт и освободить на жестком диске, по крайней мере, 150 - 300 мегабайт для распаковки архива. Все ядра поставляются в упакованном виде в двух форматах - стандартном gzip'е и более продвинутом bzip 2, который жмет на 25% плотнее, что уменьшает размер ядра чуть ли не на 10 Мегабайт, а для модемного соединения это очень ощутимая величина!
Дизассемблер - ну, лучше, чем IDA Pro вы вряд ли что-то найдете. До недавнего времени IDA Pro работала только под MS-DOS\OS/2\Windows, но теперь она перенесена и на Linux, что очень здорово! Обладателям более древних версий можно посоветовать скопировать ядро на дискету и дизассемблировать его под Windows или воспользоваться эмулятором Wine - IDA Pro замечательно работает и под ним. Кстати говоря, на Linux перенесена только консольная версия, которая лишена всех графических "вкусностей", например, диаграмм. Однако гуевый интерфейс с точки зрения хакеров - сакс и маст дай. Текстовой режим - форевер!
Рисунок 1. Дизассемблирование ядра в консольной версии IDA Pro 4.7 под Linux.
Рисунок 2. Дизассемблирование ядра в графической версии IDA Pro 4.7 под Windows 2000.
Если нет денег на IDA Pro, можно попробовать HT-editor - бесплатный hex-редактор и дизассемблер в одном флаконе. Он автоматически восстанавливает перекрестные ссылки, трассирует поток управления, поддерживает символьные имена и комментарии. Грубо говоря, это усеченная IDA Pro в миниатюре. Исходные тексты последней версии можно скачать с: http://hte.sourceforge.net/. Они успешно компилируются под Linux, FreeBSD, OpenBSD и, конечно же, Win32. Но если вам лень компилировать, можно скачать уже готовый бинарный файл, правда, далеко не первой свежести.
Рисунок 3. Дизассемблирование ядра в hex-редакторе HTE.
Рисунок 4. Редактор HTE за комплексным поиском.
Наступает волнующий миг: файл vmlinuz загружается в дизассемблер! Начинается самое интересное: IDA Pro не может опознать формат и загружает его как бинарный, а это уже нехорошо! Ядро имеет сложную структуру, состоящую из нескольких загрузчиков, последовательно отрабатывающих один за другим (ну, прямо как ступени ракеты), а основная часть ядра упакована. Как разобраться с этим хозяйством? Задача-минимум: распотрошить ядро на модули, определив базовый адрес загрузки и разрядность каждого из них. Кто-то может сказать - а в чем, собственно проблема? Ведь у нас есть исходные тексты! Что ж, исходные тексты - это, конечно, хорошо, но вот вопрос - какой файл какой части ядра соответствует? Так что без хорошего путеводителя здесь никуда!
Первые 200h байт файла vmlinuz принадлежат boot-сектору, который грузится по адресу 0000:7C00 и выполняется в 16-разряном режиме. Нажимаем <Alt-S> или обращаемся к меню Edit -> Segment -> Edit Segment (здесь и далее - горячие комбинации указаны для IDA Pro 4.7, в других версиях они могут слегка отличаться). Вводим имя сегмента: boot, начальный адрес оставляем без изменений, а конечный меняем на 200h. На все грозные предупреждения отвечаем однозначным "yes". Затем подводим курсор к первому байту кода и нажимаем <C>, чтобы IDA Pro превратила ячейки памяти в код. После этого дизассемблирование можно продолжать как обычно. Исходный код загрузчика можно найти в файле \arch\i386\boot\bootsect.S, а можно и не искать - нам он неинтересен. За долгие годы он вылизан дочиста. Даже если какие-то баги в нем есть, пробить в них дыру удастся навряд ли.
Рисунок 5. Изменение атрибутов сегмента в IDA Pro.
Мы видим, что boot-сектор перемещается по адресу 9000h:0000h и считывает с диска вторичный загрузчик, который также расположен внутри vmlinuz, сразу вслед за boot-сектором. Здесь расположены модули setup.S и video.S, загружающиеся по адресу 1000h:0000h и работающие в 16-разрядном режиме. Начало модуля setup.S опознается по сигнатуре "HdrS", следующей после jmp'а. Конец video.S легко определить по строкам: CGA/MDA/HGA/EGA/VGA/VESA/Video adapter, вслед за которыми идет "магическая последовательность" 00 00 B8 00 00. В обоих ядрах он расположен по смещению 14FF от начала файла. Таким образом, вторичный загрузчик начинается со смещения 200h и заканчивается в 14FFh. Он также исполняется в 16-разрядном режиме и представляет собой смесь кода и данных, поэтому дизассемблировать его приходится с большой осторожностью (см. рисунок 6). Но прежде необходимо создать сегмент, ведь прошлый сегмент был усечен! Говорим Edit -> Segment -> New Segment, вводим имя сегмента (например, "ldr"), адрес начала (200h) и конца (1500h), а также базовый адрес, равный стартовому адресу, деленному на 10h. Форсируем 16-битный режим и давим ОК.
Рисунок 6. Вторичный загрузчик, представляющий собой смесь кода и данных.
За вторичным загрузчик идет 100h ничейных байт, забитых нулями, а вот затем... со смещения 1500h начинается как-то дикий код, который никак не удается дизассемблировать. IDA выводит всего несколько строк, жалобно пищит и отказывается продолжать работу (см. листинг 1):
1600 cld 1601 cli 1602 mov ax, 18h 1605 db 0 1606 db 0 1607 db 8Eh ; О 1608 db 0D8h ;
Листинг 1. IDA Pro дизассемблирует "дикий" код - облом.
HTE и HIEW как будто бы дизассемблируют дикий код, но делают это неправильно! (см. листинг 2):
1600 fc cld 1601 fa cli 1602 b81800 mov ax, 0x18 1605 0000 add [bx+si], al 1607 8ed8 mov ds, ax 1609 8ec0 mov es, ax 160b 8ee0 mov fs, ax 160d 8ee8 mov gs, ax
Листинг 2. HTE дизассемблирует "дикий" код - неправильный результат.
А все потому, что начиная с этого места, ядро начинает исполняться в 32-разрядном защищенном режиме и для правильного дизассемблирования разрядность сегмента необходимо изменить. После чего IDA Pro заработает, как ни в чем не бывало. Сейчас мы находимся в распаковщике, подготавливающем основной ядерный код к работе. Он реализован в файлах \arch\i386\boot\compressed\head.S и misc.c. "Персонального" адреса загрузки не имеет и грузится вместе с первичным загрузчиком по адресу 1000h:0000h. Таким образом, первый байт распаковщика расположен в памяти по адресу 1000h:0000h + sizeof(ldf) == 1000h:01300h, что соответствует физическому адресу 101300h. Распаковщик настраивает сегментные регистры DS/ES/SS/GS/FS на селектор 18h, а регистр CS - на селектор 10h.
За концом распаковщика идут текстовые строки "System halted", "Ok, booting the kernel", "invalid compressed format (err=1)", за ними следует длинная цепочка нулей, а потом начинается упакованный код, дизассемблировать который без предварительной распаковки невозможно. А как его распаковать? Поскольку линуксоиды не любят изобретать велосипед и всегда стремятся использовать готовые компоненты, ядро упаковывается в формате gzip.
Упакованный код начинается с "магической последовательности" 1F 8B 08 00, которую легко найти в любом hex-редакторе. В ядре 2.4.27 она расположена по смещению 4904h, а в ядре 2.6.7 - по смещению 49D4h от начала файла. Выделим область отсюда и до конца файла и запишем ее в файл с расширением gz (например, kernel.gz). Пропустив ее через gzip (gzip -d kernel.gz) мы получим на выходе готовый к дизассемблированию образ ядра. IDA Pro уже ждет, когда он будет загружен в нее.
Основной код ядра исполняется в 32-разрядном режиме и грузится в память по адресу 10:C0100000h. В самом начале идет модуль \arch\i386\kernel\head.S, а затем - init.c, подгружающий все остальные модули. Как определить, какому именно модулю соответствует данная часть дизассемблерного кода?
В директории /boot лежит замечательный файл System.map-x.y.z (где x.y.z - номер версии ядра), в котором перечислены адреса публичных символьные имен, они же - метки (см. листинг 3):
c0108964 T system_call c010899c T ret_from_sys_call c01089ad t restore_all c01089bc t signal_return c01089d4 t v86_signal_return c01089e4 t tracesys c0108a07 t tracesys_exit c0108a11 t badsys
Листинг 3. Фрагмент файла System.map-2.4.27.
В частности, в ядре 2.4.27 метке ret_from_sys_call соответствует адрес C010899Ch. Отняв отсюда базовый адрес, мы получим смещение метки от начала файла: 899Ch, ну, а саму метку нетрудно найти в исходных текстах глобальным поиском. Она определена в файле \arch\i386\kernel\entry.S. Остальные метки обрабатываются аналогично.
А вот другой трюк: если в ядре встретилась текстовая строка или "редкоземельная" команда вроде lss или mov cr4,xxx, глобальный поиск легко обнаружит ее в исходных текстах. Поскольку компилятор таких команд заведомо не понимает, здесь явно имела место ассемблерная вставка, а значит дизассемблерный код будет практически полностью совпадать с соответствующим фрагментом исходного текста!
В общем, в дизассемблировании ядра нет ничего сверхъестественного и эта задача вполне по силам рядовому кодокопателю.
В прикладных программах и серверных приложениях наибольшее количество ошибок сосредоточено в переполняющихся буферах (атака типа buffer overflow или buffer overrun). В ядре также имеются буфера, некоторые из которых могут быть переполнены, однако атаки этого типа для него не так характерны.
Вот пять основных источников ошибок - спинлуки (spin lock), неожиданные выходы из функции, ELF-загрузчик, менеджер виртуальной памяти и TCP/IP-стек. Рассмотрим всех кандидатов поподробнее.
Спинлуками называют ячейки памяти, защищающими многозадачный код от воздействия посторонних потоков. При входе в охраняемую зону процессор устанавливает флаг, а при выходе - сбрасывает. До тех пор, пока флаг не будет сброшен, остальные потоки топчутся у выхода и не могут выполнять код. На многопроцессорных ядрах спинлуки начинаются с префикса LOCK, который легко найти в дизассемблерном тексте, если нажать <ALT-T>. Как мы уже говорили в статье "Захват ring 0 в Linux", поддержка многозадачности - очень сложная задача и ошибок здесь просто тьма, так что жаловаться на то, что "всех багов уже переловили до нас" никому не приходится. К сожалению, большинство "многозадачных" ошибок имеют многоступенчатый характер, наглядно продемонстрированный в уже упомянутой статье (см. "Проблемы многопоточности"), поэтому никаких универсальных методик их поиска не существует. Это работа для настоящих хакеров, способных удержать все ядро в голове и сложить разрозненную мозаику в единую картину. В общем, настоящий хардкор! Это сложно? Ну, еще бы! Но мы ведь не ищем легких путей, верно? Зато и удовлетворение от найденной дыры намного больше, чем от просмотра порно.
kernel:C010A65E loc_C010A65E: ; CODE XREF: sub_C010A984+108vj kernel:C010A65E lock dec byte ptr [ebx-3FCE77F0h] kernel:C010A665 js loc_C010AA81
Листинг 4. Классический спинлук.
Неожиданные выходы из функции (они же преждевременные) происходят всякий раз, когда из-за какой-то ошибки функция уже не может (не хочет) продолжить работу делает немедленный return. Часть работы к этому моменту уже выполнена, а часть - еще нет. Если программист допустит даже крошечную неаккуратность, структуры данных превратятся в кашу. Одна из таких ошибок содержится в функции create_elf_tables, описанной в прошлой статье.
Для поиска внеплановых выходов достаточно перейти в конец функции и проанализировать перекрестные ссылки, которые ведут наверх. Чем их больше, тем выше вероятность, что здесь окажется что-то не так. Ну а там и до дыры уже недалеко.
kernel:C010A810 loc_C010A810: ; CODE XREF: kernel:C010A7F1^j kernel:C010A810 mov eax, 0FFFFFFEAh kernel:C010A815 kernel:C010A815 loc_C010A815: ; CODE XREF: kernel:C010A7CF^j kernel:C010A815 ; kernel:C010A809^j kernel:C010A815 pop ebx kernel:C010A816 pop esi kernel:C010A817 pop edi kernel:C010A818 pop ebp kernel:C010A819 pop ecx kernel:C010A81A retn
Листинг 5. Перекрестные ссылки в конце функции ведут к местам внезапного выхода.
Загрузчик ELF-файлов, менеджер виртуальной памяти и TCP/IP-стек - это настоящие айсберги, которые словно ледяные горы торчат из ядра кишками наружу, но основная масса скрыта в глубине воды. Это сотни тысяч строк кода, сложным образом взаимодействующего между собой. Это плодотворная почва для всевозможных багов, кочующих из одной версии ядра в другую. Некоторые из них уже выявлены, некоторые только предстоит найти. В первую очередь следует обратить внимание на обработку нестандартных полей или дикое сочетание различных атрибутов (см. "Эльфы падают в дамп"). Чтобы действовать не вслепую, имеет смысл скачать свежую подшивку RFC и обзавестись спецификацией на ELF-формат. И то, и другое легко найти в сети.
Вот мы и добрались до ядра! Погрузились в настоящий дизассемблерный мир и увидели, как выглядит Linux не только извне, но и изнутри. Теперь самое главное - запастись пивом, пакетными супами и терпением. Не стоит рассчитывать на быстрый успех. На поиск первой дыры могут уйти месяцы, особенно если дизассемблер еще подрагивает в неуверенных руках и постоянно перелистывается потрепанный справочник по машинным командам. В режиме глубокого хака хакеры не отрываются от компьютера по 30 и даже 40 часов. Дизассемблирование затягивает! Попасть к нему в лапы легко, а вот вырваться очень сложно!