Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
В январе 2021 года я нашел и устранил пять уязвимостей в ядре Linux, которые получили общий идентификатор 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
gdb-peda$ x/2i 0xffffffff81711d33
0xffffffff81711d33 : xchg esp,eax
0xffffffff81711d34 : jmp QWORD PTR [rbp+0x48]
Так и есть. Код функции acpi_idle_lpi_enter()
начинается с адреса 0xffffffff81711d30
, и гаджет отображается, если смотреть на код этой функции с трехбайтовым отступом.
Однако, когда я попробовал выполнить этот гаджет при перехвате потока управления, ядро неожиданно выдало отказ страницы (page fault). Я стал отлаживать эту ошибку и заодно спросил моего друга Андрея Коновалова, известного исследователя безопасности 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
gdb-peda$ x/2i 0xffffffff81711d33
0xffffffff81711d33 : add BYTE PTR [rax],al
0xffffffff81711d35 : mov rcx,QWORD PTR gs:[rip+0x7e915f4b]
На самом деле ядро Linux может модифицировать собственный код в момент исполнения. В этом конкретном случае код функции acpi_idle_lpi_enter()
был изменен механизмом 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-цепочку.
Источник: xakep.ru