Особливості доступу до системних функцій ядра ОС GNU / Linux
A. М. Каннер
Закрите акціонерне товариство «ОКБ САПР», Москва, Росія
B. П. Лось, д-р техн. наук
Московський державний індустріальний університет (МГИУ), інститут криптографії, зв'язку та інформатики Академії ФСБ Росії, Москва
Ядро Linux на сьогоднішній день, мабуть, є єдиною уніфікованою та єдиною складовою кожного дистрибутива Linux. Більшості великих вендорів (RedHat, Novell, Canonical і т. Д.) Є внесення певних змін в вихідні коди ядра, але такі зміни або не повинні конфліктувати з еталонним кодом (так званим «ванільним» ядром), або будуть в подальшому внесені в основну гілку ядра, таким чином ядро єдине для всіх.
З огляду на цю особливість, для ОС сімейства Linux існує можливість досить просто (за винятком деяких питань сумісності з попередніми версіями ядра) розробляти різні розширення - модулі ядра, які фактично є частиною ядра і можуть перевизначати / доповнювати різні його функції, т. Е. Змінювати порядок роботи ядра ОС Linux. Причому такі модулі, з деякими застереженнями, будуть сумісні з більшістю дистрибутивів Linux без внесення в їх код будь-яких змін.
Таким чином, ядро Linux є монолітним (т. Е. Містить в собі достатній функціонал для нормального функціонування системи без інших доповнень / розширень), але при цьому підтримує завантажувані модулі ядра (LKM - Linux Kernel Modules або Loadable Kernel Modules), які виконуються в 0-м кільці захисту, з повним доступом до устаткуванню, причому завантаження / розвантаження таких модулів може здійснюватися під час роботи системи (ядра ОС) без перезавантаження.
На перший погляд такий підхід може здатися проблемним з точки зору безпеки, але необхідно розуміти, що:
- всі модулі ядра можуть завантажуватися / розвантажуватися в простір ядра Linux тільки з правами суперкористувача (root);
- в самому ядрі існують спеціальні механізми, що запобігають вивантаження критичних модулів ядра в момент їх роботи (за замовчуванням ядро збирається з опцією MODULE_FORCE_UNLOAD = 0, т. е. без можливості примусового вивантаження модулів ядра з параметром -force - 'rmmod -force module.ко' );
- самим модулів в явному вигляді не дозволено здійснювати дії, які можуть впливати на працюючу систему (наприклад, змінювати дані структур запущених процесів, здійснювати доступ до пам'яті ядра і т. п.). Для таких дій потрібні попередні маніпуляції, проте потенційно ці дії можливі і не заборонені при відключенні блокувань типу GFP (General Fault Protection).
У зв'язку з цим можна стверджувати, що завантажувані модулі ядра не можуть самі по собі бути засобом підвищення привілеїв в системі і / або бути вразливістю системи. Доцільність використання завантажувальних модулів ядра з точки зору зловмисника полягає в приховуванні своєї присутності в системі і не більше, т. Е. В діях безпосередньо після злому окремо взятої системи.
Чим можуть бути корисні завантажувані модулі ядра ОС Linux в плані підвищення безпеки системи і / або розробки «навісних» засобів захисту? Такі модулі можна використовувати для перехоплення системних функцій ядра з метою організації своєї, зовнішньої по відношенню до ОС, підсистеми розмежування доступу в ОС.
Інтерфейс системних викликів ядра ОС Linux
Інтерфейс системних викликів (system call interface) є прошарком в ядрі ОС, використовуючи яку прикладне ПО (з призначеного для користувача режиму) може отримувати доступ до обладнання, працювати з файловими системами і т. П. Таким чином, фактично будь-яка дія в системі вимагає виклику того або іншого системного виклику (будь то запис / читання файлу, зміна прав доступу, запуск нового процесу або будь-які дії з виділенням / звільненням пам'яті), а загальну схему роботи з системними викликами можна схематично уявити на рис. 1.
Мал. 1. Використання системних функцій ядра ОС Linux для користувача додатками
Перехоплення системних викликів
Мал. 2. Перехоплення системного виклику за допомогою модуля ядра Linux
Тепер якщо module_B вивантажиться першим - в системі нічого поганого не станеться - під час вивантаження module_A в таблиці системних викликів буде відновлений оригінальний виклик 'open'. Однак, якщо ж першим вивантажиться module_A, буде відновлений оригінальний системний виклик 'open', а при вивантаженні module_B системний виклик 'open' буде замінений на ореп_А (якої взагалі кажучи вже немає в пам'яті).
Використання механізму LSM для перехоплення системних викликів
Розглянемо приклад LSM-модуля ядра, який буде підміняти стандартний системний виклик mkdir своїм оброблювачем, який додатково до створення каталогу буде виводити в dmesg деяке повідомлення, кожен раз коли викликається команда mkdir:
1. Для того щоб модуль ядра почав використовувати механізм LSM, в його функції ініціалізації і деініціалізацію (module_init і module_exit) необхідно додати виклик наступних 2 функцій, відповідно:
/ * Реєстрація перехоплювачів системних викликів
* @return 0 в разі успішної реєстрації, інакше - код помилки * /
int res = register_security (hook_security_ops);
if (res) <
printk (KERN_ERR. res); return res;
>
return 0;
>
/ * Дерегістрація перехоплювачів системних викликів * /
int res = unregister_security (hook_security_ops);
if (res) printk (KERN_ERR. res);
2. Як видно з прикладу вище LSM-модуль ядра реєструє власні обробники за допомогою функцій register_security () (дерегістрірует за допомогою unregister_security (), обидві функції оголошені в), передаючи в цю функцію структуру виду:
/ * Операції, що перехоплюють системні виклики * /
static struct security operations hooksecurityops = <.inodernkdir = inodernkdir,>;
3. В даному випадку в структурі типу security_operations перераховані системні виклики разом з функціями (нашими хукамі), які виконуються безпосередньо перед виконанням зазначених системних викликів (в нашому випадку перед системним викликом inode_mkdir виконується наша функція inode_mkdir, яка повинна бути оголошена в цьому ж модулі ядра ).
4. Сама функція, що викликається до виконання системних викликів може бути, наприклад, такий:
/ * Перехоплення запиту на створення каталогу * /
static int inode_mkdir (struct inode * dir, struct dentry * dentry, int mode)
printk (); return 0;
5. В даному випадку, завантаживши модуль в ядро Linux і виконавши команду mkdir, у висновку dmesg можна буде побачити нову сходинку з фразою «mkdir hijacked!», Що свідчить про те, що LSM-модуль успішно перехоплює системний виклик inode_mkdir.
Однією особливістю використання механізму LSM є неможливість одночасного використання декількох модулів ядра для реєстрації хуков - при спробі реєстрації LSM-модуля ядра буде виведено відповідне попередження, таким чином при використанні стандартного вкомпільовані в більшість версій ядра SELinux використовувати власний модуль безпеки LSM не вийде (відключення SELinux з допомогою параметра ядра у завантажувачі типу selinux = 0 також може не принести ніякого результату). Тому для вільного використання власного LSM-модуля ядра доводиться перекомпілювати ядро Linux, виключаючи з нього будь-які додаткові засоби захисту (такі, як SELinux, AppArmor, Tomoyo та інші).
Так чи інакше, в цілях безпеки починаючи з версії ядра Linux 2.6.24 і вище механізм LSM (а саме функція, що дозволяє використовувати даний механізм) перестав експортуватися ядром ОС Linux (дана обставина пов'язано з тим, що більшість шкідливого ПО приховувало свою присутність в системі саме за допомогою механізму LSM).
Перехоплення системних викликів в завантажуваних модулях ядра Linux (LKM)
Повертаючись до звичайного перехоплення системних викликів (і відкидаючи проблему можливого псування таблиці системних викликів, т. Е. Фактично не маючи інших варіантів для коректного перехоплення системних викликів) належить додатково вирішити відразу 2 завдання:
Завдання пошуку таблиці системних викликів можна вирішити відразу декількома способами:
// значення для 32-розрядних ядер ОС Linux
#define START_MEM 0xc0000000
#define END_MEM 0xd0000000
unsigned long * syscall_table;
unsigned long ** find_syscall_table () <
unsigned long ** sctable;
unsigned long int i = START_MEM;
while (i if (sctable [_NR_close] == (unsigned long *) sys close) return sctable [0]; i + = sizeof (void *); // знайти таблицю системних викликів можна наступним чином syscalltable = (unsigned long *) find_syscall_table (); / * Відключити захищений режим, встановивши біт WP в 0 * / write_cr0 (read_cr0 () ( / * Виконати зміни таблиці системних викликів * / / * Включіт' запгдщенньгй режим, встановивши біт WP в 1 * / write_cr0 (read_cr0 () | 0? 10000); В даному випадку блокування пов'язана з архітектурою використовуваного процесора, як приклад розглядається блокування Intel, при якій 0-й біт CR (керуючого регістра, Control Register) необхідно перемикати в 0 для відключення «protected mode», а потім в 1 для включення «protected mode »вже після зміни таблиці системних викликів. Також існують інші блокування, що залежать від архітектури, на якій використовується ОС. Механізм заміни системних викликів власними функціями можна організувати наступним чином: / * Нова функція, що замінює стандартний системний виклик * / printk (<\nmkdir hijacked!\n>); / * Функція зміни таблиці системних викликів * / #if LINUX VERSION CODE> = KERNEL_VERSION (2, 5,0) #if LINUX VERSION CODE> = KERNEL_VERSION (2, 5, 0) / * Функція повернення таблиці системних викликів в початковий стан * / #if LINUX VERSION CODE> = KERNEL_VERSION (2, 5, 0) #if LINUX VERSION CODE> = KERNEL_VERSION (2, 5, 0) / * Функція ініціалізації модуля ядра * / / * Застосовуємо патч до таблиці системних викликів * / patch_sys_call_table (); printk ( "sys_call_table patched. \ n"); / * Функція закінчення роботи модуля ядра * / / * Повертаємо стан таблиці системних викликів * / Створення власної підсистеми розмежування доступу в ОС сімейства Linux в технічному плані не є чимось складним або недоступним. Отримати контроль над системними функціями ядра (системними викликами), як було показано в даній статті, досить просто - тому основним завданням є розробка тих перевірочних правил, які повинні викликатися при виконанні того чи іншого системного виклику ядра ОС. [1] Chris Wright, Crispin Cowan, Stephen Smalley, James Morris, Greg Kroah-Hartman. Linux Security Modules: General
asmlinkage int newrnkdir (struct inode * dir, struct dentry * dentry, int mode)
return orig mkdir (dir, dentry, mode);
static void patch_sys_call_table ()
writecr0 (readcr0 () (
write cr0 (readcr0 () | 0? 10000);
#endif>
static void revert_sys_call_table ()
write cr0 (read cr0 () (
write cr0 (read cr0 () | 0? 10000);
#endif>
static int init (void)
return 0;
static void exit (void)
revert_sys_call_table (); printk ( "sys_call_table reverted. \ n<); return;
Схожі статті