Пишем модуль для linux

Пишем модуль безопасности Linux

Linux Security Modules (LSM) — фреймворк, добавляющий в Linux поддержку различных моделей безопасности. LSM является частью ядра начиная с Linux версии 2.6. На данный момент в официальном ядре «обитают» модули безопасности SELinux, AppArmor, Tomoyo и Smack.

Работают модули параллельно с «родной» моделью безопасности Linux — избирательным управлением доступом (Discretionary Access Control, DAC). Проверки LSM вызываются на действия, разрешенные DAC.

Применять механизм LSM можно по-разному. В большинстве случаев это добавление мандатного управления доступом (как, например, в случае с SELinux). Кроме того, можно придумать собственную модель безопасности, реализовать ее в виде модуля и легко внедрить, используя фреймворк. Рассмотрим для примера реализацию модуля, который будет давать права на действия в системе при наличии особого USB-устройства.

Поглядим на схему и попытаемся разобраться, как работает хук LSM (на примере системного вызова open).

Ничего сложного. Главная задача LSM — предоставить модулям безопасности механизм для контроля доступа к объектам ядра (хуки вставлены в ядерный код прямо перед обращениями к объектам). Перед обращением ядра к внутреннему объекту будет обязательно вызвана функция проверки, предоставленная LSM.

Другим словами, LSM дает модулям возможность ответить на вопрос: «Может ли субъект S произвести действие OP над внутренним объектом ядра OBJ?»
Это — очень здорово. Это не sys_call_table хукать по самые указатели.

Разумно для начала написать заготовку модуля безопасности. Он будет очень скромный и будет во всем соглашаться с DAC. Необходимые нам исходники лежат в каталоге security среди исходников ядра.

Копаем исходники

Идем в include/linux/security.h (у меня исходники ядра версии 2.6.39.4). Самое важное здесь – могучая структура security_ops.
Вот её фрагмент:

Это список заранее определенных и документированных callback-функций, которые доступны модулю безопасности для выполнения проверок. По умолчанию эти функции в большинстве своем возвращают 0, тем самым разрешая любые действия. Но некоторые используют модуль безопасности POSIX. Это функции Common Capabilities, с ними можно ознакомиться в файле security/commoncap.c.

Нам в данном случае важна следующая функция из include/linux/security.c:

Пишем заготовку

У меня под рукой дистрибутив BackTrack 5 R1 (версия ядра 2.6.39.4). Взглянем на готовый модуль безопасности, например на SELinux (каталог /security/selinux/). Основной его механизм описан в файле hooks.c. На основе этого файла я создал скелет нового модуля безопасности (чуть позже мы сделаем из него что-нибудь интересное).

Монструозную структуру security_ops заполняем указателями на свои функции. Для этого достаточно у всех функции заменить selinux на название своего модуля (PtLSM в моем примере). Редактируем тела всех функций: возвращающие void делаем пустыми, int должны возвращать 0. В результате получился ничего не делающий LSM, разрешающий все, что разрешает «родной» защитный механизм. (Исходный код модуля: pastebin.com/Cst0VVQh).

Небольшое печальное отступление. Начиная с версии 2.6.24, из соображений безопасности ядро перестало экспортировать символы необходимые для написания модулей безопасности в виде загружаемых модулей ядра (Linux Kernel Module, LKM). Например, исчезла из экспорта функция register_security, которая позволяет зарегистрировать модуль и его хуки. Поэтому будем собирать ядро с нашим модулем.

Создаем каталог с именем модуля PtLSM: /usr/src/linux-2.6.39.4/security/ptlsm/.
Для сборки модуля выполняем следующие действия.

1. Создаем файл Makefile:

2. Создаем файл Kconfig:

config SECURITY_PTLSM
bool «Positive Protection»
default n
help
This module does nothing in a positive kind of way.

If you are unsure how to answer this question, answer N.

3. Редактируем /security/Makefile и /security/Kconfig — чтобы о новом модуле узнал весь мир. Добавляем строки — как у других модулей.

Далее, в каталоге с исходниками ядра делаем make menuconfig, выбираем PtLSM в пункте «Security Options».

Теперь make, make modules_install, make install. Модуль помещен в ядре, и при помощи утилиты dmesg можно посмотреть, что именно он пишет в лог.

Пишем суперкрутой модуль

Пришло время сделать наш модуль суперкрутым! Пусть модуль запрещает делать что-либо на компьютере, если к нему не подключено USB-устройство с заданными Vendor ID и Product ID (в моем примере это будут ID телефона Galaxy S II).

Я изменил тело функции ptlsm_inode_create, которая проверяет, имеет ли тот или иной процесс возможность создавать файлы. Если функция нашла «устройство высшей власти», то даст разрешение на выполнение. Аналогичные проверки можно производить с любыми другими действиями.

