В последние несколько месяцев я работал над добавлением рандомизации памяти в ядро ​​Linux для x86_64. Кодирование функций низкоуровневой и ранней загрузки может привести к странным ошибкам. Обычно у вас нет стека вызовов или информации, он просто перезагружается. Исправление этих ошибок часто больше похоже на разгадку тайны, чем на что-либо другое. Этот пост посвящен интересной проблеме, с которой я столкнулся, пытаясь завершить исходное предложение.

Если вы заинтересованы в улучшении безопасности ядра Linux, первым делом стоит присоединиться к Проекту самозащиты ядра.

KASLR и рандомизация памяти

Рандомизация адресного пространства ядра (или KASLR) была частью ядра Linux с 2013 ~ 2014, благодаря работе Kees Cook. Исходная реализация рандомизирует базовый адрес модулей ядра. Мои изменения рандомизируют три основных раздела памяти (физическое отображение, vmalloc и vmemmap), чтобы предотвратить угадывание местоположения критических структур без прямой утечки.

Этот тип атак был описан Николасом А. Эконому и Энрике Э. Ниссимом в их выступлении Физическое состояние. Я провел аналогичное исследование внутри компании Google с другим подходом, но с аналогичным выводом. График был идеальным, потому что я мог сослаться на статью Николаса и Энрике, спасибо, ребята!

Рандомизация на уровне PUD

Рандомизация раздела памяти выполняется путем генерации виртуальных адресов на ранней стадии загрузки. Нерандомизированные виртуальные адреса были выровнены на уровне таблицы 2-й страницы (PUD). Новые адреса рандомизированы и выровнены на уровне таблицы 3-й страницы (PMD). При включенной рандомизации смещение PUD может отличаться от нуля в этой схеме:

Базовый адрес раздела физического сопоставления был 0xffff880000000000 с зарезервированными 64 ТБ. При рандомизации памяти виртуальный адрес начинается с той же базы, но рандомизируется с использованием этой маски: 0x0000fffffc0000000 (30-битный сдвиг) на основе доступной памяти и размещения других разделов памяти.

Подробнее об управлении страницей в Linux вы можете узнать здесь. Также эта фиксация изменяет способ отображения ядром раздела физического сопоставления для поддержки виртуальных адресов уровня PUD. Этот коммит настраивает все необходимое для рандомизации памяти.

Он загружается только с одним процессором

После завершения первого прототипа я протестировал разные конфигурации. Со вторым процессором перезагружался напрямую без какой-либо информации. Обычно это означает, что что-то пошло не так.

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

Сбой происходит вскоре после включения в этой таблице страниц запуска процессора:

# Setup trampoline 4 level pagetables
movl    $pa_trampoline_pgd, %eax
movl    %eax, %cr3

Следующий комментарий находится поверх файла сборки батута:

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

При входе в trampoline_start процессор находится в реальном режиме
с 16-битной адресацией и 16-битными данными. CS имеет какое-то значение
, а IP равен нулю. Таким образом, адреса данных должны быть абсолютными
(без перемещения) и учитываются с учетом r_base.

С добавлением trampoline_level4_pgt этот код может
теперь вводить 64-битное ядро, которое живет по произвольным 64-битным физическим адресам.

Я нашел этот код, инициализирующий trampoline_pgd в setup_real_mode:

trampoline_pgd = (u64 *) __va(real_mode_header->trampoline_pgd); trampoline_pgd[0] = init_level4_pgt[pgd_index(__PAGE_OFFSET)].pgd; trampoline_pgd[511] = init_level4_pgt[511].pgd;

Запись PGD отображения физической памяти (__PAGE_OFFSET) копируется из текущей таблицы страниц в таблицу страниц-переходников. Однако смещение PGD другое, батут помещает ядро ​​в нулевое смещение PGD.

Ядро использует страницу-батут после выхода из реального режима и перед полным переходом в 64-битный режим. Таблица страниц должна отражать то, что происходит в реальном режиме с первой физической страницей по наименьшему виртуальному адресу (объясняя trampoline_pgd [0]).

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

Самый простой способ решить эту проблему - создать правильную таблицу страниц с батутом (выровненную на уровне PUD). Следующий код формирует правильный макет таблицы страницы батутов:

void __meminit init_trampoline(void)
{
	unsigned long paddr, paddr_next;
	pgd_t *pgd;
	pud_t *pud_page, *pud_page_tramp;
	int i;

	if (!kaslr_memory_enabled()) {
		init_trampoline_default();
		return;
	}

	pud_page_tramp = alloc_low_page();

	paddr = 0;
	pgd = pgd_offset_k((unsigned long)__va(paddr));
	pud_page = (pud_t *) pgd_page_vaddr(*pgd);

	for (i = pud_index(paddr);
             i < PTRS_PER_PUD;
             i++, paddr = paddr_next) {
		pud_t *pud, *pud_tramp;
		unsigned long vaddr = (unsigned long)__va(paddr);

		pud_tramp = pud_page_tramp + pud_index(paddr);
		pud = pud_page + pud_index(vaddr);
		paddr_next = (paddr & PUD_MASK) + PUD_SIZE;

		*pud_tramp = *pud;
	}

	set_pgd(&trampoline_pgd_entry,
		__pgd(_KERNPG_TABLE | __pa(pud_page_tramp)));
}

Вы можете видеть, что для батута выделена другая страница PUD, и каждое смещение копируется с другим выравниванием. Требуется только первая страница, больше ни к чему не обращаются в этом переходе.

Другой способ исправить эту проблему мог заключаться в том, чтобы сохранить смещение PUD в заголовке реального режима и сдвинуть каждую доступную глобальную переменную. Хотя я не уверен, что это сработало бы с переходом в 64-битный режим.

KASLR находится в стадии разработки

В качестве примечания я хотел упомянуть, что KASLR все еще находится в стадии разработки. Моя цель - внести свой вклад и сделать его лучше. Я знаю, что у него множество слабых мест и утечек. Я по-прежнему считаю, что это полезно и идет в правильном направлении, особенно для удаленных (включая облако / KVM) и изолированных сред.