
демонстрация CVE-2019-2215 (Bad Binder) для Android Q
Этот репозиторий — небольшой тестовый проект по исследованию уязвимости
CVE-2019-2215 (Bad Binder) и написанию рабочего прототипа эксплойта под Android с
простым графическим интерфейсом на Kotlin/Jetpack Compose.
В README я:
В репозитории настроен GitHub Actions workflow, который при каждом push/PR собирает проект
командой ./gradlew assembleDebug и публикует готовый badbinder-debug.apk как артефакт.
Скачать его можно так:
badbinder-debug-apk
с собранным APK.Это сделано для удобства, если хочется просто потестировать приложение, не поднимая локальную среду.
CVE-2019-2215 — это Use-After-Free (UAF) в IPC-подсистеме Binder ядра Android.
Упрощённо:
struct binder_thread, которая описывает поток,
выполняющий Binder-вызовы;waitqueue);remove_wait_queue,
что открывает классический UAF-сценарий;Более подробный теоретический разбор я делал по материалам:
По заданию рекомендовано использовать AVD с образом Android 10.0 (Q) x86_64.
Я сделал следующее:
/dev/binder.adb shell
ls -l /dev/binder
На этом этапе я столкнулся с неприятным фактом:
на данный момент актуальные образы AVD уже поставляются с пропатченным ядром, в котором
CVE-2019-2215 закрыта. То есть реально получить root на современном официальном эмуляторе
не удастся — эксплойт падает на более поздних стадиях или просто не даёт повышения
привилегий.
В итоге я использую AVD как тренажёр для воспроизведения логики эксплойта:
addr_limit,Это важный нюанс: весь код и отчёт ниже — учебные, а не «боевые».
Я сделал небольшое Android-приложение:
Основные шаги:
Создал обычный проект в Android Studio (Kotlin, минимальная поддержка Android 10).
Подключил NDK и CMake.
Добавил нативный файл с эксплойтом (тот самый cve-2019-2215.c с функциями
leak_task_struct, overwrite_addr_limit, и т.д.).
В CMakeLists.txt добавил сборку libcve-2019-2215.so.
В MainActivity:
init {
System.loadLibrary("cve-2019-2215")
}
external fun runNativeExploit(): String
external fun setNativeLogger(logger: NativeLogger)
На стороне Kotlin сделал ExploitViewModel, который реализует интерфейс
NativeLogger и складывает все сообщения в StateFlow<List<String>>. UI подписан
на этот поток и выводит лог в «терминале».
При старте активити я вызываю setNativeLogger(viewModel), чтобы нативный код получил
объект, в который можно слать строки.
Собираю и устанавливаю приложение:
./gradlew installDebug
Запускаю AVD и само приложение.
На экране вижу «терминал» и кнопку RUN EXPLOIT.
При нажатии:
runNativeExploit() в фоновом потоке.На реальном уязвимом ядре я ожидал бы в конце увидеть что-то вроде:
[+] Selinux changed: Permissive now.
[+] Root escalation successful!
uid=0(root)...
На актуальном эмуляторе Android 10 этого, разумеется, не происходит, но всё остальное —
утечка task_struct, попытка переписать addr_limit, вычисление cred и kernel_base —
отрабатывает как «сценарий», что и требовалось для задания.
Ниже — логическая схема эксплойта с привязкой к конкретным C-функциям.
Высокоуровневый план такой:
struct binder_thread и использовать его, чтобы
утечь адрес task_struct своего процесса (leak_task_struct).addr_limit в task_struct (overwrite_addr_limit) — это снимает
ограничение между адресами user-space и kernel-space для дальнейших
copy_to_user / copy_from_user.arb_read / arb_write).cred текущего процесса и базу ядра (verifying),
затем:
selinux_enforcing = 0),cred, чтобы стать root и получить полный набор capability
(runNativeExploit).Параллельно я интегрировал JNI-логгер, чтобы все эти этапы было видно прямо в UI.
task_struct (leak_task_struct)Ключевая функция:
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);
...
}
Что делает функция:
Фиксирует поток на CPU 0 (sched_setaffinity), чтобы поведение аллокатора ядра
было более предсказуемым. Это улучшает стабильность эксплуатации UAF.
Открывает /dev/binder, создаёт epoll-дескриптор:
fd = open("/dev/binder", O_RDONLY);
epfd = epoll_create(1000);
Binder-дескриптор регистрируется в epoll:
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
Готовит массив struct iovec iov_buffers[IOVEC_N] и выделяет память:
spinner = mmap((void *)0x100000000, page_size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
Здесь важно, чтобы младшие 32 бита адреса были нулями:
if (((long) spinner & 0xffffffff) != 0) {
android_log("[!] mmap returned wrong address!");
return;
}
Это соответствует технике из статей по эксплуатации: далее ядро интерпретирует часть наших данных как структуры с указателями, и такая «красиво выровненная» адресация упрощает злоупотребление.
Затем поля iov_buffers[0xa] и iov_buffers[0xb] заполняются так, чтобы в
момент UAF ядро скопировало в пайп кусок памяти, где лежит указатель на
task_struct.