Теперь неплохо было бы написать функцию find_usb_device. Она будет анализировать все USB-устройства в системе и отыскивать то, которое имеет нужный ID. Данные о USB-устройствах хранятся в виде деревьев, корни которых называются root hub device. Список всех корней лежит в списке шин usb_bus_list.

Читайте также:  Hp laserjet pro mfp m132nw linux driver

И, наконец, функция match_device, проверяющая Vendor ID и Product ID.

Для работы с USB подключим пару заголовков.

Повторяем действия для вставки модуля — и покупаем крутой телефон, чтобы пользоваться компьютером.

Источник

Пишем простой модуль ядра Linux

Захват Золотого Кольца-0

Linux предоставляет мощный и обширный API для приложений, но иногда его недостаточно. Для взаимодействия с оборудованием или осуществления операций с доступом к привилегированной информации в системе нужен драйвер ядра.

Модуль ядра Linux — это скомпилированный двоичный код, который вставляется непосредственно в ядро Linux, работая в кольце 0, внутреннем и наименее защищённом кольце выполнения команд в процессоре x86–64. Здесь код исполняется совершенно без всяких проверок, но зато на невероятной скорости и с доступом к любым ресурсам системы.

Не для простых смертных

Написание модуля ядра Linux — занятие не для слабонервных. Изменяя ядро, вы рискуете потерять данные. В коде ядра нет стандартной защиты, как в обычных приложениях Linux. Если сделать ошибку, то повесите всю систему.

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

Можно в основном забыть традиционные парадигмы разработки приложений. Кроме загрузки и выгрузки модуля, вы будете писать код, который реагирует на системные события, а не работает по последовательному шаблону. При работе с ядром вы пишете API, а не сами приложения.

У вас также нет доступа к стандартной библиотеке. Хотя ядро предоставляет некоторые функции вроде printk (которая служит заменой printf ) и kmalloc (работает похоже на malloc ), в основном вы остаётесь наедине с железом. Вдобавок, после выгрузки модуля следует полностью почистить за собой. Здесь нет сборки мусора.

Необходимые компоненты

Прежде чем начать, следует убедиться в наличии всех необходимых инструментов для работы. Самое главное, нужна машина под Linux. Знаю, это неожиданно! Хотя подойдёт любой дистрибутив Linux, в этом примере я использую Ubuntu 16.04 LTS, так что в случае использования других дистрибутивов может понадобиться слегка изменить команды установки.

Во-вторых, нужна или отдельная физическая машина, или виртуальная машина. Лично я предпочитаю работать на виртуальной машине, но выбирайте сами. Не советую использовать свою основную машину из-за потери данных, когда сделаете ошибку. Я говорю «когда», а не «если», потому что вы обязательно подвесите машину хотя бы несколько раз в процессе. Ваши последние изменения в коде могут ещё находиться в буфере записи в момент паники ядра, так что могут повредиться и ваши исходники. Тестирование в виртуальной машине устраняет эти риски.

И наконец, нужно хотя бы немного знать C. Рабочая среда C++ слишком велика для ядра, так что необходимо писать на чистом голом C. Для взаимодействия с оборудованием не помешает и некоторое знание ассемблера.

Установка среды разработки

На Ubuntu нужно запустить:

Устанавливаем самые важные инструменты разработки и заголовки ядра, необходимые для данного примера.

Примеры ниже предполагают, что вы работаете из-под обычного пользователя, а не рута, но что у вас есть привилегии sudo. Sudo необходима для загрузки модулей ядра, но мы хотим работать по возможности за пределами рута.

Начинаем

Приступим к написанию кода. Подготовим нашу среду:

Запустите любимый редактор (в моём случае это vim) и создайте файл lkm_example.c следующего содержания:

Мы сконструировали самый простой возможный модуль, рассмотрим подробнее самые важные его части:

  • В include перечислены файлы заголовков, необходимые для разработки ядра Linux.
  • В MODULE_LICENSE можно установить разные значения, в зависимости от лицензии модуля. Для просмотра полного списка запустите:
  • Мы устанавливаем init (загрузка) и exit (выгрузка) как статические функции, которые возвращают целые числа.
  • Обратите внимание на использование printk вместо printf . Также параметры printk отличаются от printf . Например, флаг KERN_INFO для объявления приоритета журналирования для конкретной строки указывается без запятой. Ядро разбирается с этими вещами внутри функции printk для экономии памяти стека.
  • В конце файла можно вызвать module_init и module_exit и указать функции загрузки и выгрузки. Это даёт возможность произвольного именования функций.
  • Впрочем, пока мы не можем скомпилировать этот файл. Нужен Makefile. Такого базового примера пока достаточно. Обратите внимание, что make очень привередлив к пробелам и табам, так что убедитесь, что используете табы вместо пробелов где положено.

    Если мы запускаем make , он должен успешно скомпилировать наш модуль. Результатом станет файл lkm_example.ko . Если выскакивают какие-то ошибки, проверьте, что кавычки в исходном коде установлены корректно, а не случайно в кодировке UTF-8.

    Читайте также:  Сжать том windows 10 больше

    Теперь можно внедрить модуль и проверить его. Для этого запускаем:

    Если всё нормально, то вы ничего не увидите. Функция printk обеспечивает выдачу не в консоль, а в журнал ядра. Для просмотра нужно запустить:

    Вы должны увидеть строку “Hello, World!” с меткой времени в начале. Это значит, что наш модуль ядра загрузился и успешно сделал запись в журнал ядра. Мы можем также проверить, что модуль ещё в памяти:

    Для удаления модуля запускаем:

    Если вы снова запустите dmesg, то увидите в журнале запись “Goodbye, World!”. Можно снова запустить lsmod и убедиться, что модуль выгрузился.

    Как видите, эта процедура тестирования слегка утомительна, но её можно автоматизировать, добавив:

    в конце Makefile, а потом запустив:

    для тестирования модуля и проверки выдачи в журнал ядра без необходимости запускать отдельные команды.

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

    Немного интереснее

    Копнём чуть глубже. Хотя модули ядра способны выполнять все виды задач, взаимодействие с приложениями — один из самых распространённых вариантов использования.

    Поскольку приложениям запрещено просматривать память в пространстве ядра, для взаимодействия с ними приходится использовать API. Хотя технически есть несколько способов такого взаимодействия, наиболее привычный — создание файла устройства.

    Вероятно, раньше вы уже имели дело с файлами устройств. Команды с упоминанием /dev/zero , /dev/null и тому подобного взаимодействуют с устройствами “zero” и “null”, которые возвращают ожидаемые значения.

    В нашем примере мы возвращаем “Hello, World”. Хотя это не особенно полезная функция для приложений, она всё равно демонстрирует процесс взаимодействия с приложением через файл устройства.

    Вот полный листинг:

    Тестирование улучшенного примера

    Теперь наш пример делает нечто большее, чем просто вывод сообщения при загрузке и выгрузке, так что понадобится менее строгая процедура тестирования. Изменим Makefile только для загрузки модуля, без его выгрузки.

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

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

    (в этом примере замените MAJOR значением, полученным в результате выполнения make test или dmesg )

    Параметр c в команде mknod говорит mknod, что нам нужно создать файл символьного устройства.

    Теперь мы можем получить содержимое с устройства:

    или даже через команду dd :

    Вы также можете получить доступ к этому файлу из приложений. Это необязательно должны быть скомпилированные приложения — даже у скриптов Python, Ruby и PHP есть доступ к этим данным.

    Когда мы закончили с устройством, удаляем его и выгружаем модуль:

    Заключение

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

    Просто помните, что в пространстве ядра всё под вашу ответственность. Там для вашего кода нет поддержки или второго шанса. Если делаете проект для клиента, заранее запланируйте двойное, если не тройное время на отладку. Код ядра должен быть идеален, насколько это возможно, чтобы гарантировать цельность и надёжность систем, на которых он запускается.

    Источник

    Пишем модуль ядра Linux: I2C

    Данная статья посвящена разработке I2C (Inter-Integrated Circuit) модуля ядра Linux. Далее описан процесс реализация базовой структуры I2C драйвера, в которую можно легко добавить реализацию необходимого функционала.

    Опишем входные данные: I2C блок для нового процессора «зашитый» на ПЛИС, запущенный Linux версии 3.18.19 и периферийные устройства (EEPROM AT24C64 и BME280).

    Принцип работы I2C достаточно прост, но если нужно освежить знания, то можно почитать тут.


    Рисунок 1. Временная диаграмма сигналов шины I2C

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

    1. Реализуем небольшое user space приложение, цель которого прочитать уникальный ID регистра I2C устройства. Данный шаг позволит понять интерфей, через который происходит обмен между модулем ядра и пользовательским приложением;
    2. Познакомимся с вариантом передачи I2C сообщений модулем ядра;
    3. Добавим модуль ядра в сборку и опишем аппаратную часть устройств в device tree;
    4. Реализуем общую структуру (скелет) I2C драйвера с небольшими пояснениями.

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

    Читайте также:  Linux iptables запретить доступ

    Шаг первый

    Для начала познакомимся с утилитой i2cdetect. Результат работы i2cdetect выглядит следующим образом:

    Утилита последовательно выставляет на I2C шину адреса устройств и при получении положительного ответа (в данном случае положительным ответом является ACK) выводит в консоль номер адреса устройства на шине.

    Напишем небольшую программку, которая считывает уникальный ID датчика температуры, и выведем результат ее работы в консоль. Выглядит очень просто:

    Становятся понятно что модуль ядра принимает данные в виде полей сообщения i2c_rdwr_ioctl_data. Структура содержит такие поля как i2c_msg и nmsgs, которые используется для передачи:

    • .addr — адреса устройства;
    • .flags — типа операции (чтение или запись);
    • .len — длины текущего сообщения;
    • .buf- буфера обмена.

    Шаг второй

    Теперь, не углубляюсь во внутренности, познакомимся с одним вариантом работы I2C драйвера.
    Как уже было установлено, модуль ядра получает сообщения в виде структуры. Для примера рассмотрим алгоритм работы драйвера при выполнении операции записи (аппаратно-зависимая часть):

    • Сначала заполняется TX FIFO: первым идет адрес устройства, а после оставшиеся данные на передачу;
    • Очищается статусный регистр прерывания ISR и разрешаются прерывания в регистре IER (в данном случае прерывание возникающее при отсутствии данных в TX FIFO);
    • Разрешается передача данных и устанавливается старт бит на шине.

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

    Шаг третий

    Добавим модуль ядра в сборку и опишем аппаратную часть устройств в device tree:

    1. Создадим source файл в следующей директории:

    В результате появится файл:

    2. Добавим конфигурацию драйвера в drivers/i2c/busses/Kconfig:

    3. Добавим в сборку драйвер drivers/i2c/busses/Makefile:

    4. Добавим в devicetree (*.dts) описание I2C блока, а также сразу поддержу eeprom устройства:

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

    Шаг четвертый

    После ознакомления с принципом работы драйвера приступим к реализации.
    Сначала подключим заголовочные файлы, опишем «виртуальную» регистровую карту, а также представление драйвера I2C.

    Главными управляющими регистрами контроллера являются:

    • Control Register (CTRL) — регистр управления;
    • Interrupt Status Register (ISR) — статусный регистр прерывания;
    • Interrupt Enable Register (IER) — регистр маски прерывания.

    Сердцем драйвера является структура skel_i2c, которая содержит такие поля как:

    • .base — указатель на начало регистровой карты;
    • .msg — указатель на текущее сообщение;
    • .adap — I2C абстракция (клик).

    Перейдем к более практической части, опишем типы поддерживаемых драйвером устройств,
    функционал I2C адаптера и интерфейс передачи I2C сообщений:

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

    • skel_i2c_driver — описывает имя драйвера, таблицу поддерживаемых устройств и функций, которые вызываются в момент загрузки или удаления модуля ядра из системы.

    Пора зарегистрировать драйвер в системе, а значит реализовать функцию инициализации контроллера, а также описать skel_i2c_probe (вызывается в момент загрузки драйвера в систему) и skel_i2c_remove (вызывается в момент удаления драйвера из системы).

    Наиболее простой функцией является skel_i2c_remove, которая отключает источник тактовой частоты и освобождает используемую память. Функция skel_i2c_init выполняет первичную инициализацию I2C контроллера.

    Как упоминалось ранее skel_i2c_probe регистрирует драйвер в системе. Последовательность действий, условно, можно разделить на два этапа:

    • Получение системных ресурсов и регистрацию обработчика прерывания skel_i2c_isr;
    • Заполнение полей структуры и вызов процедуры добавления нового I2C адаптера.

    После того как драйвер зарегистрирован в системе, можно реализовать логику передачи сообщений по интерфейсу:

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

    • skel_i2c_xfer — функция напрямую получает сообщения на передачу и последовательно передает каждое сообщение в skel_i2c_xfer_msg. Если во время передачи данных произошла ошибка, то передача данных останавливается;
    • skel_i2c_xfer_msg — функция устанавливает все необходимые поля драйвера и инициирует начало передачи сообщений;
    • skel_i2c_isr — процедура обработки прерывания. Здесь происходит обработка ошибок, а также обмен данными по шине. Если все данные отправлены/приняты устанавливается флаг done с помощью вызова функции complete, которая сигнализирует о завершении передачи сообщения.

    В статье не описаны некоторые тонкости работы. Например, последовательность действий передачи сообщений, так как реализация данного алгоритма является аппаратно зависимой. Мы же сосредоточились на реализации общей части драйвера вне зависимости от аппаратных особенностей контроллера.

    Полный скелет драйвера прикреплен ниже. Пожалуйста, если вы нашли ошибки/неточности, или вам есть что добавить — напишите в ЛС или в комментарии.

    Источник

    Оцените статью