Финансы Сайт на котором знают все про финансы

Простой контроль целостности процесса под Linux

В очередной раз решил попробовать написать драйвер (aka модуль ядра), но на этот раз под Linux. Задача, выполняемая модулем, будет аналогична рассмотренной ранее в примере драйвера под Windows.
Я не буду приводить описание методов работы со списками, которые используются в коде, так как они довольно просты, хорошо документированы и не слишком отличаются по логике от тех, которые доступны в Windows. Также за бортом останется детальное описание базового прототипа модуля, который будет использован для тестирования реализации функций контроля целостности. Хорошую развернутую статью про основы разработки модулей ядра под Linux можно почитать тут.

Перейдем непосредственно к коду. Как и раньше, начну с формата, в котором хранятся контрольные суммы областей памяти процесса.

typedef struct
{
    struct list_head list;
    int pid;
    unsigned long vm_start;
    unsigned long vm_end;
    int crc32;
    int is_present;
} memory_list;

Как видите, на этот раз я оперирую не секциями, а регионами памяти процесса. Это позволяет упростить код и избавить себя от необходимости разбирать что-либо относящееся к формату ELF.
Основные функции контроля целостности процесса стали ещё более простыми (по сравнению с реализацией под Windows), рассмотрим их ниже.

/* Функция подсчета контрольной суммы региона памяти */
int compute_checksum(struct task_struct * tsk, struct mm_struct * mm, struct vm_area_struct * vma)
{
    struct page ** pages;
    uint32_t crc32;
    int i, nr_pages;
    void * va;
    
    /* Получаем список страниц в которых размещен необходимый регион памяти */
    pages = get_pages(tsk, mm, vma, &nr_pages);
    if(pages == NULL)
        return 0;
    
    /* Предварительная инициализация CRC */
    init_crc(&crc32);
    
    /* Проходимся по списку страниц, получаем виртуальный адрес страницы */
    /* (в зависимости от типа страницы (highmem - lowmem) возвращается либо логический адрес, либо производится маппинг) */
    /* и считаем контрольную сумму */
    for(i = 0; i < nr_pages; i++) { va = kmap(pages[i]); crc32buf(&crc32, va, PAGE_SIZE); kunmap(pages[i]); } /* Освобождаем страницы */ release_pages(pages, nr_pages); return crc32; } /* Функция получения перечня страниц, содержащих заданный регион памяти */ struct page ** get_pages(struct task_struct * tsk, struct mm_struct * mm, struct vm_area_struct * vma, int * nr_pages) { int error; unsigned long start, end; struct page ** pages; /* Определяем количество страниц для указанного диапазона адресов */ /* Копипаст из исходников линукса, точнее из videobuf-dma-sg.c */ start = (vma->vm_start & PAGE_MASK) >> PAGE_SHIFT;
    end = ((vma->vm_end - 1) & PAGE_MASK) >> PAGE_SHIFT;
    
    *nr_pages = end - start + 1;
    /* Выделяем память под перечень страниц */
    pages = kmalloc(*nr_pages * sizeof(struct page *), GFP_KERNEL);
    
    if(pages == NULL)
    {
        CKSMLOG("(%d) kmalloc\n", __LINE__);
        return NULL;
    }
    
    CKSMLOG("(%d) start=0x%lx, end=0x%lx, nr_pages=%d\n", __LINE__, start, end, *nr_pages);
    
    /* Захватываем семафор для чтения */
    down_read(&mm->mmap_sem);
    
    /* Таки получаем список страниц */
    /* Про функцию детальнее можно почитать в манах или, например, тут http://www.makelinux.net/ldd3/chp-15-sect-3 */
    error = get_user_pages(tsk, mm, vma->vm_start & PAGE_MASK, *nr_pages, 0, 0, pages, NULL);
    
    /* Отпускаем семафор */
    up_read(&mm->mmap_sem);
    
    if(error != *nr_pages)
    {
        CKSMLOG("(%d) get_user_pages (%d ~ %d)\n", __LINE__, *nr_pages, error);
        return NULL;
    }
    
    return pages;
}

