«Зор­ко одно лишь сер­дце. Самого глав­ного гла­зами не уви­дишь». Эта фра­за Анту­ана де Сент‑Экзю­пери как нель­зя луч­ше опи­сыва­ет ситу­ацию, в которой ока­зыва­ется адми­нис­тра­тор взло­ман­ной сис­темы. Его гла­за — ls, ps, netstat — показы­вают ему не реаль­ность, а то, что им поз­волено. Что­бы най­ти прав­ду, нуж­но смот­реть глуб­же: в механиз­мы заг­рузки биб­лиотек, GOT, PLT. В этой статье мы научим­ся видеть то, что скры­то от глаз.

warning

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

Ме­ханизм релока­ций опре­деля­ет, как прог­рамма через PLT и GOT находит биб­лиотеч­ные фун­кции и как ленивая при­вяз­ка запол­няет GOT реаль­ными адре­сами из libc.so.6. Этот про­цесс мож­но наб­людать вжи­вую через отладчик GDB.

Но что, если пос­мотреть на это с дру­гой сто­роны: что, если в GOT ока­жет­ся не адрес из libc, а адрес из нашей собс­твен­ной биб­лиоте­ки? Что, если мы хотим, что­бы стан­дар­тные ути­литы делали не то, чего ожи­дает поль­зователь?

Имен­но так работа­ет механизм LD_PRELOAD. В этой статье мы раз­берем тех­нику работы userland-рут­китов:

  1. Пой­мем механи­ку перех­вата, а имен­но как LD_PRELOAD зас­тавля­ет ком­понов­щик заг­ружать нашу биб­лиоте­ку вмес­то libc, и уви­дим это через GDB в реаль­ном вре­мени.
  2. Стол­кнем­ся с прак­тичес­кими проб­лемами при перех­вате фун­кций: рекур­сией, segfault и необ­ходимостью исполь­зовать dlsym для кор­рек­тно­го перех­вата.
  3. Изу­чим огра­ниче­ния и выяс­ним, почему LD_PRELOAD не работа­ет с setuid-прог­рамма­ми, а так­же как ата­кующие обхо­дят это через уяз­вимос­ти (на при­мере CVE-2025-32463 в sudo).
  4. Ус­тановим рут­кит BEURK и модифи­циру­ем его для скры­тия кри­тичес­ких арте­фак­тов вро­де фай­ла /etc/ld.so.preload.
  5. Раз­берем методы обна­руже­ния userland-рут­китов. Опи­раясь на два из трех прин­ципов охо­ты на рут­киты (утеч­ки дан­ных и несог­ласован­ные отве­ты), я покажу, как исполь­зовать strace, ста­тичес­ки ском­пилиро­ван­ный BusyBox и auditd для выяв­ления рут­кита. Тре­тий прин­цип (вли­яние на сис­тему) про­явля­ется на реаль­ных и наг­ружен­ных сис­темах, поэто­му в рам­ках статьи мы рас­смот­рим его толь­ко в теории.

Ме­ханизм LD_PRELOAD появил­ся в SunOS в кон­це 1980-х — начале 1990-х годов как инс­тру­мент для раз­работ­чиков, поз­воля­ющий тес­тировать новые вер­сии биб­лиотек без перес­борки прог­рамм. Ник­то не пред­полагал, что этот механизм ста­нет осно­вой для целого клас­са вре­донос­ного ПО!

 

Структура проекта и инструментарий

Для вос­про­изве­дения всех экспе­римен­тов я под­готовил еди­ную лабора­торию. Вот ее струк­тура:

ld_preload/
├── Makefile
├── src/
├── check_pass.c # Программа для перехвата strcmp
├── empty_sleep.c # Программа для перехвата sleep
├── hook_getuid.c # Перехватчик getuid
├── hook_puts.c # Перехватчик puts
├── hook_strcmp.c # Перехватчик strcmp
├── hook.c # Перехватчик sleep
└── show_uid.c # Программа для перехвата getuid
├── bin/ # Скомпилированные программы
└── lib/ # Скомпилированные библиотеки-перехватчики

В дирек­тории src/ лежат исходни­ки — тес­товые прог­раммы и биб­лиоте­ки‑перех­ватчи­ки. Пос­ле сбор­ки исполня­емые фай­лы попада­ют в bin/, а раз­деля­емые биб­лиоте­ки — в lib/. Исходный код ты най­дешь в раз­делах, свя­зан­ных с эти­ми прог­рамма­ми.

Makefile поз­воля­ет быс­тро все соб­рать одной коман­дой:

CC = gcc
CFLAGS = -fPIC -shared
all: bin/empty_sleep bin/check_pass bin/show_uid lib/libhook.so lib/libhook_strcmp.so lib/libhook_getuid.so lib/libhook_puts.so
# Программа со sleep
bin/empty_sleep: src/empty_sleep.c | bin
$(CC) src/empty_sleep.c -o bin/empty_sleep -Wl,-z,lazy
# Программа с проверкой пароля
bin/check_pass: src/check_pass.c | bin
$(CC) src/check_pass.c -o bin/check_pass -Wl,-z,lazy
# Программа для проверки setuid
bin/show_uid: src/show_uid.c | bin
$(CC) src/show_uid.c -o bin/show_uid -Wl,-z,lazy
# Библиотеки-перехватчики
lib/libhook.so: src/hook.c | lib
$(CC) $(CFLAGS) src/hook.c -o lib/libhook.so
lib/libhook_strcmp.so: src/hook_strcmp.c | lib
$(CC) $(CFLAGS) src/hook_strcmp.c -o lib/libhook_strcmp.so -ldl
lib/libhook_puts.so: src/hook_puts.c | lib
$(CC) $(CFLAGS) src/hook_puts.c -o lib/libhook_puts.so
lib/libhook_getuid.so: src/hook_getuid.c | lib
$(CC) $(CFLAGS) src/hook_getuid.c -o lib/libhook_getuid.so
bin lib:
mkdir -p $@
clean:
rm -rf bin lib
.PHONY: all clean

Флаг -Wl,-z,lazy вклю­чает ленивую при­вяз­ку — это поз­воля­ет наб­людать изме­нение GOT имен­но в момент пер­вого вызова фун­кции, а не при заг­рузке прог­раммы. Фла­ги -shared и -fPIC соз­дают раз­деля­емую биб­лиоте­ку, которую динами­чес­кий ком­понов­щик смо­жет заг­рузить по любому адре­су в памяти.

 

Механика LD_PRELOAD

 

Суть механизма

LD_PRELOAD — это перемен­ная окру­жения, которую понима­ет динами­чес­кий ком­понов­щик (ld-linux.so). Биб­лиоте­ки, ука­зан­ные в ней, заг­ружа­ются в адресное прос­транс­тво про­цес­са до всех осталь­ных. Если в этой биб­лиоте­ке есть фун­кция с тем же име­нем, что и в любой заг­ружа­емой биб­лиоте­ке (нап­ример, libc, libssl, libcurl), ком­понов­щик при раз­решении сим­волов запишет в GOT адрес нашей фун­кции, а не ори­гиналь­ной. На этом и стро­ится перех­ват.

В этой статье мы сос­редото­чим­ся на перех­вате фун­кций из libc — это самый рас­простра­нен­ный слу­чай, так как боль­шинс­тво прог­рамм исполь­зуют стан­дар­тную биб­лиоте­ку C для работы с фай­лами, сетью и про­цес­сами.

 

Готовим программу и перехватчик

По­допыт­ная прог­рамма src/empty_sleep.c пре­дель­но прос­та — она вызыва­ет sleep(30) и завер­шает­ся:

#include <unistd.h>
int main() {
sleep(30);
return 0;
}

Биб­лиоте­ка‑перех­ватчик src/hook.c опре­деля­ет фун­кцию с таким же име­нем и сиг­натурой, как у ори­гиналь­ного sleep из libc:

#include <stdio.h>
#include <unistd.h>
unsigned int sleep(unsigned int seconds) {
printf("[HOOK] sleep(%u) перехвачен! Оригинал не вызывается.\n", seconds);
return 0;
}

Сиг­натура unsigned int sleep(unsigned int seconds) дол­жна точ­но сов­падать с той, что объ­явле­на в <unistd.h>. Если сиг­натуры разой­дут­ся, ком­понов­щик не свя­жет нашу фун­кцию с вызовом sleep в прог­рамме и перех­вата не про­изой­дет.

Обе прог­раммы мы уже соб­рали через make на пре­дыду­щем шаге; empty_sleep лежит в bin/, libhook.so — в lib/.

 

Наблюдаем перехват через GDB

За­пус­каем отладчик GDB и пря­мо в нем уста­нав­лива­ем перемен­ную окру­жения LD_PRELOAD, что­бы она дей­ство­вала толь­ко на вре­мя отладки:

gdb ./empty_sleep
(gdb) set environment LD_PRELOAD=../lib/libhook.so

Даль­ше наша задача — зафик­сировать три сос­тояния GOT для фун­кции sleep:

  1. В самом начале, до того, как ком­понов­щик начал заг­ружать биб­лиоте­ки.
  2. Пос­ле заг­рузки биб­лиотек, но до того, как sleep был выз­ван.
  3. Пос­ле пер­вого вызова sleep.

Компоновщик еще ничего не загружал

Ста­вим точ­ку оста­нова на _start. Это самая ран­няя точ­ка, куда может попасть отладчик. На самом деле _start находит­ся не в нашей прог­рамме, а в динами­чес­ком ком­понов­щике ld-linux-x86-64.so.2 — он заг­ружа­ется пер­вым:

(gdb) break _start
(gdb) run
Breakpoint 1, 0x00007ffff7fdfa80 in _start () from /lib64/ld-linux-x86-64.so.2

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

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

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

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

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

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

    Подписаться

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