
Запуск и анализ уязвимости ядра Android CVE-2019-2215
В ноябре 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-доступ. В конце мы рассмотрим, как эта уязвимость смягчается с помощью патчей.
Чтобы вызвать крах ядра путём запуска этой уязвимости, выполните следующие шаги:
Посмотрите следующее видео:
[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); }
Давайте посмотрим, что делает `trigger.cpp`.
В Android (как и в других Unix-подобных операционных системах) существуют процессы. Каждая запущенная программа создаёт один (или более) процесс, и эти процессы управляются ОС. ОС может переключаться между ними (многозадачность) или завершать процесс и т.д. По соображениям безопасности процессы по умолчанию изолированы друг от друга.
В некоторых случаях одному процессу может потребоваться обменяться данными с другим процессом. Это называется межпроцессным взаимодействием (IPC). В Linux существует несколько способов взаимодействия процессов. Android представил собственный механизм IPC, называемый **'Binder'**. Binder — это драйвер ядра, обеспечивающий межпроцессное взаимодействие.
В Android IPC может осуществляться прямым вызовом некоторых методов ядра (большинство из них находится в drivers/binder.c) или с помощью высокоуровневых реализаций (например, на Java).

Для использования **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 и добавляется в очередь.

При вызове **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).

#### Освобождение:
При вызове **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 и вызывает баг!!
Мы создали event_poll, содержащий red_black_tree, каждый узел которого — это ep_item, имеющий поле, представляющее собой список epoll_entry. Каждый epoll_entry содержит два указателя на структуру binder_thread (wait, whead).
Вызвав ioctl(), мы освободили binder_thread из памяти. Затем при выходе эта структура была доступна через указатель, который всё ещё существовал!
Динамический анализ — это тестирование и оценка программы путём выполнения данных в реальном времени; для поиска ошибок в программе во время её работы.
Шаги:
Сборка ядра Android без KASan
Мы собираем его без KASAN, чтобы отслеживать операции записи и удаления из списка (unlink) и видеть, что на самом деле происходит после операции unlink.
Загрузка эмулятора с только что собранным ядром
Запуск эмулятора
Сборка триггера уязвимости и отправка его на виртуальное устройство
Установка точек останова в 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.
Результат:
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 ) )
и это функция 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)
результат — 0xa0, и если мы укажем wait.head вместо wait в команде , результат будет 0xa8, и он содержит `0xffff88805c05cae0````
0xffff88805c05cae0 is pointer to eppoll_entry->wait.entry which is of type struct list_head
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).
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; };
Одним из полей является указатель **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).
Например, если мы хотим записать массив буферов в файл (fd), мы вызываем
writev(fd, iovecStack, count)
Этот метод (как и readv и recvmsg) сначала копирует массив iovec (iovecStack) в пространство ядра, затем читает из этих буферов и записывает в fd.
Мы можем записывать и читать буферы с помощью pipe. pipe — это структура, предоставляющая два файловых дескриптора: один для чтения, другой для записи. Pipe имеет длину в байтах; когда один процесс записывает в pipe больше, чем его длина, pipe блокирует этот процесс и ждёт, пока другой процесс прочитает из этого pipe (используя дескриптор чтения этого pipe).
Ядро пытается выделить память (для структуры) в соответствии с её размером. Например, когда мы освободили структуру binder_thread из памяти (см. статический анализ), и после этого если у нас есть структура размером, близким к binder_thread, есть высокая вероятность, что она будет выделена на том же месте, где находился освобождённый binder_thread.
Сначала мы создаём iovecStack (массив структур iovec) размером, близким к структуре binder_thread.
Затем мы освобождаем binder_thread из памяти (см. часть 'free' статического анализа).
Затем мы вызываем writev() для этого iovecStack. Первая часть этого метода копирует iovecStack в пространство ядра.
Вероятнее всего, наш iovecStack будет выделен на том же месте, что и освобождённый binder_thread.
Если в iovecStack достаточно элементов iovec, память в расположении binder_thread выглядит так:

Вы видите, что 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).

До того как наступит вторая часть 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.
Итак, чтобы получить указатель на task_struct, мы делаем следующее:
смотрите exploit.cpp в репозитории.
Теперь у нас есть адрес task_struct в пространстве ядра (task_ptr).
Здесь мы используем socket_pair вместо pipe. И мы используем recvmsg() для чтения из сокета и записи в iovecs.
Шаги:
После записи **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;
}
Эти строки были добавлены в исходный код:
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).
Идея эксплуатации проста: программное обеспечение содержит ошибки, которые приводят к неправильному поведению или некорректному выполнению задачи; эксплуатация ошибки — это превращение этого неправильного поведения в преимущество для атакующего.
Ошибки, которые можно эксплуатировать, называются уязвимостями.
Привилегированный пользователь или процесс — это тот, кто имеет полный доступ к устройству.
Большинство архитектур набора команд поддерживают как минимум два режима выполнения:
привилегированный: доступны все инструкции машинного уровня.
непривилегированный: доступно только подмножество инструкций.
Уязвимости типа Use-After-Free — это тип ошибки повреждения памяти, который может быть использован хакерами для выполнения произвольного кода.
Use-After-Free конкретно относится к попытке обращения к памяти после её освобождения, что может привести к краху программы или, в случае уязвимости Use-After-Free, потенциально к выполнению произвольного кода или даже к возможности полного удаленного выполнения кода.
Это use-after-free в Binder в ядре Android. Ошибка представляет собой локальную уязвимость повышения привилегий, позволяющую полностью скомпрометировать уязвимое устройство. В сочетании с эксплойтом для рендеринга браузера эта ошибка может полностью скомпрометировать устройство через вредоносный веб-сайт. Она достижима изнутри песочницы Chrome.
Примечание: работает на Pixel 1 и 2, но не на Pixel 3 и 3a. Смотрите эту атаку.
Бюллетени безопасности Android — это список, публикуемый Google (ежемесячно). Этот список содержит исправленные уязвимости безопасности, затрагивающие фреймворк Android, ядро Linux и т.д.
Syzkaller — это фаззер ядра. Фаззинг — это метод тестирования, при котором автоматизированная программа генерирует полу-случайные входные данные для целевой программы, чтобы проверить, не вызывается ли ошибка. Фаззинг особенно полезен для поиска ошибок повреждения памяти в программах на C или C++.
Project Zero — это команда аналитиков безопасности, нанятая Google для поиска уязвимостей нулевого дня. Уязвимость нулевого дня — это уязвимость, неизвестная тем, кто должен её исправить.
GDB расшифровывается как GNU Project Debugger — самый популярный отладчик для систем UNIX для отладки программ на C и C++. GDB позволяет запустить программу до определенной точки, затем остановиться и вывести значения определенных переменных в этой точке, или выполнять программу построчно и выводить значения каждой переменной после выполнения каждой строки.
Вы можете подключить эмулятор Android с помощью gdbserver для отладки.
Kernel Address SANitizer (KASAN) — это динамический детектор ошибок памяти, предназначенный для поиска ошибок выхода за границы и use-after-free. KASAN использует инструментирование во время компиляции для вставки проверок валидности перед каждым доступом к памяти, поэтому требуется версия компилятора, поддерживающая это. Доступ к памяти ядра может быть проверен по теневой карте на предмет валидности.
QEMU (Quick Emulator) — это свободный эмулятор с открытым исходным кодом, выполняющий аппаратную виртуализацию. Эмулятор Android основан на эмуляторе QEMU; он добавляет поддержку загрузки устройств Android, эмулирует типичное оборудование Android (OpenGL, GPS, GSM, датчики) и графический интерфейс. Эмулятор Android расширяет QEMU различными способами.
При статическом анализе мы используем исходный код программы для поиска ошибки или любой проблемы. Мы не запускаем программу.
При динамическом анализе мы анализируем поведение программы во время её выполнения. Например, путём подачи специальных входных данных.
Android NDK — это набор инструментов, позволяющий запускать нативный код, такой как C и C++, на устройстве Android.
Ядро Android goldfish используется для запуска кода ядра в эмуляторе Android. Его можно клонировать, изменять, а затем собирать для использования в эмуляторе.
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-триггера