Содержание статьи
- Пара слов об LKM-руткитах
- Подходы к поиску дескрипторов LKM-руткитов в оперативной памяти
- Об исходном module_hunter
- rkspotter и проблема с __module_address()
- Что нужно фиксить
- Реинкарнация module_hunter
- По полям, по полям...
- Виртуальная память x86
- Отображение страниц памяти и уровни трансляции
- Об уровнях трансляции (paging levels)
- Учимся приходить по адресу и не падать
- Proof-of-Concept
- Outro: о портируемости
www
Код того, что получилось в итоге, доступен на моем GitHub.
Пара слов об LKM-руткитах
С древнейших времен руткиты уровня ядра для Linux (они же LKM-руткиты) используют из всего множества механизмов сокрытия всё один и тот же: удаление своего дескриптора модуля (struct_module
) из связного списка загруженных модулей ядра modules
. Это действие скрывает их из вывода в procfs
(/
) и вывода команды lsmod
, а также защищает от выгрузки через rmmod
. Ведь ядро теперь считает, что такой модуль не загружен, вот и выгружать нечего.
info
Об основах работы ядерных руткитов в Linux читай другую мою статью: «Как приручить руткит. Предотвращаем загрузку вредоносных модулей в Linux». Здесь я не буду рассказывать базовые вещи о них.
Некоторые руткиты после удаления себя из списка модулей могут затирать некоторые артефакты в памяти, чтобы найти их следы было сложнее. Например, начиная с версии 2.5.71, Linux устанавливает значения указателей next
и prev
связного списка в LIST_POISON1
и LIST_POISON2
(0x00100100
и 0x00200200
) в структуре при исключении ее из этого списка. Это полезно для детекта ошибок, и этот же факт можно использовать для обнаружения «висящих» в памяти дескрипторов LKM-руткитов, отвязанных ранее от списка модулей. Конечно, достаточно умный руткит перезапишет столь явно выделяющиеся в памяти значения на что‑то менее заметное, обойдя таким образом проверку. Так делает, к примеру, появившийся в 2022 году KoviD LKM.
Но и после удаления из списка модулей руткиты все еще возможно обнаружить — на этот раз в sysfs
, конкретно в /
. Этот псевдофайл был даже упомянут в документации Volatility — фреймворка для анализа разнообразных дампов памяти. Исследование этого файла — тоже один из вариантов обнаружения неаккуратных руткитов. И хотя в той документации заявлено, что разработчикам не встречался руткит, который бы удалял себя из обоих мест, уже известный нам KoviD LKM и тут преуспел. Что еще забавнее: первый закоммиченный вариант Diamorphine тоже удалял себя не только лишь из списка модулей.
KoviD же использует sysfs_remove_file(
, а свой статус устанавливает при этом в MODULE_STATE_UNFORMED
. Эта константа используется для обозначения «подвешенного» состояния, когда модуль еще находится в процессе инициализации и загрузки ядром, а значит, выгружать его ну никак нельзя без неизвестных необратимых последствий для ядра. Такой финт помогает обхитрить антируткиты, использующие __module_address(
в ходе перебора содержимого виртуальной памяти, как, например, делает rkspotter (о чем поговорим чуть ниже).
Подходы к поиску дескрипторов LKM-руткитов в оперативной памяти
В этой статье мы обсуждаем способы поиска руткитов в оперативной памяти живой системы и в виртуальном адресном пространстве ядра. В теории такой поиск может осуществлять не только модуль ядра, но и гипервизор (что вообще‑то правильнее с точки зрения колец защиты). Но мы рассмотрим только первый вариант, как более простой для реализации PoC и наиболее близкий к оригиналу. Также я не затрагиваю детект вредоносов в памяти по хешам, но стараюсь рассмотреть что‑то применимое конкретно к LKM-руткитам, а не к малвари в целом. В основном это про исследовательские PoC.
Об исходном module_hunter
В 2003 году во Phrack #61 в рубрике Linenoise была опубликована заметка автора madsys о способе поиска LKM-руткитов в памяти: Finding hidden kernel modules (the extrem way). То были времена ядер 2.2–2.4 и 32-битных машин; сейчас же на горизонте Linux 6.8, а найти железку x86-32 весьма и весьма непросто (да и незачем, кроме как на опыты).
В общем, это было давно, и внутри ядра за 20 лет было удалено либо появилось очень многое. Кроме того, ядро славится нестабильным внутренним API, и оригинальный сорец из Phrack ожидаемо откажется собираться по множеству причин. Но если разобраться в самой сути предложенной идеи, ее таки можно успешно воплотить в нынешних реалиях.
В той заметке многое было вынесено за скобки, и без должной подготовки авторская логика решения понятна далеко не сразу. В целом предлагаемый там метод чем‑то похож на блуждание в потемках содержимого оперативки на ощупь: пройтись по региону памяти, в котором аллоцируются дескрипторы модулей, и, как только обнаруживается нечто, имеющее сходство с валидным struct
, вывести содержимое из потенциальных полей согласно известной структуре дескриптора.
Например, известно, что по какому‑то смещению должен быть указатель на init-функцию, а по другим — размеры различных секций загруженного модуля, код его текущего статуса и тому подобное. Это значит, что диапазон нужных нам значений памяти по таким смещениям ограничен, и можно прикинуть, насколько текущий адрес похож на начало struct
. То есть можно выработать проверки, чтобы не выводить откровенный мусор из памяти и детектить нужное по максимуму.
Конечно, как ты понимаешь, за прошедшее с момента написания той статьи время изменились не только внутренние функции ядра, но и куча структур. В первоначальной реализации madsys проверялось только, чтобы поле с именем модуля содержало нормальный текст. В случае x86-64 мы не можем себе этого позволить: виртуальное адресное пространство сильно больше, так как больше стало различных возможных структур, и в итоге такому скромному условию удовлетворит огромная куча данных в памяти.
Другая проблема, которая решается в module_hunter
, — проверка того факта, что текущий исследуемый виртуальный адрес имеет отображение в физической памяти. Это значит, что, обращаясь по этому адресу, модуль не свалится в панику, таща за собой всю систему. Проверку тоже придется переработать, поскольку она привязана к архитектуре.
rkspotter и проблема с __module_address()
Нужны были способы пройтись по памяти так, чтобы не уронить систему. И тут мне попался уже знакомый нам rkspotter. Он обнаруживает применение нескольких техник сокрытия, которые в ходу у LKM-руткитов. Это позволяет ему преуспеть в своих задачах даже в том случае, когда один из методов не отрабатывает. Проблема, однако, в том, что этот антируткит полагается на функцию __module_address(
, которую в 2020-м убирали из числа экспортируемых, и с версии Linux 5.4.118 она недоступна для модулей.
Идея rkspotter в том, чтобы пройтись по региону памяти под названием module
(где оказываются LKM после загрузки) и с помощью этой самой функции проверять, какому модулю принадлежит очередной адрес. Для заданного адреса __module_address(
возвращает сразу указатель на дескриптор соответствующего модуля, что позволяло удобно по одному‑единственному адресу получить информацию о LKM. Вся грязная работа по проверке валидности трансляции виртуального адреса выполнялась под капотом.
Конечно, можно было бы просто попытаться скопипастить __module_address(
, но мой спортивный интерес был в том, чтобы перевоплотить изначальную идею madsys. Какие еще есть подводные камни интересные задачи на пути к новой реализации?
Что нужно фиксить
Чтобы написать новую рабочую тулзу, нужно изучить все, что менялось в ядре за последние 20 лет и связано с «висящими» дескрипторами LKM. Точнее, придется исправить все ошибки компиляции, с которыми мы столкнемся по ходу дела.
То есть задачи примерно такие:
- пофиксить вызовы изменившихся ядерных API. Код оригинала на самом деле очень мал, и единственный используемый ядерный API касается
procfs
, так что этот пункт не потребует много времени; - выделить поля
struct
, наиболее подходящие для детекта отвязанной от общего списка модулей структуры;module - изучить и учесть изменения управления памятью на x86-64 в сравнении с i386;
- а также учесть, что на 64-битной архитектуре совершенно иначе распределено виртуальное адресное пространство, и оно несоизмеримо больше: 128 Тбайт на ядерную часть и столько же на юзерспейс — в противовес 1 Гбайт и 3 Гбайт соответственно на 32-битной архитектуре по умолчанию.
Что ж, пора переходить к самому интересному!
Реинкарнация module_hunter
По полям, по полям...
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»