
Эксплойт ядра Android GKI 6.12 для CVE-2026-43499, объединяющий ошибку отката rt_mutex с перезаписью стека pselect для получения root на устройствах Samsung и Pixel.
Эксплойт ядра Linux для линейки Android GKI 6.12 (устройства Samsung и Pixel),
нацеленный на CVE-2026-43499: ошибочный откат remove_waiter() в
rt_mutex_start_proxy_lock(), который использует current вместо waiter::task,
оставляя pi_blocked_on ожидающего указывающим на его (позже извлечённый из стека)
rt_mutex_waiter в стеке ядра. В сочетании с перезаписью стека fd_set через
pselect() и обходом поддельного rb_tree, управляемым потребителем через
sched_setattr, это даёт детерминированный примитив записи в ядро и полный root.
Эксплойт работает как разделяемая библиотека LD_PRELOAD (preload.so) и
устанавливает root-демон su плюс обои как пост-root артефакты.
Статус: активная разработка. Цель m1q (ZF1) — основной фокус bring-up. pipei tmp_page-uname bootstrap — текущий активный маршрут: обход доказанно чист на устройстве (сборка #33: посеянные rt_mutex-регионы для каждого потомка), и полный перебор 168 кандидатов в процессе. Маршрут configfs CFI — тупик на m1q (Rust ashmem — нет инжектируемого слота fops) и остаётся маршрутом для целей C-ashmem. Смещения для каждой цели различаются по устройству — проверяйте их по фактическому бинарнику ядра, прежде чем доверять им.
Когда FUTEX_CMP_REQUEUE_PI переставляет ожидающего в очередь и обход цепочки
обнаружения взаимоблокировок при перестановке возвращает -EDEADLK,
__rt_mutex_start_proxy_lock() выполняет откат через remove_waiter()
(rtmutex.c:1535). Откат корректно удаляет ожидающего из дерева ожидания, но
очищает pi_blocked_on вызывающего перестановку, а не
waiter->task->pi_blocked_on. pi_blocked_on ОЖИДАЮЩЕГО остаётся висящим на его
стековом rt_mutex_waiter, который извлекается из стека, как только futex
истекает по таймауту.
После таймаута область стека ядра ожидающего переиспользуется: core_sys_select()
копирует три fd_set в этот стековый буфер (путь стека nfds < 344 на ZF1).
Специально сформированный массив слов fd_set воссоздаёт поддельный
rt_mutex_waiter / поддельную задачу / поддельный rt_mutex (с корнем rb_tree,
указатели которого контролируются). Затем поток-потребитель вызывает
sched_setattr_tid(waiter) → rt_mutex_adjust_pi() → rb_erase_cached, что
производит запись контролируемого значения по произвольному адресу.
Весь примитив требует цикла в PI-цепочке: владелец удерживает f_pi_target
и также блокируется на f_pi_chain (удерживаемом ожидающим), поэтому обход
цепочки попадает в owner → chain → waiter → target → owner и завершается
ошибкой -EDEADLK. Удаление цикла заставляет перестановку вернуть успех с
нулевым эффектом в ядре.
sched_blocked_reason (основной на m1q, доказан на устройстве):
сохранённый PC возврата заблокированного kworker (stack_trace_save_tsk)
читается из кольцевого буфера и сравнивается со вкомпилированным смещением
worker_thread. Выполняется до использования любых слов маршрута boot_id,
чтобы цели записи через data-alias (data_addr() = p0 alias +
slide_p0_offset) оставались корректными при ненулевом slide.SLIDE_LOGGERS_0_1 в данные sysctl boot_id через alias линейного
отображения; утёкшее значение восстанавливает stext. Отложен на m1q:
его цель W1 (SLIDE_RANDOM_BOOT_ID_DATA_OFF, младшая физическая RAM) имеет
зависящую от страницы записываемость — slide перемещает цель через границы
страниц при каждой загрузке, поэтому ~6/7 запусков дают сбой. Используется
только явно через SLIDE_FORCE_BOOTID (тест механизма) с перенаправлением
tree_pc/tree_left на заспреенную страницу.mm_struct и использование
коллизии futex-хэша для обнаружения в куче, что утекает адрес
страницы кучи ядра, используемый как база спрея поддельных объектов.
Ожидающие отсоединяются (не присоединяются) при очистке, чтобы избежать OOM
стека ядра, который приводил к краху сборок до #26.Слова fd_set pselect воссоздают поддельный rt_mutex_waiter в стеке ядра
ожидающего; sched_setattr_tid(waiter) потока-потребителя обходит его через
rt_mutex_adjust_pi(). Карта слов ZF1 (проверено по дизассемблеру):
PSELECT_WAITER_WORD_SHIFT = 0, слово 12 = task (@+0x50), слово 13 = lock
(@+0x58), слово 14 = wake_state (@+0x60 = 3). С посеянным поддельным
rt_mutex обход завершается чисто: rb_erase W1 *(tree_left)=tree_pc / W2
*(tree_pc&~3+8)=tree_left из [7] — единственные записи в ядро; [11] (setprio /
dequeue_pi) и пробуждение [9] оба пропускаются (локальный поддельный ожидающий с
приоритетом 100 удерживает стековый узел вне top-waiter). Каждое завершение
обхода, входившее в него, начиная со сборки #19 выживает на устройстве;
детерминированные крахи до этого были ошибкой размещения 8-байтовой полезной
нагрузки (SKB_DATA_DELTA), а не сбоем тела обхода.
Опциональная фаза STAGE3=1, которая форкает дочерний мост, подменяет его
mm->pgd на подготовленную поддельную таблицу страниц (3 уровня: PGD→L1→L2,
RX-лист цикла + RW-лист стека) и позволяет потомку выполнить ассемблерный блоб
только из регистров, который патчит его собственный cred через окно физического
сканирования 2MB — физическое чтение/запись всей RAM без примитивов
configfs/pipe. В настоящее время подключено только в цель m1q и не
запускалось на устройстве.
41 цель в src/targets/<codename>-<build>/, каждая требует как минимум
target.h (смещения символов ядра, KIMAGE_TEXT_BASE, база прямого отображения,
смещения структур). Цели Pixel (comet, tokay, tegu, caiman, komodo,
frankel, mustang, rango, stallion, blazer) переопределяют общие
исходники; цели Samsung (m1q-*) добавляют специфичную для устройства логику.
make list-projects # полный список
Раскладки структур ядра различаются по устройству — смещения должны быть проверены по фактическому бинарнику ядра / kallsyms для каждой цели.
Выход: build/<PROJECT>/bin/preload.so (разделяемая библиотека, загружаемая
через LD_PRELOAD).
# Проект по умолчанию
CC=clang make
# Конкретная цель устройства (по умолчанию: blazer-CP2A.260605.012)
CC=clang make PROJECT=m1q-BP4A.251205.006
# Без CC=clang: использует NDK, если найден ($NDK_ROOT / $ANDROID_NDK_HOME /
# $ANDROID_NDK_ROOT), иначе host clang + Android sysroot
make PROJECT=m1q-BP4A.251205.006
# Показать конфигурацию сборки
make info
# Очистить
make clean
Сборка встраивает PIE-бинарник su_daemon (src/su_daemon.c, собирается в
build/embed/su_daemon_aarch64_pie) и assets/wallpaper.webp в preload.so
через src/su_blob.S / src/wallpaper_blob.S.
Загрузите выходные файлы сборки в /data/local/tmp, затем запустите эксплойт
под LD_PRELOAD. Запускайте БЕЗ tee на живой цели bring-up — tee буферизует
свой stdio и теряет хвост лога при панике ядра (stdout эксплойта небуферизован,
поэтому перенаправление прямо в файл сохраняет каждую строку):
adb push build/m1q-BP4A.251205.006/bin/preload.so /data/local/tmp/preload.so
adb push build/embed/su_daemon_aarch64_pie /data/local/tmp/su_daemon_aarch64_pie
adb shell "chmod 755 /data/local/tmp/preload.so /data/local/tmp/su_daemon_aarch64_pie"
adb shell "LD_PRELOAD=/data/local/tmp/preload.so \
/data/local/tmp/su_daemon_aarch64_pie \
> /data/local/tmp/output.log 2>&1"
При успехе демон слушает на /data/local/tmp/temp_su.sock, а su установлен
под /apex/com.android.virt/bin.
tools/run_pipei.sh загружает бинарник, проверяет его sha256 на устройстве
(устаревший preload.so даёт молча нечитаемые вердикты — лестница однажды
запускала сборку < #22, чей формат лога отличается), затем запускает полный
перебор 168 кандидатов, разбитый на 3x56, чтобы паника теряла не более одного
фрагмента (~10 мин). stdout эксплойта стримится напрямую через adb (без
перенаправления на устройстве — tee на стороне устройства теряет хвост при
панике); хостовая копия каждого фрагмента хранится в
/tmp/m1q_pipei_chunk{1..3}_stream.log:
sh tools/run_pipei.sh
PASS = любой фрагмент показывает tmp_page uname changed + after uname='CatOS.
PIPEI_CHILD_REGIONS должен быть установлен как минимум в размер фрагмента,
иначе rt_mutex-регионы для каждого потомка остаются не посеянными и перебор
паникует на кандидате 0. Полное покрытие также требует
TMP_UNAME_PIPEI_SPLIT_ORDER_PAGES=1 — без него перебирается только одна
страница освобождённого mm-блока из 8 страниц. run_pipei_slim.sh — вариант
диагностики одного кандидата; run_m1q_ladder.sh — батарея
обхода/изоляции (оракул B16-2, механизм slide, шаги перебора).
exploit/
Makefile
assets/ # wallpaper.webp (встроенный пост-root артефакт)
src/
main.c # точка входа run_exploit() (общая)
preload.c # конструктор LD_PRELOAD, установка su/обоев
util.c # трансляция адресов, подготовка страниц, помощники configfs
pipe.c # физическое чтение/запись через pipe
root.c # патчинг cred / обход задач
slide.c # slide KASLR (маршруты tracefs + boot_id)
fops.c # повреждение pselect + манипуляция fops
faketables.c # построители поддельных таблиц страниц стадии 3 (3 уровня)
stage3.c # дочерний мост pgd-swap (no-op, если не настроен)
stage3_loop.S, stage3_poll.S # блоб моста + трамплин входа
su_daemon.c # root-демон (компилируется как отдельный PIE)
su_blob.S, wallpaper_blob.S # встроенные бинарники
offset.h # подтягивает target.h для каждой цели через -DTARGET_CONFIG_H
kernelsnitch/ # библиотека утечки mm_struct через futex-хэш
targets/<codename>-<build>/ # target.h для каждого устройства + опциональные переопределения
poc/poc.c # автономный POC (отдельно от эксплойта)
Цели переопределяют общие исходники через механизм pick_src в Makefile:
если src/targets/<PROJECT>/<file>.c существует, он используется вместо общего
src/<file>.c; иначе используется общая версия.
-EDEADLK (errno 35). Отсутствующий цикл
проявляется как success=1 с нулевым эффектом в ядре.PSELECT_WAITER_WORD_SHIFT должен быть 0 (глубины стека ZF1
проверены по ELF). Другие цели всё ещё по умолчанию используют 1 — проверяйте
по устройству, прежде чем доверять им.p0_data_alias() использует DIRECT_MAP_BASE (линейное отображение),
которое отличается от P0_PAGE_OFFSET на ZF1. Data-alias для разыменовываемых
объектов ядра должны использовать линейное отображение.pr_error() фатален (exit(-1)), в отличие от макросов
предупреждения/информации.SLIDE_LOGGERS_0_1, а не
SLIDE_NFULNL_LOGGER; слово — это нерандомизированный alias линейного
отображения, поэтому маршрут boot_id не зависит от slide (используйте его
только при slide=0 — сначала запустите утечку tracefs).Это эксплойт-код для устройств Android, предназначенный для исследований в области безопасности, авторизованного тестирования уязвимостей и защитной работы на оборудовании, которым вы владеете или на которое у вас есть явное разрешение на тестирование. Неправомерное использование на устройствах, которыми вы не владеете, незаконно. Автор не несёт ответственности за превращённые в кирпич устройства, превращённые в кирпич ядра или аннулированные гарантии.
mm_structwrite_iter, а кучевый объект ASHMEM_SET_NAME — это KVec,
чья раскладка не совпадает с private_data configfs_bin_write_iter. m1q
переключается на запись в kmalloc: возвращает утёкший mm-блок порядка 3 как
объекты pipe_inode_info и использует запись pselect W1 (*(tree_left) = tree_pc) для перезаписи слота tmp_page кандидата (+0x90) страницей
пространства имён UTS (init_uts_ns). Веерная запись помещает маркерное имя
по смещению sysname, и uname() сообщает "CatOS" — проверяемая запись в
ядро без зависимости от статических объектов. Цели C-ashmem сохраняют путь
configfs.preload.c устанавливает встроенный демон su (смонтированный через tmpfs в
/apex/com.android.virt/bin, плюс варианты в пространстве имён adbd и
локальные) и меняет обои.| Переменная | По умолчанию | Назначение |
|---|
PSELECT_ROUTE_SHIFT | время компиляции | A/B сдвиг слова fd_set для основного маршрута (m1q) |
SLIDE_SHIFT | время компиляции | A/B сдвиг слова fd_set для маршрута slide |
PSELECT_ROUTE_DELAY_USEC | 50000 | Задержка потребителя перед sched_setattr (должна быть > 0) |
SLIDE_CONSUME_USEC, SLIDE_ENTER_DELAY_USEC, SLIDE_CONSUMER_CORE | — | Тайминг/привязка маршрута slide |
SKIP_SLIDE | 0 | Использовать запасной вариант прямого отображения (slide = 0) |
SLIDE_ONLY | 0 | Выполнить только slide KASLR и выйти |
SLIDE_P0_OFFSET, SLIDE_BOOTID_OFF | — | Переопределить смещения slide p0-alias / boot_id |
SLIDE_FORCE_BOOTID | 0 | Принудительно использовать маршрут boot_id pselect после slide tracefs (тест механизма) |
KSNITCH_COLLISIONS | 4 | Количество коллизий KernelSnitch |
STAGE3 | 0 | Включить фазу дочернего моста pgd-swap (m1q) |
STAGE3_DRYRUN | 0 | Подготовить/проверить таблицы, затем прервать до подмены |
PAGE_RECLAIM_SENDS, PSELECT_SIMPLE_LAYOUT | — | Настройки спрея/раскладки fd_set |
TMP_PAGE_UNAME | 0 | Запустить pipei tmp_page-uname bootstrap на m1q |
TMP_UNAME_PIPEI_SWEEP, TMP_UNAME_PIPEI_ALIGN_ORDER, TMP_UNAME_PIPEI_SPLIT_ORDER_PAGES | — | Режим перебора; split_order перебирает все 8 страниц освобождённого mm-блока |
TMP_UNAME_PIPEI_SLOT, TMP_UNAME_PIPEI_SLOT_CANDIDATES | — | Диапазон перебора (начало фрагмента / количество) |
PIPEI_CHILD_REGIONS | 0 | Посеянные rt_mutex-регионы для каждого потомка; ДОЛЖНО быть >= фрагмента перебора, иначе не посеянные регионы паникуют на кандидате 0 |
PIPEI_RECLAIM_COUNT, PIPEI_DRAIN_COUNT | — | Возврат/слив пар pipe; увеличьте при промахе возврата |
PIPEI_SPONGE_MB | 3072 | Губка-флуд порядка 0, удерживаемая во время возврата (сборка #29) |
PSELECT_W0_PRIO_OVERRIDE | 130 | Приоритет top-waiter; инертен с посеянными регионами (устаревшая защита от устаревшего дерева, только A/B) |
PSELECT_SKB_DELTA_OVERRIDE | 0 | A/B смещение полезной нагрузки внутри страницы (только если устройство отличается от -0xe78) |
TMP_UNAME_NAME, TMP_UNAME_HOLD_SEC | "CatOS", 5 | Маркерное имя / время удержания после успеха |
SKB_DATA_DELTA равен -0xe78 (измерено на устройстве, сборка #19) —
реальный frag head skb на 8 байт меньше, чем выведенный из дизассемблера -0xe80;
эта 8-байтовая ошибка вызывала каждый детерминированный крах pselect со сборок
#6-18 (вход в обход → разыменование мусора). Общее значение GKI 6.12; копии
util.c для каждой цели несут A/B-ручку окружения SKB_DATA_DELTA. НЕ
накладывайте PSELECT_SKB_DELTA_OVERRIDE поверх вкомпилированной константы.ASHMEM_SET_NAME не совпадает с
configfs_bin_write_iter. Используйте pipei tmp_page bootstrap. Защиты
PSELECT_WRITE_RIGHT_SPRAYED / PSELECT_PI_TOP_TASK_SELF в target.h m1q
перенаправляют записи [11] обхода прочь от RO-статических fops.payload+0x6000 + idx*0x40) внутри возвращённой страницы. Переборы с общим
fake_lock паниковали на кандидате ~9-12 (переработанные устаревшие узлы стека
ядра), а пустые регионы сборки #32 паниковали на кандидате 0 (пробуждение [9]
owner==NULL на fake_task, заполненном 0x41). PIPEI_CHILD_REGIONS должен быть
установлен, иначе регионы остаются не посеянными.