December 17, 2024

Linux kernel: Пособие по эксплуатации переполнения кучи (по идее часть 1)

Предисловие⚙️

Короче говоря, друзья, сегодня мы разберём крайне увлекательный вопрос: как грамотно взломать кучу в ядре Linux? И не просто взломать, а как настоящий джедай ядра (да, да, именно ядра, а не тех, кто хвалится sudo apt install htop).

Статья построена на базе одной задачки, так что считайте, мы будем учиться на практике. Методов много — их не уместишь в один пост, поэтому готовься, материал будет выходить в нескольких частях. Это не Netflix, но будет трилогия!

Перед тем как ворваться с боевым пингвином в мир уязвимостей, полезно освежить в памяти, что такое куча и как она вообще устроена. Если ты вдруг новичок — открой Википедию, посмотри пару графиков и найди где-нибудь "введение в ядро кучи". Это как документация по C++, но читать чуть легче.


Обзор📜

Значит так, система управления памятью в Linux выглядит как бабушка, сортирующая картошку: память делится на три уровня:

  1. Страницы (page) — это базовый уровень.
  2. Зоны (zone) — чуть выше, распределяет страницы по нуждам.
  3. Узлы (node) — верхний слой, объединяющий все зоны для любителей многопроцессорных серверов и бубнов.

В главных ролях у нас сегодня два знаменитых менеджера памяти:

  • 🪙 Buddy system — менеджер, отвечающий за раздачу памяти целыми страницами. Он, как строгий админ, просто кидает куски памяти налево и направо по принципу «на держи, только не разбазарь».
  • 🎛️ Slub allocator — более утончённый менеджер, который сидит на buddy system-е и говорит: «Не-не-не, одну страницу на мелкие объекты порежем! Давай как в IKEA: каждому объекту своё отделение».

Slub работает с объектами и позволяет управлять памятью на микроуровне, чтобы каждому процессику хватило места и никто не жаловался на переполнения.

Buddy System: Где мой "бадди" для страниц?🛠️


Buddy system — это такой менеджер памяти, который работает с целыми страницами (page), и если бы у него был девиз, он бы звучал как: "Делим и властвуем, но всегда парными кусками!".

Каждая зона (zone) в ядре Linux содержит массив структур free_area, которые управляют страницами по принципу "order".

🧙 Order — это степень двойки, по которой определяется размер блока страниц. Например:order 0 = 1 страница, order 1 = 2 страницы, order 2 = 4 страницы и так далее...

Как это всё работает?🔍

🧩 Аллокация (выделение памяти):

  1. Сначала buddy system округляет размер запроса до ближайшей степени двойки страниц. Если ты просишь 3 страницы — держи 4 (всё как у продакшн-разработчиков: «запросишь одно — получишь с запасом»).
  2. Затем идём в нужный "order" и пытаемся вытащить блок из соответствующего списка.
  3. Если в списке пусто (кто-то всё уже растащил), система делает следующее:Берёт блок из следующего order (более крупный).
  4. Разрезает его пополам ("cut here", как сказали бы в GNU).
  5. Один кусок отдаёт запрашивающему процессу, второй кладёт в текущий order, как добропорядочный менеджер склада.
  6. Если и в следующем order пусто, то процесс продолжается на более высоких уровнях. Buddy system лезет всё выше, пока не найдёт подходящий блок.
Короче говоря, это как раздача пиццы на вечеринке: если нет кусочка на одного, придётся резать большую пополам. 🍕

Освобождение памяти:🗑️

  1. Когда ты возвращаешь блок памяти (а ты же возвращаешь, правда? 😏), buddy system аккуратно кладёт его в список свободных блоков текущего order.
  2. Затем он начинает проверять:
  3. «А не завалялся ли тут свободный "бадди", чтобы можно было объединить блоки и вернуть их в более крупный order?»
  4. Если такой "бадди" найден (то есть страница рядом тоже свободна), блоки объединяются и переходят на следующий уровень (order выше).
  5. Этот процесс повторяется, пока не удастся объединить страницы по максимуму.

📦 Вывод для IT-шников:

Buddy system — это как Tetris для памяти:

  • Выделение памяти: ищем блок, режем при необходимости, возвращаем результат.
  • Освобождение памяти: кладём на место, проверяем соседей, сливаем куски.

И да, куча свободных блоков — это не повод радоваться. Если рядом не будет "бадди", память так и останется в фрагментированном состоянии. Как сказал бы старший сисадмин:

«Оптимизация — это искусство собирать всё в кучу, но не по частям».

🧵 SLUB Allocator: Разделяй и властвуй на микроуровне


SLUB allocator — это более продвинутая и стильная версия старого доброго slab allocator. Если buddy system был мясником, рубящим страницы на крупные куски, то SLUB — это ювелир памяти, который всё кромсает на объекты одинакового размера.

Как это работает?

  1. SLUB сначала просит у buddy system одну или несколько страниц памяти.
Затем аккуратно режет их на объекты и раздаёт эти кусочки для мелких операций.Важно: SLUB не разбазаривает память, как твой коллега на премию, а держит всё структурированно и по полочкам.

🚀 Аллокация (выделение объектов):

  1. Первым делом SLUB идёт в kmem_cache_cpu — локальный кэш текущего CPU:
  2. Если там есть свободный объект, его сразу возвращают. Всё быстро и просто, никакой суеты.
  3. Что делать, если кэш пуст?
  4. SLUB снимает текущий блок (slub) с kmem_cache_cpu и говорит: «Хорошо, что у нас есть план Б!»
  5. Пытается вытащить слегка заполненный блок из partial-цепочки (partial list) в kmem_cache_node.Partial list — это такой склад, где живут блоки с частично свободными объектами.
  6. Подключив новый SLUB-блок, SLUB вытаскивает из него свободный объект и возвращает его наверх.
  7. А если partial-цепочка тоже пустая?
  8. Тогда SLUB идёт к buddy system как к старшему брату: «Дай ещё страниц!».
  9. Получив страницу, SLUB режет её на объекты, загружает в kmem_cache_cpu, и всё снова работает, как часы.

🗑️ Освобождение памяти (возврат объектов):

  1. Если освобождаемый объект находится в kmem_cache_cpu:
  2. Объект возвращается в локальный список свободных объектов (freelist) методом «головой вперёд» — то есть ставится в начало списка.
  3. Если освобождаемый объект из partial-цепочки в kmem_cache_node:
  4. Объект также добавляется в начало freelist в соответствующем SLUB-блоке.
  5. Если освобождаемый объект находится в full slub (полностью заполненном блоке):
  6. Этот объект становится головой freelist.
  7. Блок переводится из статуса full в partial и возвращается в partial-цепочку kmem_cache_node.

📦 Подытожим, как это выглядит:

  • SLUB умный: сначала берёт из локального CPU-кэша, если там пусто — ищет среди частично заполненных блоков.
  • Если и там пусто — идёт к buddy system и просит новые страницы.
  • Возвращение объектов — тоже строго по правилам: всегда в начало списка, чтобы всё было быстро и эффективно.

🎩 Мудрость для IT-шника:

SLUB allocator — это как менеджер на складе Амазона: всё упорядочено, оптимизировано и готово к мгновенной доставке. Система думает быстрее, чем ты успеешь сказать «malloc»! 🚀

heap_bof: Разбор полётов ✈️

Итак, у нас тут модуль heap_bof, который, судя по названию, обещает нам кучу боли и переполнение. Автор сего чуда, exp_ttt, видимо, большой любитель острых ощущений! 🌶️

Нам дали исходники, и там прямым текстом написано: UAF и heap overflow. Это как если бы на двери было написано: "Входите, грабьте, у нас всё равно всё плохо". Ну и вишенка на торте 🍒 — ядро старое, 4.4.27. Прямо музейный экспонат!