/* Освобождаем страницы и память, выделенную под список страниц */
void release_pages(struct page ** pages, int nr_pages)
{
    int i;
    
    for(i = 0; i < nr_pages; i++)
        if(pages[i])
            put_page(pages[i]);
        
    kfree(pages);
}

Перейдем к паре оставшихся основых функций, которые оперируют со списком контрольных сумм и осуществляют обход памяти процесса.

int process_entry(int pid, unsigned long start, unsigned long end, uint32_t crc32)
{
    int error = 0;
    memory_list * entry;
    
    /* Ищем запись по PID пользовательского процесса и диапазону виртуальных адресов */
    entry = find_entry(&cksm_mlist, pid, start, end);
    
    /* Если записи нет - добавляем, если есть и CRC совпадает, то все нормально, если нет, то сообщаем об ошибке */
    /* Также выставляем флаг присутствия в памяти для того, чтобы потом очистить список от старых записей */
    if(entry == NULL)
    {
        CKSMLOG("(%d) adding entry (PID=%d; vm_start=0x%lx; vm_end=0x%lx; crc32=%08X)\n", __LINE__, pid, start, end, crc32);
        add_entry(&cksm_mlist, pid, start, end, crc32);
    }
    else if(entry->crc32 == crc32)
    {
        CKSMLOG("(%d) checksum validated (PID=%d; vm_start=0x%lx; vm_end=0x%lx; crc32=%08X)\n", __LINE__, pid, start, end, crc32);
        entry->is_present = 1;
    }
    else
    {
        CKSMLOG("(%d) erroneous checksum (PID=%d; vm_start=0x%lx; vm_end=0x%lx; crc32=%08X~%08X)\n", __LINE__, pid, start, end, crc32, entry->crc32);
        entry->is_present = 1;
        error = 1;
    }
    
    return error;
}

int process_memory(void)
{
    struct task_struct * task;
    struct mm_struct * mm;
    struct vm_area_struct * vma;
    uint32_t crc32 = 0;
    int error = 0, is_monitored;
   
    CKSMLOG("(%d) process_memory\n", __LINE__);
    
    /* Получаем указатель на task_struct для вызывающего процесса */
    task = get_current();
    if(task == NULL)
    {
        CKSMLOG("(%d) get_current - failed\n", __LINE__);
        return 1;
    }
    
    CKSMLOG("(%d) task=%p, process=%s[%d]\n", __LINE__, task, task->comm, task->pid);
    
    mm = task->mm;
    
    /* Обходим регионы памяти процесса */
    for(vma = mm->mmap; vma; vma = vma->vm_next)
    {
        /* Проверяем атрибуты региона. Нас интересуют только исполняемые участки. */
        is_monitored = (vma->vm_flags & VM_READ) && (vma->vm_flags & VM_EXEC);
        
        if(!is_monitored)
            continue;
        
        /* Вычисляем контрольную сумму региона и проверяем её */
        crc32 = compute_checksum(task, mm, vma);
        error = process_entry(task->pid, vma->vm_start, vma->vm_end, crc32);
    }
    
    /* Удаляем из списка регионы, которые не присутствуют в памяти процесса */
    del_unused_entries(&cksm_mlist, task->pid);
    /* Сбрасываем флаг присутствия для оставшихся регионов */
    zero_is_present(&cksm_mlist, task->pid);
    
    return error;
}

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

void cksm_init(void)
{
    init_list(&cksm_mlist);
}

void cksm_fini(void)
{
    clean_list(&cksm_mlist);
}

int cksm_check_user()
{
    return process_memory();
}

Теперь воспользуемся статьей, ссылку на которую я приводил в самом начале, и напишем простой модуль-заготовку для тестирования вышеописанных функций.

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <asm/uaccess.h>

