Ког­да‑то, еще в начале пог­ружения в тему ядер­ных рут­китов в Linux, мне попалась замет­ка из Phrack об их обна­руже­нии с реали­заци­ей для i386. Статья была не новая, и речь в ней шла о ядре Linux образца 2003 года. Что‑то в этой замет­ке меня зацепи­ло, хотя мно­гое оста­валось непонят­ным. Мне захоте­лось воп­лотить ту идею анти­рут­кита, но уже на сов­ремен­ных сис­темах.

www

Код того, что получи­лось в ито­ге, дос­тупен на мо­ем GitHub.

 

Пара слов об LKM-руткитах

С древ­ней­ших вре­мен рут­киты уров­ня ядра для Linux (они же LKM-рут­киты) исполь­зуют из все­го мно­жес­тва механиз­мов сок­рытия всё один и тот же: уда­ление сво­его дес­крип­тора модуля (struct_module) из связ­ного спис­ка заг­ружен­ных модулей ядра modules. Это дей­ствие скры­вает их из вывода в procfs (/proc/modules) и вывода коман­ды lsmod, а так­же защища­ет от выг­рузки через rmmod. Ведь ядро теперь счи­тает, что такой модуль не заг­ружен, вот и выг­ружать нечего.

Как скрывался Adore LKM 0.13 (1999-2000). Тогда список модулей был односвязным, а структуры `list` в ядре еще не было
Как скры­вал­ся Adore LKM 0.13 (1999-2000). Тог­да спи­сок модулей был односвяз­ным, а струк­туры `list` в ядре еще не было
Как скрывается сейчас Diamorphine
Как скры­вает­ся сей­час 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
Как скры­вал­ся Diamorphine образца нояб­ря 2013

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 module, вывес­ти содер­жимое из потен­циаль­ных полей сог­ласно извес­тной струк­туре дес­крип­тора.

Нап­ример, извес­тно, что по какому‑то сме­щению дол­жен быть ука­затель на init-фун­кцию, а по дру­гим — раз­меры раз­личных сек­ций заг­ружен­ного модуля, код его текуще­го ста­туса и тому подоб­ное. Это зна­чит, что диапа­зон нуж­ных нам зна­чений памяти по таким сме­щени­ям огра­ничен, и мож­но при­кинуть, нас­коль­ко текущий адрес похож на начало struct module. То есть мож­но вырабо­тать про­вер­ки, что­бы не выводить откро­вен­ный мусор из памяти, и детек­тить нуж­ное по мак­симуму.

Ко­неч­но, как ты понима­ешь, за про­шед­шее с момен­та написа­ния той статьи вре­мя изме­нились не толь­ко внут­ренние фун­кции ядра, но и куча струк­тур. В пер­воначаль­ной реали­зации madsys про­веря­лось толь­ко, что­бы поле с име­нем модуля содер­жало нор­маль­ный текст. В слу­чае x86-64 мы не можем себе это­го поз­волить: вир­туаль­ное адресное прос­транс­тво силь­но боль­ше, так как боль­ше ста­ло раз­личных воз­можных струк­тур, и в ито­ге такому скром­ному усло­вию удов­летво­рит огромная куча дан­ных в памяти.

Дру­гая проб­лема, которая реша­ется в module_hunter — про­вер­ка того фак­та, что текущий иссле­дуемый вир­туаль­ный адрес име­ет отоб­ражение в физичес­кой памяти. Это зна­чит, что обра­щаясь по это­му адре­су, модуль не сва­лит­ся в панику, таща за собой всю сис­тему. Про­вер­ку тоже при­дет­ся перера­ботать, пос­коль­ку она при­вяза­на к архи­тек­туре.

 

rkspotter и проблема с __module_address()

Нуж­ны были спо­собы прой­тись по памяти так, что­бы не уро­нить сис­тему. И тут мне попал­ся уже зна­комый нам rkspotter. Он обна­ружи­вает при­мене­ние нес­коль­ких тех­ник сок­рытия, которые в ходу у LKM-рут­китов. Это поз­воля­ет ему пре­успеть в сво­их задачах даже в том слу­чае, ког­да один из методов не отра­баты­вает. Проб­лема, одна­ко, в том, что этот анти­рут­кит полага­ется на фун­кцию __module_address(), которую в 2020-м уби­рали из чис­ла экспор­тиру­емых, и с вер­сии Linux 5.4.118 она не­дос­тупна для модулей.

rkspotter в dmesg ругается на рептилию (Ubuntu 18.10)
rkspotter в dmesg руга­ется на реп­тилию (Ubuntu 18.10)

Идея rkspotter — в том, что­бы прой­тись по реги­ону памяти под наз­вани­ем module mapping space (где ока­зыва­ются LKM пос­ле заг­рузки) и с помощью этой самой фун­кции про­верять, какому модулю при­над­лежит оче­ред­ной адрес. Для задан­ного адре­са __module_address() воз­вра­щает сра­зу ука­затель на дес­крип­тор соот­ветс­тву­юще­го модуля, что поз­воляло удоб­но по одно­му‑единс­твен­ному адре­су получить информа­цию об LKM. Вся гряз­ная работа по про­вер­ке валид­ности тран­сля­ции вир­туаль­ного адре­са выпол­нялась под капотом.

Ко­неч­но, мож­но было бы прос­то попытать­ся ско­пипас­тить __module_address(), но мой спор­тивный инте­рес был в том, что­бы перевоп­лотить изна­чаль­ную идею madsys. Какие еще есть под­водные кам­ни инте­рес­ные задачи на пути к новой реали­зации?

 

Что нужно фиксить

Что­бы написать новую рабочую тул­зу, нуж­но изу­чить все, что менялось в ядре за пос­ледние 20 лет и свя­зано с «висящи­ми» дес­крип­торами LKM. Точ­нее, при­дет­ся испра­вить все ошиб­ки ком­пиляции, с которы­ми мы стол­кнем­ся по ходу дела.

То есть, задачи при­мер­но такие:

  • по­фик­сить вызовы изме­нив­шихся ядер­ных API. Код ори­гина­ла, на самом деле, очень мал, и единс­твен­ный исполь­зуемый ядер­ный API каса­ется procfs, так что этот пункт не пот­ребу­ет мно­го вре­мени;
  • вы­делить поля struct module, наибо­лее под­ходящие для детек­та отвя­зан­ной от обще­го спис­ка модулей струк­туры;
  • изу­чить и учесть изме­нения управле­ния памятью на x86-64 в срав­нении с i386;
  • а так­же учесть, что на 64-бит­ной архи­тек­туре совер­шенно ина­че рас­пре­деле­но вир­туаль­ное адресное прос­транс­тво, и оно несо­изме­римо боль­ше: 128 Тбайт на ядер­ную часть и столь­ко же на юзер­спейс — в про­тиво­вес 1 Гбайт и 3 Гбайт соот­ветс­тве­но на 32-бит­ной архи­тек­туре по умол­чанию.

Что ж, пора перехо­дить к самому инте­рес­ному!

 

Реинкарнация module_hunter

 

По полям, по полям...

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    1 Комментарий
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии