[ru] Как STACKLEAK улучшает безопасность ядра Linux
STACKLEAK - это функция безопасности ядра Linux, изначально разработанная создателями Grsecurity/PaX. Я довел STACKLEAK до официального ванильного ядра (Linux kernel mainline). В этой статье будет рассказано о внутреннем устройстве, свойствах данной функции безопасности и ее очень долгом непростом пути в mainline.
STACKLEAK защищает от нескольких классов уязвимостей в ядре Linux, а именно:
Схема 0. Свойства безопасности STACKLEAK
Данная функция безопасности отлично укладывается в концепцию проекта Kernel Self Protection Project (KSPP): безопасность - это больше, чем только исправление ошибок. Абсолютно все ошибки в коде исправить невозможно, и поэтому ядро Linux должно безопасно отрабатывать в ошибочных ситуациях, в том числе при попытках эксплуатации уязвимостей. Больше подробностей о KSPP доступно на wiki проекта.
STACKLEAK присутствует как PAX_MEMORY_STACKLEAK в grsecurity/PaX патче. Однако grsecurity/PaX патч перестал распространяться свободно с апреля 2017 года. Поэтому появление STACKLEAK в ванильном ядре ценно для пользователей Linux с повышенными требованиями к информационной безопасности.
Порядок работы:
Пример утечки информации из стека ядра представлен на схеме 1.
Схема 1.
Однако утечки такого типа становятся бесполезны, если в конце системного вызова использованная часть стека ядра заполняется фиксированным значением (схема 2).
Схема 2.
Как следствие, STACKLEAK блокирует некоторые атаки на неинициализированные переменные в стеке ядра. Примеры таких уязвимостей: CVE-2017-17712, CVE-2010-2963. Описание методики эксплуатации уязвимости CVE-2010-2963 можем найти в статье Кейса Кука (Kees Cook).
Суть атаки на неинициализированную переменную в стеке ядра представлена на схеме 3.
Схема 3.
STACKLEAK блокирует атаки такого типа, так как значение, которым заполняется ядерный стек в конце системного вызова, указывает на неиспользованную область в виртуальном адресном пространстве (схема 4).
Схема 4.
При этом важным ограничением является то, что STACKLEAK не защищает от аналогичных атак, выполняемых за один системный вызов.
Простейший вариант эксплуатации данного типа уязвимостей отражен на схеме 5.
Схема 5.
Перезапись определенных полей в структуре thread_info на дне ядерного стека позволяет повысить привилегии процесса. Однако при включении опции CONFIG_THREAD_INFO_IN_TASK данная структура выносится из ядерного стека, что устраняет описанный способ эксплуатации уязвимости.
Более продвинутый вариант данной атаки состоит в том, чтобы с помощью выхода за границу стека переписать данные в соседнем регионе памяти. Подробнее о данном подходе:
Схема 6.
Защитой в данном случае служит CONFIG_VMAP_STACK. При включении данной опции рядом с ядерным стеком помещается специальная страница памяти (guard page), доступ к которой приводит к исключению (схема 7).
Схема 7.
Наконец, самым интересным вариантом переполнения стека в глубину является атака типа Stack Clash. Идею еще в 2005 году выдвинул Гаэль Дэлалю (Gael Delalleau).
В 2017 году ее переосмыслили исследователи из компании Qualys, назвав данную технику Stack Clash. Дело в том, что существует способ перепрыгнуть guard page и перезаписать данные из соседнего региона памяти (схема 8). Это делается с помощью массива переменной длинны (VLA, variable length array), размер которого контролирует атакующий.
Схема 8.
Больше информации о STACKLEAK и Stack Clash содержится в блоге grsecurity.
Как STACKLEAK защищает от Stack Clash в ядерном стеке? Перед каждым вызовом alloca() выполняется проверка на переполнение стека в глубину. Вот соответствующий код из 14 версии серии патчей:
void __used stackleak_check_alloca(unsigned long size)
{
unsigned long sp = (unsigned long)&sp;
struct stack_info stack_info = {0};
unsigned long visit_mask = 0;
unsigned long stack_left;
BUG_ON(get_stack_info(&sp, current, &stack_info, &visit_mask));
stack_left = sp - (unsigned long)stack_info.begin;
if (size >= stack_left) {
/*
* Kernel stack depth overflow is detected, let's report that.
* If CONFIG_VMAP_STACK is enabled, we can safely use BUG().
* If CONFIG_VMAP_STACK is disabled, BUG() handling can corrupt
* the neighbour memory. CONFIG_SCHED_STACK_END_CHECK calls
* panic() in a similar situation, so let's do the same if that
* option is on. Otherwise just use BUG() and hope for the best.
*/
#if !defined(CONFIG_VMAP_STACK) && defined(CONFIG_SCHED_STACK_END_CHECK)
panic("alloca() over the kernel stack boundary\n");
#else
BUG();
#endif
}
}
Однако данный функционал был исключен из 15 версии. Это было сделано в первую очередь из-за спорного запрета Линуса Торвальдса использовать BUG_ON() в патчах по безопасности ядра Linux.
Кроме того, 9-я версия серии патчей привела к дискуссии, в результате которой было решено устранить все массивы переменной длинны из mainline-ядра.
Тест №1, привлекательный: сборка ядра Linux на одном процессорном ядре
# time make
Result on 4.18:
real 12m14.124s
user 11m17.565s
sys 1m6.943s
Result on 4 .18+stackleak:
real 12m20.335s (+0.85%)
user 11m23.283s
sys 1m8.221s
Тест №2, непривлекательный:
# hackbench -s 4096 -l 2000 -g 15 -f 25 –P
Average on 4.18: 9.08 s
Average on 4.18+stackleak: 9.47 s (+4.3%)
Таким образом влияние STACKLEAK на производительность системы зависит от типа нагрузки. В частности, большое количество коротких системных вызовов повышает накладные расходы. Т.о. необходимо оценивать производительность STACKLEAK для планируемой нагрузки перед промышленной эксплуатацией.
Стадии работы stackleak_erase() отражены на схемах 9 и 10.
Схема 9.
Схема 10.
Т.о. stackleak_erase() очищает только использованную часть ядерного стека. Именно поэтому STACKLEAK такой быстрый. А если на x86_64 очищать все 16 кБ стека ядра в конце каждого системного вызова, hackbench показывает падение производительности 40%.
Инструментация кода ядра на этапе компиляции выполняется в STACKLEAK GCC плагине.
GCC плагины - это загружаемые модули для компилятора GCC, специфичные для проекта. Они регистрируют новые проходы с помощью GCC Pass Manager, предоставляя обратные вызовы (callbacks) для данных проходов.
Итак, для полноценной работы STACKLEAK в код функций с большим стековым кадром (stack frame) вставляются вызовы stackleak_track_stack(). Также перед каждой alloca() вставляется вызов уже упомянутой stackleak_check_alloca(), а после — вызов stackleak_track_stack().
Как уже было сказано, в 15 версии серии патчей из GCC-плагина была исключена вставка вызовов stackleak_check_alloca().
Схема 11. Ход работ по внедрению STACKLEAK в Linux kernel mainline.
В апреле 2017 года создатели grsecurity закрыли свои патчи для сообщества, начав распространять их только на коммерческой основе. В мае 2017 года я взялся за задачу внедрения STACKLEAK в ванильное ядро. Так начался путь длиной более года. Компания Positive Technologies, в которой я работаю, давала мне возможность заниматься этой задачей некоторую часть моего рабочего времени. Но в основном я тратил на нее свободное время.
С прошлого мая моя серия патчей прошла многократное ревью, претерпела значительные изменения, дважды была раскритикована Линусом Торвальдсом. Мне хотелось оставить всю эту затею уже много раз. Но в определенный момент появилось твердое желание все же дойти до конца. 1 ноября 2018 года 15-я версия серии патчей была принята в ядро Linux 4.20.
Месяц назад я сделал доклад о данной работе на Linux Security Summit. Привожу ссылки на слайды и видео.
- сокращает полезную для атакующего информацию, которую могут выдать утечки из ядерного стека в пользовательское пространство;
- блокирует некоторые атаки на неинициализированные переменные в стеке ядра;
- предоставляет средства динамического обнаружения переполнения ядерного стека.
Схема 0. Свойства безопасности STACKLEAK
Данная функция безопасности отлично укладывается в концепцию проекта Kernel Self Protection Project (KSPP): безопасность - это больше, чем только исправление ошибок. Абсолютно все ошибки в коде исправить невозможно, и поэтому ядро Linux должно безопасно отрабатывать в ошибочных ситуациях, в том числе при попытках эксплуатации уязвимостей. Больше подробностей о KSPP доступно на wiki проекта.
STACKLEAK присутствует как PAX_MEMORY_STACKLEAK в grsecurity/PaX патче. Однако grsecurity/PaX патч перестал распространяться свободно с апреля 2017 года. Поэтому появление STACKLEAK в ванильном ядре ценно для пользователей Linux с повышенными требованиями к информационной безопасности.
Порядок работы:
- выделить STACKLEAK из grsecurity/PaX патча,
- тщательно изучить код и сформировать патч,
- отправить в LKML, получить обратную связь, улучшить, повторять заново до принятия в mainline.
STACKLEAK: свойства безопасности
Очистка остаточной информации в стеке ядра
Данная мера сокращает полезную информацию, которую могут выдать некоторые утечки из ядерного стека в пользовательское пространство.Пример утечки информации из стека ядра представлен на схеме 1.
Схема 1.
Однако утечки такого типа становятся бесполезны, если в конце системного вызова использованная часть стека ядра заполняется фиксированным значением (схема 2).
Схема 2.
Как следствие, STACKLEAK блокирует некоторые атаки на неинициализированные переменные в стеке ядра. Примеры таких уязвимостей: CVE-2017-17712, CVE-2010-2963. Описание методики эксплуатации уязвимости CVE-2010-2963 можем найти в статье Кейса Кука (Kees Cook).
Суть атаки на неинициализированную переменную в стеке ядра представлена на схеме 3.
Схема 3.
STACKLEAK блокирует атаки такого типа, так как значение, которым заполняется ядерный стек в конце системного вызова, указывает на неиспользованную область в виртуальном адресном пространстве (схема 4).
Схема 4.
При этом важным ограничением является то, что STACKLEAK не защищает от аналогичных атак, выполняемых за один системный вызов.
Обнаружение переполнения стека ядра «в глубину»
В ванильном ядре (Linux kernel mainline) STACKLEAK эффективен против переполнения стека «в глубину» (kernel stack depth overflow) только в сочетании с CONFIG_THREAD_INFO_IN_TASK и CONFIG_VMAP_STACK. Обе эти меры внедрены Энди Лутомирски (Andy Lutomirski).Простейший вариант эксплуатации данного типа уязвимостей отражен на схеме 5.
Схема 5.
Перезапись определенных полей в структуре thread_info на дне ядерного стека позволяет повысить привилегии процесса. Однако при включении опции CONFIG_THREAD_INFO_IN_TASK данная структура выносится из ядерного стека, что устраняет описанный способ эксплуатации уязвимости.
Более продвинутый вариант данной атаки состоит в том, чтобы с помощью выхода за границу стека переписать данные в соседнем регионе памяти. Подробнее о данном подходе:
- в презентации "The Stack is Back" Джона Оберхайда (Jon Oberheide),
- в статье "Exploiting Recursion in the Linux Kernel" Яна Хорна (Jann Horn).
Схема 6.
Защитой в данном случае служит CONFIG_VMAP_STACK. При включении данной опции рядом с ядерным стеком помещается специальная страница памяти (guard page), доступ к которой приводит к исключению (схема 7).
Схема 7.
Наконец, самым интересным вариантом переполнения стека в глубину является атака типа Stack Clash. Идею еще в 2005 году выдвинул Гаэль Дэлалю (Gael Delalleau).
В 2017 году ее переосмыслили исследователи из компании Qualys, назвав данную технику Stack Clash. Дело в том, что существует способ перепрыгнуть guard page и перезаписать данные из соседнего региона памяти (схема 8). Это делается с помощью массива переменной длинны (VLA, variable length array), размер которого контролирует атакующий.
Схема 8.
Больше информации о STACKLEAK и Stack Clash содержится в блоге grsecurity.
Как STACKLEAK защищает от Stack Clash в ядерном стеке? Перед каждым вызовом alloca() выполняется проверка на переполнение стека в глубину. Вот соответствующий код из 14 версии серии патчей:
void __used stackleak_check_alloca(unsigned long size)
{
unsigned long sp = (unsigned long)&sp;
struct stack_info stack_info = {0};
unsigned long visit_mask = 0;
unsigned long stack_left;
BUG_ON(get_stack_info(&sp, current, &stack_info, &visit_mask));
stack_left = sp - (unsigned long)stack_info.begin;
if (size >= stack_left) {
/*
* Kernel stack depth overflow is detected, let's report that.
* If CONFIG_VMAP_STACK is enabled, we can safely use BUG().
* If CONFIG_VMAP_STACK is disabled, BUG() handling can corrupt
* the neighbour memory. CONFIG_SCHED_STACK_END_CHECK calls
* panic() in a similar situation, so let's do the same if that
* option is on. Otherwise just use BUG() and hope for the best.
*/
#if !defined(CONFIG_VMAP_STACK) && defined(CONFIG_SCHED_STACK_END_CHECK)
panic("alloca() over the kernel stack boundary\n");
#else
BUG();
#endif
}
}
Однако данный функционал был исключен из 15 версии. Это было сделано в первую очередь из-за спорного запрета Линуса Торвальдса использовать BUG_ON() в патчах по безопасности ядра Linux.
Кроме того, 9-я версия серии патчей привела к дискуссии, в результате которой было решено устранить все массивы переменной длинны из mainline-ядра.
Влияние STACKLEAK на производительность
Привожу результаты тестирования производительности на x86_64. Оборудование: Intel Core i7-4770, 16 GB RAM.Тест №1, привлекательный: сборка ядра Linux на одном процессорном ядре
# time make
Result on 4.18:
real 12m14.124s
user 11m17.565s
sys 1m6.943s
Result on 4 .18+stackleak:
real 12m20.335s (+0.85%)
user 11m23.283s
sys 1m8.221s
Тест №2, непривлекательный:
# hackbench -s 4096 -l 2000 -g 15 -f 25 –P
Average on 4.18: 9.08 s
Average on 4.18+stackleak: 9.47 s (+4.3%)
Таким образом влияние STACKLEAK на производительность системы зависит от типа нагрузки. В частности, большое количество коротких системных вызовов повышает накладные расходы. Т.о. необходимо оценивать производительность STACKLEAK для планируемой нагрузки перед промышленной эксплуатацией.
Внутреннее устройство STACKLEAK
STACKLEAK состоит из:- Кода, очищающего стек ядра в конце системного вызова (изначально был написан на ассемблере),
- GCC плагина для инструментации кода ядра на этапе компиляции.
Стадии работы stackleak_erase() отражены на схемах 9 и 10.
Схема 9.
Схема 10.
Т.о. stackleak_erase() очищает только использованную часть ядерного стека. Именно поэтому STACKLEAK такой быстрый. А если на x86_64 очищать все 16 кБ стека ядра в конце каждого системного вызова, hackbench показывает падение производительности 40%.
Инструментация кода ядра на этапе компиляции выполняется в STACKLEAK GCC плагине.
GCC плагины - это загружаемые модули для компилятора GCC, специфичные для проекта. Они регистрируют новые проходы с помощью GCC Pass Manager, предоставляя обратные вызовы (callbacks) для данных проходов.
Итак, для полноценной работы STACKLEAK в код функций с большим стековым кадром (stack frame) вставляются вызовы stackleak_track_stack(). Также перед каждой alloca() вставляется вызов уже упомянутой stackleak_check_alloca(), а после — вызов stackleak_track_stack().
Как уже было сказано, в 15 версии серии патчей из GCC-плагина была исключена вставка вызовов stackleak_check_alloca().
Путь в Linux kernel mainline
Путь STACKLEAK в mainline очень долгий и непростой (схема 11).Схема 11. Ход работ по внедрению STACKLEAK в Linux kernel mainline.
В апреле 2017 года создатели grsecurity закрыли свои патчи для сообщества, начав распространять их только на коммерческой основе. В мае 2017 года я взялся за задачу внедрения STACKLEAK в ванильное ядро. Так начался путь длиной более года. Компания Positive Technologies, в которой я работаю, давала мне возможность заниматься этой задачей некоторую часть моего рабочего времени. Но в основном я тратил на нее свободное время.
С прошлого мая моя серия патчей прошла многократное ревью, претерпела значительные изменения, дважды была раскритикована Линусом Торвальдсом. Мне хотелось оставить всю эту затею уже много раз. Но в определенный момент появилось твердое желание все же дойти до конца. 1 ноября 2018 года 15-я версия серии патчей была принята в ядро Linux 4.20.
Месяц назад я сделал доклад о данной работе на Linux Security Summit. Привожу ссылки на слайды и видео.