Pull to refresh

Особенности встраивания в ключевые механизмы ядра Linux с использованием LSM

Reading time7 min
Views6.9K
Под встраиванием в программную систему понимается процесс внедрения в неё дополнительных (сторонних) программных элементов осуществляемый таким образом, чтобы с одной стороны сохранялось её функционирование, а с другой — расширялись или изменялись её функциональные возможности.

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

О реализации LSM в ядре Linux


Особенности реализации LSM, о которых будет сказано далее, позволяют использовать данную подсистему для встраивания в ключевые механизмы ядра Linux. При этом, необходимо иметь в виду, что основное назначение LSM состоит в том, чтобы предоставить пользователю унифицированный интерфейс расширения действующей модели безопасности Linux и, следовательно, имеет связанные с этим ограничения.

Архитектура подсистемы LSM отличается своей простотой. Технически она представляет собой набор установленных в ядре ОС хуков, которые предоставляют интерфейс для внедрения собственных обработчиков, вызываемых системой в ходе выполнения различных операций. Базовой структурой-описателем модели безопасности является структура security_operations, описание которой можно найти в файле include/linux/security.h:

struct security_operations {
	char name[SECURITY_NAME_MAX + 1];
	...
	int inode_permission(struct inode * inode, int mask);
	...
}


Как видно, модель безопасности описывается следующими параметрами: именем и набором указателей на функции-обработчики, суть которых и составляет главную особенность реализуемой модели безопасности — будь то мандатная, ролевая или иная модель. Главным здесь является наличие возможности перехвата управления в ходе выполнения тех участков кода ядра, которые отвечают за реализацию доступа субъектов к объектам.

Будучи расставленными в ключевых местах системы, LSM-хуки представляют собой специальным образом оформленные вызовы той или иной функции текущей модели безопасности, оставляя при этом возможность её смены. Технически это реализовано с помощью введения дополнительной сущности – указателя текущей активной модели безопасности security_ops:

static struct security_o perations * security_ops;
static struct security_o perations default_security_ops = {
	. name = "default",
};


Начальное значение этого указателя при старте системы соответствует стандартной модели безопасности, описываемой структурой default_security_ops и включающей в себя реализацию поведения системы «по умолчанию». При этом, наполнение функций-обработчиков минимально. Однако, если в процессе работы системы возникает необходимость активизации другой модели, то это может быть осуществлено путём простой замены содержимого данного указателя. Ниже приведён пример использования security_ops в соответствующем хуке:

int security_inode_permission(struct inode *inode, int mask)
{
        if (unlikely(IS_PRIVATE(inode)))
                return 0;
        return security_ops->inode_permission(inode, mask);
}


Как видно, вызов функции-обработчика inode_permission осуществляется косвенным образом с использованием значения указателя security_ops, что соответствует сказанному. Хорошим примером может стать иллюстрация реализации контроля доступа на примере inode_permission — ключевой функции виртуальной файловой системы (VFS) ядра, управляющей доступом текущего процесса (субъекта) к объекту файловой системы:

int inode_permission(struct inode *inode, int mask)
{
	int retval;

	retval = sb_permission(inode->i_sb, inode, mask);
	if (retval)
		return retval;
	return __inode_permission(inode, mask);
}

int __inode_permission(struct inode *inode, int mask)
{
	int retval;

	if (unlikely(mask & MAY_WRITE)) {
		/*
		 * Nobody gets write access to an immutable file.
		 */
		if (IS_IMMUTABLE(inode))
			return -EACCES;
	}

	retval = do_inode_permission(inode, mask);
	if (retval)
		return retval;

	retval = devcgroup_inode_permission(inode, mask);
	if (retval)
		return retval;

	return security_inode_permission(inode, mask);
}


LSM-хук security_inode_permission завершает функцию и является последним рубежом на этапе проверки возможности доступа процесса (субъекта) к файлу (объекту), представленному структурой inode файловой системы. Таким образом, он сработает только при успешном прохождении всех предшествующих ему проверок, что соответствует концепции LSM при которой модуль безопасности не заменяет собой базовую системную модель, а лишь дополняет её и расширяет.

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



Во-первых, обращает на себя внимание их число. Действительно, если на ядро версии v2.6.11 приходилось 134 хука, то на ядро версии 3.8 их приходится уже 189, что является показателем степени глубины интеграции LSM в ядро. Во-вторых, динамика изменения количества хуков свидетельствует о бурном развитии LSM до версии v2.6.34 и некоторую стабилизацию после, что является показателем зрелости подсистемы, её относительной полноты и архитектурной стабильности.

О встраивании с использованием LSM


Являясь средством расширения базовой модели безопасности ядра Linux, LSM не является его неотъемлемым компонентом. Наличие в ядре данного фреймворка определяется состоянием конфигурационной переменной CONFIG_SECURITY и задаётся на этапе компиляции. На практике это означает, что ядро целевой системы может быть собрано без LSM. Тем не менее, в остальных случаях использование данной возможности для встраивания представляется в достаточной степени практичным, однако имеет свои особенности, связанные прежде всего с архитектурой самой LSM. Действительно, возможности встраивания определяются полнотой покрытия LSM-хуками кода ядра. Иными словами, встраиваться можно только в те места, в которых это предусмотрено.

