Эксплуатация ядра Linux на виртуалке c Hack The Box

Hack The Box

В этой статье будем раз­бирать одну из самых слож­ных тем в сфе­ре пентеста и взлома — экс­плу­ата­цию ядра Linux. Ты узна­ешь, какие инс­тру­мен­ты при­меня­ются для отладки ядра, что такое LKM, KGDB, IOCTL, TTY, и мно­го дру­гих инте­рес­ных вещей!

Еще по теме: Захват Active Directory на виртуальной машине с HackTheBox

В предыдущих стать­ях мы про­ложи­ли себе путь к поль­зовате­лю r4j на хар­дкор­ной вир­туал­ке RopeTwo. Что­бы доб­рать­ся до рута, оста­ется пос­ледний шаг, но какой! Нас ждет ROP (не зря же вир­туал­ку так наз­вали) и kernel exploitation. Моз­ги будут закипать, обе­щаю! Запасай­ся поп­корном дебаг­гером и поеха­ли!

Разведка

Как и в слу­чае с фла­гом поль­зовате­ля из пре­дыду­щей статьи, пер­вым делом запус­каем LinPEAS и вни­матель­но смот­рим, за что мож­но зацепить­ся. В гла­за бро­сают­ся две подоз­ритель­ные строч­ки:

Статический анализ

Пер­вое, что нам нуж­но, — это ска­чать себе ralloc.ko и нат­равить на него «Гид­ру».

Ви­дим, что ralloc — это LKM, который выпол­няет раз­личные опе­рации с памятью при получе­нии сис­темных вызовов ioctl. По сути, это самопис­ный драй­вер управле­ния памятью, (Superfast memory allocator, как опи­сыва­ет его сам автор), оче­вид­но, что не без уяз­вимос­тей.

LKM (loadable kernel module) — объ­ектный файл, содер­жащий код, который рас­ширя­ет воз­можнос­ти ядра опе­раци­онной сис­темы. В нем реали­зова­ны все­го четыре фун­кции:

  • вы­деле­ние памяти в адресном прос­транс­тве ядра (kmalloc) — вызов ioctl <wbr />0x1000;
  • очи­щение памяти в адресном прос­транс­тве ядра (kfree) — вызов ioctl <wbr />0x1001;
  • ко­пиро­вание информа­ции из адресно­го прос­транс­тва поль­зовате­ля в прос­транс­тво ядра ( memcpy(<wbr />kernel_addr, <wbr />user_addr, <wbr />size)) — вызов ioctl <wbr />0x1002;
  • ко­пиро­вание информа­ции из адресно­го прос­транс­тва ядра в прос­транс­тво поль­зовате­ля ( memcpy(<wbr />user_addr, <wbr />kernel_addr, <wbr />size)) — вызов ioctl <wbr />0x1003.

Ни­же дизас­сем­бли­рован­ный и при­веден­ный в чита­емый вид лис­тинг этих фун­кций:

Пос­мотри вни­матель­но на лис­тинг. Воз­можно, ты най­дешь уяз­вимость, она поч­ти сра­зу бро­сает­ся в гла­за! А пока зай­мем­ся под­готов­кой стен­да.

Разворачиваем стенд

Оче­вид­но, что для отладки ядра нам понадо­бит­ся вир­туаль­ная машина. Да не одна, а целых две! Одна сыг­рает роль хос­та, где уста­нов­лено ядро с отла­доч­ными сим­волами и где мы при­меним отладчик GDB, вто­рая будет запус­кать­ся в режиме KGDB (отладчик ядра Linux). Связь меж­ду вир­туаль­ными машина­ми уста­нав­лива­ется либо по пос­ледова­тель­ному пор­ту, либо по локаль­ной сети. Схе­матич­но это выг­лядит так.

Схема отладки ядра Linux
Схе­ма отладки ядра Linux

