CVE-2021-26708. В этой статье я расскажу, как я доработал свой прототип эксплоита и с его помощью исследовал средство защиты Linux Kernel Runtime Guard (LKRG) с позиции атакующего. Мы поговорим о том, как мне удалось найти новый метод обхода защиты LKRG и как я выполнил ответственное разглашение результатов своего исследования.
Летом я выступил с докладом по этой теме на конференции ZeroNights 2021.
www
Слайды доклада в PDF
Зачем я продолжил исследование
В предыдущей статье я описал прототип эксплоита для локального повышения привилегий на Fedora 33 Server для платформы x86_64 . Я рассказал, как состояние гонки в реализации виртуальных сокетов ядра Linux может привести к повреждению четырех байтов ядерной памяти. Я показал, как атакующий может шаг за шагом превратить эту ошибку в произвольное чтение‑запись памяти ядра и повысить свои привилегии в системе. Но некоторые ограничения этого способа повысить привилегии мешали мне экспериментировать в системе под защитой LKRG. Я решил продолжить исследование и выяснить, можно ли их устранить.
Мой прототип эксплоита выполнял произвольную запись с помощью перехвата потока управления при вызове деструктора destructor_arg в атакованном ядерном объекте sk_buff .
Этот деструктор имеет следующий прототип:
void (*callback)(struct ubuf_info *, bool zerocopy_success);
Когда ядро вызывает его в функции skb_zcopy_clear(), регистр RDI содержит первый аргумент функции. Это адрес самой структуры ubuf_info . А регистр RSI хранит единицу в качестве второго аргумента функции.
Содержимое этой структуры ubuf_info контролируется эксплоитом. Однако первые восемь байтов в ней должны быть заняты адресом функции‑деструктора, как видно на схеме. В этом и есть основное ограничение. Из‑за него ROP-гаджет для переключения ядерного стека на контролируемую область памяти (stack pivoting) должен выглядеть примерно так:
mov rsp, qword ptr [rdi + 8] ; ret
К сожалению, ничего похожего в ядре Fedora vmlinuz-5.10.11-200.fc33.x86_64 обнаружить не удалось. Но зато с помощью ROPgadget я нашел такой гаджет, который удовлетворяет этим ограничениям и выполняет запись ядерной памяти вообще без переключения ядерного стека:
mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rdx + rcx*8], rsi ; ret
Как сказано выше, RDI + 8 — это адрес ядерной памяти, содержимое которой контролирует атакующий. В регистре RSI содержится единица, а в RCX — ноль. То есть этот гаджет записывает семь нулевых байтов и один байт с единицей по адресу, который задает атакующий. Как выполнить повышение привилегий процесса с помощью этого ROP-гаджета? Мой прототип эксплоита записывает ноль в поля uid , gid , effective uid и effective gid структуры cred .
Мне удалось придумать хоть и странный, но вполне рабочий эксплоит‑примитив. При этом я не был полностью удовлетворен этим решением, потому что оно не давало возможности полноценного ROP. Кроме того, приходилось выполнять перехват потока управления дважды, чтобы перезаписать все необходимые поля в struct cred . Это делало прототип эксплоита менее надежным. Поэтому я решил немного отдохнуть и продолжить исследование. Регистры под контролем атакующего
Первым делом я решил еще раз посмотреть на состояние регистров процессора в момент перехвата потока управления. Я поставил точку останова в функции skb_zcopy_clear(), которая вызывает обработчик callback из destructor_arg :
$ gdb vmlinux
gdb-peda$ target remote :1234
gdb-peda$ break ./include/linux/skbuff.h:1481
Вот что отладчик показывает прямо перед перехватом потока управления.
Какие ядерные адреса хранятся в регистрах процессора? RDI и R8 содержат адрес ubuf_info . Разыменование этого указателя дает указатель на функцию callback , который загружен в регистр RAX . В регистре R9 содержится некоторый указатель на память в ядерном стеке (его значение близко к значению RSP ). В регистрах R12 и R14 находятся какие‑то адреса памяти в ядерной куче, и мне не удалось выяснить, на какие объекты они ссылаются.
А вот регистр RBP , как оказалось, содержит адрес skb_shared_info . Это адрес моего объекта sk_buff плюс отступ SKB_SHINFO_OFFSET , который равен 3776 или 0xec0 (больше деталей в предыдущей статье). Этот адрес дал мне надежду на успех, потому что он указывает на память, содержимое которой находится под контролем эксплоита. Я начал искать ROP/JOP-гаджеты, использующие RBP . Исчезающие JOP-гаджеты
Я стал просматривать все доступные гаджеты с участием RBP и нашел множество JOP-гаджетов, похожих на этот:
0xffffffff81711d33 : xchg eax, esp ; jmp qword ptr [rbp + 0x48]
Адрес RBP + 0x48 также указывает на ядерную память под контролем атакующего. Я понял, что могу выполнить stack pivoting с помощью цепочки таких JOP-гаджетов, после чего выполнить полноценную ROP-цепочку. Отлично!
Для быстрого эксперимента я взял этот гаджет:
xchg eax, esp ; jmp qword ptr [rbp + 0x48]
Он переключает ядерный стек на память в пользовательском пространстве. Сначала я удостоверился, что гаджет действительно находится в коде ядра:
$ gdb vmlinux
gdb-peda$ disassemble 0xffffffff81711d33 Dump of assembler code for function acpi_idle_lpi_enter: 0xffffffff81711d30 <+0>: call 0xffffffff810611c0 <fentry 0xffffffff81711d35 <+5>: mov rcx,QWORD PTR gs:[rip+0x7e915f4b] 0xffffffff81711d3d <+13>: test rcx,rcx 0xffffffff81711d40 <+16>: je 0xffffffff81711d5e Андрея Коновалова, известного исследователя безопасности Linux, не сталкивался ли он с таким эффектом. Андрей обратил внимание, что байты кода, которые распечатало ядро, отличались от вывода утилиты objdump для исполняемого файла ядра.
Это был первый случай в моей практике с ядром Linux, когда дамп кода в ядерном журнале оказался полезен. Я подключился отладчиком к работающему ядру и обнаружил, что код функции acpi_idle_lpi_enter() действительно изменился:
$ gdb vmlinux gdb-peda$ target remote :1234
gdb-peda$ disassemble 0xffffffff81711d33 Dump of assembler code for function acpi_idle_lpi_enter: 0xffffffff81711d30 <+0>: nop DWORD PTR [rax+rax*1+0x0] 0xffffffff81711d35 <+5>: mov rcx,QWORD PTR gs:[rip+0x7e915f4b] 0xffffffff81711d3d <+13>: test rcx,rcx 0xffffffff81711d40 <+16>: je 0xffffffff81711d5e CONFIG_DYNAMIC_FTRACE. Он также испортил множество других JOP-гаджетов, на которые я рассчитывал! Чтобы не столкнуться с этим снова, я решил попробовать искать нужные ROP/JOP-гаджеты в памяти ядра живой виртуальной машины. Евгений Корнеев. Портрет академика Л. К. Богуша. 1980Сначала я опробовал команду ropsearch из инструмента gdb-peda, но у нее оказалась слишком ограниченная функциональность. Тогда я зашел с другой стороны и сделал снимок всей области памяти с ядерным кодом с помощью команды gdb-peda dumpmem . В первую очередь нужно было определить расположение ядерного кода в памяти:
[root@localhost ~]# grep "_text" /proc/kallsyms ffffffff81000000 T _text
[root@localhost ~]# grep "_etext" /proc/kallsyms ffffffff81e026d7 T _etext
Затем я сделал снимок памяти между адресами _text и _etext :
gdb-peda$ dumpmem kerndump 0xffffffff81000000 0xffffffff81e03000 Dumped 14692352 bytes to 'kerndump'
После этого я применил к полученному файлу утилиту ROPgadget. Она может искать ROP/JOP-гаджеты в сыром снимке памяти, если задать дополнительные опции (спасибо за подсказку моему другу Максиму Горячему, известному исследователю безопасности железа):
# ./ROPgadget.py --binary kerndump --rawArch=x86 --rawMode=64 > rop_gadgets_5.10.11_kerndump
Теперь я был готов составить JOP/ROP-цепочку.
Перейти обратно к новости
|