По следам Phrack. Ищем LKM-руткиты в оперативке и изучаем устройство памяти x64

Содержание статьи

  • Пара слов об LKM-руткитах
  • Подходы к поиску дескрипторов LKM-руткитов в оперативной памяти
  • Об исходном module_hunter
  • rkspotter и проблема с __module_address()
  • Что нужно фиксить
  • Реинкарнация module_hunter
  • По полям, по полям…
  • Виртуальная памть x86
  • Отображение страниц памяти и уровни трансляции
  • Об уровнях трансляции (paging levels)
  • Учимся приходить по адресу и не падать
  • Proof-of-Concept
  • Outro: о портируемости

Ког­да‑то, еще в начале пог­ружения в тему ядер­ных рут­китов в 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` в ядре еще не было

Как скры­вает­ся сей­час 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 (о чем погово­рим чуть ниже).

 

Подходы к поиску дескрипторов 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 — в том, что­бы прой­тись по реги­ону памяти под наз­вани­ем 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

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *