Создание руткита в Linux с помощью LD_PRELOAD

Как написать локер, шифровальщик и вирус на Python

В ник­сах сущес­тву­ет перемен­ная сре­ды, при ука­зании которой ваши биб­лиоте­ки будут заг­ружать­ся рань­ше осталь­ных. А это зна­чит, что появ­ляет­ся воз­можность под­менить сис­темные вызовы. Называ­ется перемен­ная LD_PRELOAD, и в этой статье мы под­робно обсу­дим ее зна­чение в сок­рытии (и обна­руже­нии!) рут­китов.

Еще по теме: Способы получить права root в Linux

Офи­циаль­но глав­ное пред­назна­чение LD_PRELOAD — отладка или про­вер­ка фун­кций в динами­чес­ки под­клю­чаемых биб­лиоте­ках. Если не хотите исправ­лять и переком­пилиро­вать саму биб­лиоте­ку, то мож­но вос­поль­зовать­ся перемен­ной сре­ды.

К при­меру, если нам нуж­но пред­загру­зить биб­лиоте­ку ld.so, то у нас будет два спо­соба:

  1. Ус­тановить перемен­ную сре­ды LD_PRELOAD с фай­лом биб­лиоте­ки.
  2. За­писать путь к биб­лиоте­ке в файл /<wbr />etc/<wbr />ld.<wbr />so.<wbr />preload.

В пер­вом слу­чае мы объ­явля­ем перемен­ную с биб­лиоте­кой для текуще­го поль­зовате­ля и его окру­жения. Во вто­ром же наша биб­лиоте­ка будет заг­ружена рань­ше осталь­ных для всех поль­зовате­лей сис­темы.

Нам инте­ресен как раз вто­рой спо­соб: имен­но он час­то исполь­зует­ся в рут­китах для перех­вата некото­рых вызовов, таких как чте­ние фай­ла, лис­тинг дирек­тории, про­цес­сов и про­чих фун­кций, поз­воля­ющих зло­умыш­ленни­ку скры­вать свое при­сутс­твие в сис­теме.

Вся информация предоставлена исключительно в ознакомительных целях. Статья написана для пентестеров. Ни автор, ни редакция сайта spy-soft.net не несут ответственности за любой возможный вред, причиненный материалами данной статьи.

Переопределение системных вызовов

Преж­де чем мы нач­нем сбли­жение с реаль­ными фун­кци­ями рут­китов, давайте на неболь­шом при­мере покажу, как мож­но перех­ватить вызов стан­дар­тной фун­кции malloc().

Для это­го напишем прос­тую прог­рамму, которая выделя­ет блок памяти с помощью фун­кции malloc(<wbr />), затем помеща­ет в него фун­кци­ей strncpy(<wbr />) стро­ку I'll <wbr />be <wbr />back и выводит ее пос­редс­твом fprintf(<wbr />) по адре­су, который вер­нула malloc(<wbr />).

Соз­даем файл call_malloc.<wbr />c:

Те­перь напишем прог­рамму, пере­опре­деля­ющую malloc(<wbr />). Внут­ри — фун­кция с тем же име­нем, что и в libc. Наша фун­кция не дела­ет ничего, кро­ме вывода стро­ки в  STDERR c помощью fprintf(<wbr />). Соз­дадим файл libmalloc.<wbr />c:

Те­перь с помощью GCC ском­пилиру­ем наш код:

Вы­пол­ним нашу прог­рамму call_malloc:

Пос­мотрим, какие биб­лиоте­ки исполь­зует наша прог­рамма, с помощью ути­литы ldd:

От­лично вид­но, что без исполь­зования пред­загруз­чика LD_PRELOAD стан­дар­тно заг­ружа­ются три биб­лиоте­ки:

  1. linux-vdso.<wbr />so.<wbr />1 — пред­став­ляет собой вир­туаль­ный динами­чес­кий раз­деля­емый объ­ект (Virtual Dynamic Shared Object, VDSO), исполь­зуемый для опти­миза­ции час­то исполь­зуемых сис­темных вызовов. Его мож­но игно­риро­вать (под­робнее — man <wbr />7 <wbr />vdso).
  2. libc.<wbr />so.<wbr />6 — биб­лиоте­ка libc с исполь­зуемой нами фун­кци­ей malloc(<wbr />) в прог­рамме call_malloc.
  3. ld-linux-x86-64.<wbr />so.<wbr />2 — сам динами­чес­кий ком­понов­щик.

Те­перь давайте опре­делим перемен­ную LD_PRELOAD и поп­робу­ем перех­ватить malloc(<wbr />). Здесь я не буду исполь­зовать export и огра­ничусь однос­троч­ной коман­дой для прос­тоты:

Мы успешно перех­ватили malloc(<wbr />) из биб­лиоте­ки libc.<wbr />so, но сде­лали это не сов­сем чис­то. Фун­кция воз­вра­щает зна­чение ука­зате­ля NULL, что при разыме­нова­нии strncpy(<wbr />) в прог­рамме ./<wbr />call_malloc вызыва­ет ошиб­ку сег­менти­рова­ния. Испра­вим это.

Еще по теме: Перехват вызовов функций WinAPI

Обработка сбоев

Что­бы иметь воз­можность незамет­но выпол­нить полез­ную наг­рузку рут­кита, нам нуж­но вер­нуть зна­чение, которое вер­нула бы пер­воначаль­но выз­ванная фун­кция. У нас есть два спо­соба решить эту проб­лему:

  • на­ша фун­кция malloc(<wbr />) дол­жна реали­зовы­вать фун­кци­ональ­ность malloc(<wbr />) биб­лиоте­ки libc по зап­росу поль­зовате­ля. Это пол­ностью изба­вит от необ­ходимос­ти исполь­зовать malloc(<wbr />) из  libc.<wbr />so;
  • libmalloc.<wbr />so каким‑то обра­зом дол­жна иметь воз­можность вызывать malloc(<wbr />) из биб­лиоте­ки libc и воз­вра­щать резуль­таты вызыва­ющей прог­рамме.

Каж­дый раз при вызове malloc(<wbr />) динами­чес­кий ком­понов­щик вызыва­ет вер­сию malloc(<wbr />) из  libmalloc.<wbr />so, пос­коль­ку это пер­вое вхож­дение malloc(<wbr />). Но мы хотим выз­вать сле­дующее вхож­дение malloc(<wbr />) — то, что находит­ся в  libc.<wbr />so.

Так про­исхо­дит потому, что динами­чес­кий ком­понов­щик внут­ри исполь­зует фун­кцию dlsym(<wbr />) из  /<wbr />usr/<wbr />include/<wbr />dlfcn.<wbr />h для поис­ка адре­са заг­ружен­ного в память.

По умол­чанию в качес­тве пер­вого аргу­мен­та для  dlsym(<wbr />) исполь­зует­ся дес­крип­тор RTLD_DEFAULT, который воз­вра­щает адрес пер­вого вхож­дения сим­вола. Одна­ко есть еще один псев­доука­затель динами­чес­кой биб­лиоте­ки — RTLD_NEXT, который ищет сле­дующее вхож­дение. Исполь­зуя RTLD_NEXT, мы можем най­ти фун­кцию malloc(<wbr />) биб­лиоте­ки libc.<wbr />so.

От­редак­тиру­ем libmalloc.<wbr />с. Ком­мента­рии объ­ясня­ют, что про­исхо­дит внут­ри прог­раммы:

В цик­ле про­веря­ется, не NULL ли зна­чение дирек­тории, затем вызыва­ется strncmp(<wbr />) для про­вер­ки, сов­пада­ет ли d_name катало­га с RKIT (фай­ла с рут­китом). Если оба усло­вия вер­ны, вызыва­ется фун­кция orig_readdir(<wbr />) для чте­ния сле­дующей записи катало­га. При этом про­пус­кают­ся все дирек­тории, у которых d_name начина­ется с  rootkit.<wbr />so.

Те­перь давайте пос­мотрим, как отра­бота­ет наша биб­лиоте­ка в этот раз. Сно­ва ком­пилиру­ем и смот­рим на резуль­тат работы:

От­лично! Как мы видим, все прош­ло глад­ко. Сна­чала при пер­вом вхож­дении malloc(<wbr />) была исполь­зована наша реали­зация этой фун­кции, а затем ори­гиналь­ная реали­зация из биб­лиоте­ки libc.<wbr />so.

Те­перь, ког­да мы понима­ем, как работа­ет LD_PRELOAD и каким обра­зом мы можем пре­доп­ределять работу со стан­дар­тны­ми фун­кци­ями сис­темы, самое вре­мя при­менить эти зна­ния на прак­тике.

Поп­робу­ем сде­лать так, что­бы ути­лита ls, ког­да выводит спи­сок фай­лов, про­пус­кала рут­кит.

Скрываем файл из листинга ls

Боль­шинс­тво динами­чес­ки ском­пилиро­ван­ных прог­рамм исполь­зуют сис­темные вызовы стан­дар­тной биб­лиоте­ки libc. С помощью ути­литы ldd пос­мотрим, какие биб­лиоте­ки исполь­зует прог­рамма ls:

По­луча­ется, ls динами­чес­ки ском­пилиро­вана с исполь­зовани­ем фун­кций биб­лиоте­ки libc.<wbr />so. Теперь пос­мотрим, какие сис­темные вызовы для чте­ния дирек­тории исполь­зует ути­лита ls. Для это­го в пус­той дирек­тории выпол­ним ltrace <wbr />ls:

Оче­вид­но, что при выпол­нении коман­ды без аргу­мен­тов ls исполь­зует сис­темные вызовы opendir(<wbr />), readdir(<wbr />) и  closedir(<wbr />), которые вхо­дят в биб­лиоте­ку libc. Давайте теперь задей­ству­ем LD_PRELOAD и пере­опре­делим эти стан­дар­тные вызовы сво­ими. Напишем прос­тую биб­лиоте­ку, в которой изме­ним фун­кцию readdir(<wbr />), что­бы она скры­вала наш файл с кодом.

Здесь мы уже перехо­дим к написа­нию прос­того рут­кита без наг­рузки. Все, что он будет делать, — это пря­тать сам себя от глаз адми­нис­тра­тора сис­темы.

Я соз­дал дирек­торию rootkit и даль­ше буду работать в ней. Соз­дадим файл rkit.<wbr />c.

Ком­пилиру­ем и про­веря­ем работу:

Нам уда­лось скрыть файл rootkit.<wbr />so от пос­торон­них глаз. Пока мы тес­тирова­ли биб­лиоте­ку исклю­читель­но в пре­делах одной коман­ды.

Используем /etc/ld.so.preload

Да­вайте вос­поль­зуем­ся записью в /<wbr />etc/<wbr />ld.<wbr />so.<wbr />preload для сок­рытия нашего фай­ла от всех поль­зовате­лей сис­темы. Для это­го запишем в  ld.<wbr />so.<wbr />preload путь до нашей биб­лиоте­ки:

Те­перь мы скры­ли файл ото всех поль­зовате­лей (хотя это не сов­сем так, но об этом поз­же). Но опыт­ный адми­нис­тра­тор доволь­но лег­ко нас обна­ружит, так как само по себе наличие фай­ла /<wbr />etc/<wbr />ld.<wbr />so.<wbr />preload может говорить о при­сутс­твии рут­кита — осо­бен­но если рань­ше такого фай­ла не было.

Скрываем ld.so.preload

Да­вайте попыта­емся скрыть из лис­тинга и сам файл ld.<wbr />so.<wbr />preload. Нем­ного модифи­циру­ем код rkit.<wbr />c:

Для наг­ляднос­ти я добавил к пре­дыду­щей прог­рамме еще один мак­рос LD_PL c име­нем фай­ла ld.<wbr />so.<wbr />preload, который мы так­же добави­ли в цикл while, где срав­нива­ем имя фай­ла для скры­тия.

Пос­ле ком­пиляции исходный файл rootkit.<wbr />so будет переза­писан и из вывода ути­литы ls про­падет и нуж­ный файл ld.<wbr />so.<wbr />preload. Про­веря­ем:

Здо­рово! Мы толь­ко что ста­ли на один шаг бли­же к пол­ной кон­спи­рации. Вро­де бы это победа, но не спе­шите радовать­ся.

Погружаемся глубже

Попробуем про­верить, смо­жем ли мы про­читать файл ld.<wbr />so.<wbr />preload коман­дой cat:

Так‑так‑так. Получа­ется, мы пло­хо спря­тались, если наличие нашего фай­ла мож­но про­верить прос­тым чте­нием. Почему так выш­ло?

Оче­вид­но, что для получе­ния содер­жимого ути­лита cat вызыва­ет дру­гую фун­кцию — не  readdir(<wbr />), которую мы так ста­ратель­но перепи­сыва­ли. Что ж, пос­мотрим, что исполь­зует cat:

На этот раз нам нуж­но порабо­тать с фун­кци­ей open(<wbr />). Пос­коль­ку мы уже опыт­ные, добавим в наш рут­кит фун­кцию, которая при обра­щении к фай­лу /<wbr />etc/<wbr />ld.<wbr />so.<wbr />preload будет веж­ливо говорить, что фай­ла не сущес­тву­ет (Error no entry или прос­то ENOENT).

Сно­ва модифи­циру­ем rkit.<wbr />c:

Здесь мы добави­ли кусок кода, который дела­ет то же самое, что и с readdir(<wbr />). Ком­пилиру­ем и про­веря­ем:

Так гораз­до луч­ше, но это еще далеко не все вари­анты обна­руже­ния /<wbr />etc/<wbr />ld.<wbr />so.<wbr />preload.

Мы до сих пор можем без проб­лем уда­лить файл, перемес­тить его со сме­ной наз­вания (и тог­да ls сно­ва его уви­дит), поменять ему пра­ва без уве­дом­ления об ошиб­ке. Даже bash услужли­во про­дол­жит его имя при нажатии на Tab.

В хороших рут­китах, экс­плу­ати­рующих лазей­ку с  LD_PRELOAD, реали­зован перех­ват сле­дующих фун­кций:

  • listxattr, llistxattr, flistxattr;
  • getxattr, lgetxattr, fgetxattr;
  • setxattr, lsetxattr, fsetxattr;
  • removexattr, lremovexattr, fremovexattr;
  • open, open64, openat, creat;
  • unlink, unlinkat, rmdir;
  • symlink, symlinkat;
  • mkdir, mkdirat, chdir, fchdir, opendir, opendir64, fdopendir, readdir, readdir64;
  • execve.

Раз­бирать под­мену каж­дой из них мы, конеч­но же, не будем. Можете в качес­тве при­мера перех­вата перечис­ленных фун­кций пос­мотреть рут­кит cub3 — там все те же dlsym(<wbr />) и  RTLD_NEXT.

Скрываем процесс с помощью LD_PRELOAD

При работе рут­киту нуж­но как‑то скры­вать свою активность от стан­дар­тных ути­лит монито­рин­га, таких как lsof, ps, top.

Мы уже доволь­но деталь­но разоб­рались, как работа­ет пере­опре­деле­ние фун­кций LD_PRELOAD. Для про­цес­сов все то же самое. Более того, стан­дар­тные прог­раммы исполь­зуют в сво­ей работе procfs, вир­туаль­ную фай­ловую сис­тему, которая пред­став­ляет собой интерфейс для вза­имо­дей­ствия с ядром ОС.

Чте­ние и запись в procfs реали­зова­ны так же, как и в обыч­ной фай­ловой сис­теме. То есть, как вы можете догадать­ся, наш опыт с readdir(<wbr />) здесь при­дет­ся кста­ти.

libprocesshider

Как скрыть активность из монито­рин­га, пред­лагаю рас­смот­реть на хорошем при­мере libprocesshider, который раз­работал Джан­лука Борел­ло (Gianluca Borello), автор Sysdig.com (о Sysdig и методах обна­руже­ния рут­китов LD_PRELOAD мы погово­рим в кон­це статьи).

Теперь ско­пиру­ем код с GitHub и раз­берем­ся, что к чему:

В опи­сании к  libprocesshider все прос­то: дела­ем make, копиру­ем в  /<wbr />usr/<wbr />local/<wbr />lib/ и добав­ляем в  /<wbr />etc/<wbr />ld.<wbr />so.<wbr />preload. Сде­лаем все, кро­ме пос­ледне­го:

Пос­мотрим, каким обра­зом ps получа­ет информа­цию о про­цес­сах. Для это­го запус­тим ltrace:

Ин­форма­цию о про­цес­се получа­ем при помощи фун­кции readproc(<wbr />). Пос­мотрим реали­зацию этой фун­кции в фай­ле readproc.<wbr />c:

Из это­го кода понят­но, что PID про­цес­сов получа­ют, вызывая readdir(<wbr />) в цик­ле for. Дру­гими сло­вами, если нет дирек­тории про­цес­са — нет и самого про­цес­са для ути­лит монито­рин­га. При­веду при­мер час­ти кода libprocesshider, где уже зна­комым нам методом мы скры­ваем дирек­торию про­цес­са:

При­чем само имя про­цес­са get_process_name(<wbr />) берет­ся из  /<wbr />proc/<wbr />pid/<wbr />stat.

Про­верим наши догад­ки. Для это­го запус­тим пред­лага­емый evil_script.<wbr />py в фоне:

3435 — это PID нашего работа­юще­го про­цес­са evil_script.<wbr />py. Про­верим вывод ути­литы htop и убе­дим­ся, что evil_script.<wbr />py при­сутс­тву­ет в спис­ке про­цес­сов.

Создание руткита Linux LD_PRELOAD
evil_script.py в спис­ке про­цес­сов htop

Про­верим вывод ps и lsof для обна­руже­ния сетевой активнос­ти:

Те­перь пос­мотрим, сущес­тву­ет ли дирек­тория с PID про­цес­са evil_script.<wbr />py:

Все пред­ска­зуемо. Теперь самое вре­мя добавить биб­лиоте­ку libprocesshider.<wbr />so в пред­загруз­ку гло­баль­но для всей сис­темы. Про­пишем ее в  /<wbr />etc/<wbr />ld.<wbr />so.<wbr />preload:

Про­веря­ем дирек­торию /<wbr />proc, а так­же вывод lsof и ps.

Ре­зуль­тат налицо. Теперь в  /<wbr />proc нель­зя пос­мотреть дирек­торию с PID скрип­та evil_script.<wbr />py. Одна­ко ста­тус про­цес­са по‑преж­нему виден в фай­ле /<wbr />proc/<wbr />3435/<wbr />status.

По­дыто­жим. В пер­вой час­ти статьи мы изу­чили методы перех­вата фун­кций и их изме­нение, что поз­воля­ет скры­вать рут­киты. А даль­ше про­дела­ли то же самое для скры­тия про­цес­сов от стан­дар­тных ути­лит монито­рин­га.

Как вы догады­ваетесь, прос­тые рут­киты, нес­мотря на все хит­рости, под­дают­ся детек­ту. Нап­ример, при помощи раз­ных манипу­ляций с фай­лом /<wbr />etc/<wbr />ld.<wbr />so.<wbr />preload или изу­чения исполь­зуемых биб­лиотек при помощи ldd.

Но что делать, если автор рут­кита нас­толь­ко хорош, что захукал все воз­можные фун­кции, ldd мол­чит, а подоз­рения на сетевую или иную активность все же есть?

Sysdig как решение

В отли­чие от стан­дар­тных инс­тру­мен­тов, ути­лита Sysdig устро­ена по‑дру­гому. По архи­тек­туре она близ­ка к таким про­дук­там, как libcap, tcpdump и Wireshark.

Спе­циаль­ный драй­вер sysdig-probe перех­ватыва­ет сис­темные события на уров­не ядра, пос­ле чего акти­виру­ется фун­кция ядра tracepoints, которая, в свою оче­редь, запус­кает обра­бот­чики этих событий. Обра­бот­чики сох­раня­ют информа­цию о событии в сов­мес­тно исполь­зуемом буфере. Затем эта информа­ция может быть выведе­на на экран или сох­ранена в тек­сто­вом фай­ле.

Пос­мотрим, как с помощью Sysdig най­ти evil_script.<wbr />py. К при­меру, по заг­рузке цен­траль­ного про­цес­сора:

Мож­но пос­мотреть выпол­нение ps. Бонусом Sysdig покажет, что динами­чес­кий ком­понов­щик заг­ружал поль­зователь­скую биб­лиоте­ку libprocesshide рань­ше, чем libc:

Схо­жие фун­кции пре­дос­тавля­ют ути­литы SystemTap, DTrace и его све­жая пол­ноцен­ная замена — BpfTrace.

Еще по теме: Создание VPN-туннеля в Linux для доступа во внутреннюю сеть

ВКонтакте
OK
Telegram
WhatsApp
Viber

Добавить комментарий

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