В этой статье не пойдёт речь ни о правильной раздаче привилегий пользователям, ни о настройке ip-tables, ни о каких-либо других общеизвестных методах защиты (для каждого из которых найдётся такой же общеизвестный метод взлома). Мы заглянем в ядро ОС Linux. Предполагается, что читатель уже изучил всё то, что окружает ядро снаружи, сносно программирует на Си, и решил попробовать себя на поприще kernel-хакера. «…«Вот оно!», – подумал князь Андрей, схватив древко знамени…» Флаг в руки. Но сразу говорю, первый блин (и второй, и третий) будут комом.

Начинаем с разработки общей концепции. Отвечаем на вопросы типа:

  • Что мы собираемся защищать?
  • От кого мы собираемся защищать?
  • Как мы собираемся защищать?

Далее читаем документацию по существующим системам (RSBAC, SELinux, Lids, SidsCore и т.д.). Читаем документацию по формальным моделям защиты данных (Белла-Ла Падулы, Биба, Харрисона-Рузо-Ульмана и
т.д.). Читать документацию НАДО, а то получится как в анекдоте: «Чукча не читатель, чукча писатель…». Анализируем всё это хозяйство. И убедившись в том, что ничто не удовлетворяет нашим требованиям, приступаем к работе. С миру берём по нитке, учитываем опыт и ошибки наших предшественников. Если вы работаете в команде, то помните, что собрав девять рожениц в одной комнате вы не получите новорожденного через месяц. Если вы приступаете к делу в одиночку, не забывайте, что каждая роженица за девять месяцев беременности проходит через одни и те же этапы. Не усложняйте свою систему – максимальной защищённости всегда соответствует наиболее простое решение. В идеале лучше компьютерами вообще не пользоваться.

Итак, Вы дошли до «ядерного» этапа. Для наших экспериментов понадобится ПК (i386), с установленным на нём ОС Linux (Fedora Core, RedHat 9, Alt, Suse) и исходные коды ядра ветки 2.6. В нашем случае при выборе лучше остановиться на дистрибутиве, уже поставляемом с ядром из ветки 2.6. – это значительно может упростить процесс. Почему 2.6? Начиная с ядер этой ветки, в код всех функций, критичных к вопросам информационной безопасности, были добавлены вызовы специальных процедур-проверок, объединённых в единую модульную систему Linux Security Modules. К сожалению, этому событию общественность не уделила должного внимания. Благодаря интерфейсу LSM каждый может «взять в руки компилятор» и написать такой модуль защиты для ядра Linux, который будет реализовывать интересующую политику безопасности.

Всегда лучше брать последнюю стабильную версию ядра. Архив можно свободно скачать с ресурсов
kernel.org. В формате *.tar.gz он занимает около 40 Мбайт.

Весьма полезными для Вас будут книги Daniel P. Bovet, Marco Cesati «Understanding the Linux Kernel» (1,2 издания) от O’Reilly (к сожалению, на русский язык они не переведены), книги Э. Таненбаума, а также ресурсы
tldp.org (The Linux Documentation
Project).

Итак, dы скачали ядро, распаковали (tar –xzvf) и обновили необходимое программное обеспечение (подробно в Documentation/Changes). Начинаем изучение исходных кодов. Да простит нас Линус Торвальдс. НИЧЕГО НЕ МЕНЯЕМ. ПЕРЕД ИЗУЧЕНИЕМ ДЕЛАЕМ backup!!! Перед нами каталоги: arch crypto Documentation drivers fs include init ipc kernel lib mm net scripts security sound usr, а также ряд файлов. Для начального изучения нам будут интересны в первую очередь Documentation, далее security, include, init, arch/i386/kernel/, fs. Перед тем как лезь в остальные подойдите к зеркалу и убедитесь в том, что перед
dами некто, похожий на Ричарда Столмана, Линуса Торвальдса или Эндрю Таненбаума. Каталог arch содержит практически весь машинно-зависимый код. Проанализируем содержимое файла arch/i386/kernel/entry.S – здесь содержится таблица системных вызовов. В ОС Linux системные вызовы организованы на основе программных прерываний.

Пока ничего не трогаем!!! Идём дальше. Директория include – она также как и arch содержит некоторый машинно-зависимый код. Здесь содержатся заголовочные файлы (*.h). Нас будут интересовать файлы include/linux/fs.h, include/linux/capability.h и include/linux/security.h – их код машинно-независим. Также далее мы попробуем создать свои заголовочные файлы.

Директория init содержит код, необходимый для загрузки и инициализации ядра ОС. Самый интересный файл – init/main.c – он содержит функцию init(), которая запускает всем известный процесс init – родитель всех процессов в системе. Если внести некоторые изменения в эту функцию можно заставить ядро выполнять некоторые функции непосредственно во время высокоуровневой инициализации системы!

