поиск по сайту
Особенности перехвата системных вызовов при построении подсистемы разграничения доступа в ОС Linux

А. М. Каннер,

Россия, ОКБ САПР

Особенности перехвата системных вызовов при построении подсистемы разграничения доступа в ОС Linux

Ядро Linux на сегодняшний день, пожалуй, является единственной унифицированной и единой составляющей каждого дистрибутива Linux. Большинству крупных вендоров (RedHat, Novell, Canonical т. д.) доступно внесение определенных изменений в исходные коды ядра, но такие изменения либо не должны конфликтовать с эталонным кодом (т. н. «ванильным» ядром), либо будут в дальнейшем внесены в основную ветку ядра — т.о. ядро едино для всех.

Учитывая данную особенность, для ОС семейства Linux существует возможность достаточно просто (за исключением некоторых вопросов совместимости с предыдущими версиями ядра) разрабатывать различные расширения — модули ядра, которые фактически являются частью ядра и могут переопределять/дополнять различные его функции — т. е. изменять порядок работы ядра ОС Linux.

Таким образом, ядро Linux является монолитным (т. е. содержит в себе достаточный функционал для нормального функционирования системы без прочих дополнений/расширений), но при этом поддерживает загружаемые модули ядра (LKM — Linux Kernel Modules или Loadable Kernel Modules), которые выполняются в 0-м кольце защиты, с полным доступом к оборудованию, причем загрузка/выгрузка таких модулей может осуществляться во время работы системы (ядра ОС) без перезагрузки.

На первый взгляд такой подход может показаться проблемным с точки зрения безопасности, но необходимо понимать, что:

  1. все модули ядра могут загружаться/выгружаться в пространство ядра Linux только с правами суперпользователя (root);

  2. в самом ядре существуют специальные механизмы, предотвращающие выгрузку критичных модулей ядра в момент их работы (по умолчанию ядро собирается с опцией MODULE_FORCE_UNLOAD=0, т. е. без возможности принудительной выгрузки модулей ядра с параметром --force — rmmod --force module.ko);

  3. самим модулям в явном виде не разрешено совершать действия, которые могут влиять на работающую систему (например, изменять данные структур запущенных процессов, осуществлять доступ к памяти ядра и т.п..). Для таких действий требуются предварительные манипуляции, однако потенциально эти действия возможны и не запрещены при отключении блокировок типа GFP (General Fault Protection).

В связи с этим можно утверждать, что загружаемые модули ядра не могут сами по себе являться средством повышения привилегий в системе и/или быть уязвимостью системы. Целесообразность использования загружаемых модулей ядра с точки зрения злоумышленника заключается в сокрытии собственного присутствия в системе и не более — т. е. в действиях непосредственно после взлома отдельно взятой системы.

Чем могут быть полезны загружаемые модули ядра ОС Linux в плане повышения безопасности системы и/или разработки «навесных» средств защиты? Такие модули можно использовать для перехвата системных вызовов с целью организации своей, внешней по отношению к ОС, подсистемы разграничения доступа в ОС.

Системные вызовы (system calls) служат некой прослойкой в ядре ОС, используя которую прикладное ПО (из пользовательского режима) может получать доступ к оборудованию, работать с файловыми системами и т. п.. Т.о. фактический любое действие в системе требует вызова того или иного системного вызова (будь то запись/чтение файла, изменение прав доступа, запуск нового процесса или любые действия с выделением/освобождением памяти).

Итак любое приложение, используя функции стандартной библиотеки (glibc), неявно для себя вызывает определенные системные вызовы (system_call), специальная процедура ядра ОС просматривает таблицу системных вызовов (sys_call_table), находит адрес нужной функции ядра, вызывает эту функцию и передает управление обратно приложению только после выполнения этого системного вызова (при этом ядро также осуществляет разграничение доступа разных приложений к одним и тем же системным вызовам по времени).

 

Что требуется для внедрения собственных механизмов безопасности в ОС Linux? Фактически требуется написать собственные реализации основных системных вызовов, которые в зависимости от успешности/неуспешности определенных проверок будут вызывать/не вызывать выполнение эталонных системных вызовов. Т.о. становится возможным реализовать собственные дискреционные и мандатные механизмы разграничения доступа, которые будут работать непосредственно до отработки всех штатных подсистем разграничения доступа ОС. Сами функции, «переопределяющие» работу системных вызовов логично описать в загружаемом модуле ядра, а при инициализации этого модуля необходимо заменять адреса системных вызовов в таблице системных вызовов на адреса переопределяющих их функций (при этом желательно при выгрузке модуля совершать обратное действие).

 

