Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Когда‑то, еще в начале погружения в тему ядерных руткитов в Linux, мне попалась заметка из Phrack об их обнаружении с реализацией для i386. Статья была не новая, и речь в ней шла о ядре Linux образца 2003 года. Что‑то в этой заметке меня зацепило, хотя многое оставалось непонятным. Мне захотелось воплотить ту идею антируткита, но уже на современных системах. www
Код того, что получилось в итоге, доступен на моем GitHub.
С древнейших времен руткиты уровня ядра для Linux (они же LKM-руткиты) используют из всего множества механизмов сокрытия всё один и тот же: удаление своего дескриптора модуля (struct_module
) из связного списка загруженных модулей ядра modules
. Это действие скрывает их из вывода в procfs
(/proc/modules
) и вывода команды lsmod
, а также защищает от выгрузки через rmmod
. Ведь ядро теперь считает, что такой модуль не загружен, вот и выгружать нечего.
Как скрывался Adore LKM 0.13 (1999-2000). Тогда список модулей был односвязным, а структуры `list` в ядре еще не было
Как скрывается сейчас Diamorphine info
Об основах работы ядерных руткитов в Linux читай другую мою статью: «Как приручить руткит. Предотвращаем загрузку вредоносных модулей в Linux». Здесь я не буду рассказывать базовые вещи о них.
Некоторые руткиты после удаления себя из списка модулей могут затирать некоторые артефакты в памяти, чтобы найти их следы было сложнее. Например, начиная с версии 2.5.71 Linux устанавливает значения указателей next
и prev
связного списка в LIST_POISON1
и LIST_POISON2
(0x00100100
и 0x00200200
) в структуре при исключении ее из этого списка. Это полезно для детекта ошибок, и этот же факт можно использовать для обнаружения «висящих» в памяти дескрипторов LKM-руткитов, отвязанных ранее от списка модулей. Конечно, достаточно умный руткит перезапишет столь явно выделяющиеся в памяти значения на что‑то менее заметное, обойдя таким образом проверку. Так делает, к примеру, появившийся в 2022 году KoviD LKM.
Но и после удаления из списка модулей руткиты все еще возможно обнаружить — на этот раз в sysfs
, конкретно в /sys/modules
. Этот псевдофайл был даже упомянут в документации Volatility — фреймворка для анализа разнообразных дампов памяти. Исследование этого файла — тоже один из вариантов обнаружения неаккуратных руткитов. И хотя в той документации заявлено, что разработчикам не встречался руткит, который бы удалял себя из обоих мест, уже известный нам KoviD LKM и тут преуспел. Что еще забавнее: первый закоммиченный вариант Diamorphine тоже удалял себя не только лишь из списка модулей.
Как скрывался Diamorphine образца ноября 2013
KoviD же использует sysfs_remove_file()
, а свой статус устанавливает при этом в MODULE_STATE_UNFORMED
. Эта константа используется для обозначения «подвешенного» состояния, когда модуль еще находится в процессе инициализации и загрузки ядром, а значит, выгружать его ну никак нельзя без неизвестных необратимых последствий для ядра. Такой финт помогает обхитрить антируткиты, использующие __module_address()
в ходе перебора содержимого виртуальной памяти, как, например, делает rkspotter (о чем поговорим чуть ниже).
В этой статье мы обсуждаем способы поиска руткитов в оперативной памяти живой системы и в виртуальном адресном пространстве ядра. В теории, такой поиск может осуществлять не только модуль ядра, но и гипервизор (что вообще‑то правильнее с точки зрения колец защиты). Но мы рассмотрим только первый вариант как более простой для реализации PoC и наиболее близкий к оригиналу. Также я не затрагиваю детект вредоносов в памяти по хешам, но стараюсь рассмотреть что‑то применимое конкретно к LKM-руткитам, а не к малвари в целом. В основном это про исследовательские PoC.
В 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 module
, вывести содержимое из потенциальных полей согласно известной структуре дескриптора.
Например, известно, что по какому‑то смещению должен быть указатель на init-функцию, а по другим — размеры различных секций загруженного модуля, код его текущего статуса и тому подобное. Это значит, что диапазон нужных нам значений памяти по таким смещениям ограничен, и можно прикинуть, насколько текущий адрес похож на начало struct module
. То есть можно выработать проверки, чтобы не выводить откровенный мусор из памяти, и детектить нужное по максимуму.
Конечно, как ты понимаешь, за прошедшее с момента написания той статьи время изменились не только внутренние функции ядра, но и куча структур. В первоначальной реализации madsys проверялось только, чтобы поле с именем модуля содержало нормальный текст. В случае x86-64 мы не можем себе этого позволить: виртуальное адресное пространство сильно больше, так как больше стало различных возможных структур, и в итоге такому скромному условию удовлетворит огромная куча данных в памяти.
Другая проблема, которая решается в module_hunter
— проверка того факта, что текущий исследуемый виртуальный адрес имеет отображение в физической памяти. Это значит, что обращаясь по этому адресу, модуль не свалится в панику, таща за собой всю систему. Проверку тоже придется переработать, поскольку она привязана к архитектуре.
Нужны были способы пройтись по памяти так, чтобы не уронить систему. И тут мне попался уже знакомый нам rkspotter. Он обнаруживает применение нескольких техник сокрытия, которые в ходу у LKM-руткитов. Это позволяет ему преуспеть в своих задачах даже том в случае, когда один из методов не отрабатывает. Проблема, однако, в том, что этот антируткит полагается на функцию __module_address()
, которую в 2020-м убирали из числа экспортируемых, и с версии Linux 5.4.118 она недоступна для модулей.
rkspotter в dmesg ругается на рептилию (Ubuntu 18.10)
Идея rkspotter — в том, чтобы пройтись по региону памяти под названием module mapping space
(где оказываются LKM после загрузки) и с помощью этой самой функции проверять, какому модулю принадлежит очередной адрес. Для заданного адреса __module_address()
возвращает сразу указатель на дескриптор соответствующего модуля, что позволяло удобно по одному‑единственному адресу получить информацию об LKM. Вся грязная работа по проверке валидности трансляции виртуального адреса выполнялась под капотом.
Конечно, можно было бы просто попытаться скопипастить __module_address()
, но мой спортивный интерес был в том, чтобы перевоплотить изначальную идею madsys. Какие еще есть подводные камни интересные задачи на пути к новой реализации?
Чтобы написать новую рабочую тулзу, нужно изучить все, что менялось в ядре за послдение 20 лет и связано с «висящими» дескрипторами LKM. Точнее, придется исправить все ошибки компиляции, с которыми мы столкнемся по ходу дела.
То есть, задачи примерно такие:
procfs
, так что этот пункт не потребует много времени; struct module
, наиболее подходящие для детекта отвязанной от общего списка модулей структуры; Что ж, пора переходить к самому интересному!
Источник: xakep.ru