Что мы имеем:

  • ptr[40]: Массив из 40 указателей, которые могут указывать куда угодно. Это наша песочница для игр с памятью.
  • struct param: Структура, через которую мы будем общаться с юзерспейсом. Поля len, buf и idx — это наши главные инструменты.
  • bof_ioctl: Функция, которая обрабатывает команды из юзерспейса. Тут-то и кроется всё веселье!
  • case 9: Копируем данные из ядра (из ptr[p_arg.idx]) в юзерспейс (p_arg.buf). Размер — p_arg.len. Если ptr[p_arg.idx] уже освободили, а мы пытаемся его прочитать, то это UAF. Ба-бах! 💥
  • case 8: Копируем данные из юзерспейса (p_arg.buf) в ядро (ptr[p_arg.idx]). Размер — p_arg.len. Если записать больше, чем выделили, то это heap overflow. Ещё один ба-бах! 💥💥
  • case 7: Освобождаем память по адресу ptr[p_arg.idx]. Но сам указатель не обнуляем! Привет, UAF!
  • case 5: Выделяем память размером p_arg.len и сохраняем указатель в ptr[p_arg.idx].

Короче говоря, этот модуль — просто кладезь уязвимостей! 🎉 Можно и память попереполнять, и почитать уже освобождённую память. Автор явно перестарался, пытаясь написать что-то "интересное".

Ага, вот и скрипт запуска подъехал! 🚚 boot.sh — это наш пропуск в мир боли и страданий, который приготовил нам автор этого безобразия.

boot.sh: Коротко о главном

Что тут интересного:

  • qemu-system-x86_64: Запускаем QEMU, эмулятор, который будет нашим полигоном.
  • -initrd rootfs.cpio: Это наша файловая система, которую QEMU подсунет ядру.
  • -kernel bzImage: А это само ядро Linux, которое мы будем мучать.
  • -m 512M: Выделяем 512 МБ оперативной памяти. Не густо, но нам хватит.
  • -nographic: Запускаем без графического интерфейса. Мы же суровые консольные ниндзя! 🥷
  • -append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet kaslr': Параметры ядра:
  • console=ttyS0: Вывод консоли на последовательный порт (чтобы мы видели, что там происходит).
  • root=/dev/ram: Корневая файловая система в RAM.
  • oops=panic: Если ядро словит Oops (ошибку), то сразу паникуем.
  • panic=1: Перезагрузка через 1 секунду после паники. Чтобы не расслаблялись!
  • quiet: Поменьше болтовни в консоли.
  • kaslr: Включаем рандомизацию адресного пространства ядра. Это чтобы жизнь мёдом не казалась! 🍯
  • -monitor /dev/null: Монитор QEMU нам не нужен, отправляем его в /dev/null.
  • -smp cores=2,threads=2: Включаем многоядерность и многопоточность: 2 ядра, 2 потока на каждое. То есть всего 4 потока. Это важно! Нам же сказали, что задача многопоточная.
  • -cpu kvm64,+smep,+smap: Включаем плюшки процессора:
  • kvm64: Используем аппаратную виртуализацию KVM.
  • +smep: Включаем SMEP (Supervisor Mode Execution Prevention). Это значит, что ядро не сможет выполнять код из юзерспейса. Облом для нас! 😥
  • +smap: Включаем SMAP (Supervisor Mode Access Prevention). Теперь ядро не сможет ещё и читать данные из юзерспейса. Совсем грустно! 😭

Вывод:

Нам подкинули задачку с многоядерным ядром, включенным KASLR, SMEP и SMAP. Автор явно хочет, чтобы мы страдали! 😭 Но мы не сдаёмся! 💪 Будем искать обходные пути и ломать эту систему, как орешки! 🌰

