Автор: (c)Крис Касперски ака мыщъх
Нулевое кольцо дает полную власть над процессором, позволяя делать с ним все, что угодно. На этом уровне исполняется код операционной системы, модули ядра и кое-что еще. Считается, что LINUX надежно оберегает нулевое кольцо от хакерского вторжения, но это не так. За последние несколько лет обнаружено множество дыр и некоторые из которых остаются незалатанными до сих пор.
Что можно сделать с прикладного уровня? Выполнить непривилегированную команду процессора, обратиться к пользовательской ячейке памяти, дернуть системным вызовом... Запись в порты ввода/вывода, перепрограммирование BIOS, маскировка процессов и сетевых соединений возможны только с уровня ядра. Все хакеры стремятся в этот священный Грааль, но не все его находят. Много дорог ведет туда, поэтому мы расскажем только о самых интересных из них.
С правами root'а проникнуть в ядро не проблема. Можно, например, написать свой LKM-модуль (Loadable Kernel Module - загружаемый модуль ядра) и загрузить его командой insmod. LKM-модули пишутся очень просто (это вам не Windows-драйвера!). Примеры готовых модулей можно найти в статье "Прятки в Linux", там же рассказывается, как их замаскировать от взора бдительного администратора.
Другой вариант. Ядро монтирует два псевдоустройства - /dev/mem (физическая память до виртуальной трансляции) и /dev/kmem (физическая память после виртуальной трансляции). Из-под root'а мы можем манипулировать с кодом и данными ядра.
Короче, весь вопрос в том - как этого самого root'а заполучить? Легальным образом этого не сделать никак! LINUX поддерживает целый комплекс мер безопасности (только охранников в бронежилетах и машин с мигалками не хватает), однако в системе защиты имеется множество дыр, делающих ее похожей на дуршлаг. Вот этими дырами мы и воспользуемся!
Крошечный чип Голубого Зуба использует довольно навороченный протокол связи, поддержка которого проходит довольно болезненно. Практически ни одному коллективу разработчиков не удалось предотвратить появление новых дыр, в которые и слон пролезет, но если не слон, но червь - точно. Не стала исключением и LINUX. В апреле 2005 года появилось сообщение о дыре, а следом за этим был написан Kernel Bluetooth Local Root Exploit, работающий на ядрах 2.6.4-52, 2.6.11 и некоторых других.
Ошибка разработчиков состояла в том, что эти редиски разместили структуры сокета Голубого Зуба в пользовательской области памяти, тем самым открыв полный доступ к модификации всех полей. Одним из таких полей оказался указатель на код, вызываемый с уровня ядра. При нормальном развитии событий он указывает на библиотеку поддержки Голубого Зуба, но нам ничего не стоит перенаправить его на shell-код!
Ключевой фрагмент эксплоита, дающего права root'а из-под юзера, приведен ниже. Оригинальный исходный текст лежит на http://home.paf.net/qobaiashi/ong_bak.c, а здесь копия: http://www.securiteam.com/exploits/5KP0F0AFFO.html (оригинальный адрес у меня так и не открылся).
Листинг 1. Ключевой фрагмент Kernel Bluetooth Local Root эксплоита, дающий root'а из-под юзера.
Самой свежей дырой, которая только была найдена на момент написания этих строк, оказалась уязвимость в ELF-загрузчике, обнаженная 11 мая 2005 года и поражающая целую серию ядер: 2.2.27-rc2, 2.4, 2.4.31-pr1, 2.6, 2.6.12-rc4 и т. д.
Ошибка сидит в функции elf_core_dump(), расположенной в файле binfmt_elf.c. Ключевой фрагмент уязвимого листинга выглядит так:
Листинг 2. Ключевой фрагмент функции elf_core_dump(), подверженной переполнению.
Типичное переполнение буфера! Программист объявляет знаковую переменную len (см. /* 1 */) и спустя некоторое время передает ее функции copy_form_user, копирующей данные из пользовательской памяти в область ядра. Проверка на отрицательное значение не выполняется (см. /* 2 */). Что это значит для нас в практическом плане? А вот что! Если current->mm->arg_start будет больше, чем current->mm->arg_end, в ядро скопируется о-о-очень большой регион пользовательского пространства.
А как этого можно добиться? Анализ показывает, что переменные current->mm->arg_start и current->mm->arg_end инициализируются в функции create_elf_tables, причем - если функция strnlen_user возвратит ошибку, то будет инициализирована лишь переменная current->mm->arg_start, а current->mm->arg_end сохранит свое значение, унаследованное от предыдущего файла.
Листинг 3. Ключевой фрагмент функции create_elf_tables.
Остается сущая мелочь. Обломать функцию strnlen_user, расположив обе переменных в секции ELF файла с закрытым доступом (PROT_NONE), при обращении к которой произойдет исключение. Для сброса коры программы, ядро вызовет core_dump(). Она в свою очередь вызовет elf_core_dump() и... тут-то и произойдет переполнение! Перезапись области ядра открывает практически неограниченные возможности, ведь shell-код выполняется на нулевом кольце!
Демонстрационной эксплоит лежит здесь: http://www.isec.pl/vulnerabilities/isec-0023-coredump.txt.
В классической UNIX никаких потоков вообще не было, а потому не существовало проблемы их синхронизации. С функцией fork() и развитыми средствами межпроцессорного взаимодействия потоки не очень-то и нужны. Но все-таки они появились, продырявив систему до самого дна. Ядро превратилось в настоящее скопище багов. Вот только один из них, обнаруженный в начале января 2005 года и поражающий все ядра версии 2.2, а ядра с версиями от 2.4 до 2.4.29-pre3 и от 2.6 до 2.6.10 включительно.
Рассмотрим фрагмент функции load_elf_library(), автоматически вызываемой функцией sys_uselib() при загрузке новой библиотеки:
Листинг 4. Ключевой фрагмент функции load_elf_library, содержащей ошибку синхронизации потоков.
Как мы видим, семафор mmap_sem освобождается до вызова do_brk() функции, порождая тем самым проблему синхронизации потоков. В то же время анализ функции sys_brk(), убеждает нас в том, что функция do_brk() должна вызываться с взведенным семафором. Рассмотрим фрагмент исходного кода, позаимствованный из файла mm/mmap.c:
Листинг 5. Ключевой фрагмент функции sys_brk(), страдающей нарушением конегентности служебных структур данных.
В отсутствии семафора состояние виртуальной памяти может быть изменено между вызовами функций kmem_cache_alloc и vma_link, и тогда вновь созданный VMA-дескриптор будет размещен совсем не в том месте, на которое рассчитывали разработчики! Для захвата root'а этого более чем достаточно.
К сожалению, даже простейший эксплоит занимает слишком много места и поэтому не может быть приведен здесь, однако его исходный код легко найти в Интернете. Оригинальная версия (с подробным описанием техники взлома) лежит на: http://www.isec.pl/vulnerabilities/isec-0021-uselib.txt.
А вот другая интересная уязвимость, затрагивающая большое количество ядер с версиями 2.4/2.6 и поражающая многопроцессорные машины. Обнаруженная в самом начале 2005 года, она все еще остается актуальной, поскольку далеко не все администраторы установили соответствующие заплатки, а многопроцессорные машины (включая микропроцессоры с поддержкой Hyper-Threading) в наши дни скорее правило, чем редкость.
Во всем виноват обработчик ошибок доступа к страницам (page fault handler), который вызывается всякий раз, когда приложение обращается к невыделенной или защищенной странице памяти. Не все ошибки одинаково фатальны. В частности, LINUX (как и большинство других систем) выделяет стековую память не сразу, а по частям. На вершине выделенной памяти находится станица, доступ к которой умышленно запрещен. Она называется "сторожевой" (GUARD_PAGE). Стек постепенно растет и в какой-то момент "врезается" в сторожевую страницу, возбуждая исключение. Его перехватывает page fault handler и операционная система выделяет стеку некоторое количество памяти, перемещая сторожевую страницу наверх. На однопроцессорных машинах эта схема работает как часы, а вот на многопроцессорных...
Листинг 6. Ключевой фрагмент функции /mm/fault.c, содержащий ошибку синхронизации.
Поскольку page fault handler выполняется с семафором, доступным только на чтение, несколько конкурирующих потоков могут одновременно войти в обработчик за строкой /* * */. Рассмотрим, что произойдет, если два потока, разделяющих одну и ту же виртуальную память, одновременно вызовут page fault handler. Приблизительный сценарий атаки выглядит так: поток 1 обращается к сторожевой странице и вызывает исключение fault_1. Поток 2 обращается к странице GUARD_PAGE + PAGE_SIZE и вызывает исключение fault_2.
Состояние виртуальной памяти при этом будет выглядеть так:
[ NOPAGE ] [ fault_1 ] [ VMA ] ---> higher addresses [ fault_2 ] [ NOPAGE ] [ VMA ]
Рисунок 1. Состояние виртуальной памяти на момент вызова page fault handler'а двумя потоками.
Если поток 2 опередит поток 1 и первым выделит свою страницу PAGE 1, поток 1 вызовет серьезное нарушение в работе менеджера виртуальной памяти, поскольку нижняя граница стека теперь находится выше fault 2 и потому страница PAGE 2 реально не выделяется, но становится доступной на чтение/запись обоим потокам, причем после завершения процесса она не будет удалена!
[ PAGE2 ] [ PAGE1 ] [ VMA ]
Рисунок 2. Состояние виртуальной памяти на момент выхода из page fault handler'а.
Что находится в PAGE 2? Зависит от состояния каталога страниц (page table). Поскольку в LINUX физическая память представляет собой своеобразный кэш виртуального адресного пространства, одна и та же страница в разное время может использоваться как ядром, так и пользовательскими приложениями (в том числе и привилегированными процессами).
Дождавшись, когда в PAGE2 попадает код ядра или какого-нибудь привилегированного процесса (это легко определить по его сигнатуре), хакер может внедрить сюда shell-код или просто устроить грандиозный DoS, забросав PAGE2 бессмысленным мусором. Несмотря на довольно почетный возраст этой уязвимости, готового эксплоита найти так и не удалось, однако, его нетрудно написать самостоятельно. Как именно это сделать написано здесь: http://www.isec.pl/vulnerabilities/isec-0022-pagefault.txt.
Долгое время LINUX считалась "правильной" операционной системой, надежно защищенной от вирусов и хакерских атак. Но это оказалось не так. Дыр в LINUX'е даже больше чем в Windows и многие из них носят критический характер. Загрузчик ELF-файлов - это настоящее гнездо. Баги отсюда так и прут. Еще больше ошибок порождается поддержкой многопоточности. Если в Windows потоки существовали изначально и проблемы синхронизации решались на фундаментальном уровне, то "чужеродная" для LINUX'а многопоточность была синхронизована "впопыхах".
Ошибки гнездятся вокруг семафоров. Ищите семафоры и вы найдете ошибки. Какой смысл использовать готовые эксплоиты, для которых уже существуют заплатки? Хакерский код получается слишком хлипким и нежизнеспособным. Активность администраторов растет с каждым днем, сервера оснащаются системами автоматического обновления и выживать в этом мире становится все труднее и труднее. Поэтому необходимо вести самостоятельные исследования, уметь анализировать исходный и машинный код, обнаруживая еще никому неизвестные ошибки, противоядия против которых еще не существует. На заре истории человек с винтовкой мог завоевать мир. Так чем же мы хуже?