#include "cksm_func.h"

#define DEV_NAME "testdev"

#define IOCTL_SAMPLE _IOWR(800, 0, int)

static int device_open(struct inode *, struct file *);
static int device_release(struct inode *, struct file *);
static long device_ioctl(struct file * file, unsigned int ioctl_num, unsigned long ioctl_param);

static int major;
static int is_dev_open = 0;

static struct file_operations fops =
{
    .open = device_open,
    .unlocked_ioctl = device_ioctl,
    .release = device_release
};


static long device_ioctl(struct file * file, unsigned int ioctl_num, unsigned long ioctl_param)
{
    int result;
    
    switch(ioctl_num)
    {
        case IOCTL_SAMPLE:
            printk(KERN_ALERT "IOCTL_SAMPLE\n");
            
            result = cksm_check();
            
            put_user(result, (int *)ioctl_param);
        break;
    }

    return 0;
}

static int __init mymodule_init(void)
{
    printk("Module init\n");
    
    major = register_chrdev(0, DEV_NAME, &fops);
    if(major < 0)
    {
        printk(KERN_ALERT "register_chrdev (%d)\n", major);
        return major;
    }
    
    cksm_init();

    printk(KERN_INFO "'mknod /dev/%s c %d 0'\n", DEV_NAME, major);    

    return 0;
}

static void __exit mymodule_exit(void)
{
    printk("Module exit\n");
    
    unregister_chrdev(major, DEV_NAME);
    
    cksm_fini();
}

static int device_open(struct inode * inode, struct file * file)
{
    printk("Device open\n");
    
    if(is_dev_open)
        return -EBUSY;

    is_dev_open++;
    
    try_module_get(THIS_MODULE);

    return 0;
}

static int device_release(struct inode * inode, struct file * file)
{
    printk("Device release\n");
    
    is_dev_open--;
    module_put(THIS_MODULE);

    return 0;
}

module_init(mymodule_init);
module_exit(mymodule_exit);

MODULE_LICENSE("GPL");

Ещё нам понадобится небольшая тестовая программа, которая будет общаться с нашим модулем. С помощью неё и GDB мы проведем тестирование модуля. Исходный код:

#include 
#include 
#include 
#include 
#include <sys/ioctl.h>
#include <sys/types.h>

/* Определяем IOCTL команду по аналогии с модулем */
#define IOCTL_SAMPLE _IOWR(800, 0, int)

main(int argc, char ** argv)
{
    int i, fd, ret, buf;
    /* Открываем хендл для общения с модулем */
    fd = open("/dev/testdev", 0);
    if(fd < 0)
    {
        printf("(%d) fail\n", __LINE__);
        exit(-1);
    }
    /* Шлем запрос на проверку */
    for(i = 0; i < 2; i++)
    {
        ret = ioctl(fd, IOCTL_SAMPLE, &buf);
        
        if(ret < 0)
        {
            printf("(%d) fail - %d\n", __LINE__, ret);
            break;
        }
        
        printf("Result: %d\n", buf);
    }
    /* Закрываем хендл */
    close(fd);
}

Скомпилируем тестовую программу с помощью GCC

gcc sample_ioctl.c -g -o sample_ioctl

Модуль соберем с помощью простого мейкфайла:

obj-m += test.o
test-objs := crc32.o cksm_list.o cksm_func.o sample.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Установим модуль в систему с помощью modprobe или скажем insmod (в успешной установке модуля можем убедиться с помощью lsmod).

Выполним команду, которую модуль вывел в системный лог (можно посмотреть с помощью dmesg), чтобы создать «специальный файл» для общения с нашим модулем. Запустим программу под GDB и поставим брейкпоинт в цикле.

Контрольные суммы посчитаны, всё нормально.

Теперь поменяем один байт в памяти процесса и продолжим выполнение программы.

Контрольная сумма не совпала и модуль ядра сообщил нам об этом.

Исходный код полностью: integrity-module-linux