Кошерный способ модификации защищённых от записи областей ядра Linux

в 21:09, , рубрики: kernel, linux, x86, системное программирование, метки: , ,

Те, кто хоть однажды сталкивался с необходимостью поменять что-то в ядре на лету не понаслышке знают, что данный вопрос требует детальной проработки, ведь страницы памяти ядра, хранящие код и некоторые данные, помечены как «read-only» и защищены от записи!

Для x86 известным решением является временное отключение страничной защиты посредством сброса бита WP регистра CR0. Но следует применять это с осторожностью, ведь страничная защита является основой для многих механизмов ядра. Кроме того, необходимо учитывать особенности работы на SMP-системах, когда возможно возникновение разных неприятных ситуаций.

Отключение страничной защиты

В архитектуре x86 существует специальный защитный механизм, в соответствии с которым попытка записи в защищённые от записи области памяти может приводить к генерации исключения. Данный механизм носит название «страничной защиты» и является базовым для реализации многих функций ядра, таких, как например COW. Поведение процессора в этой ситуации определяется битом WP регистра CR0, а права доступа к странице описываются в соответствующей ей структуре-описателе PTE. При установленном бите WP регистра CR0 попытка записи в защищённые от записи страницы (cброшен бит RW в PTE) ведёт к генерации процессором соответствующего исключения (#GP).

Простейшим решением данной проблемы является временное отключение страничной защиты сбросом бита WP регистра CR0. Это решение имеет место быть, однако применять его нужно с осторожностью, ведь как было отмечено, механизм страничной является основой для многих механизмов ядра. Кроме того, на SMP-системах, поток, выполняющийся на одном из процессоров и там же снимающий бит WP, может быть прерван и перемещён на другой процессор!

Тем не менее, если очень хочется, нужно делать это отключая preemption так, как рекомендуют тут:

static inline unsigned long native_pax_open_kernel(void)
{
    unsigned long cr0;

    preempt_disable();
    barrier();
    cr0 = read_cr0() ^ X86_CR0_WP;
    BUG_ON(unlikely(cr0 & X86_CR0_WP));
    write_cr0(cr0);
    return cr0 ^ X86_CR0_WP;
}

static inline unsigned long native_pax_close_kernel(void)
{
    unsigned long cr0;

    cr0 = read_cr0() ^ X86_CR0_WP;
    BUG_ON(unlikely(!(cr0 & X86_CR0_WP)));
    write_cr0(cr0);
    barrier();
    preempt_enable_no_resched();
    return cr0 ^ X86_CR0_WP;
}

Использование отображений

Более лучшим и в достаточной степени универсальным, является способ создания временных отображений. В силу особенностей работы MMU, для каждого физического фрейма памяти может быть создано несколько ссылающихся на него описателей, имеющих различные атрибуты. Это позволяет создать для целевой области памяти отображение, доступное для записи. Такой метод используется в проекте Ksplice (форк на github'е). Ниже приведена функция map_writable, которая и создаёт такое отображение:

/*
 * map_writable creates a shadow page mapping of the range
 * [addr, addr + len) so that we can write to code mapped read-only.
 *
 * It is similar to a generalized version of x86's text_poke.  But
 * because one cannot use vmalloc/vfree() inside stop_machine, we use
 * map_writable to map the pages before stop_machine, then use the
 * mapping inside stop_machine, and unmap the pages afterwards.
 */
static void *map_writable(void *addr, size_t len)
{
        void *vaddr;
        int nr_pages = DIV_ROUND_UP(offset_in_page(addr) + len, PAGE_SIZE);
        struct page **pages = kmalloc(nr_pages * sizeof(*pages), GFP_KERNEL);
        void *page_addr = (void *)((unsigned long)addr & PAGE_MASK);
        int i;

        if (pages == NULL)
                return NULL;

        for (i = 0; i < nr_pages; i++) {
                if (__module_address((unsigned long)page_addr) == NULL) {
                        pages[i] = virt_to_page(page_addr);
                        WARN_ON(!PageReserved(pages[i]));
                } else {
                        pages[i] = vmalloc_to_page(page_addr);
                }
                if (pages[i] == NULL) {
                        kfree(pages);
                        return NULL;
                }
                page_addr += PAGE_SIZE;
        }
        vaddr = vmap(pages, nr_pages, VM_MAP, PAGE_KERNEL);
        kfree(pages);
        if (vaddr == NULL)
                return NULL;
        return vaddr + offset_in_page(addr);
}

Использование данной функции позволит создать доступное для записи отображение для любой области памяти. Освобождение созданного таким образом региона осуществляется с использованием функции vfree, аргументом которой должно служить выравненное на границу страницы значение адреса.

Стоп машина!

Последним элементом, позволяющим сделать модификацию кода ядра безопасной, является механизм stop_machine:

#include <linux/stop_machine.h>
int stop_machine(int (*fn)(void *), void *data, const struct cpumask *cpus)

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

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

Автор: milabs

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js