vmlinuz

исследования ядра LINUX'а

крис касперски ака мыщъх

ядро — это фундамент всей системы. на багистром ядре хорошего линуха не постоишь. разработчики не отходят от клавиатуры, выявляя все новые и новые ошибки, но баги размножаются быстрее! далеко не все ошибки «заразы» и лишь немногие из них допускают удаленное проникновение в систему. найти такой баг — большая удача! как хакеры исследуют ядро? какие инструменты используют? во об этом мы сейчас и поговорим!

Линуховое ядро это довольно сложное инженерное сооружение, исходные тексты которого занимают свыше сотни мегабайт. Чего тут только нет! Драйвера, 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-редакторе THE

Рисунок 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/Videoadapter, вслед за которыми идет «магическая последовательность» 00 00 B8 00 00. В обоих ядрах он расположен по смещению 14FF от начала файла. Таким образом, вторичный загрузчик начинается со смещения 200h и заканчивается в 14FFh. Он так же исполняется в 16-разрядном режиме и представляет собой смесь кода и данных, поэтому дизассемблировать его приходится с большой осторожностью (см. рисунок 6). Но прежде необходимо создать сегмент, ведь прошлый сегмент был усечен! Говорим Edit  Segment  New Segment, вводим имя сегмента (например, «ldr»), адрес начала (200h) и конца (1500h), а так же базовый адрес, равный стартовому адресу, деленному на 10h. Форсируем 16-битный режим и давим ОК.

Рисунок 6 вторичный загрузчик, представляющий собой смесь кода и данных

За вторичным загрузчик идет 100h ничейных байт, забитых нулями, а вот затем… со смещения 1500h начинается как-то дикий код, который никак не удается дизассемблировать. IDA выводит всего несколько строк, жалобно пришит и отказывается продолжать работу (см. листинг 1):

1600cld

1601cli

1602movax, 18h

1605db0

1606db0

1607db8Eh ; О

1608db0D8h ; ╪

Листинг 1 IDA Pro дизассемблирует «дикий» код — облом

HTE и HIEW как будто бы дизассемблируют дикий код, но делают это неправильно! (см. листинг 2).

1600 fccld

1601 facli

1602 b81800movax, 0x18

1605 0000add [bx+si], al

1607 8ed8movds, ax

1609 8ec0moves, ax

160b 8ee0movfs, ax

160d 8ee8movgs, 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.

