Категория > Новости > Проверка на прочность. Как я исследовал защиту LKRG с помощью уязвимости в ядре Linux - «Новости»

Проверка на прочность. Как я исследовал защиту LKRG с помощью уязвимости в ядре Linux - «Новости»


28-01-2022, 00:00. Автор: Мефодий
CVE-2021-26708. В этой статье я рас­ска­жу, как я дорабо­тал свой про­тотип экс­пло­ита и с его помощью иссле­довал средс­тво защиты Linux Kernel Runtime Guard (LKRG) с позиции ата­кующе­го. Мы погово­рим о том, как мне уда­лось най­ти новый метод обхо­да защиты LKRG и как я выпол­нил ответс­твен­ное раз­гла­шение резуль­татов сво­его иссле­дова­ния.

Ле­том я выс­тупил с док­ладом по этой теме на кон­ферен­ции ZeroNights 2021.


Проверка на прочность. Как я исследовал защиту LKRG с помощью уязвимости в ядре Linux - «Новости»

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-цепоч­ку.



Перейти обратно к новости