STACKLEAK — это функция безопасности ядра Linux, изначально разработанная создателями Grsecurity/PaX. Я решил довести STACKLEAK до официального ванильного ядра (Linux kernel mainline). В этой статье будет рассказано о внутреннем устройстве, свойствах данной функции безопасности и ее очень долгом непростом пути в mainline.
![](https://habrastorage.org/r/w1560/webt/px/95/j_/px95j_edhs60awthgfeytj9uz84.png)
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.
![](https://habrastorage.org/r/w1560/webt/kt/d2/6v/ktd26vqpdxlk4vk8lobb7ixrybc.png)
Схема 1.
Однако утечки такого типа становятся бесполезны, если в конце системного вызова использованная часть стека ядра заполняется фиксированным значением (схема 2).
![](https://habrastorage.org/r/w1560/webt/fy/rr/w4/fyrrw4y-hyyms3pqjlumc_4k01w.png)
Схема 2.
Как следствие, STACKLEAK блокирует некоторые атаки на неинициализированные переменные в стеке ядра. Примеры таких уязвимостей: CVE-2017-17712, CVE-2010-2963. Описание методики эксплуатации уязвимости CVE-2010-2963 можем найти в статье Кейса Кука (Kees Cook).
Суть атаки на неинициализированную переменную в стеке ядра представлена на схеме 3.
![](https://habrastorage.org/r/w1560/webt/8j/0p/po/8j0pposbamjhyq2jlsmuxpoqhfq.png)
Схема 3.
STACKLEAK блокирует атаки такого типа, так как значение, которым заполняется ядерный стек в конце системного вызова, указывает на неиспользованную область в виртуальном адресном пространстве (схема 4).
![](https://habrastorage.org/r/w1560/webt/-t/zp/ok/-tzpokwwsamgqqe0ij7tafszzps.png)
Схема 4.
При этом важным ограничением является то, что STACKLEAK не защищает от аналогичных атак, выполняемых за один системный вызов.
В ванильном ядре (Linux kernel mainline) STACKLEAK эффективен против переполнения стека «в глубину» (kernel stack depth overflow) только в сочетании с CONFIG_THREAD_INFO_IN_TASK и CONFIG_VMAP_STACK. Обе эти меры внедрены Энди Лутомирски (Andy Lutomirski).
Простейший вариант эксплуатации данного типа уязвимостей отражен на схеме 5.
![](https://habrastorage.org/r/w1560/webt/1a/vd/ft/1avdft3jdwd9utgtpletdnv_nuu.png)
Схема 5.
Перезапись определенных полей в структуре thread_info на дне ядерного стека позволяет повысить привилегии процесса. Однако при включении опции CONFIG_THREAD_INFO_IN_TASK данная структура выносится из ядерного стека, что устраняет описанный способ эксплуатации уязвимости.
Более продвинутый вариант данной атаки состоит в том, чтобы с помощью выхода за границу стека переписать данные в соседнем регионе памяти. Подробнее о данном подходе:
Атака такого типа отражена на схеме 6.
![](https://habrastorage.org/r/w1560/webt/un/zy/3s/unzy3s_w29yrpp020kyj8bokvtw.png)
Схема 6.
Защитой в данном случае служит CONFIG_VMAP_STACK. При включении данной опции рядом с ядерным стеком помещается специальная страница памяти (guard page), доступ к которой приводит к исключению (схема 7).
![](https://habrastorage.org/r/w1560/webt/9w/4b/ht/9w4bht4brf9z7zu6t8udygbr48q.png)
Схема 7.
Наконец, самым интересным вариантом переполнения стека в глубину является атака типа Stack Clash. Идею еще в 2005 году выдвинул Гаэль Дэлалю (Gael Delalleau).
В 2017 году ее переосмыслили исследователи из компании Qualys, назвав данную технику Stack Clash. Дело в том, что существует способ перепрыгнуть guard page и перезаписать данные из соседнего региона памяти (схема 8). Это делается с помощью массива переменной длинны (VLA, variable length array), размер которого контролирует атакующий.
![](https://habrastorage.org/r/w1560/webt/vs/si/z2/vssiz2shuuuswmz96rmjongjjbs.png)
Схема 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.
![](https://habrastorage.org/r/w1560/webt/o0/qu/ar/o0quaropj1l3lcpm_wkucohbrwq.png)
Схема 9.
![](https://habrastorage.org/r/w1560/webt/vb/qg/2v/vbqg2vqqzsqrstwaqew21svaids.png)
Схема 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).
![](https://habrastorage.org/r/w1560/webt/px/95/j_/px95j_edhs60awthgfeytj9uz84.png)
Схема 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
![](https://habrastorage.org/webt/px/95/j_/px95j_edhs60awthgfeytj9uz84.png)
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.
![](https://habrastorage.org/webt/kt/d2/6v/ktd26vqpdxlk4vk8lobb7ixrybc.png)
Схема 1.
Однако утечки такого типа становятся бесполезны, если в конце системного вызова использованная часть стека ядра заполняется фиксированным значением (схема 2).
![](https://habrastorage.org/webt/fy/rr/w4/fyrrw4y-hyyms3pqjlumc_4k01w.png)
Схема 2.
Как следствие, STACKLEAK блокирует некоторые атаки на неинициализированные переменные в стеке ядра. Примеры таких уязвимостей: CVE-2017-17712, CVE-2010-2963. Описание методики эксплуатации уязвимости CVE-2010-2963 можем найти в статье Кейса Кука (Kees Cook).
Суть атаки на неинициализированную переменную в стеке ядра представлена на схеме 3.
![](https://habrastorage.org/webt/8j/0p/po/8j0pposbamjhyq2jlsmuxpoqhfq.png)
Схема 3.
STACKLEAK блокирует атаки такого типа, так как значение, которым заполняется ядерный стек в конце системного вызова, указывает на неиспользованную область в виртуальном адресном пространстве (схема 4).
![](https://habrastorage.org/webt/-t/zp/ok/-tzpokwwsamgqqe0ij7tafszzps.png)
Схема 4.
При этом важным ограничением является то, что STACKLEAK не защищает от аналогичных атак, выполняемых за один системный вызов.
Обнаружение переполнения стека ядра «в глубину»
В ванильном ядре (Linux kernel mainline) STACKLEAK эффективен против переполнения стека «в глубину» (kernel stack depth overflow) только в сочетании с CONFIG_THREAD_INFO_IN_TASK и CONFIG_VMAP_STACK. Обе эти меры внедрены Энди Лутомирски (Andy Lutomirski).
Простейший вариант эксплуатации данного типа уязвимостей отражен на схеме 5.
![](https://habrastorage.org/webt/1a/vd/ft/1avdft3jdwd9utgtpletdnv_nuu.png)
Схема 5.
Перезапись определенных полей в структуре thread_info на дне ядерного стека позволяет повысить привилегии процесса. Однако при включении опции CONFIG_THREAD_INFO_IN_TASK данная структура выносится из ядерного стека, что устраняет описанный способ эксплуатации уязвимости.
Более продвинутый вариант данной атаки состоит в том, чтобы с помощью выхода за границу стека переписать данные в соседнем регионе памяти. Подробнее о данном подходе:
- в презентации "The Stack is Back" Джона Оберхайда (Jon Oberheide),
- в статье "Exploiting Recursion in the Linux Kernel" Яна Хорна (Jann Horn).
Атака такого типа отражена на схеме 6.
![](https://habrastorage.org/webt/un/zy/3s/unzy3s_w29yrpp020kyj8bokvtw.png)
Схема 6.
Защитой в данном случае служит CONFIG_VMAP_STACK. При включении данной опции рядом с ядерным стеком помещается специальная страница памяти (guard page), доступ к которой приводит к исключению (схема 7).
![](https://habrastorage.org/webt/9w/4b/ht/9w4bht4brf9z7zu6t8udygbr48q.png)
Схема 7.
Наконец, самым интересным вариантом переполнения стека в глубину является атака типа Stack Clash. Идею еще в 2005 году выдвинул Гаэль Дэлалю (Gael Delalleau).
В 2017 году ее переосмыслили исследователи из компании Qualys, назвав данную технику Stack Clash. Дело в том, что существует способ перепрыгнуть guard page и перезаписать данные из соседнего региона памяти (схема 8). Это делается с помощью массива переменной длинны (VLA, variable length array), размер которого контролирует атакующий.
![](https://habrastorage.org/webt/vs/si/z2/vssiz2shuuuswmz96rmjongjjbs.png)
Схема 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.
![](https://habrastorage.org/webt/o0/qu/ar/o0quaropj1l3lcpm_wkucohbrwq.png)
Схема 9.
![](https://habrastorage.org/webt/vb/qg/2v/vbqg2vqqzsqrstwaqew21svaids.png)
Схема 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).
![](https://habrastorage.org/webt/px/95/j_/px95j_edhs60awthgfeytj9uz84.png)
Схема 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