Су­щес­тву­ет нес­коль­ко сред вир­туали­зации, на которых мож­но раз­вернуть стенд: VirtualBox, QEMU (самый прос­той вари­ант) или VMware. Я выб­рал пер­вый вари­ант. Если захочешь поп­ракти­ковать­ся с QEMU, то на GitHub есть ру­ководс­тво.

Так­же я нашел неп­лохое видео, которое под­робно показы­вает нас­трой­ку VirtualBox для отладки ядра.

Эксплуатация ядра Linux на виртуалке c Hack The Box

Ос­тановим­ся на глав­ных момен­тах. Пер­вым делом пос­мотрим, какая вер­сия ядра исполь­зует­ся в RopeTwo:

r4j@rope2:<wbr />~$ <wbr />lsb_release <wbr />-r <wbr />&& <wbr />uname <wbr />-r Release: 19.04
5.0.0-38-generic

Ска­чива­ем и раз­ворачи­ваем ВМ с Ubuntu 19.04. Далее уста­нав­лива­ем ядро нуж­ной вер­сии (и свои любимые средс­тва отладки и ути­литы):

Те­перь мож­но сде­лать клон ВМ. На хост нам нуж­но заг­рузить яд­ро с сим­волами отладки. Нам нужен файл linux-image-unsigned-5.<wbr />0.<wbr />0-38-generic-dbgsym_5.<wbr />0.<wbr />0-38.<wbr />41_amd64.<wbr />ddeb (838,2 Мибайт).

На тар­гете нуж­но вклю­чить режим отладки ядра (KGDB). Для это­го сна­чала нас­тро­им заг­рузчик, изме­ним в фай­ле /<wbr />etc/<wbr />default/<wbr />grub сле­дующие строч­ки:

Тем самым мы даем KGDB коман­ду слу­шать под­клю­чения отладчи­ка на пор­те ttyS0, а так­же отклю­чаем KASLR (kernel address space layout randomization) и очис­тку кон­соли.

Вы­пол­няем коман­ду update-grub, что­бы записать парамет­ры в заг­рузчик. Пос­ле это­го мож­но удос­товерить­ся, что зна­чения попали в кон­фиг GRUB, — ищи их в фай­ле /<wbr />boot/<wbr />grub/<wbr />grub.<wbr />cfg.

Ес­ли бы мы хотели отла­живать само ядро, было бы необ­ходимо добавить параметр kgdbwait, что­бы заг­рузчик оста­новил­ся перед заг­рузкой ядра и ждал под­клю­чения GDB с хос­та. Но так как нас инте­ресу­ет не само ядро, а LKM, то это не тре­бует­ся.

Да­лее про­верим, что у нас в сис­теме вклю­чены пре­рыва­ния отладки:

и текущие фла­ги пре­рыва­ний:

Вклю­чим на тар­гете все фун­кции «магичес­ких» пре­рыва­ний сис­темы:

Под­робнее о них мож­но почитать в до­кумен­тации.

Те­перь, если ты вве­дешь echo <wbr />g > /<wbr />proc/<wbr />sysrq-trigger, сис­тема завис­нет в ожи­дании под­клю­чения отладчи­ка.

Ос­талось свя­зать хост и тар­гет меж­ду собой. Для это­го необ­ходимо вклю­чить в нас­трой­ках обе­их ВМ Serial Port. На тар­гете это выг­лядит так.

Настройки Serial port на таргете
Нас­трой­ки Serial port на тар­гете

А на хос­те — так.

Настройки Serial port на хост
Нас­трой­ки Serial port на хост

Об­рати вни­мание, что на хос­те уста­нов­лена галоч­ка Connect to existing pipe/socket! Поэто­му сна­чала мы заг­ружа­ем ВМ тар­гета и толь­ко потом ВМ хос­та.

Те­перь вся готово для отладки, про­веря­ем.

Проверка работы KGDB
Про­вер­ка работы KGDB

KGDB так­же мож­но акти­виро­вать «магичес­кой» ком­бинаци­ей кла­виш в VirtualBox: Alt-PrintScr-g.

За­киды­ваем в тар­гет модуль ralloc.ko и заг­ружа­ем его коман­дой insmod <wbr />ralloc.<wbr />ko. Основные коман­ды для работы с модуля­ми ядра:

  • depmod — вывод спис­ка зависи­мос­тей и свя­зан­ных map-фай­лов для модулей ядра;
  • insmod — заг­рузка модуля в ядро;
  • lsmod — вывод текуще­го ста­туса модулей ядра;
  • modinfo — вывод информа­ции о модуле ядра;
  • rmmod — уда­ление модуля из ядра;
  • uname — вывод информа­ции о сис­теме.

Пос­ле заг­рузки модуля можем пос­мотреть его кар­ту адре­сов коман­дой grep <wbr />ralloc /<wbr />proc/<wbr />kallsyms. Запом­ни ее — эта коман­да еще не раз нам при­годит­ся.

Карта адресов модуля ralloc
Кар­та адре­сов модуля ralloc

Для отладки нам понадо­бят­ся адре­са областей .text, .data и .bss:

Пос­мотрим, какие защит­ные механиз­мы вклю­чены в ядре.

Ви­дим, что SMEP вклю­чен, а SMAP — нет. В этом мож­но убе­дить­ся сле­дующим обра­зом:

Supervisor mode execution protection (SMEP) и supervisor mode access prevention (SMAP) — фун­кции безопас­ности, которые исполь­зуют­ся в пос­ледних поколе­ниях CPU. SMEP пре­дот­вра­щает исполне­ние кода из режима ядра в адресном прос­транс­тве поль­зовате­ля, SMAP —неп­редна­мерен­ный дос­туп из режима ядра в адресное прос­транс­тво поль­зовате­ля. Эти опции кон­тро­лиру­ются вклю­чени­ем опре­делен­ных битов в регис­тре CR4. Под­робнее об этом мож­но почитать в докумен­тации Intel (PDF).

Так­же вклю­чен KASLR — это умол­чатель­ный вари­ант в новых вер­сиях ядра Linux.

Создание эксплоита

Итак, какая же уяз­вимость при­сутс­тву­ет в ralloc? Раз­гадка кро­ется в стро­ке arr[<wbr />idx].<wbr />size <wbr />= <wbr />size_alloc <wbr />+ <wbr />0x20;, это зна­чит, что мы можем читать и писать на 32 бай­та боль­ше реаль­ного объ­ема выделен­ной памяти. Неп­лохо! Но как мы можем это исполь­зовать?

По­ка я изу­чал при­меры kernel exploitation (зна­читель­ная часть которых на китай­ском, но Google Translate нам в помощь), появил­ся план дей­ствий. Пер­вое, на что надо обра­тить вни­мание, — мак­сималь­ный раз­мер учас­тка памяти сос­тавля­ет 1024 бай­та. Есть еще одна извес­тная струк­тура, которая исполь­зует такой же раз­мер, — tty_struct.

tty_struct исполь­зует­ся ядром TTY для кон­тро­ля за текущим сос­тоянием на кон­крет­ном пор­те.

Ес­ли мы выделим два сосед­них учас­тка памяти, а потом осво­бодим вто­рой из них и вызовем псев­дотер­минал, с боль­шой веро­ятностью менед­жер памяти ядра заг­рузит tty_struct в толь­ко что осво­бож­денный учас­ток. В таком слу­чае у нас будет воз­можность читать пер­вые 32 бай­та этой струк­туры и писать в них, исполь­зуя уяз­вимость OOB (out-of-bounds). Бла­года­ря это­му мы можем про­читать и переза­писать ука­затель *ops, который рас­положен в начале tty_struct и содер­жит адрес ptm_unix98_ops, и вычис­лить по его сме­щению адрес kernel base. Зная его, мы можем сос­тавить ROP chain, помес­тить его по опре­делен­ному адре­су и перенап­равить на него ука­затель сте­ка. В этом нам поможет замена ука­зате­ля *ops под­дель­ной струк­турой tty_operations, в которой ука­затель ioctl заменен гад­жетом xchg <wbr />eax <wbr />esp. Дол­жно сра­ботать, поп­робу­ем это реали­зовать.

