Перехват системных вызовов linux

Перехват системных вызовов Linux с помощью LSM

Недавно поступили такие задачи: собрать ядро Linux, написать для него модуль и с его помощью перехватывать системные вызовы. И если первые две я выполнил без проблем, то в процессе выполнения третьей у меня возникло впечатление, что работа с системными вызовами вышла из моды лет 10 назад.

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

Начальные условия

  • 4 ядра процессора Intel Core i7
  • 4 Гб оперативной памяти + 4 Гб swap
  • Ubuntu 16.10 x64 на виртуальной машине VirtualBox 5.1.10
  • Ядро Linux 4.9.0
  • gcc 6.2.0

Для редактирования конфигурации ядра в псевдографическом режиме нужен ncurses:

Сборка чистого ядра

Я рекомендую собрать чистое ядро прежде, чем начать разработку модулей. На это есть 2 причины:

    Первая сборка ядра — довольно продолжительный процесс. Чаще всего он длится от 20 минут до 3 часов. Если же провести сборку заранее, вы получите большую часть бинарников ядра, которые не будут нуждаться в перекомпиляции. Это позволит полностью сосредоточиться на разработке модуля, не мучаясь в ожидании ответа на вопрос “Запустится ли мой первый Hello World?”

  • Успешно собрав чистое ядро, вы убедитесь, что на этом этапе нет проблем и что можно приступать к следующему. Иногда загрузка со свежесобранным ядром может быть неуспешной, и, если вы собирали его вместе с модулем, сложно будет понять, что именно положило систему.
  • Итак, сборка ядра:

      Скачиваем архив с исходниками:

    где x.x.x — версия ядра.

    Либо можно скачать архив руками с kernel.org

    Извлекаем данные из архива:

    Переходим в только что распакованную папку:

    Генерируем конфигурацию ядра по умолчанию:

    Запускаем непосредственно сборку ядра и модулей:

    Сборка будет длиться от 20 минут до 3х часов.

    Где x — количество ядер процессора + 1. То есть в моём случае x = 5.
    Такое значение рекомендуют установить во всех руководствах, но на самом деле значение можно установить любое. Я решил “увеличить количество ядер вдвое”, то есть запустить сборку c параметром -j 9. Это не ускоряет сборку в 2 раза, но увеличивает конкурентоспособность процессов сборки по отношения ко всем другим процессам в системе.

    Кроме того, в системном мониторе(gnome-system-monitor) всем make-процессам я установил максимальный приоритет. Система после этого буквально зависла, но сборка прошла за 6 минут. Используйте этот метод на свой страх и риск.

    После успешной сборки нужно установить всё то, что мы собрали. Это требует root-прав.

    Установка непосредственно ядра:

    Команды установки должны сгенерировать начальный RAM-диск и обновить grub. Если вдруг начальный RAM-диск не сгенерировался — система с новым ядром не запустится.

    Проверить это можно по наличию файла «/boot/initrd.img-x.x.x» (x.x.x — версия ядра)
    Если файла не обнаружилось — генерируем его руками:

    Обновляем загрузчик grub:

    Готово! После перезапуска система запустится с новым ядром. Проверить текущую версию ядра:

    Если вдруг что-то пошло не так, и система не загружается с новым ядром, перезагрузите компьютер, в меню grub перейдите в advanced options и выберите другую версию ядра(ту, под которой вы загружались раньше, обычно у версий по умолчанию добавляют суффикс -general)

    Создание модуля

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

    Читайте также:  Mac os для леново

      Ядро не имеет доступа к стандартным библиотекам языка C. Причина этого – скорость выполнения и объем кода. Часть функций, однако, можно найти в исходниках ядра. Например, обычные функции работы со строками описаны в файле lib/string.c

    Отсутствие защиты памяти. Если обычная программа предпринимает попытку некорректного обращения к памяти, ядро может аварийно завершить процесс. Если ядро предпримет попытку некорректного обращения к памяти, результаты будут менее контролируемыми. К тому же ядро не использует замещение страниц: каждый байт, используемый в ядре, – это один байт физической памяти.

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

  • Фиксированный стек(причём довольно небольшой). Именно поэтому не рекомендуется использовать рекурсию в ядре.
  • Hello world!

    Давайте на конкретном примере рассмотрим “Hello world” в виде модуля ядра. Создадим файл hello.c в любой удобной для вас папке:

    Обычная пользовательская программа начинается с вызова функции main() и работает, пока не возвратит системе какое-то значение.

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

    Функции, которые обрабатывают эти события — соответственно

    static int __init myinit(void)
    static void __exit myexit(void)

    Они отмечены макросами __init, __exit и зарегистрированы с помощью module_init и module_exit как обработчики событий. Название этих функций может быть любым, но не должно конфликтовать с другими функциями в ядре.

    Поскольку ядро не использует стандартную библиотеку C, мы не можем использовать stdio.h. Вместо этого мы подключаем файл kernel.h, в котором реализована функция printk. Эта функция аналогична printf с тем лишь отличием, что выводит сообщения не в окно терминала, а в системный лог (/var/log/syslog).

    В этот лог пишется очень много сообщений со всей системы, поэтому наши нужно пометить каким-то оригинальным «>тегом , чтобы потом с помощью утилиты grep можно было выделить только сообщения нашего модуля.

    Ещё одна непонятная строчка — MODULE_LICENSE(«GPL»);

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

    Сборка

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

    После создания Makefile переходим непосредственно к сборке:

    Через пару секунд в нашей папке появится файл hello.ko — готовый скомпилированный модуль.

    Загрузка и выгрузка

    Существует 2 способа загрузки модуля в ядро:

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

  • Динамическая загрузка в уже запущенной системе. Вышеописанный способ создания модуля предполагает именно такой способ загрузки. В этом случае загрузка модуля больше похожа на запуск обычной пользовательской программы.
  • Загрузить модуль:

    Команда insmod загружает модуль в пространство ядра, тем самым вызывая функцию инициализации.

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

    В функции инициализации мы добавили вызов printk, который выводит в системный лог наше сообщение.

    Для просмотра системного лога существует утилита dmesg:

    Вышеуказанная команда выведет

    После того, как мы загрузили модуль, он так и останется висеть в ядре до тех пор, пока его не выгрузят. Чтобы сделать это:

    Эта команда вызовет обработчик события __exit, но поскольку у нас там пустая функция, кроме выгрузки модуля из ядра ничего не произойдёт.

    Для того, чтобы каждый раз не вводить 2 команды для загрузки и выгрузки модуля во время отладки, в функции инициализации возвращают значение -1. Такой модуль при попытке загрузки выводит в терминал ошибку, после чего прекращает работу, но при этом функция инициализации отрабатывает полностью и корректно, превращаясь, по сути, в аналог функции main() пользовательских программ.

    Читайте также:  Gdb установить astra linux

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

    Перехват системных вызовов

    Небезопасный способ

    Когда-то давно, ещё до ядра версии 2.6, для того, чтобы перехватить системный вызов, писали функцию-хук, которая её заменяла: выполняла другой код + вызывала непосредственно сам syscall(чтобы не нарушить работоспособность системы).

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

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

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

    Самое ценное для нас в данном фреймворке то, что он реализован через набор заранее предустановленных в ядро хуков (по сути, тот способ, который я описывал выше, но безопасный, потому что ядро заранее рассчитано на наличие таких хуков).

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

    Всё предельно просто. Рассмотрим пример создания модуля безопасности foobar, который перехватывает системный вызов mk_dir.

    Написание кода

      Находим в исходниках ядра папку security, создаём в ней папку для нашего модуля, а в ней — его исходный код foobar.c:

    Файл lsm_hooks.h содержит заголовки тех самых предустановленных хуков, LSM_HOOK_INIT регистрирует соответствие foobar_inode_mkdir() хуку inode_mkdir(), а security_add_hooks() добавляет нашу функцию в общий список пользовательских хуков LSM.

    Таким образом, при каждом вызове mkdir будет вызываться наша функция foobar_inode_mkdir().

    Добавляем заголовок нашей функции в файл “/include/linux/lsm_hooks.h”:

    Все вызовы происходят в исходном файле security.c (далее), этим шагом мы оповещаем его о существовании нашей функции.

    В файле “/security/security.c” находим функцию “int __init security_init(void)” и добавляем в её тело следующий вызов:

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

    Конфигурация сборки

      В папке с нашим модулем(/security/foobar/) создадим файл Kconfig:

    Это создаст пункт меню с нашим модулем.

    Откроем файл /security/Kconfig и добавим следующий текст сразу за строчкой “menu «Security options»»:

    Это добавит наш пункт меню в глобальное меню настроек ядра.

    Создадим Makefile в папке с нашим модулем:

    Откроем Makefile всего раздела безопасности(/security/Makefile) и добавим в него следующие строчки(по аналогии с такими же строчками для других модулей):

    Запустим конфигурирование в псевдографическом режиме:

    Если перейти в подменю “Security options”, первым пунктом мы увидим наш модуль, отмеченный символом “y” (мы установили это значение по умолчанию, когда создавали файл Kconfig), что означает, что мы интегрируем наш модуль непосредственно в код ядра.

    Сборка

    На этом этапе проводим самую обыкновенную сборку ядра, как это было описано в начале статьи. Но поскольку мы уже предварительно собрали чистое ядро, процесс немного упростился:

    make не требует параметра -j, поскольку пересоберёт ядро с нашим модулем за несколько секунд.

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

    Осталось перезагрузить систему, после чего в ядре будет висеть наш модуль с перехватом mkdir. Как и говорил ранее, проверяем так:

    Читайте также:  Ошибка операционная система не найдена при загрузке windows

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

    Надеюсь, кому-то это руководство будет полезным(если бы кто-то написал его вместо меня до того, как я начал копаться в ядре — он сэкономил бы мне 2-3 недели жизни).

    Любая критика приветствуется.
    Спасибо за внимание.

    Источник

    Перехват системных вызовов в linux под x86-64

    Введение

    В интернете опубликовано множество статей по перехвату системных вызовов под x32. В рамках решения одной задачи появилась необходимость в перехвате системных вызовов под архитектурой x86-64 при помощи загружаемого модуля ядра. Приступим:

    Перехват системных вызовов

    Алгоритм:

    • Поиск адреса таблицы системных вызовов
    • Подмена на адреса новых системных вызовов
    Поиск адреса таблицы системных вызовов

    Первый вариант: можно найти через таблицу дескрипторов прерываний (IDT), IDT — служит для связи обработчика прерывания с номером прерывания. В защищённом режиме адрес в физической памяти и размер таблицы прерываний определяется 80-битным регистром IDTR.В защищённом режиме элементом IDT является шлюз прерывания длиной 10 байт, содержащий сегментный (логический) адрес обработчика прерывания, права доступа и др. Нам такой метод не интересен, т.к. мы получим адрес обработчика, который сделан для совместимости с х32

    Второй вариант, более интересен.

    Для начала не большой экскурс: MSR – machine state register это набор регистров процессоров Интел, используемых в семействе x86 и x86-64 процессоров. Эти регистры предоставляют возможность контролировать и получать информацию о состоянии процессора. Все MSR регистры доступны только для системных функций и не доступны из пользовательских программ. Нас в частности интересует следующий регистр:MSR_LSTAR — 0xc0000082 (long mode SYSCALL target)
    (полный список можно посмотреть в /usr/include/asm/msr-index.h).
    В этом регистре хранится адрес обработчика прерываний для x86-64.
    Получить адрес можно следующим образом:
    int i, lo, hi;
    asm volatile(«rdmsr» : «=a» (lo), «=d» (hi) : «c» (MSR_LSTAR));
    system_call = (void*)(((long)hi
    Далее найдем адрес самой таблицы. Перейдем на только что полученный адрес и найдем в памяти последовательность \xff\x14\xc5(эти магические числа берутся, если посмотреть на код ядра, в частности, на код функции system_call, в которой происходит вызов обработчика из искомой). Считав следующие за ней 4 байта, мы получим адрес таблицы системных вызовов syscall_table. Зная ее адрес, мы можем получить содержимое этой таблицы (адреса всех системных функций) и изменить адрес любого системного вызова, перехватив его.
    код для нахождения адреса таблицы системных вызовов:
    unsigned char *ptr;
    for (ptr=system_call, i=0; i

    Подмена на адреса новых системных вызовов

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

    • Отключаем защиту памяти
    • Переписываем адрес на адрес нашего обработчика
    • Включаем защиту памяти

    Для снятие и установки защиты необходимо знать следующее: регистр CR0 — содержит системные флаги управления, управляющие поведением и состоянием процессора.Флаг WP — защита от записи (Write Protect), 48-й бит CR0. Когда установлен, запрещает системным процедурам запись в пользовательские страницы с доступом только для чтения (когда флаг WP сброшен — разрешает). В отличие от х32 изменился только размер регистра и номер флага
    код снятия защиты:
    asm(«pushq %rax»);
    asm(«movq %cr0, %rax»);
    asm(«andq $0xfffffffffffeffff, %rax»);
    asm(«movq %rax, %cr0»);
    asm(«popq %rax»);

    код включения защиты:
    asm(«pushq %rax»);
    asm(«movq %cr0, %rax»);
    asm(«xorq $0x0000000000001000, %rax»);
    asm(«movq %rax, %cr0»);
    asm(«popq %rax»);

    Этих знаний достаточно для подмены системных вызовов в Linux x86-64. Надеюсь кому-нибудь это будет полезным.
    Спасибо за внимание.

    Источник

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