Так, у нас тут нарисовался план по захвату мира с помощью UAF! 🌍 Давайте разберёмся, что тут к чему.

Kernel Use After Free: План действий 📝

Нам предлагают использовать уязвимость Use After Free (UAF) в ядре, чтобы поднять свои привилегии до рута. Звучит заманчиво! 😏

Суть идеи:

  1. cred структура: В ядре Linux есть структура cred, которая хранит информацию о привилегиях процесса. Размер этой структуры — 0xa8 байт.
  2. SLUB allocator: Когда мы выделяем память в ядре с помощью kmalloc, за кулисами работает SLUB аллокатор. Он выделяет память блоками определённых размеров. Если мы выделим блок размером 0xa8 (реальный размер с учётом метаданных будет 0xc0), то он попадёт в кеш kmalloc-192.
  3. Магия UAF: Если мы выделим блок размером 0xa8, а потом освободим его (kfree), он вернётся в кеш kmalloc-192. Но! Если в этот момент создать новый поток, то его структура cred может быть выделена как раз из этого освобождённого блока! 🤯
  4. Профит! Если мы сможем записать что-то в этот освобождённый блок (а у нас ведь есть уязвимость UAF!), то мы перезапишем данные в структуре cred нового потока. И если мы перезапишем UID на 0 (UID рута), то получим рутовые привилегии! 🎉

EXP:

Что делает этот код:

  1. Выделяет блок памяти в ядре размером 0xa8 байт.
  2. Освобождает этот блок.
  3. Создаёт новый поток.
  4. В дочернем потоке записывает нули в освобождённый блок, надеясь перезаписать cred и получить UID 0.
  5. Проверяет, получилось ли стать рутом.

Проблема: 😥

Этот эксплоит работал раньше, но в новых версиях ядра (начиная с 4.5) его пофиксили. Теперь при создании кеша cred_jar (где хранятся структуры cred) устанавливается флаг SLAB_ACCOUNT. Из-за этого cred_jar больше не объединяется с kmalloc-192, и мы не можем выделить cred в том же месте, где был наш освобождённый блок.

Итог:

План был хорош, но устарел. 🙁 В новых ядрах этот трюк не пройдёт. Придётся искать другие способы обхода защиты. Но сама идея использования UAF для перезаписи cred — это классика! 👍

А вот и второй раунд! 🥊 Теперь попробуем пробить защиту с помощью heap overflow.

Heap Overflow: Старая школа 🏫

Идея та же, что и с UAF: перезаписать структуру cred, чтобы получить рута. Но вместо UAF используем переполнение буфера в куче.

Проблема:

Как и в случае с UAF, этот способ тоже устарел для новых ядер из-за флага SLAB_ACCOUNT у cred_jar. Но, допустим, у нас старое ядро, где этот трюк ещё работает.

Ещё одна проблема:

В многопоточной среде (а у нас именно такая, судя по boot.sh) блоки в куче могут выделяться и освобождаться в непредсказуемом порядке. Это значит, что после освобождения блока размером 0xa8 (наш кандидат на перезапись cred) следующий выделенный блок такого же размера может оказаться совсем не там, где мы ожидаем. 😩

Решение:

Чтобы увеличить шансы на успех, нужно "выровнять" кучу. Для этого выделим много блоков размером 0xa8 перед тем, как делать что-то важное. Это заполнит "дыры" в kmalloc-192 и сделает расположение блоков более предсказуемым.

План действий:

  1. Подготовка: Выделяем кучу блоков размером 0xa8, чтобы "очистить" kmalloc-192 от старых освобождённых блоков.
  2. Выделение: Выделяем ещё 10 блоков размером 0xa8.
  3. Освобождение: Освобождаем блок с индексом 5 (ptr[5]).
  4. Fork: Создаём новый поток. Надеемся, что его cred попадёт в освобождённый нами блок.
  5. Переполнение: Пишем в блок с индексом 4 (ptr[4]) данные размером больше, чем 0xa8. Если повезёт, мы затрём начало структуры cred следующего блока (который, как мы надеемся, принадлежит нашему новому потоку).
  6. Профит? Проверяем, стали ли мы рутом в дочернем потоке.

EXP:

Что делает этот код:

  1. Открывает устройство /dev/bof.
  2. Выделяет много (0x40) блоков по 0xa8 байт, чтобы "выровнять" кучу.
  3. Выделяет ещё 10 блоков по 0xa8 байт.
  4. Освобождает ptr[5].
  5. Создаёт новый поток.
  6. В родительском потоке переполняет ptr[4], записывая туда 0xc0 + 0x30 байт нулей.
  7. В дочернем потоке проверяет, стал ли он рутом.

Итог:

Этот способ тоже основан на устаревшем трюке, который не работает в новых ядрах из-за SLAB_ACCOUNT. Но даже в старых ядрах он ненадёжен из-за непредсказуемости поведения аллокатора в многопоточной среде. Шансы на успех есть, но они невелики.

Опа, а вот и новый поворот! 🔀 Нам предлагают отвлечься от cred и поиграться с tty_struct. А ещё автор смилостивился и отключил SMEP в boot.sh. Прямо праздник какой-то! 🎉

tty_struct: Что это за зверь?

tty_struct — это структура в ядре Linux, которая представляет терминальное устройство (TTY). Она содержит кучу всего интересного, в том числе указатели на функции, которые вызываются при различных операциях с TTY (например, при чтении и записи).

Идея:

Если мы сможем перезаписать указатели на функции в tty_struct, то сможем заставить ядро выполнить наш код! 😎

boot.sh (обновлённый):

Что изменилось:

  • -s: Добавили флаг -s. Это шорткат для -gdb tcp::1234. Теперь QEMU будет ждать подключения GDB на порту 1234. Удобно для отладки! 🐞
  • -cpu kvm64: Убрали +smep и +smap. SMEP отключили (ура!), SMAP остался (ну и ладно, он нам не сильно мешает в данном случае).
  • -smp cores=1,threads=1: Теперь у нас одно ядро и один поток. Это упрощает эксплуатацию, так как не нужно беспокоиться о гонках.

Почему отключили SMEP?

Вероятно, потому что в ядре мало гаджетов, которые можно было бы использовать для обхода SMEP. Без SMEP мы можем просто поместить шеллкод в юзерспейс и прыгнуть на него.

План действий (предварительный или нет):

  1. Найти способ выделить блок памяти, который потом будет использоваться как tty_struct.
  2. Найти способ перезаписать этот блок своими данными, включая поддельные указатели на функции.
  3. Вызвать одну из функций, на которую мы теперь указываем, чтобы выполнить свой код.
  4. Поднять привилегии до рута и запустить шелл.

Итог:

Появилась новая зацепка — tty_struct. Отключение SMEP упрощает задачу, но расслабляться рано. Впереди ещё много работы! 💪

tty_struct: План взлома 💣

Вспоминаем, что tty_struct — это структура, представляющая терминальное устройство. В ней есть поле ops, которое указывает на таблицу функций tty_operations, ответственных за операции с TTY. Наша задача — подменить эту таблицу на свою.

Как это сделать?

  1. Выделяем блок памяти: Когда мы открываем устройство /dev/ptmx (псевдотерминал), ядро выделяет память под tty_struct.
  2. Находим Размер tty_struct — 0x2e0 байт. В начале структуры есть магическое число 0x5401. По нему можно определить, что мы нашли именно tty_struct.
  3. Подменяем Поле ops находится по смещению 0x18 от начала tty_struct. Записываем туда адрес нашей поддельной таблицы tty_operations.
  4. Триггерим вызов: Вызываем ioctl для псевдотерминала. Это приведёт к вызову функции из нашей поддельной таблицы.
  5. Выполняем код: В поддельной таблице tty_operations указываем адрес функции get_root, которая поднимет нам привилегии.
  6. Profit! 🎉

EXP:

Что делает этот код:

  1. get_shell: Просто запускает /bin/sh.
  2. save_status: Сохраняет регистры пользовательского режима, чтобы потом корректно вернуться из ядра.
  3. get_root:
  • Определяет смещение KASLR, украв адрес из стека.
  • Вычисляет реальные адреса commit_creds и init_cred.
  • Вызывает commit_creds(init_cred), чтобы получить привилегии рута.
  • Использует iretq для возврата в юзерспейс с изменённым контекстом (регистрами), чтобы запустить get_shell.

main:

  • Выделяет блок размером 0x2e0 через наше устройство.
  • Заполняет его мусором (0xff) и освобождает.
  • Открывает /dev/ptmx, что приводит к выделению tty_struct в том же месте.
  • Читает данные из этого блока, чтобы найти tty_struct (по магическому числу 0x5401).
  • Подменяет указатель ops на адрес fake_tty_ops.
  • Вызывает ioctl для /dev/ptmx, что приводит к вызову get_root из нашей поддельной таблицы.
  • get_root повышает привилегии и возвращается в юзерспейс, где запускается шелл от рута.

Важные моменты:

  • KASLR обходится за счёт утечки адреса из стека.
  • SMEP отключен, поэтому мы можем прыгнуть на код в юзерспейсе (get_root).
  • SMAP не мешает, так как мы не читаем и не пишем данные в юзерспейсе из ядра.

Итог:

Мы успешно использовали tty_struct для повышения привилегий! 🎉 Этот способ обходит защиту SLAB_ACCOUNT, которая мешала нам использовать cred в новых ядрах.

seq_operations: Что это и с чем его едят? 🍽️

seq_operations — это структура, которая используется в ядре Linux для работы с файлами в директории /proc. Она содержит указатели на функции, которые отвечают за чтение данных из этих файлов.

В чём суть?

Если мы сможем подменить указатели в seq_operations, то при чтении определённого файла в /proc мы сможем выполнить свой код в контексте ядра! 🤯

Как это использовать?

  1. Найти Нужно найти способ выделить блок памяти, который затем будет использоваться как seq_operations для какого-нибудь файла в /proc.
  2. Подменить указатели: Записать в этот блок адреса своих функций.
  3. Триггернуть вызов: Прочитать файл, связанный с нашей поддельной seq_operations. Это приведёт к вызову одной из функций, на которые мы указали.
  4. Выполнить код: В одной из поддельных функций повысить привилегии и запустить шелл.
boot.sh (без изменений)

Сравнение с tty_struct:

Плюсы:

  • seq_operations может использоваться для большего количества файлов, чем tty_struct (которая связана только с TTY). Это даёт больше возможностей для атаки.

Минусы:

  • Сложнее найти подходящий файл в /proc, который использует seq_operations и при этом позволяет нам перезаписать его структуру.

Итог:

seq_operations — это ещё один перспективный вектор атаки. 👍 Нужно провести разведку и выяснить, как его можно использовать в нашем случае.

seq_operations: План по шагам 👣

  1. seq_operations и /proc/self/stat: Структура seq_operations используется, в частности, для файла /proc/self/stat, который содержит информацию о текущем процессе. Когда мы открываем этот файл, ядро выделяет память под seq_operations из кеша kmalloc-32.
  2. Размер имеет значение: Размер seq_operations — 32 байта (0x20). Это как раз тот размер, который мы можем выделить с помощью нашего драйвера heap_bof.
  3. Переполнение: Если мы выделим много блоков по 32 байта, а затем откроем много файлов /proc/self/stat, то с большой вероятностью один из seq_operations окажется рядом с одним из наших блоков. И если мы сможем переполнить наш блок, то сможем перезаписать seq_operations!
  4. start — наше всё: При чтении файла /proc/self/stat вызывается функция start из seq_operations. Если мы подменим указатель на start, то сможем выполнить свой код.
  5. Обход защиты:
  6. SMEP отключен, поэтому мы можем прыгнуть на код в юзерспейсе.
  7. SMAP и KPTI тоже отключены, так что с этим проблем нет.
  8. KASLR обойдём, украв адрес из стека (как и в случае с tty_struct).