Еще по теме: Повышение привилегий в Linux

Пи­сать экс­пло­ит мы будем на ста­ром доб­ром (а для кого‑то не очень) С. Для начала, как всег­да, напишем вспо­мога­тель­ные фун­кции. Точ­нее, это будет все­го одна фун­кция, которая вызыва­ется мак­росами с нуж­ными парамет­рами:

Те­перь про­верим, смо­жем ли мы заг­рузить tty_struct в осво­бож­денный учас­ток памяти. Пом­нишь, что нас инте­ресу­ет ука­затель на  ptm_unix98_ops? Для начала най­дем его адрес:

А теперь исполь­зуем сле­дующий код (я при­вожу толь­ко клю­чевые стро­ки кода для эко­номии мес­та, пол­ный код для ком­пиляции ты можешь вос­ста­новить из исходни­ка экс­пло­ита в кон­це статьи):

/<wbr />dev/<wbr />ptmx исполь­зует­ся для соз­дания пары основно­го и под­чинен­ного псев­дотер­минала. Ког­да про­цесс откры­вает /<wbr />dev/<wbr />ptmx, то он получа­ет опи­сатель фай­ла для основно­го псев­дотер­минала (PTM, pseudo-terminal master), а в катало­ге /<wbr />dev/<wbr />pts соз­дает­ся устрой­ство под­чинен­ного псев­дотер­минала (PTS, pseudo-terminal slave).

Ском­пилиру­ем код коман­дой gcc <wbr />-static <wbr />tty_test.<wbr />c <wbr />-o <wbr />tty_test и пос­мотрим, что получа­ется.

Ключ -static зас­тавля­ет ком­пилятор вклю­чить в бинар­ный файл все необ­ходимые биб­лиоте­ки.

Пос­ле нес­коль­ких запус­ков мы видим нуж­ный ука­затель на  ptm_unix98_ops!

Но иног­да мы видим прос­то мусор или нули. Поп­робу­ем разоб­рать­ся в этом с помощью GDB.

Мо­дифи­циру­ем код:

На­ша цель — выделить два чан­ка, записать дан­ные во вто­рой, а про­читать началь­ные 32 бай­та, обра­щаясь к пер­вому. Теперь, если ском­пилиро­вать и запус­тить нашу тес­товую прог­рамму нес­коль­ко раз, уви­дим, что пос­ледова­тель­ность 0xdeadbeef мы получа­ем далеко не всег­да. Про­верим в GDB.

Для начала сде­лаем на тар­гете пре­рыва­ние, заг­рузим в GDB на хос­те адре­са ralloc и уста­новим брейк перед вызовом фун­кции __kmalloc (сме­щение от начала rope2_ioctl — 0x156):

Пос­ледова­тель­ность дей­ствий в GDB дол­жна быть такой:

Ко­ман­да step поз­воля­ет нам перей­ти к сле­дующей инс­трук­ции — вызову фун­кции kmalloc, а  finish — получить воз­вра­щаемое ей зна­чение.

Ви­дим, что адре­са двух чан­ков не сле­дуют друг за дру­гом, точ­нее такое быва­ет, но далеко не всег­да. Сле­дова­тель­но, преж­де чем осво­бож­дать память для  tty_struct, нам необ­ходимо вста­вить про­вер­ку смеж­ности чан­ков. Для это­го исполь­зует­ся цикл с алго­рит­мом, похожим на про­вер­ку выше (смот­ри код экс­пло­ита в кон­це статьи).

Пос­ле того как мы наш­ли смеж­ные чан­ки и записа­ли tty_struct в осво­бож­денный учас­ток памяти, мы можем про­читать нуж­ный адрес и вычис­лить по нему kernel base — базовый адрес ядра. Далее перехо­дим к самому инте­рес­ному — пос­тро­ению ROPchain.

