Как я разбиралась с CVE-2019-2215: путь от сборки ядра до root на Android
Заметка о том, как я применила навыки реверс‑инжиниринга бинарных файлов к исследованию уязвимости ядра Android, с какими трудностями столкнулась и что из этого вынесла.
Дисклеймер: Материал носит исследовательский характер. Уязвимость закрыта, PoC не предназначен для атаки на реальные устройства.
Вместо вступления
В моей повседневной работе я занимаюсь реверс‑инжинирингом и разработкой под Android: статический анализ APK и нативных библиотек, разбор алгоритмов аутентификации в IDA Pro и Jadx, автоматизация через Python.
Когда мне на тестовом задании предложили воспроизвести CVE-2019-2215 (Use‑After‑Free в драйвере Binder), это было логичным продолжением моего стека, но с погружением на уровень ниже — на уровень ядра Linux 4.14.150+ (goldfish, x86_64). Здесь привычные методы вроде Jadx уже не работали. Нужно было собирать кастомное ядро, разбираться с отладкой vmlinux через GDB и работать со структурами памяти напрямую.
Две недели я занималась этим. Итог — успешное локальное повышение привилегий до root. Но самое интересное было не в результате, а в том, как я к нему шла, и какие трудности решала по пути. Этот опыт стал для меня отличным тестом на умение адаптировать инструменты реверса под ядерную специфику.
Подготовка: почему нельзя взять готовое ядро
Первое, с чем я столкнулась — в актуальных ветках ядра уязвимость уже была закрыта. Значит, чтобы её воспроизвести, нужно было принудительно вернуть код в уязвимое состояние. Я сделала реверс‑патчинг функции binder_poll: убрала вызовы wake_up_poll с флагом POLLFREE и synchronize_rcu(), которые предотвращали сохранение «висячего» указателя в epoll.
Отдельной задачей стала сборка. В базовой конфигурации был активен KASAN (KernelAddressSanitizer), который моментально вызывал Kernel Panic при обращении к освобождённому объекту. Пришлось пересобирать ядро (ревизия #2) с отключённым KASAN — только так можно было вернуть стандартное поведение аллокатора SLUB. Патчи и конфигурация сборки лежат в репозитории.
Статический анализ: где искать addr_limit
Эксплойт не работает «на глазок». Нужно было точно знать, где в памяти ядра лежат нужные структуры. Для этого я использовала связку pahole и gdb‑multiarch.
Главная сложность была в том, что в тестируемом ядре активна опция CONFIG_THREAD_INFO_IN_TASK. Я ожидала найти переменную addr_limit внутри thread_info, но её там не оказалось — структура занимала всего 16 байт. Проверка через ptype /o struct thread_info в GDB подтвердила это.
Пришлось копать глубже. С помощью pahole и анализа нулевого указателя в GDB (p &((struct task_struct )0)→thread.addr_limit) я вычислила точное смещение: 0xa18 внутри встроенной thread_struct. После контроля через GDB ($1 = (mm_segment_t ) 0xa18) макросы были исправлены, и эксплойт перестал промахиваться.
Race condition и мусор в iovec
Самой мучительной частью было заставить эксплойт работать стабильно. Две проблемы, которые я решала:
Race condition при утечке данных. Вызов writev() возвращал 0×1000 вместо 0×2000. Перезапись структуры в куче не происходила, и я долго не могла понять, почему. Мониторинг через ps показал, что процесс уходил в бесконечный сон pipe_wait из‑за нарушения логики наполнения пайпа. Я переписала алгоритм синхронизации: пайп оставлен пустым, а 10-й элемент iovec сам забивает его до предела, усыпляя родительский поток на 11-м элементе. Это дало дочернему процессу стабильное окно на выполнение unlink.
Случайные Kernel Panic. Смещение указателей в куче приводило к Spinlock Lockup при вызове unlink. Причина оказалась банальной: стековый мусор попадал в проверки access_ok(). Решение — очистка локальных массивов iovec через memset() с правильным вычислением размера (sizeof(struct iovec) * IOVEC_COUNT) и строгое выравнивание отправляемого буфера до 40 байт.
Для обхода проверок ядра при чтении/записи в память также потребовалось внести изменения в lib/iov_iter.c (принудительный вызов raw_copy_to_user/raw_copy_from_user). Это позволило снять ограничение addr_limit и получить примитив Arbitrary R/W через обычные системные вызовы на пайпах.
Результат
После снятия addr_limit (0 xfffffffffffffffe) я прочитала указатель на struct cred из task_struct и обнулила первые 8 полей (UID/GID). Получение root подтверждено выводом getuid().
[+] addr_limit = 0xfffffffffffffffe
[+] Модификация структуры cred (UID/GID -> 0)...
[*] Текущий UID: 0
[==================================================]
[ УСПЕХ! Мы получили права ROOT (UID=0) в ядре! ]
[==================================================]Что я вынесла из этой задачи
Инструменты решают. Когда я перестала гадать и пошла в GDB и pahole, смещения нашлись быстро. Ошибка была не в непонимании ядра, а в том, что я искала addr_limit не там, где он лежит в этой конфигурации.
Синхронизация важнее самого эксплойта. Половина падений была не в примитивах записи, а в том, кто и когда блокируется.
Отчёт — это часть инженерной работы. Подробный write‑up со скриншотами и таблицами занял времени не меньше, чем сам PoC. Без него результат невоспроизводим, а значит — бесполезен для команды.
Полный отчёт, код и патчи ядра доступны в моём репозитории на GitHub.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.