STACKLEAK — это функция безопасности ядра Linux, изначально разработанная создателями Grsecurity/PaX. Я решил довести STACKLEAK до официального ванильного ядра (Linux kernel mainline). В этой статье будет рассказано о внутреннем устройстве, свойствах данной функции безопасности и ее очень долгом непростом пути в mainline.
STACKLEAK защищает от нескольких классов уязвимостей в ядре Linux, а именно:
Данная функция безопасности отлично укладывается в концепцию проекта Kernel Self Protection Project (KSPP): безопасность — это больше, чем только исправление ошибок. Абсолютно все ошибки в коде исправить невозможно, и поэтому ядро Linux должно безопасно отрабатывать в ошибочных ситуациях, в том числе при попытках эксплуатации уязвимостей. Больше подробностей о KSPP доступно на wiki проекта.
STACKLEAK присутствует как PAX_MEMORY_STACKLEAK в grsecurity/PaX патче. Однако grsecurity/PaX патч перестал распространяться свободно с апреля 2017 года. Поэтому появление STACKLEAK в ванильном ядре было бы ценным для пользователей Linux с повышенными требованиями к информационной безопасности.
Порядок работы:
На момент написания статьи (25.09.2018) была отправлена 15 версия серии патчей. Она содержит архитектурно независимую часть и код для x86_64 и x86_32. Поддержка STACKLEAK для arm64, разработанная Лорой Эббот (Laura Abbott) из Red Hat, уже успела попасть в ванильное ядро 4.19.
Данная мера сокращает полезную информацию, которую могут выдать некоторые утечки из ядерного стека в пользовательское пространство.
Пример утечки информации из стека ядра представлен на схеме 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 данная структура выносится из ядерного стека, что устраняет описанный способ эксплуатации уязвимости.
Более продвинутый вариант данной атаки состоит в том, чтобы с помощью выхода за границу стека переписать данные в соседнем регионе памяти. Подробнее о данном подходе:
Атака такого типа отражена на схеме 6.
Схема 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 версии серии патчей:
Однако данный функционал был исключен из 15 версии. Это было сделано в первую очередь из-за спорного запрета Линуса Торвальдса использовать BUG_ON() в патчах по безопасности ядра Linux.
Кроме того, 9-я версия серии патчей привела к дискуссии, в результате которой было решено устранить все массивы переменной длинны из mainline-ядра. В эту работу включилось около 15 разработчиков, и она скоро будет закончена.
Привожу результаты тестирования производительности на x86_64. Оборудование: Intel Core i7-4770, 16 GB RAM.
Тест №1, привлекательный: сборка ядра Linux на одном процессорном ядре
Тест №2, непривлекательный:
Таким образом влияние STACKLEAK на производительность системы зависит от типа нагрузки. В частности, большое количество коротких системных вызовов повышает накладные расходы. Т.о. необходимо оценивать производительность STACKLEAK для планируемой нагрузки перед промышленной эксплуатацией.
STACKLEAK состоит из:
Очистка стека ядра выполняется в функции stackleak_erase(). Данная функция отрабатывает перед возвращением в пользовательское пространство после системного вызова. В использованную часть стека thread’а записывается STACKLEAK_POISON (-0xBEEF). На начальную точку очистки указывает переменная lowest_stack, постоянно обновляемая в stackleak_track_stack().
Стадии работы 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().
Путь STACKLEAK в mainline очень долгий и непростой (схема 11).
Схема 11. Ход работ по внедрению STACKLEAK в Linux kernel mainline.
В апреле 2017 года создатели grsecurity закрыли свои патчи для сообщества, начав распространять их только на коммерческой основе. В мае 2017 года я принял решение взяться за задачу внедрения STACKLEAK в ванильное ядро. Так начался путь длиной более года. Компания Positive Technologies, в которой я работаю, дает мне возможность заниматься этой задачей некоторую часть моего рабочего времени. Но в основном я трачу на нее «свободное» время.
С прошлого мая моя серия патчей прошла многократное ревью, претерпела значительные изменения, дважды была раскритикована Линусом Торвальдсом. Мне хотелось оставить всю эту затею уже много раз. Но в определенный момент появилось твердое желание все же дойти до конца. На момент написания статьи (25.09.2018) 15 версия серии патчей находится в ветке linux-next, соответствует всем озвученным требованиям Линуса и готова к merge-window ядра 4.20 / 5.0.
Месяц назад я сделал доклад о данной работе на Linux Security Summit. Привожу ссылки на слайды и видео:
STACKLEAK — очень полезная функция безопасности ядра Linux, блокирующая эксплуатацию сразу несколько типов уязвимостей. Помимо этого изначальный автор PaX Team смог сделать ее быстрой и красивой в инженерном плане. Поэтому появление STACKLEAK в ванильном ядре было бы ценным для пользователей Linux с повышенными требованиями к информационной безопасности. Более того, работа в данном направлении привлекает внимание сообщества разработчиков Linux к средствам самозащиты ядра.
STACKLEAK в итоге принят в ядро Linux 4.20:
git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=2d6bb6adb714b133db92ccd4bfc9c20f75f71f3f
Поддерживаются архитектуры x86_64, x86_32 и arm64.
Кроме того, закончены работы по устранению массивов переменной длины из кода ядра Linux. В версии ядра 4.20 включено предупреждение "-Wvla" компилятора gcc: lkml.org/lkml/2018/10/28/189
STACKLEAK защищает от нескольких классов уязвимостей в ядре Linux, а именно:
- сокращает полезную для атакующего информацию, которую могут выдать утечки из ядерного стека в пользовательское пространство;
- блокирует некоторые атаки на неинициализированные переменные в стеке ядра;
- предоставляет средства динамического обнаружения переполнения ядерного стека.
Данная функция безопасности отлично укладывается в концепцию проекта Kernel Self Protection Project (KSPP): безопасность — это больше, чем только исправление ошибок. Абсолютно все ошибки в коде исправить невозможно, и поэтому ядро Linux должно безопасно отрабатывать в ошибочных ситуациях, в том числе при попытках эксплуатации уязвимостей. Больше подробностей о KSPP доступно на wiki проекта.
STACKLEAK присутствует как PAX_MEMORY_STACKLEAK в grsecurity/PaX патче. Однако grsecurity/PaX патч перестал распространяться свободно с апреля 2017 года. Поэтому появление STACKLEAK в ванильном ядре было бы ценным для пользователей Linux с повышенными требованиями к информационной безопасности.
Порядок работы:
- выделить STACKLEAK из grsecurity/PaX патча,
- тщательно изучить код и сформировать патч,
- отправить в LKML, получить обратную связь, улучшить, повторять заново до принятия в mainline.
На момент написания статьи (25.09.2018) была отправлена 15 версия серии патчей. Она содержит архитектурно независимую часть и код для x86_64 и x86_32. Поддержка STACKLEAK для arm64, разработанная Лорой Эббот (Laura Abbott) из Red Hat, уже успела попасть в ванильное ядро 4.19.
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.
Схема 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-ядра. В эту работу включилось около 15 разработчиков, и она скоро будет закончена.
Влияние STACKLEAK на производительность
Привожу результаты тестирования производительности на x86_64. Оборудование: Intel Core i7-4770, 16 GB RAM.
Тест №1, привлекательный: сборка ядра Linux на одном процессорном ядре
# time make
Результат на 4.18:
real 12m14.124s
user 11m17.565s
sys 1m6.943s
Результат на 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
Средний результат на 4.18: 9.08 сек
Средний результат на 4.18+stackleak: 9.47 сек (+4.3%)
Таким образом влияние STACKLEAK на производительность системы зависит от типа нагрузки. В частности, большое количество коротких системных вызовов повышает накладные расходы. Т.о. необходимо оценивать производительность STACKLEAK для планируемой нагрузки перед промышленной эксплуатацией.
Внутреннее устройство STACKLEAK
STACKLEAK состоит из:
- Кода, очищающего стек ядра в конце системного вызова (изначально был написан на ассемблере),
- GCC плагина для инструментации кода ядра на этапе компиляции.
Очистка стека ядра выполняется в функции stackleak_erase(). Данная функция отрабатывает перед возвращением в пользовательское пространство после системного вызова. В использованную часть стека thread’а записывается STACKLEAK_POISON (-0xBEEF). На начальную точку очистки указывает переменная lowest_stack, постоянно обновляемая в stackleak_track_stack().
Стадии работы 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, в которой я работаю, дает мне возможность заниматься этой задачей некоторую часть моего рабочего времени. Но в основном я трачу на нее «свободное» время.
С прошлого мая моя серия патчей прошла многократное ревью, претерпела значительные изменения, дважды была раскритикована Линусом Торвальдсом. Мне хотелось оставить всю эту затею уже много раз. Но в определенный момент появилось твердое желание все же дойти до конца. На момент написания статьи (25.09.2018) 15 версия серии патчей находится в ветке linux-next, соответствует всем озвученным требованиям Линуса и готова к merge-window ядра 4.20 / 5.0.
Месяц назад я сделал доклад о данной работе на Linux Security Summit. Привожу ссылки на слайды и видео:
Заключение
STACKLEAK — очень полезная функция безопасности ядра Linux, блокирующая эксплуатацию сразу несколько типов уязвимостей. Помимо этого изначальный автор PaX Team смог сделать ее быстрой и красивой в инженерном плане. Поэтому появление STACKLEAK в ванильном ядре было бы ценным для пользователей Linux с повышенными требованиями к информационной безопасности. Более того, работа в данном направлении привлекает внимание сообщества разработчиков Linux к средствам самозащиты ядра.
P.S.
STACKLEAK в итоге принят в ядро Linux 4.20:
git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=2d6bb6adb714b133db92ccd4bfc9c20f75f71f3f
Поддерживаются архитектуры x86_64, x86_32 и arm64.
Кроме того, закончены работы по устранению массивов переменной длины из кода ядра Linux. В версии ядра 4.20 включено предупреждение "-Wvla" компилятора gcc: lkml.org/lkml/2018/10/28/189