В соответствии с архитектурой, ключевым объектом ядра Linux, обеспечивающим функционирование LSM механизма, является указатель security_ops, представляющий собой ячейку памяти ядра, хранящую адрес структуры-описателя действующей (активной) LSM-модели. При этом, в силу того, что все операции, связанные с обращением к модели, осуществляются через данный указатель, можно утверждать, что для встраивания необходимо и достаточно иметь возможность управления этим значением.

Говоря о реализации LSM-модели модулем ядра, стоит отметить, что существуют особенности, связанные с поэтапным введением в ядро Linux ограничений, накладываемых на LSM. Так, из соображений безопасности из ядра последовательно были исключены интерфейсы, позволяющие осуществлять смену действующей модели безопасности в процессе работы системы, а именно:

  • начиная с версии v2.6.24, указатель security_ops не экспортируется и отмечен как static (см. коммит 189b3b1c89761054fee3438f063d7f257306e2d8);
  • начиная с версии v2.6.35, функция register_security не экспортируется и отмечена как __init, что означает выгрузку содержащей её код области памяти после выполнения (см. коммит c1e992b99603a84d7debb188542b64f2d9232c07);


Перечисленные особенности призваны ограничить область применения LSM для встраивания, однако, стоит отметить, что на деле они не являются критическими и существует способ их обхода, речь о котором пойдёт далее.

Практические аспекты встраивания


Выше было отмечено, что начиная с ядра v2.6.34 отсутствует штатная возможность смены действующей модели безопасности в процессе работы системы. Однако, данное обстоятельство может быть преодолено, если будет найден способ определения адреса указателя текущей модели — security_ops. Одним из простых и надёжных способов, позволяющих его определить, является дизассемблирование кода ядра, а именно одной из функций, где используется данный указатель. Действительно, стоит рассмотреть в качестве примера листинг экспортируемой функции-хука security_sb_copy_data, полученный для 64-битной x86-системы с использованием отладчика gdb:

int security_sb_copy_data(char *orig, char *copy)
{
        return security_ops->sb_copy_data(orig, copy);
}
EXPORT_SYMBOL(security_sb_copy_data);

# gdb vmlinux /proc/kcore
(gdb) x/7i security_sb_copy_data 
   0xffffffff811f61b0:  push   %rbp
   0xffffffff811f61b1:  mov    %rsp,%rbp
   0xffffffff811f61b4:  data32 data32 data32 xchg %ax,%ax
   0xffffffff811f61b9:  mov    0x881690(%rip),%rax        # 0xffffffff81a77850
   0xffffffff811f61c0:  callq  *0x98(%rax)
   0xffffffff811f61c6:  pop    %rbp
   0xffffffff811f61c7:  retq

(gdb) x/s* 0xffffffff81a77850
0xffffffff81850fa0:  "default"


Как видно, инструкция диспетчеризации callq *0x98(%rax) осуществляет вызов функции-обработчика sb_copy_data по смещению 0x98, относительно указателя, хранящегося в %rax. А именно %rax и содержит в себе security_ops. Таким образом, для того, чтобы получить данный указатель, необходимо последовательно, инструкция за инструкцией, сканировать команды от начала данной функции до нахождения команды mov 0x881690(%rip),%rax, загружающей в %rax искомое значение и, проведя простой анализ её операндов, получить требуемый указатель. Ниже представлен код, позволяющий это осуществить для x86 с использованием дизассемблера udis86:

static void * get_lsm_entry(void)
{
        /* this one is exported */
        return (void *)&security_sb_copy_data;
}

static struct security_operations ** get_lsm_sop(void)
{
        ud_t ud;
        void * entry = get_lsm_entry(), * result = NULL;

        ud_initialize(&ud, BITS_PER_LONG, UD_VENDOR_ANY, entry, 128);

        while (ud_disassemble(&ud) && ud.mnemonic != UD_Iret) {
                if (ud.mnemonic == UD_Imov && \
                    ud.operand[0].type == UD_OP_REG && ud.operand[1].type == UD_OP_MEM)
                {
#ifdef CONFIG_X86_64
                        result = entry + ud_insn_off(&ud) + ud_insn_len(&ud);
#endif
                        result = result + ud.operand[1].lval.sdword;

                        break;
                }
        }

        return result;
}


Таким образом, получив указатель на указатель security_ops, можно заменить активную модель безопасности на собственную реализацию простой заменой значения этого указателя. В качестве примера привожу код проекта fakelsm, заменяющего функцию inode_permission.

Заключение


В заключение следует сказать следующее. Несмотря на последовательно вводимые ограничения использования LSM вне ядра, этот фреймворк представляет интерес, т.к. даёт большие возможности для встраивания в ядро. Следует, однако, понимать, что его возможности ограничены степенью покрытия кода ядра LSM-хуками. И хотя на современных ядрах таковых насчитывается огромное количество, всё же эти ограничения очевидны.

Кроме этого, немаловажным является тот факт, что сама по себе концепция архитектуры LSM, будучи простой и эффективной, не является безопасной, т.к. по-сути вся расширенная модель безопасности ядра основывается на одном единственном указателе — security_ops!

На почитать


  1. Linux Security Modules
  2. udis86’s documentation
  3. Критика LSM от grsecurity и RSBAC
Tags:
Hubs:
Total votes 12: ↑10 and ↓2+8
Comments3

Articles