На­чина­ем с модели­рова­ния. Нам нуж­на фун­кция, которая поз­волит повысить наши при­виле­гии до root. Один из самых дос­тупных вари­антов для это­го — фун­кция commit_creds(<wbr />prepare_kernel_cred(<wbr />NULL))<wbr />;. Если мы вызовем commit_creds от име­ни сис­темы и переда­дим ей в качес­тве парамет­ра prepare_kernel_cred(<wbr />NULL), мы заменим UID нашего про­цес­са на 0. Это про­исхо­дит потому, что, получив в качес­тве парамет­ра NULL, prepare_kernel_cred воз­вра­щает пол­номочия про­цес­са ини­циали­зации init_cred, а эти пол­номочия соот­ветс­тву­ют пол­номочи­ям root. Так­же мож­но сра­зу передать фун­кции commit_creds струк­туру init_cred, что мы и сде­лаем. Под­робнее о пол­номочи­ях в Linux читай в до­кумен­тации.

Пос­коль­ку у нас акти­вен SMEP, мы не можем выпол­нять код в прос­транс­тве User, а дол­жны исполь­зовать гад­жеты из ядра сис­темы. Вто­рой путь — отклю­чить SMEP, помес­тив в регистр cr4 зна­чение 0x6f0. Можешь поп­робовать реали­зовать это самос­тоятель­но в качес­тве упражне­ния, а мы пой­дем по пер­вому пути. Для это­го нам тре­бует­ся:

  • по­мес­тить init_cred в регистр RDI (пер­вый параметр фун­кции);
  • выз­вать commit_creds;
  • ак­курат­но вер­нуть­ся в кон­текст поль­зовате­ля, ничего не сло­мав, и запус­тить shell.

Для перек­лючения кон­тек­ста сущес­тву­ет инс­трук­ция iretq, но перед ее вызовом тре­бует­ся выпол­нить инс­трук­цию swapgs, что­бы вос­ста­новить зна­чение IA32_KERNEL_GS_BASE MSR (model-specific register). Так как при перехо­де в режим ядра (нап­ример, при вызове syscall) вызыва­ется swapgs для получе­ния ука­зате­ля на струк­туры дан­ных ядра, при воз­вра­щении в режим поль­зовате­ля нуж­но вер­нуть это зна­чение обратно в MSR. Озна­комить­ся с опи­сани­ем swapgs мож­но на сай­те Фелик­са Клутье. Для кор­рек­тно­го воз­вра­щения в user space инс­трук­ция iretq так­же тре­бует соб­людения опре­делен­ной струк­туры сте­ка.

Структура стека, необходимая iretq
Струк­тура сте­ка, необ­ходимая iretq

По­это­му нам нуж­но сох­ранить зна­чения этих регис­тров перед вызовом ROPchain и вос­ста­новить в момент воз­вра­щения в user space. Сох­ранить регис­тры мож­но с помощью сле­дующей фун­кции:

Ос­талось най­ти ROP-гад­жеты и сос­тавить ROPchain.

Есть нес­коль­ко ути­лит для поис­ка гад­жетов ROP, нап­ример ROPgadget, xrop, ropper. Все они при­меня­ют раз­ные алго­рит­мы поис­ка, и спис­ки най­ден­ных гад­жетов нем­ного отли­чают­ся. Поэто­му, если ты не смог най­ти нуж­ный гад­жет одной из ути­лит, можешь поп­робовать дру­гую. Итак, нам нуж­но:

  • рас­паковать ядро;
  • най­ти гра­ницы области .text (гад­жеты из дру­гих областей час­то могут не работать);
  • ус­тановить и запус­тить ROPgadget, сох­ранив все най­ден­ные гад­жеты в файл.