Во многих современных ОС идёт тенденция к унификации всех ресурсов системы (файлов, процессов, устройств ввода-вывода и др.). В *nix подход заключается в интеграции всех ресурсов в единую ВИРТУАЛЬНУЮ ФАЙЛОВУЮ СИСТЕМУ (VFS). Такой подход в частности позволяет нам реализовывать формальные модели защиты, ориентированные на отношения типа СУБЪЕКТ-ОБЪЕКТ в конкретных системах. Существует понятие метки безопасности – security label. Каждому конкретному объекту можно поставить в соответствие метку безопасности, которая будет определять, например, степень секретности и важности объекта или, например, уровень допуска к секретной информации. Так вот, в каталоге fs мы найдём код, относящийся к реализации механизмов VFS в ОС
Linux.

Мы дошли до директории security – температура в помещении повышается. Внимательно изучите файл security/root_plug.c – этот простой пример поставляется в ядре специально для Вас!
Теперь Ваша задача собрать ядро – но то ядро, которое Вы скачали с kernel.org. Убедитесь ещё раз в том, что Вы ничего не изменили в коде ядра! 

Итак, Вы сконфигурировали (make O=«куда собирать» menuconfig), собрали ядро (make O=«куда собирать»), установили в /boot/ (make O=«куда собирать» install), прописали всё необходимое в загрузчике, удачно перезагрузились с новым ядром ОС. Поздравляем. Теперь сделайте backup-ы, и Вы можете смело приступить к редактированию кода ядра.

Для начала определитесь с тем, как будет происходить обмен данными между уровнем пользователя и уровнем ядра. Если
вы знакомы с встроенными механизмами защиты процессоров Intel, то
вы прекрасно понимаете, что нельзя просто так взять и изменить данные уровня ядра. У
вас также нет ни каких библиотек системных вызовов, так как самих системных вызовов тоже нет. Начнём с создания своего собственного системного вызова. Это не совсем правильно, точнее сказать совсем не правильно. Но в нашем случае это допустимо.

Полезными будут навыки работы с редактором vi. Напоминаю, что более подробную информацию о работе с vi можно получить, набрав команду «man vi».

#vi arch/i386/kernel/entry.S

Находим таблицу: / ENTRY(sys_call_table)

……….
ENTRY(sys_call_table)
.long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
.long sys_exit /* 1 */
.long sys_fork
.long sys_read
……….
.long sys_ni_syscall /* sys_vserver */
……….
/* сюда добавим наш вызов, назовём его sys_sec_multiplexor*/
.long sys_sec_multiplexor
……….
syscall_table_size=(.-sys_call_table)
/*конец*/

Сохраняем и выходим из редактора: wq

Теперь нам необходимо написать каркас модуля защиты и код системного вызова. Назовем нашу систему
super_secure_system.

#vi security/super_secure_system.c

Для начала, модуль должен содержать некоторый минимальный код. Сделайте его по примеру файла root_plug.c.
В своём модуле Вы должны объявить структуру типа security_operations и предопределить её значения:

………….
static struct security_operations super_secure_oper = {
………….
Адреса вывовов процедур защиты данных
………….
}
………….

Сама структура security_operations объявлена в заголовочном файле include/linux/security.h – там даны все процедуры защиты, которые можно использовать, вместе с подробным описанием.
Также вам понадобится функция регистрации и дерегистрации
вашего модуля в ядре – ни чем не отличаются от root_plug.c – разве что поменяйте названия.
Обмен пользовательской части с «ядерной» в Вашей системы безопасности будет наверняка не хилым – понадобится выполнение большого количества различных функций. Но не добавлять же каждый раз новый системный вызов.
Поэтому организуем механизм мультиплексирования внутренних подпроцедур в Вашем модуле защиты:

