Безопасность операционных систем: права доступа, изоляция и контейнеризация🔗
Архитектура безопасности ОС: модель нарушителя и уровни защиты🔗
Безопасность ОС — это система барьеров, защищающая конфиденциальность, целостность и доступность ресурсов от вмешательства процессов или пользователей. В современном дизайне это не внешний плагин, а встроенный механизм контроля на границе пользовательского пространства и ядра.
Также Возьмём пример с кухней ресторана: процессы-повара готовят блюда, используя общее оборудование. Безопасность здесь — это не только охранник на входе, но и магнитные замки на холодильниках. Повар холодного цеха технически не может открыть шкаф кондитера, потому что система контроля блокирует доступ без нужного ключа.
Модель нарушителя и Reference Monitor🔗
В то же время В основе архитектуры лежит Reference Monitor (монитор обращений). Это механизм в ядре, который перехватывает каждый системный вызов к файлам, памяти или сетевым сокетам. Он всегда активен, защищен от изменений и спроектирован так, чтобы его надежность можно было подтвердить аудитом кода.
Диаграмма загружается…
Ключевые принципы защиты🔗
Важно: * Минимизация привилегий (Least Privilege): процесс владеет только теми правами, которые необходимы для работы. Веб-серверу не нужно видеть файл /etc/shadow. Поэтому доступа у него нет по умолчанию. * Полное посредничество (Complete Mediation): ни один объект недоступен в обход системы контроля. В архитектуре VFS ядро проверяет права доступа к дескрипторам файлов при каждом обращении. * Эшелонированная защита (Defense in Depth): безопасность строится слоями. Если злоумышленник взломал процесс через переполнение буфера, механизмы изоляции не дадут ему захватить управление всей системой.
Современные ОС реализуют эти идеи через уровни: от базовых прав владения до «песочниц». Пока права доступа решают, кто входит в систему, изоляция гарантирует, что гость не выйдет за пределы своей комнаты. Насколько эффективно такая система работает на практике, зависит от выбранной политики управления доступом. Можно ли сделать эти правила абсолютно гибкими и при этом непробиваемыми?
Механизмы прав доступа: разница между DAC и MAC🔗
Контроль доступа — это системный барьер, проверяющий права пользователей и процессов при обращении к ресурсам. Механизм работает на уровне ядра, одобряя или отклоняя системные вызовы вроде чтения файла или отправки сетевого пакета.
Дискреционное управление доступом (DAC)🔗
Discretionary Access Control (DAC) — модель, где владелец ресурса сам решает, кому и какие права предоставить. Это «избирательный» подход, ставший стандартом в классических ОС: Linux, Windows и macOS.
С другой стороны, В основе системы лежит право собственности. Если вы создали файл, вы определяете полномочия для остальных. В Unix-подобных системах эта механика опирается на триплет прав: Read (r), Write (w) и Execute (x). Когда программа пытается открыть объект через вызов open(), ядро сопоставляет идентификаторы процесса (UID и GID) с маской доступа.
// Пример проверки полномочий в стиле POSIX
struct stat st;
stat("secret_recipe.txt", &st);
// Маска доступа: rwxr-xr-- (754)
// Владелец имеет полный доступ, группа — чтение и запуск, прочие — только чтение.
if (st.st_mode & S_IRUSR) {
/* Доступ разрешен для владельца */
}
Слабость DAC — в избыточном доверии. Если пользователь по неосторожности откроет полный доступ к своим данным для всех, ОС не станет препятствовать. Ошибка одного человека ставит под угрозу конфиденциальность всей директории.
Мандатное управление доступом (MAC)🔗
Mandatory Access Control (MAC) — жесткая модель, где права диктуются глобальной политикой. Здесь решение принимает не владелец файла, а администратор безопасности. Субъектам и объектам присваиваются метки безопасности (security labels).
Ядро сравнивает уровень допуска приложения (допустим, категорию «Confidential») с меткой документа. Даже если суперпользователь попытается передать права на файл обычному аккаунту, система заблокирует операцию, если у получателя нет нужной метки в профиле. Так работают SELinux и AppArmor.
| Характеристика |
DAC (Дискреционное) |
MAC (Мандатное) |
| Кто управляет |
Владелец объекта |
Администратор (через политики) |
| Критерий доступа |
Идентификатор (UID) |
Метки и уровни доступа |
| Гибкость |
Высокая, права меняются сразу |
Низкая, правила регламентированы |
| Надежность |
Риск ошибки пользователя |
Защита при взломе аккаунта |
| Пример |
Команда chmod 644 file.txt |
Политики SELinux в Android |
Реализация: списки доступа (ACL) и возможности🔗
Теоретически разграничение прав описывается матрицей доступа, где строки — субъекты, а столбцы — объекты. На практике такая таблица была бы слишком громоздкой. Чтобы сэкономить ресурсы, используют две альтернативы:
В то же время 1. ACL (Access Control Lists): Список полномочий хранится внутри объекта. Файл «знает», кому разрешено с ним работать. Это расширение DAC в NTFS или ext4, позволяющее выдавать права конкретным пользователям персонально, а не только группам.
2. Capabilities (Возможности): «Пропуск» находится у процесса. Субъект предъявляет его ядру при каждой попытке совершить действие.
Разница в векторе проверки: ОС либо сверяется со списком у двери (ACL), либо требует лицензию у входящего (Capabilities). Как именно современные системы разбивают безграничную власть root на десятки мелких привилегий, чтобы минимизировать ущерб от взлома? Попробуйте предположить, какие минимальные права нужны веб-серверу только для старта на 80-м порту.
Изоляция ресурсов: пространства имен Namespaces и ограничения Cgroups🔗
Изоляция ресурсов — это технология разделения системных сущностей, которая создает для группы процессов выделенное окружение с ограниченным доступом к чужим данным и мощностям. Если виртуальная память обеспечивает иллюзию монопольного владения ОЗУ, то связка Namespaces и Cgroups масштабирует этот подход на всю операционную систему.
Linux Namespaces: виртуализация обзора🔗
Пространства имен (Namespaces) определяют границы видимости для процесса. Когда он попадает в определенный Namespace, ядро подменяет для него глобальные таблицы ресурсов на локальные копии. В результате процесс с PID 1 внутри контейнера считает себя главным в системе, хотя в основной таблице хоста ему может быть присвоен PID 4502.
Ключевые типы Namespaces:
- PID: изолирует дерево процессов. Субъект не видит «соседей» и не может отправить им сигнал
SIGKILL. * Net: выделяет персональный сетевой стек со своими IP-адресами. Таблицами маршрутизации и портами. * Mount: формирует независимую иерархию файловых систем, развивая концепцию chroot. * UTS: позволяет назначить уникальное имя узла (hostname) конкретной группе процессов.
Диаграмма загружается…
Control Groups (Cgroups): лимиты потребления🔗
Если Namespaces управляют тем, что процесс видит, то Cgroups (контрольные группы) отвечают за распределение физических мощностей: CPU, RAM и пропускной способности ввода-вывода. Без этого механизма один ресурсоемкий поток в изолированном окружении мог бы захватить все ресурсы хоста, спровоцировав отказ в обслуживании (DoS) соседних сервисов.
Управление лимитами реализовано через иерархическую файловую систему (традиционно в /sys/fs/cgroup/):
- CPU: распределение процессорного времени через планировщик CFS.
- Memory: установка предельного объема страниц в ОЗУ. При достижении потолка ядро активирует OOM Killer для принудительной остановки процесса.
- Blkio: контроль скорости чтения и записи на диск.
Взаимодействие механизмов🔗
Синергия этих инструментов превращает обычный процесс в основу контейнера. С помощью системного вызова unshare() или флагов в clone() создается контекст исполнения, где ресурсы надежно заперты. Как вы считаете, достаточно ли этих мер, чтобы гарантировать полную безопасность ядра от действий привилегированного пользователя внутри такой «песочницы»?
Как работает контейнеризация: системные вызовы chroot, unshare и clone🔗
Контейнеризация — это способ изоляции процессов на уровне ядра, при котором приложение работает в обособленном окружении без лишних трат на эмуляцию «железа». Контейнер остается обычным процессом в системе, но его доступ к ресурсам жестко ограничен специфическими системными вызовами.
Виртуализация файловой системы: chroot🔗
Первым инструментом для создания такой среды стал вызов chroot (change root). Он подменяет корневой каталог для процесса и его потомков, заставляя их верить, что выбранная директория — это и есть корень файловой системы /.
Стоит помнить, что chroot не гарантирует абсолютную безопасность. Процесс с правами root способен совершить «jailbreak», создав вложенную директорию и принудительно выйдя за границы навязанного корня. К тому же этот механизм никак не ограничивает доступ к сети, таблице процессов или списку пользователей.
Механизмы изоляции: clone и unshare🔗
Фундамент современной контейнеризации в Linux — системный вызов clone(), который развивает идеи стандартного fork(). В отличие от предшественника, clone позволяет создавать дочерние процессы, которые не наследуют ресурсы родителя, а получают личные пространства имен (Namespaces).
// Создание процесса в новых PID и сетевом пространствах
long child_stack[16384];
int pid = clone(child_func, child_stack + 16384,
CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | SIGCHLD, NULL);
С помощью флагов CLONE_NEW* мы указываем ядру, что именно нужно изолировать:
CLONE_NEWPID: внутри окружения процесс будет считать себя первым в иерархии (PID 1).
CLONE_NEWNET: выделяется свой набор IP-адресов и таблица маршрутизации.
CLONE_NEWNS: создаются независимые точки монтирования (Mount Namespace).
В то же время Если нужно изолировать текущий поток без создания нового процесса, используется вызов unshare. Он отрезает работающий процесс от общих ресурсов «на лету».
Динамика создания изоляции🔗
Диаграмма загружается…
Благодаря этим примитивам ядро просто помечает структуры процесса специальными тегами. Это обеспечивает мгновенный старт. Экономию памяти по сравнению с виртуальными машинами. Но как сдержать такой процесс, если злоумышленник внутри получит права суперпользователя? Это решается дополнительными механизмами квот и фильтрации прав.
Безопасность системных вызовов: Seccomp и Capabilities🔗
Безопасность системных вызовов сужает поверхность атаки между приложением и ядром ОС. Ограничение низкоуровневых операций гарантирует, что даже скомпрометированный процесс не сможет использовать критические функции ядра для дестабилизации системы.
Внутри изолированных Namespaces процесс всё еще видит сотни системных вызовов. Без дополнительной фильтрации риск эксплуатации уязвимостей в коде ядра остается высоким, так как стандартные права доступа не регулируют взаимодействие с интерфейсом syscalls.
Linux Capabilities: дробление власти root🔗
Традиционная модель Unix делит субъектов на обычных пользователей и всемогущего суперпользователя. Чтобы программа могла выполнить специфическую задачу — к примеру, занять 80-й порт, — её часто запускали от имени root. Это создавало брешь: захват контроля над сервисом давал взломщику полные права в системе. POSIX Capabilities решают эту проблему, разделяя монолитные полномочия администратора на десятки независимых привилегий.
| Capability |
Описание полномочий |
CAP_NET_BIND_SERVICE |
Привязка сокетов к привилегированным портам (ниже 1024). |
CAP_SYS_ADMIN |
Широкий доступ: монтирование, управление namespaces и настройка сети. |
CAP_CHOWN |
Смена владельца любых файлов в системе. |
CAP_NET_RAW |
Использование RAW-сокетов для диагностики или перехвата трафика. |
Выдавая сервису только CAP_NET_BIND_SERVICE, мы позволяем ему слушать нужный порт, но лишаем возможности читать чужие файлы или перезагружать ОС.
Seccomp: фильтрация системных запросов🔗
С другой стороны, Если механизм Capabilities определяет права на действия, то Seccomp (Secure Computing) устанавливает правила общения с ядром. Он превращает среду выполнения в жесткую песочницу, блокируя вызовы вне разрешенного списка. Если процесс попробует выполнить execve() или mount() там, где согласованы только read() и write(), ядро мгновенно завершит его работу.
Современный Seccomp использует фильтры BPF (Berkeley Packet Filter), что позволяет проверять не только тип вызова, но и его аргументы:
#include <seccomp.h>
#include <unistd.h>
// Создаем фильтр: всё, что не разрешено явно — убить (SCMP_ACT_KILL)
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
seccomp_load(ctx); // Любой вызов, кроме этих трех, приведет к гибели процесса
Принцип эшелонированной защиты🔗
Связка из Namespaces, Cgroups, Capabilities и Seccomp создает глубокую эшелонированную оборону. Преодоление одного барьера не означает победу взломщика: ему придется последовательно бороться с изоляцией ресурсов, лимитами и запретами на исполнение кода. Такая многослойность делает атаку экономически невыгодной и технически сложной.
Проверьте конфигурации своих контейнеров: часто ли вы оставляете в них лишние привилегии просто для «удобства» запуска? Концепция наименьших полномочий требует дисциплины, но именно она определяет живучесть системы под нагрузкой и атакой.