В очередной раз решил попробовать написать драйвер (aka модуль ядра), но на этот раз под Linux. Задача, выполняемая модулем, будет аналогична рассмотренной ранее в примере драйвера под Windows.
Я не буду приводить описание методов работы со списками, которые используются в коде, так как они довольно просты, хорошо документированы (например, тут) и не слишком отличаются по логике от тех, которые доступны в Windows. Также за бортом останется детальное описание базового прототипа модуля, который будет использован для тестирования реализации функций контроля целостности. Хорошую развернутую статью про основы разработки модулей ядра под Linux можно почитать тут.
Перейдем непосредственно к коду. Как и раньше, начну с формата, в котором хранятся контрольные суммы областей памяти процесса.
1 2 3 4 5 6 7 8 9 |
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), рассмотрим их ниже.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
/* Функция подсчета контрольной суммы региона памяти */ 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); } |
Перейдем к паре оставшихся основых функций, которые оперируют со списком контрольных сумм и осуществляют обход памяти процесса.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void cksm_init(void) { init_list(&cksm_mlist); } void cksm_fini(void) { clean_list(&cksm_mlist); } int cksm_check_user() { return process_memory(); } |
Теперь воспользуемся статьей, ссылку на которую я приводил в самом начале, и напишем простой модуль-заготовку для тестирования вышеописанных функций.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
#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 мы проведем тестирование модуля. Исходный код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <unistd.h> #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:
1 |
gcc sample_ioctl.c -g -o sample_ioctl |
Модуль соберем с помощью простого мейкфайла:
1 2 3 4 5 6 7 8 |
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
Во, отлично, молодцы! Давно ждал чего-нить под Линукс. Да и вообще - что-то заглох Ваш блог. Может скрипт сделать кастомизации и сборке своего дистра на основе Gentoo Hardened?
Он не заглох, просто мы были на отдыхе какое-то время. Что-то всегда делается, вот например две статьи появилось сразу почти)
Здорово. DX, планируется ли сделать акцент на информационную безопасность, в частности на криптографию? Какие вообще планы по блогу и статьям - тематика и тд. Если можно конечно. Просто интересно, что сейчас планируется.
Спасибо.
Конкретно ничего не планируется заранее. Приходит идея, пишем) Из планов только пятый квест, больше ничего. На криптографию упор вряд ли будет делаться, разве что если что-то интересное в этой области попадется.
А официальный анонс пятого квеста будет?)
Пока еще ничего не делалось, рано, только идеи с Каими собираем)
DX, поясни, а почему ты не видишь интереса в криптографии и ИБ? Может и мне не стоит туда засматриваться.
Спасибо.
ИБ это слишком широкое понятие. Контроль целостности программы под linux от каими - это тоже ИБ) Защита или взлом веб-страниц - тоже ИБ.
А вот криптография лично мне не очень интересна потому, что там уже всё и так изобретено до нас. Если хочется придумать какой-то новый алгоритм, то нужно иметь соответствующие знания. Да и даже если придумаешь что-то стоящее, кому это надо будет? Все ведь уже используют RSA, ГОСТы всякие... Не хочу сказать, что это бесполезно, но очень уж узко.
Понял.
Скажи, а ты сам Linux используешь? Может посвятить статью кастомизации дистрибутива под свои нужды? Выгрузка в память LiveCD из шифрованного криптоконтейнера и тд. Неужели совсем неинтересно?
Линукс использую по рабочим нуждам, и моё сугубо личное мнение: десктопный линукс - полное говно по многим причинам) Не хочу разводить тут холивары, поэтому подчеркиваю, что это только мое мнение.
В последнее время, когда что-то программирую, обычно пишу так, чтобы получалось сразу кроссплатформенно (т.е. и под linux тоже), если это не конкретно windows-related. Кастомизациями дистрибутивов никогда не занимался и думаю, что и не придется)
А мы вот , с преподавателем из Новосибирска придумали свой алгоритм , теперь ждем патент ;)
Ну так изложи суть алгоритма, интересно да и порадоваться за соотечественников тоже хочется.
Из знаний - математика первых трех четырех курсов Шерсть-Мата :)
DX, я конечно согласен, что линукс для декстопа недопилен, да и самому приходится на виртуалке держать винду для Корелла и прочих редакторов. Только для меня важна безопасность, поэтому и перешел на линь. Да, согласен - красноглазить сам не люблю, но порой приходиться порыться в консоли. А кастомизация - ну это отдельная тема, мне кажется, что никому не помешает свой собранный и настроенный под себя дистрибутив, базирующийся в криптоконтейнере, работающий с флешки и напичканный нужным софтом. Меня на это вдохновил Liberte Linux, который к сожалению канул в небытие. Потом пересобрал дистр на дебиане, поместил образ в криптоконтейнер, прикрутил Grub, напичкал офисом, прозрачной торификацией, i2p, gpg, otr и несколько эрайз-скриптов написал. Собственно не сложно все, но мои знания далеки от твоих и знаний Каими, поэтому и обратился, подумав, что может Вам это тоже интересно и уделите внимание.
Проблема любой такой сборки в том, что через годик устареет софт, ядро и прочее, в текущем софте появятся баги плюс никто не даст гарантий, что в сборке нет бэкдора или чего-нибудь эдакого, не говоря уже о том, что каждый предпочитает свой софт.
И уж тем более непонятно зачем все эти торы и криптоконтейнеры, АНБ домой постучится? Прям какая-то сборка для wanna be кардера, судя по описанию.
А вот и напрасно ты так думаешь. Я, к примеру, к ИТ отношения не имею, но в силу некоторых рабочих моментов вынужден иметь криптодистр. Кстати, тоже на Debian. К кардингу и прочей ерунде отношусь отрицательно. А дистр настраиваю редко, т.к. использую stable-релизы. На счет АНБ, ну придут и? Флешка выдергивается, питание отключается, сама ось в контейнере, раздел зашифрован, плюс все данные нарыт EncFS. Я им ее сам отдам - пусть дешифровывают сколько их душе угодно)
Ну, хреново так жить. Придут и снимут ключи из оперативной памяти выключенного компьютера.
Kaimi, я не знаю, какой у него дистрибутив, если человек далекий от IT, но сейчас почти любой дистр при внезапном вытаскивании USB-накопителя из компьютера немедленно приводит к инициированию выключения питания. В процессе выполнения завершения работы в штатном режиме вся память очищается с целью защиты от атак методом холодной перезагрузки, т.ч. снять ничего из памяти не получится.
Теперь на счет меня - я не кардер, я простой параноик))
>>Проблема любой такой сборки в том, что через годик устареет софт, ядро и прочее, в текущем софте появятся баги
LTS релизы решают.
>>плюс никто не даст гарантий, что в сборке нет бэкдора или чего-нибудь эдакого
Ну на счет этого все просто - делается скрипт, который качает исходники той же Hardened Gentoo и оф.репозитория и начинает компиллить ее - все просто и более, чем надежно, когда сорцы приложений и самой оси собираются на глазах. А вот самое главное - это конечно реализация скрипта, чего увы я сам не могу сделать.
DX, Kaimi, а для OS X существует джойнер *.app файлов, наподобие джойнера *.exe в windows?
Без понятия, гугл в помощь
гугл тоже не знает :(
Я вот увоил эффективность эврики на статистике запросов по старинке буду должен еще ? допилить матросы.