Те­перь коман­дой grep ищем адре­са нуж­ных нам гад­жетов в  rgadget.<wbr />lst, а адре­са init_cred и  commit_creds — в  /<wbr />proc/<wbr />kallsyms (это будет тво­им домаш­ним задани­ем). А вот iretq при­дет­ся искать в ядре с помощью objdump:

Пос­ле того как адре­са соб­раны, нам нуж­но рас­счи­тать их сме­щения от базово­го адре­са, пос­коль­ку на сер­вере акти­виро­ван KASLR и работать мы будем со сме­щени­ями. Для это­го я написал мак­росы:

В ито­ге у нас получил­ся вот такой ROPchain:

Ос­талось пос­леднее и самое глав­ное — зас­тавить RSP (Stack Pointer) сте­ка ядра перек­лючить­ся на наш ROPchain и получить шелл. Пом­нишь, мы записа­ли tty_struct в осво­бож­денный чанк и можем переза­писать пер­вые 32 бай­та этой струк­туры? Начало этой струк­туры выг­лядит так:

Мы можем под­делать струк­туру tty_operations и заменить этот ука­затель нашей под­дель­ной струк­турой. Что нам это даст? Вот как выг­лядит начало tty_operations (пол­ное ее опи­сание мож­но най­ти в ис­ходни­ках):

Ее три­над­цатый эле­мент — ука­затель на  ioctl. Если мы заменим этот ука­затель и вызовем ioctl, передав ему fd (file descriptor), который вер­нул ptmx, мы смо­жем выз­вать нуж­ную нам инс­трук­цию.

А так как мы хотим заменить RSP на нуж­ный нам адрес, заменим *ioctl адре­сом гад­жета xchg <wbr />eax, <wbr />esp; <wbr />ret;. Так как этот адрес будет содер­жать­ся в  rax при вызове ioctl, наш гад­жет xchg «перек­лючит» ука­затель сте­ка на адрес ADDR(<wbr />xchg_eax_esp) <wbr />& <wbr />0xFFFFFFFF (млад­шие 32 бита rax). Имен­но туда мы и запишем наш ROPchain!

Схе­матич­но весь про­цесс экс­плу­ата­ции мож­но пред­ста­вить так.

Алгоритм работы эксплоита
Ал­горитм работы экс­пло­ита

Для отладки веша­ем в отладчи­ке брейк на  xchg_eax_esp ( break <wbr />*0xffffffff8104cba4), дела­ем шаг впе­ред ( step) и убеж­даем­ся, что ука­затель сте­ка ядра теперь «смот­рит» на наш ROPchain.

ROPchain в отладчике
ROPchain в отладчи­ке

На­конец‑то наш экс­пло­ит готов, при­вожу его пол­ный лис­тинг:

Од­нако пос­ле воз­вра­щения в user space наш экс­пло­ит «пада­ет» с ошиб­кой Segmentation fault, и если для обыч­ного при­ложе­ния это беда, то в слу­чае с экс­пло­итом мы можем даже не раз­бирать­ся с проб­лемой, а исполь­зовать это как пре­иму­щес­тво! Ведь, пой­мав этот сиг­нал, мы можем повесить на него запуск шел­ла и сде­лать экс­пло­ит еще более уни­вер­саль­ным.

До­бав­ляем в исходник заголо­воч­ный файл signal.<wbr />h и перех­ватыва­ем сиг­нал SIGSEGV с помощью фун­кции signal(<wbr />SIGSEGV, <wbr />shell)<wbr />;:

Те­перь все готово, что­бы добыть завет­ный флаг рута, путь к которо­му мы прок­ладыва­ли с таким тру­дом!

Получаем root!
По­луча­ем root!

Бу­ду рад, если ты почер­пнул новые зна­ния и у тебя про­будил­ся инте­рес иссле­довать их еще глуб­же. Несом­ненно, они тебе еще не раз при­годят­ся!

Еще по теме: Лучшие сканеры уязвимостей

ВКонтакте
OK
Telegram
WhatsApp
Viber

Добавить комментарий

Ваш адрес email не будет опубликован.