[image]

Реализовать описанный выше подход можно в случае, когда нам известен адрес таблицы системных вызовов в памяти ядра Linux. Несмотря на всю простоту данного подхода в нем кроется большая проблема, попробуем описать ее на примере:

Пример:

Предположим, у нас есть 2 загружаемых модуля ядра ОС Linux module_А и module_B и каждый из них изменяет адрес одного и того же системного вызова 'open' на адрес своей функции (open_A и open_B соответственно). Первым загружается module_A и заменяет адрес в sys_call_table[__NR_open] на адрес своей функции open_A. Затем загружает module_B и заменяет адрес open_A адресов своей функции open_B (module_B при этом подозревает, что подменил оригинальный системный вызов).

Теперь если module_B выгрузится первым — в системе ничего плохого не произойдет — при выгрузке module_A в таблице системных вызовов будет восстановлен оригинальный вызов 'open'. Однако если же первым выгрузится module_A, будет восстановлен оригинальный системный вызов 'open', а при выгрузке module_B системный вызов 'open' будет заменен на open_A (которой вообще говоря уже нет в памяти).

На первый взгляд проблему из примера можно решить, заменяя адрес обратно только в случае, если адрес в таблице системных вызовов совпадает с адресом функции модуля (open_A или open_B), но в таком случае при выгрузке в качестве первого модуля module_A — адрес sys_call_table переписан не будет (т. е. не вернется оригинальный системный вызов 'open'), т.о. данный подход также не решает проблему.

С целью предотвращения потенциальной опасности, связанной с изменением адресов системных вызовов начиная с версии ядра ОС Linux >= 2.5.41 более не экспортируется адрес таблицы системных вызовов (sys_call_table). Вместо этого появился механизм LSM (Linux Security Modules), который позволяет перехватывать системные вызовы и вставлять свои обработчики системных вызовов без необходимости самому подменять адреса системных вызовов и/или вникать в структуру и последовательность использования функций и ресурсов ядра. Т.е. грубо говоря LSM — набор предустановленных в ядре ОС хуков, которые предоставляют API для внедрения собственных обработчиков непосредственно перед выполнением определенного системного вызова.

Одной особенностью использования механизма LSM является невозможность одновременного использования нескольких модулей ядра для регистрации одинаковых хуков — при попытке регистрации LSM-модуля ядра будет выведено соответствующее предупреждение, таким образом при использовании стандартного вкомпилированного в большинство версий ядра SELinux использовать собственный модуль безопасности LSM не получится (отключение SELinux с помощью параметра ядра в загрузчике типа selinux=0 также может не принести никакого результата).

Начиная с версии ядра 2.6.24 и выше механизм LSM (а именно функция позволяющая использовать данный механизм) перестал экспортироваться ядром ОС Linux, в связи с чем для реализации перехвата системных вызовов на сегодняшний момент приходится использовать:

  1. описанный ранее механизм подмены системных вызовов в таблице системных вызовов, при этом адрес таблицы системных вызовов:

    • получать из файла System.map (как правила создается вместе с ядром Linux при компиляции, также копируется вместе с ядром в /boot)

    • получать поиском в памяти ядра (по адресу от 0xc0000000 до 0xd0000000 для 32-разрядных версий ОС)

  2. механизм LSM без регистрации обработчиков — т. е. фактически поиском и использованием указателей из структуры security_ops (выставленных хуков LSM, которые не были убраны из ядра ОС Linux). В данном случае расстановка хуков в большинстве случаев будет осуществляться проще п.1 в связи с тем, что большинство блокировок ядра автоматически снимается механизмами LSM.

Подводя итог, можно сказать, что создание собственной подсистемы разграничения доступа в ОС семейства Linux в техническом плане не является чем-то сложным или недоступным. Получить контроль над системными вызовами, как было показано в данной статье, достаточно просто — поэтому основной задачей является разработка тех проверочных правил, которые вызываются при выполнении того или иного системного вызова ядра ОС.


ФорумФорум
Форум ОКБ САПР
Вопросы специалистовВопросы специалистов
Вопросы, которые нам присылают, и наши ответы на них