asmlinkage long sys_sec_multiplexor (u32 routine_type, «другие параметры») {
switch(routine_type) {
case ROUTINE1: { printk(“Hello, World!!!\n”);}
case ROUTINE2: { printk(“How are you?\n”);} 
……………
default: return SUPER_SECURE_UNKNOWN_SYS_ROUTINE_ERR;
}

Ах да, забыли! Если уж мы разрабатываем серьёзный продукт, не плохо бы продумать систему обработки ошибок и отладки. Первым шагом к этому будет создание файла include/linux/super_secure_errno.h, в котором нужно будет размещать коды ошибок. Полезно будет также продумать свой собственный механизм выдачи строкового сообщения по коду ошибки.
Также нам понадобится некоторый основной заголовочный файл, в котором мы будем держать различные константы.
Полезной для Вас будет функция printk (не путайте с printf). Интерфейс похож на printf. Детальную информацию об этой функции Вы найдёте в коде ядра. «Лучшая документация – это исходники», – ещё один важный тезис.
Теперь мы можем попробовать собрать наше ядро. Для этого включаем наш модуль в security/Makefile, а также не забываем добавить конфигурационную информацию в
security/Kconfig. При конфигурировании нашего нового ядра в разделе Security options включаем Enable different security models и ставим галочку напротив нашего модуля, который должен был там появиться после внесения изменений в
security/Kconfig.

Собираем, устанавливаем, перезагружаем. Готов поспорить ядро не прогрузилось – знаю по опыту. Не отчаивайтесь, внимательно всё проверьте.
Если ядро прогрузилось, то внимательно анализируем содержимое /var/log/dmesg. Если ни каких проблем нет, продолжаем.
Нам необходимо как-то производить вызов наших функций в ядре. Чтобы лучше понять механизм организации системных вызовов через программные прерывания напишем следующую функцию. Внимание! Сейчас мы пишем отдельную самостоятельную программу вне ядра.

#vi /home/thats_me/bin/test.c

//---------------- test.c ------------------------
#include
#include <Заголовочный файл с нашими константами>
unsigned int routine_type;
long sys_call_result;
int secure_sys_call(«здесь прописываем наши параметры») {
………………
asm(“movl $274,%eax”); // 1
………………
asm(“movl routine_type,%ebx”); // 2
………………
asm(“int 0x80”);
asm(“movl %eax, sys_call_result”); //3
………………
return sys_call_result;
}
int main(int argc,char **argv) {
………………
return secure_sys_call(«здесь прописываем наши параметры»);
………………
}
// конец

#gcc /home/thats_me/bin/test.c
#./a.out параметр1
Hello, World!!!
#./a.out параметр2
How are you?

Теперь мы приручили ядро! Но как нам это удалось? Передача параметров производится через основные регистры. Мы помним, что сейчас работаем с Intel IA-32 (i386). Через еах передаётся номер нашего вызова в таблице системных вызовов (помните файл entry.S) – наша функция стояла на 274 месте. А вот передачу параметров советую
вам продумать самостоятельно. Особо внимательно отнеситесь к передаче указателей и их проверки в коде вашего системного вызова. Помните, никто не мешает написать злоумышленнику программу, аналогичную нашей, которая будет передавать указатели на несуществующие или критичные области памяти. Полезным будет прочесть книжку по безопасному программированию. Также даже не пробуйте возвращать на уровень пользователя указатели на области памяти, принадлежащие ядру – сработают встроенные механизмы защиты
вашего процессора.

Следующим шагом будет разработка механизма ограничения использования
вашего системного вызова пользователями ОС, включая root. Только не надо как в детском саду передавать в ядро пароли для системного вызова – это не только глупо, но и не надёжно. Могу порекомендовать сессии на основе одноразовых случайных ключей, с защитой от подбора. Помните, что не следует усложнять код ядра ОС – только минимальные изменения.

Кстати, забыл сказать ещё об одном нововведении в 2.6.х. Теперь исполняемые модули не могут модифицировать каким-либо образом таблицу системных вызовов по собственному усмотрению. Это в некотором смысле улучшает защиту ядра от заражения вирусами и троянским кодом. Если заблокировать динамическую загрузку модулей, то решение вопросов обеспечения целостности и конфиденциальности кода и данных в режиме операционной системы значительно упрощается.

Ядра ветки 2.6 могут теперь работать с аппаратными генераторами случайных чисел, реализованными в некоторых современных процессорах.

Продолжаем после лирического отступления. Внимательно изучим механизм LSM. Он сейчас нам понадобится. Подход Linux Security Module Framework был официально представлен в 2002 году в городе Оттава (Канада) на симпозиуме, посвящённом ОС
Linux.

Программно интерфейс реализован в виде структуры security_operations, элементами которой являются адреса вызовов процедур обработки. Во время загрузки, происходит инициализация элементов структуры стандартными адресами. Модуль защиты может быть собран как загружаемый динамически, или статически. В зависимости от этого немного меняется способ регистрации вызовов.
Вызов процедур обработки, как элементов структуры, происходит из специальных функций. Если в системе происходит какое-либо событие, затрагивающее вопросы безопасности, то просто происходит вызов соответствующей специальной функции.
На процедуру обработки возлагаются обязанности анализа правомерности запроса. Однако конечное решение не обязательно совпадает «с точкой зрения» загруженного модуля. То есть модуль в состоянии перекрыть доступ, но не может его предоставить без ведома других подсистем, – в этом заключается принципиальный момент концепции защиты LSM. Интерфейс предоставляет возможность загрузить помимо основного ещё один вспомогательный модуль защиты – но основной модуль может быть только один.

Продолжение следует…

Check Also

KRACK на практике. Как работает атака на Wi-Fi с применением нашумевшей техники

Осенью 2017 года мир узнал о новой угрозе безопасности сетей Wi-Fi. Она затрагивает абсолю…

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