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

Содержание статьи

  • Зачем я продолжил исследование
  • Регистры под контролем атакующего
  • JOP/ROP-цепочка для stack pivoting
  • ROP-цепочка для повышения привилегий
  • Проверить LKRG на прочность
  • Вперед, в атаку на LKRG!
  • Успешная атака на LKRG
  • Ответственное разглашение информации
  • Заключение

В янва­ре 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

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *