RFC (Request for Comments, Запрос на комментарии) - серия документов, публикуемая сообществом исследователей и разработчиков, руководствующихся практическими интересами, в которой описывается набор протоколов и обобщается опыт функционирования Интернет.
Ядро изолированно от адресного пространства прикладных приложений, и для взаимодействия с ним операционная система представляет ряд интерфейсов. FreeeBSD и NetBSD имеют монолитное ядро, поддерживающее загрузку динамических модулей, очень похожих на модули LINUX и чем-то напоминающие NT-драйвера. Загрузка модуля осуществляется «на лету», не требуя перезагрузки операционной системы, что очень хорошо. Естественно, для этого требуется права root'а, которые необходимо каким-то образом заполучить (но это уже тема для отдельного разговора). В GENERIC-ядре OpenBSD модули по умолчанию включены, но многие администраторы собирают монолитное ядро без поддержки модульности, считая, что это увеличивает защищенность, лишая атакующего возможности внедрять в ядро вредоносный код, однако...
Все xBSD-системы поддерживают псевдоустройство /dev/[k]mem (аналогичное тому, что имеется в LINIX) и библиотеку функций libkvm для работы с ним, прямых аналогов которой в LINUX нет. Поэтому, даже когда модули недоступны, у нас по-прежнему остается возможность модификации ядра, осуществляемая непосредственно с прикладного уровня, при условии, что мы владеем правами root'а.
LKM-модули NetBSD и OpenBSD
Операционные системы NetBSD и OpenBSD поддерживают LKM-модули (Loadable Kernel Module, загружаемые модули ядра), «слизанные» с SunOS 4 и совместимые на интерфейсном уровне, реализованном через псевдоустройство /dev/lkm, с которым прикладные приложения взаимодействуют посредством вызовов ioctl (подробнее в «man 4 lkm»). В OpenBSD модули могут быть загружены только на нулевом уровне безопасности. Если же уровень отличен от нуля, а загрузить модули все-таки необходимо, следует отредактировать файл /etc/rc.securelevel, загружая модули до того, как уровень безопасно будет установлен на необходимую величину. Но в этом случае о динамической загрузке следует забыть, расставшись с одним из наиболее элегантных свойств оси.
Модули бывают разных типов:
- System Call Modules - реализующие новые системные вызовы (или замещающие уже существующие).
- Virtual File System Modules - поддерживающие виртуальные файловые системы.
- Device Drive Modules - управляющие существующими или несуществующими шинами и устройствами.
- Execution Interpreter Modules - отвечающие за загрузку различных исполняемых форматов.
- Miscellaneous Modules - к ним относятся все модули, не попадающие ни под одну из классификаций. Подробнее - в «man 9 module».
Каждый тип модуля имеет свои особенности реализации, но нам они без разницы. Управлять оборудованием мы не собираемся, устанавливать новую файловую систему — тоже. Перехват системных вызовов может быть осуществлен из любого модуля, а не только из MOD_SYSCALL. Это можно сделать непосредственно в процедуре начальной инициализации модуля, что избавит от необходимости заполнять все служебные структуры, которые в случае Device Drive модулей довольно громоздки.
Примеры готовых модулей можно найти непосредственно в самой NetBSD/OpenBSD, обратившись к каталогу /usr/share/lkm/, или скачать их напрямую из Сети (www.openbsd.org/cgi-bin/cvsweb/src/share/lkm). Забавно, но в OpenBSD эти файлы не модифицировались свыше 6 лет! Примеры из NetBSD посвежее будут — «всего» 5 лет выдержки, но, по большому счету, никакой разницы между ними нет, и они практически один в один повторяют друг друга.
MOD_SYSCALL-модуль
Рассмотрим скелет простейшего MOD_SYSCALL-модуля, перехватывающего системный вызов #1 (mkdir) и устанавливающего на него свою хакерскую «заглушку», выводящую на экран «rock you» и мерзко пищащую спикером. При желании, из нее можно вызвать оригинальную функцию mkdir, передав управление по адресу, сохраненному в переменной old_mkdir.
ЛИСТИНГ
простейший LMK-модуль, демонстрирующий технику перехвата системных вызовов под NetBSD и OpenBSD
/* модуль, перехватывающий mkdir и работающий под Net- и OpenBSD */
Сердцем модуля является макрос DISPATCH, передающий управление функции инициализации и деинициализации (в нашем случае называется load), вызываемой при загрузке и выгрузке модуля. Для перехвата/освобождения системного вызова mkdir используется прямая правка таблицы системных вызовов sysent. В принципе, можно было воспользоваться макросом MOD_SYSCALL, но это малоинтересно.
ЛИСТИНГ
компиляция нашего LKM-модуля компилятором gcc
# gcc -D_LKM -D_KERNEL -I/sys -c syshack.c
ЛИСТИНГ
Make-файл, собирающий LKM-модули
KSRCS=syshack.c
KOBJS=syshack.o
KMOD=syshack
CFLAGS= -D_LKM -D_KERNEL -I/sys
За загрузку модуля в память ядра отвечает утилита modload («man 8 modload»).
ЛИСТИНГ
загрузка LKM-модуля в память ядра утилитой modload (hack – имя модуля в памяти, entry – точка входа в модуль, syshack.o – имя скомпилированного объектного файла)
# modload -o hack -eentry syshack.o
Module loaded as ID 0
Проверить успешность загрузки модуля можно утилитой «modstat» («man 8 modstat»).
ЛИСТИНГ
утилита modstat показывает наличие модуля hack в памяти - значит, загрузка прошла успешно
# modstat
Type Id Off Loadaddr Size Info Rev Module Name
SYSCALL 0 210 e0b92000 0002 e0b93008 2 hack
Если модуль действительно загружен, то появится строчка с его именем (в данном случае — «hack»), и с этого момента любые попытки создать новый каталог утилитой mkdir будут обречены на провал, вплоть до того времени, пока не выгрузим модуль из памяти утилитой modunload («man 8 modunload»).
ЛИСТИНГ
выгрузка модуля из памяти утилитой modunload
# modunload -n hack
Перехват остальных системных вызовов осуществляется аналогично. Таким образом, модуль может скрывать от глаз администратора некоторые процессы или файлы, «стелсируясь» на уровне ядра. А вот в FreeBSD модули реализованы совсем иначе...
KLD-модули FreeBSD
Ранние версии FreeBSD поддерживали LKM-модули наравне со своими конкурентами, но, начиная с FreeBSD 3.0, интерфейс модулей был изменен на KLD, что расшифровывается как Dynamic Kernel Linker – динамическое связывание ядра. И LKM-модули отошли на задний план (в текущих версиях FreeBSD их поддержка прекращена).
В практическом плане это, в первую очередь, означает, что старые исходные тексты необходимо переделывать, а в некоторых случаях — чуть ли не переписывать заново. На этом фоне преимущества нового типа модулей полностью девальвируются. Кстати говоря, штатное руководство («man KLD») лишь заявляет о преимуществах, но не перечисляет их, и за разъяснением приходится обращаться к другим источникам.
Если не углубляться в детали, то LKM-модуль – это ELF-файл, загружаемый в адресное пространство ядра, а KLD – это часть самого ядра, которая, в отличие от LKM, может быть загружена в любое время без поддержки прикладного уровня. То есть ядро в процессе старта системы как бы собирает себя из блоков, загружаемых/выгружаемых в любой момент времени.
KLD-модули предоставляют намного больше возможностей для разработчиков драйверов, но на нас это никак не распространяется. Перехват системных модулей реализуется так же, как и раньше. Меняется только декларация модуля, макросы и некоторые структуры данных.
Примеры готовых модулей можно найти в каталоге /usr/share/examples/kld/ или, опять же, стянуть их из Сети: www.freebsd.org/cgi/cvsweb.cgi/src/share/examples/kld/. «Зрелость» файлов варьируется от нескольких месяцев до 7 (!) лет.
ЛИСТИНГ
техника перехвата системного вызова под FreeBSD из KLD-модуля
/* модуль, перехватывающий mkdir, и работающий под FreeBSD */
/* based on: */
/* syscall.c by Assar Westerlund and hacked_mkdir.c by Joseph Kong */
#include <sys/types.h>
#include <sys/param.h>
#include <sys/proc.h>
#include <sys/module.h>
#include <sys/sysent.h>
#include <sys/kernel.h>
#include <sys/sysproto.h>
#include <sys/systm.h>
#include <sys/syscall.h>
/* функция-«заглушка», устанавливаемая на место mkdir */
static int hack (struct proc *p, void *arg)
{
printf ("rock you!\x7\n"); return 0;
}
/* элемент структуры sysent, описывающий наш системный вызов */
static struct sysent hack_sysent = {
1, /* sy_narg */
hack /* sy_call */
};
/* процедура начальной загрузки модуля */
static int load (struct module *module, int cmd, void *arg)
{
int error = 0;
switch (cmd)
{
case MOD_LOAD: /* загрузка модуля */
printf ("syshack loadedd\n");
/* устанавливаем вместо mkdir свою «заглушку» */
sysent[SYS_mkdir]=hack_sysent;
break;
case MOD_UNLOAD: /* выгрузка модуля */
printf ("syshack unloadedd\n");
/* снимаем свою «заглушку», возвращая на место mkdir */
sysent[SYS_mkdir].sy_call=(sy_call_t*)mkdir;
break;
default:
error = EINVAL;
break;
}
return error;
}
/* структура, описывающая основные параметры модуля */
static moduledata_t syscall_mod = {
"Intercept",
load,
NULL
};
/* сердце программы — макрос DECLARE_MODULE, декларирующий модуль */
Базовая процедура load практически никак не изменилась (поменялись лишь определения cmd-кодов), а вот в декларации модуля произошли большие перемены. Функция с макросом DISPATCH исчезла, а вместе с ней исчезла и необходимость указывать точку входа в модуль при его загрузке в память ядра. Новый макрос DECLARE_MODULE не только задает точку входа в модуль вместе с его типом, но так же определяет порядок загрузки! Вообще-то, этот макрос — не единственный, и с не меньшим успехом мы могли бы воспользоваться DEV_MODULE или SYSCALL_MODULE («man 9 DEV_MODULE» и «man 9 SYSCALL_MODULE»), но это уже дело вкуса, споры о котором рискуют превратиться в священные войны. А ведь прежде, чем воевать, модуль еще откомпилировать надо!
В общем случае, сборка осуществляется следующим make-файлом, причем строки KO и KLMOD не являются обязательными:
ЛИСТИНГ
SRCS = syshack.c
KMOD = syshack
KO = ${KMOD}.ko
KLDMOD = t
.include <bsd.kmod.mk>
Если компиляция прерывается сообщением «can't find kernel source tree», это значит, что у тебя не установлены исходные тексты ядра, или bsd.kmod.mk-файл не может их найти. Установить недостающие компоненты можно в любой момент, запустив утилиту /stand/sysinstall и отметив пункт «Kernel Developer – Full binaries and doc, kernel source only». Выходит, что, чтобы откомпилировать KLD-модуль, необходимо иметь сырцы ядра! Вот такая она, FreeBSD! Ни NetBSD, ни OpenBSD ничего подобного не требуют (что вполне логично: LKM-модули, в отличие от KLD, не являются частью ядра).
После компиляции на диске образуется множество «левых» файлов и ссылок на системные каталоги (которые можно тут же удалить), среди которых затерялся файл с расширением .ko – это и есть наш модуль (в данном случае он называется syshack.ko).
Загрузка модуля в память осуществляется утилитой kldload («man 8 kldload»), которой указывается имя модуля (если модуль расположен в текущей директории, необходимо предварить его ./), и, при желании, ключ -v - для более жестокой проверки корректности модуля.
Убедиться в успешности загрузки поможет утилита kldstat («man 8 kldstat»), которая, будучи запущенной без аргументов, выводит «свиток» всех имеющихся модулей. Если среди них присутствует syshack.ko, то операция перехвата прошла успешно, и теперь всякая попытка создания новой директории будет обречена на провал. Вплоть до выгрузки модуля из памяти, что можно сделать в любое время утилитой kldunload («man 8 kldunload»), указав ей имя модуля без расширения.
ЛИСТИНГ
полный протокол перехвата и освобождения системного вызова mkdir посредством KLD-модулей
# kldstat ; запускаем kldstat, чтобы просмотреть список модулей
Все операционные системы семейства BSD поддерживают библиотеку libkvm (Kernel Virtual Memory), предоставляющую унифицированный доступ к памяти ядра, как KLM-модули, но в отличие от них, сохранившую полную обратную совместимость. Другими словами, программа, написанная для FreeBSD, при переносе на OpenBSD или NetBSD не потребует никаких изменений!
Фактически, библиотека libkvm представляет собой высокоуровневую обертку вокруг псевдоустройства /dev/mem, изображающего из себя физическую оперативную память (псевдоустройство /dev/kmem включает в себя лишь виртуальную память ядра после трансляции адресов). Аналогичное псевдоустройство имеется и в LINUX'е, но соответствующей библиотеки для него нет, что жутко напрягает. Тем не менее, с псевдоустройством /dev/[k]mem на всех системах можно работать и напрямую, через обычный ввод/вывод, для обеспечения полной переносимости. Однако, целесообразность этого решения весьма сомнительна, поэтому сосредоточимся исключительно на библиотеке libkvm, а остальные способы доступа к ядерной памяти оставим за кадром.
Прежде чем работать с виртуальной памятью ядра, ее необходимо открыть, вызвав функцию kvm_open («man kvm_open») и передав ей в качестве имени файла NULL. Тогда, при успешном завершении операции, обеспеченная правами root'а, она вернет дескриптор. Если вместо NULL указать имя файла-образа ядра или кору, то открыты будут они, а не «живое» ядро в памяти, но нам это не нужно.
Передавая полученный дескриптор функциям kvm_read и kvm_write, мы сможем читать/писать память по заданным виртуальным адресам. Но какие именно адреса мы хотим читать? Вернее, как найти среди множества адресов полезную информацию, например, таблицу системных вызовов? В этом поможет функция kvm_nlist, разбирающая таблицу символов и возвращающая адрес элемента по его имени. Единственным ее аргументом (не считая дескриптора памяти ядра) является указатель на массив структур nlist, описанных в одноименном включаемом файле. В поле n_name заносится имя интересующего элемента и, если этот элемент действительно присутствует в таблице символов, то после завершения функции в поле n_value возвращается его виртуальный адрес.
Приведенная ниже программа (любезно позаимствованная из статьи «Playing Games With Kernel Memory... FreeBSD Style», опубликованной в #63 PHACK'е) определяет адрес таблицы системных вызов, адрес «нашего» системного вызова и адрес функции, по которой данный вызов располагается в памяти. Программа требует два аргумента — имя системного вызова (например, mkdir) и его номер (в случае mkdir равный 1), рекомендуя обратиться к файлу /usr/src/sys/sys/syscall.h, если номер вызова не известен (вообще-то данный файл располагается в каталоге /usr/include/sys/, но это неважно). На самом деле, имя системного вызова используется только для того, чтобы контролировать его существование, то есть никак не используется и для вычисления адреса используется номер syscall'а, который преобразуется в индекс таблицы системных вызовов. Это грубая недоработка! Если мы успешно определи адрес syscall'а по имени, то зачем нам его номер?! Если же мы можем (а мы можем) определять адреса syscall'ов по номеру через индекс в таблице системных вызовов, зачем нам нужно имя?!
ЛИСТИНГ
определение виртуальных адресов системных вызовов на FreeBSD, NetBSD и OpenBSD
/* программа, демонстрирующая технику определения адресов системных вызовов, */
/* работающая на всем зоопарке BSD-подобных систем */
/* Based on Stephanie Wehner's checkcall.c,v 1.1.1.1 */
#include <stdio.h>
#include <fcntl.h>
#include <kvm.h>
#include <nlist.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/sysent.h>
#include <sys/syscall.h>
int main(int argc, char *argv[])
{
char errbuf[_POSIX2_LINE_MAX];
kvm_t *kd; u_int32_t addr; int callnum; struct sysent call;
printf("sysent[%d] is at 0x%x and will execute function"
" located at 0x%x\n", callnum, addr, call.sy_call);
kvm_close(kd);
}
При трансляции листинга компилятору необходимо указать на библиотеку libkvm (при этом «lib», как всегда, опускается), иначе линкер начнет материться на неразрешимые ссылки.
ЛИСТИНГ
компиляция программы find_syscall.c
#gcc find_syscall.c -o find_syscall -lkvm
Откомпилировав программу, попробуем определить адрес системного вызова mkdir. На тестируемой машине (FreeBSD 4.5) результат выглядит так:
ЛИСТИНГ
#./find_syscall mkdir 1
Finding syscall 1: mkdir
sysent is 0x4 at 0xc03ca480
sysent[1] is at 0xc03ca488 and will execute function located at 0xc01ba2cc
Воспользовавшись функцией kvm_write, без труда поменяем указатель на mkdir в таблице системных вызовов или внедрим jump в начало самой mkdir (но последний способ не очень надежен и даже на однопроцессорных машинах может приводить к сбоям, поскольку существует вероятность, что правка функции совпадет с ее вызовом).
Остается решить последний вопрос — куда перенаправлять перехваченный системный вызов. На пользовательское адресное пространство — нельзя, система таких шуток не понимает. Теоретически, можно найти свободное место в ядре (заполненное, например, NOP'ми), записав в него крошечный «бустер», выделяющий немного памяти через malloc для размещения основного кода перехватчика, который затягивается внутрь ядра через copyin. Но никакой гарантии, что свободное место найдется, нет, поэтому лучше (и надежнее!) размещать перехватчик поверх какого-нибудь редко используемого системного вызова, например, устаревшего, но до сих пор поддерживаемого lstat, проходящего под номером 40. Или SYS_ptrace/SYS_ktrace, «ослепив» кучу утилит, предназначенных для выявления вредоносных программ, что, в конечном счете, не помешает собственной маскировке.
[Перехват системных вызовов —] прерогатива не только зловредных программ. Тем же самым занимаются и средства защиты, активно использующие интерфейс kvm, который поддерживает даже суперзащищенная OpenBSD. И вообще, следует различать действие и его мораль. А мораль такова, что распространенность BSD-систем создает все предпосылки для локальных и удаленных атак с применением всех доступных средств и интерфейсов. Главное — знать как. Все остальное — дело техники и... фантазии. В модификации ядра есть свое непередаваемое очарование, притягивающее словно магнитом и заставляющее рыскать в поисках скудной документации по всей Сети, перечитывать man и, конечно же, экспериментировать!
Проблема в том, что код, работающий на одной системе, может оказаться совершенно неработоспособным на другой. Поэтому желательно иметь в своем распоряжении хотя бы по одной версии каждой из BSD-систем. Для этой цели хорошо подходят виртуальные машины типа VM Ware. Дисковое пространство давно перестало быть проблемой, а в нормальной конфигурации (то есть без иксов) BSD-системы свободно умещаются в половину гигабайта — смехотворная по нынешним временам величина!
WWW.R4K.NET/MOD/FBSDFUN.HTML СТАТЬЯ, ПОСВЯЩЕННАЯ ВНЕДРЕНИЮ В ЯДРО FREEBSD И СОКРЫТИЮ ФАЙЛОВ, МОДУЛЕЙ, ПРОЦЕССОВ И СЕТЕВЫХ СОЕДИНЕНИЙ ОТ АДМИНИСТРАТОРА (НА АНГЛИЙСКОМ ЯЗЫКЕ)