Детали:

План действий:

  1. Выделить много блоков по 32 байта с помощью heap_bof.
  2. Открыть много файлов /proc/self/stat.
  3. Найти блок, который переполнился, и перезаписать seq_operations, подменив указатель start на адрес нашей функции повышения привилегий в юзерспейсе.
  4. Прочитать /proc/self/stat, чтобы вызвать нашу функцию.
  5. В функции повышения привилегий:
  • Обойти KASLR, найдя адрес в стеке.
  • Вызвать commit_creds(init_cred), чтобы стать рутом.
  • Вернуться в юзерспейс и запустить шелл.

Преимущества:

  • Не нужно угадывать, где окажется cred (как в старых методах с kmalloc-192).
  • Достаточно надёжный способ, так как мы контролируем выделение памяти и можем увеличить шансы на успех, выделив много блоков.

Недостатки:

  • Всё ещё требуется переполнение буфера, что может быть не так просто реализовать.
  • Нужно много раз открывать /proc/self/stat, что может выглядеть подозрительно.

Итог:

План выглядит многообещающе! 🎉 Использование seq_operations — это более современный и надёжный способ эксплуатации по сравнению со старыми методами, основанными на cred.

Вот и сам эксплоит! 🔥 Давайте разберём его по косточкам.

Что делает этот код:

  1. get_shell, save_status, get_root: Эти функции аналогичны тем, что были в эксплоите с tty_struct.
  2. main:
  • Открывает устройство /dev/bof.
  • Выделяет 0x40 блоков по 0x20 байт.
  • Заполняет последний выделенный блок значением 0xff - вероятно, это нужно, чтобы сломать какие-то метаданные, обеспечив нужное место для записи в seq_operations.
  • Открывает 0x200 (512) файлов /proc/self/stat. Это приводит к выделению seq_operations в kmalloc-32.
  • Выделяет буфер p.buf размером DATA_SIZE (0x20 * 8 = 0x100 байт) и заполняет его адресом get_root.
  • Переполняет ptr[0], записывая туда DATA_SIZE байт. Так как ptr[0] был выделен последним из 0x40 блоков, и с большой вероятностью где-то рядом расположены seq_operations, то начнётся перезапись данных за пределами ptr[0].
  • Читает из каждого открытого файла /proc/self/stat, что приводит к вызову m->op->start(m, &pos). Если мы успешно перезаписали seq_operations, то вместо start вызовется get_root.
  • get_root повышает привилегии и возвращается в юзерспейс, вызывая get_shell, которая, в свою очередь, запускает /bin/sh.

Ключевые моменты:

  • Спрей Открытие большого количества файлов /proc/self/stat увеличивает шансы на то, что seq_operations окажется рядом с нашим переполняемым блоком.
  • Переполнение: Запись DATA_SIZE байт вместо 0x20 приводит к переполнению и перезаписи соседних данных в куче.
  • Обход KASLR: Функция get_root крадёт адрес из стека, чтобы вычислить смещение KASLR.

Итог:

Эксплоит использует seq_operations для повышения привилегий, обходя защиту SLAB_ACCOUNT, которая мешала использовать cred в новых ядрах. 🎉 Это более современный и, вероятно, более надёжный метод по сравнению с tty_struct, так как не требует угадывать местоположение tty_struct в памяти.

ПРОДОЛЖЕНИЕ БУДЕТ, НИКТО НЕ ЗНАЕТ КОГДА, НО ОБЕЩАЮ ВЕРНУТСЯ...