За концом распаковщика идут текстовые строки «Systemhalted», «Ok, bootingthekernel», «invalidcompressedformat (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, глобальный поиск легко обнаружит ее в исходных текстах. Поскольку компилятор таких команд заведомо не понимает, здесь явно имела место ассемблерная вставка, а, значит, дизассемблерный код будет практически полностью совпадать с соответствующим фрагментом исходного текста!

В общем, в дизассемблировании ядра нет ничего сверхъестественного и эта задача вполне по силам рядовому кодокопателю.

В прикладных программах и серверных приложениях наибольшее количество ошибок сосредоточено в переполняющихся буферах (атака типа bufferoverflow или bufferoverrun). В ядре так же имеются буфера, некоторые из которых могут быть переполнены, однако, атаки этого типа для него не так характерны.

Вот пять основных источников ошибок — спинлуки (spinlock), неожиданные выходы из функции, ELF-загрузчик, менеджер виртуальной памяти и TCP/IP-стек. Рассмотрим всех кандидатов поподробнее.

Спинлуками называют ячейки памяти, защищающими многозадачный код от воздействия посторонних потоков. При входе в охраняемую зону, процессор устанавливает флаг, а при выходе — сбрасывает. До тех пор пока флаг не будет сброшен, остальные потоки топчутся у выхода и не могут выполнять код. На многопроцессорных ядрах, спинлуки начинаются с префикса LOCK, который легко найти в дизассемблерном тексте, если нажать <ALT-T>. Как мы уже говорили в статье «захватыват ring 0 в Linux» – поддержка многозадачности очень сложная задача и ошибок здесь просто тьма, так что жаловаться на то, что «всех багов уже переловили до нас» никому не приходится. К сожалению, большинство «многозадачных» ошибок имеют многоступенчатый характер, наглядно продемонстрированный в уже упомянутой статье (см. «проблемы многопоточности»), поэтому никаких универсальных методик их поиска не существует. Это работа для настоящих хакеров, способных удержать все ядро в голове и сложить разрозненную мозаику в единую картину. В общем, настоящий хардкор. Это сложно? Ну еще бы! Но мы ведь не ищем легких путей, верно? Зато и удовлетворение от найденной дыры намного больше, чем от просмотра порно.

ernel:C010A65E loc_C010A65E:; CODE XREF: sub_C010A984+108↓j

ernel:C010A65Elockdec byte ptr [ebx-3FCE77F0h]

ernel:C010A665jsloc_C010AA81

Листинг 4 классический спинлук

Неожиданные выходы из функции (они же преждевременные) происходят всякий раз, когда из-за какой-то ошибки функция уже не может (не хочет) продолжить работу делает немедленный return. Часть работы к этому моменту уже выполнена, а часть еще нет. Если программист допустит даже крошечную неаккуратность, структуры данных превратятся в кашу. Одна из таких ошибок содержится в функции create_elf_tables, описанной в прошлой статье.

Для поиска внеплановых выходов достаточно перейти в конец функции и проанализировать перекрестные ссылки, которые ведут наверх. Чем их больше, тем выше вероятность, что здесь окажется что-то не так. Ну а там и до дыры уже недалеко.

kernel:C010A810 loc_C010A810:; CODE XREF: kernel:C010A7F1↑j

kernel:C010A810moveax, 0FFFFFFEAh

kernel:C010A815

kernel:C010A815 loc_C010A815:; CODE XREF: kernel:C010A7CF↑j

kernel:C010A815; kernel:C010A809↑j

kernel:C010A815popebx

kernel:C010A816popesi

kernel:C010A817popedi

kernel:C010A818popebp

kernel:C010A819popecx

kernel:C010A81Aretn

Листинг 5 перекрестные ссылки в конце функции ведут к местам внезапного выхода

Загрузчик ELF-файлов, менеджер виртуальной памяти и TCP/IP стек — это настоящие айсберги, которые словно ледяные горы точат из ядра кишками наружу, но основная масса скрыта в глубине воды. Это сотни тысяч строк кода, сложным образом взаимодействующего между собой. Это плодотворная почва для всевозможных багов, кочующих из одну версию ядра в другую. Некоторые из них уже выявлены, некоторые только предстоит найти. В первую очередь следует обратить внимание на обработку нестандартных полей или дикое сочетание различных атрибутов (см. «эльфы падают в дамп»). Чтобы действовать не вслепую, имеет смысл скачать свежую подшивку RFC и обзавестись спецификацией на ELF формат. И то, и другое легко найти в сети.

  1. www.idapro.com:
    1. официальный сайт лучшего в мире коммерческого дизассемблера IDA Pro (на английском и русских языках);
  2. http:hte.sourceforge.net: - официальный сайт некоммерческого HEX-редактора, по возможностям приближающегося к IDA Pro (на английском языке); - http://www.kernel.org/pub/linux/kernel: - официальный сервер, раздающий исходные тексты линуховых ядер; - www.rfc-editor.org: - сборник RFC-стандартов, описывающих сетевые протоколы, бесплатно (на английском языке); - www.skyfree.org/linux/references/ELF_Format.pdf — ELF:** - спецификация ELF-формата, бесплатно (на английском языке); ===== заключение ===== Вот мы и добрались до ядра! Погрузились в настоящий дизассемблерный мир и увидели как выглядит LINUX не только извне, но и изнутри. Теперь самое главное запастись пивом, пакетными супами и терпением. Не стоит рассчитывать на быстрый успех. На поиск первой дыры могут уйти месяцы, особенно, если дизассемблер еще подрагивает в неуверенных рукам и постоянно перелистывается потрепанный справочник по машинным командам. В режиме глубокого хачинья, хакеры не отрываются от компьютера по 30 и даже 40 часов. Дизассемблирование затягивает! Попасть к нему в лапы легко, а вот вырваться очень сложно!