Привет, Хабр! На связи Даниэль из InfoWatch, разработчик решений класса информационной безопасности. В предыдущей статье мы рассматривали задачу контроля целостности в среде Linux с помощью системного интерфейса inotify. Поговорили о ключевых недостатках, с которыми приходится сталкиваться в ходе работы с самим инструментом напрямую.
Наверное, многие из вас знают, что существует другой более совершенный и более функциональный системный интерфейс — fanotify. В рамках данной статьи мы продолжим рассматривать задачу мониторинга объектов файловой системы, но уже через призму fanotify. Попробуем ответить на вопросы, решает ли данный инструмент описанные ранее проблемы.
Введение
Начнём с краткой сводки по fanotify. fanotify представляет собой kernel-интерфейс, позволяющий мониторить события файловой системы в режиме реального времени. Определение такое же, как и для inotify. Почти. Существенная разница в том, что список обрабатываемых событий у fanotify гораздо шире, а также есть возможность принимать решение kernel-характера в user space. Простыми словами: вы можете разрешать/запрещать запуск программ без написания драйверов (модулей ядра), так как fanotify позволяет разрешить/запретить создание процесса в пользовательском пространстве.
Интерфейс прост, всего две функции:
/* Создать и инициализировать fanotify группу. /extern int fanotify_init (unsigned int flags, unsigned int event_f_flags) __THROW;/ Добавить/удалить или модифицировать объект файловой системы. /extern int fanotify_mark (int __fanotify_fd, unsigned int __flags, uint64_t mask, int dfd, const char pathname) THROW;
Тестовое ПО
Необходимый минимум информации у нас есть, можно идти дальше и проводить анализ. Будем его выполнять с помощью набора небольших программ на языке Go.
Чтобы облегчить понимание материала, на каждую рассматриваемую ситуацию, описанную в предыдущей статье, будем создавать свою программу. Так у нас выйдет набор маленьких и простых для чтения утилит.
Говорим о проблемах
Достаточно ли метаданных предоставляет fanotify с точки зрения ИБ?
Да, достаточно.
Событие fanotify описывается структурой fanotify_event_metadata:
struct fanotify_event_metadata { __u32 event_len; u8 vers; u8 reserved; __u16 metadata_len; __aligned_u64 mask; s32 fd; s32 pid;};
В случае с inotify нам не хватало информации о том, какая именно программа (какой процесс ОС) послужила причиной возникновения события, а также не было возможности узнать, какой именно пользователь нарушил целостность. Офицер безопасности узнает, что с конкретным файлом произошло определённое действие. Но в результате чего и кто виновник — это останется неизвестным.
К счастью, в структуре fanotify_event_metadata есть поле pid. С его помощью мы можем определить как пользователя, так и путь к программе-инициатору события.
Определяем пользователя. Нам необходимо прочитать файл /proc/<pid>/status, содержащий информацию о процессе, отыскав в нём строку ‘Uid:’ и взять первое число в ней (реальный UID). Затем, зная UID, можно обратиться к файлу /etc/passwd для сопоставления UID с именем пользователя.
На языке Go функции решения данной задачи могли бы выглядеть так:
// Determ user name due to pidfunc getUsernameByPID(pid int) (string, error) {// 1.Read file /proc/<pid>/statusstatusPath := fmt.Sprintf("/proc/%d/status", pid)data, err := os.ReadFile(statusPath)if err != nil {return "", fmt.Errorf("can't read %s: %w", statusPath, err)}// 2.Find string 'Uid:' and get first value (real UID)var uid int = -1scanner := bufio.NewScanner(bytes.NewReader(data))for scanner.Scan() {line := scanner.Text()if strings.HasPrefix(line, "Uid:") {var real, effective, saved, fs uint32if _, err := fmt.Sscanf(line, "Uid:\t%d\t%d\t%d\t%d", &real, &effective, &saved, &fs); err != nil {return "", err}uid = int(real)break}}if uid == -1 {return "", fmt.Errorf("uid not found")}// 3.Read file /etc/passwdpasswdData, err := os.ReadFile("/etc/passwd")if err != nil {return "", fmt.Errorf("can't read /etc/passwd: %w", err)}// 4.Parse and find user name by UIDreturn findUsernameByUID(passwdData, uid)}// Find name of user by UIDfunc findUsernameByUID(passwdData []byte, uid int) (string, error) {content := string(passwdData)start := 0for start < len(content) {// 1.Find the end of the current rowend := startfor end < len(content) && content[end] != '\n' {end++}line := content[start:end]line = strings.TrimSpace(line)if line == "" || strings.HasPrefix(line, "#") {start = end + 1continue}// Format: name:passwd:uid:gid:gecos:home:shellparts := strings.Split(line, ":")if len(parts) >= 3 {parsedUID, err := strconv.Atoi(parts[2])if err == nil && parsedUID == uid {return parts[0], nil}}start = end + 1}return "", fmt.Errorf("user with UID %d was not found", uid)}
Определяем путь к программе-инициатору. В Linux для каждого процесса в директории /proc/<pid>/ есть символьная ссылка exe, которая указывает на полный путь к исполняемому файлу этого процесса. Можно воспользоваться системным вызовом readlink и получить путь, на который указывает ссылка.
Примерный вид функции:
func getExecutablePathByPID(pid int) (string, error) {exePath := fmt.Sprintf("/proc/%d/exe", pid)path, err := os.Readlink(exePath)if err != nil {return "", fmt.Errorf("can't get path for PID %d: %v", pid, err)}return path, nil}
Собираем демо-программу и тестируем. Итак, теперь у нас достаточно вводных, чтобы написать первую демо-программу. Приводить здесь её код не буду, чтобы не раздувать размер статьи. На первый её «черновик» можно взглянуть тут.
Если запустить программу:
# Для fanotify нужен sudo# Пока в примере используем регулярный файлsudo ./main test.txt
То ничего работать не будет. В стандартном потоке вывода будет написано:
Error while adding test.txt to the fanotify group: invalid argument
Я решил привести первый пример программы в её незавершённом виде неспроста, а чтобы показать, что хоть fanotify и предоставляет больше данных офицеру безопасности для проведения расследования, но в то же время является гораздо более сложным в плане конфигурации.
Проблема кроется в строках:
// В этих двух строках определяется маска интересующих нас событийeventsMask := unix.FAN_MODIFY | unix.FAN_ATTRIB | unix.FAN_DELETE_SELF | unix.FAN_MOVE_SELFerr = unix.FanotifyMark(fanFd, uint(markFlags), uint64(eventsMask), unix.AT_FDCWD, *testFilePath)/*<...>*/// В этой строке происходит создание fanotify группыfanFd, err := unix.FanotifyInit(unix.FAN_CLOEXEC|unix.FAN_CLASS_NOTIF|unix.FAN_NONBLOCK, unix.O_RDONLY|unix.O_LARGEFILE)
Как в случае с inotify, нам интересны следующие события:
-
FAN_MODIFY (модификация объекта)
-
FAN_ATTRIB (изменение прав доступа)
-
FAN_DELETE_SELF (удаление самого объекта)
-
FAN_MOVE_SELF (перемещение самого объекта)
Суть в том, что для одновременного использования всех их нужно вызывать функцию unix.FanotifyInit с правильными аргументами. С более подробным ответом на то, почему так надо делать, можно ознакомиться тут.
Исправление простое:
// Меняем вот эту строчку:fanFd, err := unix.FanotifyInit(unix.FAN_CLOEXEC|unix.FAN_CLASS_NOTIF|unix.FAN_NONBLOCK, unix.O_RDONLY|unix.O_LARGEFILE)// На эту:fanFd, err := unix.FanotifyInit(unix.FAN_CLOEXEC|unix.FAN_REPORT_FID|unix.FAN_NONBLOCK, unix.O_RDONLY|unix.O_LARGEFILE)// Вместо unix.FAN_CLASS_NOTIF нужно применить unix.FAN_REPORT_FID
Однако вывод исправленной программы всё равно не устраивает:
Got event: pid=436 mask=0x2 fd=-1Caused by user: agirreCaused by program: /home/agirre/.vscode-server/bin/8b640eef5a6c6089c029249d48efa5c99adf7d51/nodeGot event: pid=436 mask=0x2 fd=-1Caused by user: agirreCaused by program: /home/agirre/.vscode-server/bin/8b640eef5a6c6089c029249d48efa5c99adf7d51/node
Видим, что файловый дескриптор имеет значение -1, значит мы не можем узнать путь изменённого файла, картина не является полной.
Чтобы сделать второе исправление, необходимо опираться на данный отрывок из официальной документации.
Так как в ходе инициализации fanotify группы используется FAN_REPORT_FID, то fd в fanotify_event_metadata может быть (и очень часто) -1. Использование флага FAN_REPORT_FID сообщает ядру о том, что user space “просит” file ID изменённого объекта (идентификатор файла — в Linux это специальная структура file_handle). Вместо fd ядро дописывает к событию info records (структуры fanotify_event_info_*) сразу после fanotify_event_metadata.
Появляется дополнительная сложность, заключающаяся в правильном чтении данных fanotifyEventInfoFid. Конечный вариант программы по данному параграфу находится тут: https://github.com/Daniel-Ager-Okker/fanotify-article-materials/blob/36c5ca14b63e272738a60bb7cb74fb18c49a6ee0/2.security_metadata_reg_file/util/util.go#L118. Вся соль в аккуратном чтении бинарных данных и их последующем представлении в виде нужных структур.
Запускаем ещё раз и смотрим:
Got event: pid=2259 mask=modification fd=-1Changed path: /home/agirre/fanotify-article-materials/2.security_metadata_reg_file/test.txtCaused by user: agirreCaused by program: /home/agirre/.vscode-server/bin/8b640eef5a6c6089c029249d48efa5c99adf7d51/nodeGot event: pid=2259 mask=modification fd=-1Changed path: /home/agirre/fanotify-article-materials/2.security_metadata_reg_file/test.txtCaused by user: agirreCaused by program: /home/agirre/.vscode-server/bin/8b640eef5a6c6089c029249d48efa5c99adf7d51/node
Отлично, в логе видим путь к изменённому объекту файловой системы, имя напакостившего пользователя и использованную им программу.
Не слишком ли много событий генерирует fanotify?
Все равно слишком много.
Во время использования inotify возникает проблема, которая сказывается именно на обрабатывающей события стороне. Дело в том, что вместо одного ожидаемого события можно получить сразу несколько. Этот недостаток не так очевиден, если рассматривать само ED-приложение (endpoint detection). Но если подойти со стороны SIEM-системы, у которой сотни (или даже тысячи) ED-агентов в качестве источников, то последствия очевидны: резкое увеличение нагрузки на модули корреляции и агрегации.
Выше приведённый лог показывает, что fanotify тоже “генерирует” множество событий вместо одного. Кавычки здесь уместны, так как интерфейс ядра не генерирует эти события сам, а лишь извещает нас о них. Причина кроется в механизмах редактирования файлов со стороны ОС и приложений-редакторов. Чтобы понять, что происходит «под капотом», запустим тестовое ПО с учётом отслеживания директории, а не регулярного файла.
Здесь нам потребуется написать программу, которая будет уметь работать с директориями. Она очень похожа на предыдущий вариант, но с учётом следующих нововведений:
-
расширяем eventMask следующими флагами: unix.FAN_CREATE, unix.FAN_DELETE, unix.FAN_MOVED_FROM, unix.FAN_MOVED_TO, unix.FAN_EVENT_ON_CHILD (файл app.go)
-
в функцию unix.FanotifyInit вносим дополнительно: unix.FAN_REPORT_DIR_FID, unix.FAN_REPORT_NAME (файл app.go)
-
дорабатываем логику функций-обработчиков событий так, чтобы они перехватывали события, связанные с дочерними объектами отслеживаемой директории (файл util.go)
Окончательный вариант: https://github.com/Daniel-Ager-Okker/fanotify-article-materials/tree/main/3.security_metadata_directory
Если не хочется погружаться в код, то обратите внимание на схему расположения структур в памяти в текущем рассматриваемом случае. Ключевая разница: вследствие использования флагов unix.FAN_REPORT_DIR_FID, unix.FAN_REPORT_NAME fanotify дописывает в конец буфера после f_handle нуль-терминированную строку, определяющую имя дочернего объекта, с которым связано событие:
Теперь откроем дочерний файл подконтрольной директории в Microsoft Office Word, изменим его, закроем. С позиции человека ожидается всего одно событие (модификация), видим:
Got eventpid=6mask=createfd=-1Changed path: /home/agirre/fanotify-article-materials/3.security_metadata_directory/test_dirInner object name: ~$ok.txtCaused by user: rootCaused by program: /initGot eventpid=6mask=modificationfd=-1Changed path: /home/agirre/fanotify-article-materials/3.security_metadata_directory/test_dirInner object name: ~$ok.txtCaused by user: rootCaused by program: /initGot eventpid=6mask=modificationfd=-1Changed path: /home/agirre/fanotify-article-materials/3.security_metadata_directory/test_dirInner object name: ~$ok.txtCaused by user: rootCaused by program: /initGot eventpid=6mask=createfd=-1Changed path: /home/agirre/fanotify-article-materials/3.security_metadata_directory/test_dirInner object name: AE4E02F5.tmpCaused by user: rootCaused by program: /initGot eventpid=6mask=modificationfd=-1Changed path: /home/agirre/fanotify-article-materials/3.security_metadata_directory/test_dirInner object name: AE4E02F5.tmpCaused by user: rootCaused by program: /initGot eventpid=6mask=modificationfd=-1Changed path: /home/agirre/fanotify-article-materials/3.security_metadata_directory/test_dirInner object name: AE4E02F5.tmpCaused by user: rootCaused by program: /initGot eventpid=6mask=modificationfd=-1Changed path: /home/agirre/fanotify-article-materials/3.security_metadata_directory/test_dirInner object name: AE4E02F5.tmpCaused by user: rootCaused by program: /initGot eventpid=6mask=move fromfd=-1Changed path: /home/agirre/fanotify-article-materials/3.security_metadata_directory/test_dirInner object name: ok.txtCaused by user: rootCaused by program: /initGot eventpid=6mask=move tofd=-1Changed path: /home/agirre/fanotify-article-materials/3.security_metadata_directory/test_dirInner object name: 3910C472.tmpCaused by user: rootCaused by program: /initGot eventpid=6mask=move fromfd=-1Changed path: /home/agirre/fanotify-article-materials/3.security_metadata_directory/test_dirInner object name: AE4E02F5.tmpCaused by user: rootCaused by program: /initGot eventpid=6mask=move tofd=-1Changed path: /home/agirre/fanotify-article-materials/3.security_metadata_directory/test_dirInner object name: ok.txtCaused by user: rootCaused by program: /initGot eventpid=6mask=deletefd=-1Changed path: /home/agirre/fanotify-article-materials/3.security_metadata_directory/test_dirInner object name: 3910C472.tmpCaused by user: rootCaused by program: /initGot eventpid=6mask=deletefd=-1Changed path: /home/agirre/fanotify-article-materials/3.security_metadata_directory/test_dirInner object name: ~$ok.txtCaused by user: rootCaused by program: /init
Разбираем по порядку:
-
Пачка create/modification/modification с именем дочернего объекта ‘~ok.txt’; создание и двойное изменение некоторого файла-копии ‘~ok.txt’
-
Ещё одна пачка create/modification/modification/modification с именем дочернего объекта ‘AE4E02F5.tmp’ (уже другой временный файл);
-
Два события move from ‘ok.txt’ / move to ‘3910C472.tmp’; переименование старого файла ‘ok.txt —> 3910C472.tmp’
-
Два события move from ‘AE4E02F5.tmp’ / move to ‘ok.txt’; переименование нового файла, содержащего актуальную информацию ‘AE4E02F5.tmp —> ok.txt’
-
Два события delete; удаление временных файлов ‘3910C472.tmp’, ‘~ok.txt’
Логика редактора понятна: создать временную копию файла, вносить изменения в ней, после изменяемый файл подменить копией.
Кстати, обратите внимание на то, какая программа является причиной событий — /init. Дело в том, что тестовый файл расположен в WSL, а его редактирование происходило посредством программы, путь которой привязан к файловой системе хоста, то есть Windows. Часть I/O с Windows-приложений проходит через прослойку хоста; ядро часто отдаёт pid посредника, а не приложения, которое пользователь «видит».
Инструмент fanotify агрегацией событий на своей стороне не занимается.
Если пользователь удалит контролируемый файл, то продолжится ли его мониторинг?
Не продолжится.
Проводим такой эксперимент: возьмём подконтрольный файл, внесём в него ряд изменений, после чего удалим. Вывод:
Got event: pid=570 mask=modification fd=-1Changed path: /home/agirre/fanotify-article-materials/2.security_metadata_reg_file/test.txtCaused by user: agirreCaused by program: /home/agirre/.vscode-server/bin/8b640eef5a6c6089c029249d48efa5c99adf7d51/nodeGot event: pid=570 mask=modification fd=-1Changed path: /home/agirre/fanotify-article-materials/2.security_metadata_reg_file/test.txtCaused by user: agirreCaused by program: /home/agirre/.vscode-server/bin/8b640eef5a6c6089c029249d48efa5c99adf7d51/nodeGot event: pid=570 mask=self movement fd=-1Changed path: /tmp/ujq30QmwCaused by user: agirreCaused by program: /home/agirre/.vscode-server/bin/8b640eef5a6c6089c029249d48efa5c99adf7d51/nodeGot event: pid=570 mask=change attributes fd=-1Problem while getting file descriptor by file handle: stale file handleCaused by user: agirreCaused by program: /home/agirre/.vscode-server/bin/8b640eef5a6c6089c029249d48efa5c99adf7d51/node
Кстати, обратите внимание, что “удаление” означает перемещение в корзину, так как mask имеет значение ‘self movement’. Ещё один важный момент: ‘stale file handle’ в последнем событии. Формулировка ‘stale file handle’ говорит о том, что уникальный файловый идентификатор “протух”, а получить файловый дескриптор по нему стало невозможно.
Как и в случае с inotify после удаления наблюдается полное отсутствие каких-либо событий вообще. В предыдущей статье мы приводили ряд гипотез, почему наблюдается ситуация, когда при создании файла с точно таким же путём, что и раньше, его отслеживание прекращается. Сейчас, копнув глубже в ядро Linux и в суть fanotify, мы можем с большей долей уверенности утверждать: f_handle, уникально идентифицирующий объект файловой системы, меняется. То есть до удаления файла и после создания файла по-прежнему пути имеем совершенно новый f_handle.
Что с рекурсивностью при работе с директориями?
Она есть, но с некоторыми ограничениями. В fanotify есть два mark-флага: FAN_MARK_MOUNT и FAN_MARK_FILESYSTEM. Они оба реализуют логику рекурсивности событий “под капотом”, однако их нужно использовать с умом.
FAN_MARK_MOUNT. Во-первых, этот флаг работает следующим образом: если path сам по себе не является точкой монтирования, будет помечен mount point, содержащий этот путь; под наблюдением оказывается всё дерево этого mount (каталоги, подкаталоги, файлы внутри него и так далее). Во-вторых, есть нюанс: с ним нельзя сочетать маску с событиями, которые требуют идентификации объектов через file handle (fid_* структуры, разобранные в начале статьи), поэтому львиная доля интересующих нас событий отваливается. Пример кода: https://github.com/Daniel-Ager-Okker/fanotify-article-materials/blob/f066ba394abb19a3dfdcdac57803bb88b0b62415/4.security_metadata_filesystem_or_mount/app/app.go#L29
FAN_MARK_FILESYSTEM. Этот флаг работает так: когда разработчик передаёт в функцию FanotifyMark какой-либо путь, то будет отслеживаться вся файловая система, содержащая этот путь.
А если написать логику рекурсивности самостоятельно, то будут ли ограничения на количество отслеживаемых объектов?
Да, ситуация здесь ровно такая же, как в случае с inotify.
Версия программы под данный параграф: https://github.com/Daniel-Ager-Okker/fanotify-article-materials/tree/main/5.security_metadata_dir_my_recursive.
При запуске программы с домашней директории в качестве параметра вывод следующий:
# sudo ./main /homeError while adding /home/agirre/libssh-mirror/tests/ssh_ping.c to the fanotify group: no space left on deviceError while mark /home: no space left on deviceMarked count is 65004
На файле /home/agirre/libssh-mirror/tests/ssh_ping.c “память” закончилась, так как видим ‘no space left on device’. Дискового пространства на ПК более чем достаточно. Замечу, что число 65004 у меня повторяется из раза в раз. Истинная причина — установленные лимиты.
У fanotify есть три ключевых ограничения (https://manpages.debian.org/testing/manpages-ru/fanotify.7.ru.html):
-
max_user_marks — верхний предел количества меток fanotify, которые могут быть созданы для каждого реального идентификатора пользователя; простыми словами: сколько раз пользователь может вызвать fanotify_mark
-
max_user_group — верхний предел количества групп fanotify, которые можно создать для каждого реального идентификатора пользователя; простыми словами: сколько раз пользователь может вызвать fanotify_init
-
max_queued_events — верхний предел количества событий, которые могут быть поставлены в очередь в соответствующую группу fanotify; события, превышающие этот предел, отбрасываются, но при этом генерируется событие FAN_Q_OVERFLOW
Значения этих ограничений находятся в соответствующих файлах директории /proc/sys/fs/fanotify/:
ls /proc/sys/fs/fanotify/# Вывод: max_queued_events max_user_groups max_user_marks
Увеличить лимит можно с помощью команды sysctl fs.fanotify.max_user_marks=80000:
# увеличить лимитsudo sysctl fs.fanotify.max_user_marks=80000 # проверить значениеsudo cat /proc/sys/fs/fanotify/max_user_marks
После увеличения программа прекращает свою работу уже на большем количестве помеченных объектов:
Error while adding /home/agirre/rsu/build/doxygen/html/dir_2b34e6ed9557b6509eb3ead344d14f30_dep.md5 to the fanotify group: no space left on deviceError while mark /home: no space left on deviceMarked count is 80004
Если увеличить лимиты, то закрываю ли я все проблемы? Или остаётся что-то ещё?
С inotify у нас оставалось еще две проблемы:
-
Существенное возрастание нагрузки на CPU
-
“Проскальзывание” событий в случае с огромными директориями
Возрастание нагрузки на CPU.
На этот вопрос взглянем “под микроскопом”. Во-первых, в саму программу добавим небольшой фрагмент кода, который будет снимать CPU-нагрузку и выводить её в файл: https://github.com/Daniel-Ager-Okker/fanotify-article-materials/blob/02d1de014852f735bc3120e44acf40e3eb477841/6.security_metadata_profiling/main.go#L13; так мы увидим в разрезе, какая из функций программы потребляет больше процессорной мощности относительно других. Во-вторых, сам процесс, порожденный программой, параллельно будем анализировать посредством утилиты pidstat.
Дадим программе некоторое время поработать. Во время работы будем снимать дамп потребления CPU, а после остановки воспользуемся утилитой для профилирования:
# 1.Запускаем программуsudo ./main /home# 2.Параллельно снимаем CPU дамп (шаг - 1 с)sudo pidstat -p 38014 1# 3.По окончании смотрим результаты профилирования программыgo tool pprof -http=:8080 cpu.out
Вывод pidstat следующий:
Linux 6.6.87.2-microsoft-standard-WSL2 (agirre-PC) 05/17/26 _x86_64_ (24 CPU)12:56:04 UID PID %usr %system %guest %wait %CPU CPU Command12:56:05 0 38014 0.00 0.00 0.00 0.00 0.00 0 main12:56:06 0 38014 0.00 0.00 0.00 0.00 0.00 0 main12:56:07 0 38014 0.00 0.00 0.00 0.00 0.00 0 main12:56:08 0 38014 0.00 0.00 0.00 0.00 0.00 0 main12:56:09 0 38014 0.00 1.00 0.00 0.00 1.00 0 main12:56:10 0 38014 1.00 0.00 0.00 0.00 1.00 0 main12:56:11 0 38014 0.00 0.00 0.00 0.00 0.00 0 main12:56:12 0 38014 0.00 0.00 0.00 0.00 0.00 0 main12:56:13 0 38014 0.00 0.00 0.00 0.00 0.00 0 main12:56:14 0 38014 0.00 0.00 0.00 0.00 0.00 0 main12:56:15 0 38014 0.00 0.00 0.00 0.00 0.00 0 main12:56:16 0 38014 0.00 0.00 0.00 0.00 0.00 0 main12:56:17 0 38014 0.00 0.00 0.00 0.00 0.00 0 main12:56:18 0 38014 0.00 0.00 0.00 0.00 0.00 0 main12:56:19 0 38014 0.00 0.00 0.00 0.00 0.00 0 main12:56:20 0 38014 1.00 0.00 0.00 0.00 1.00 0 main12:56:18 0 38014 0.00 0.00 0.00 0.00 0.00 0 main12:56:19 0 38014 0.00 0.00 0.00 0.00 0.00 0 main12:56:20 0 38014 0.00 1.00 0.00 0.00 1.00 0 main12:56:21 0 38014 0.00 0.00 0.00 0.00 0.00 0 main12:56:22 0 38014 0.00 0.00 0.00 0.00 0.00 0 main12:56:23 0 38014 0.00 0.00 0.00 0.00 0.00 0 main12:56:24 0 38014 0.00 0.00 0.00 0.00 0.00 0 main12:56:25 0 38014 0.00 0.00 0.00 0.00 0.00 0 main12:56:26 0 38014 0.00 0.00 0.00 0.00 0.00 0 main12:56:27 0 38014 0.99 0.00 0.00 0.00 0.99 0 main12:56:28 0 38014 0.00 0.00 0.00 0.00 0.00 0 main^CAverage: 0 38014 0.11 0.07 0.00 0.00 0.18 - main
Бывают пики, когда нагрузка возрастает до 1% мощности одного логического потока. Среднее же значение потребления составило всего лишь 0.18 %.
Далее взглянем на результаты профилирования, в первую очередь интересен Flame Graph:
Читается он так: чем шире полоска, соответствующая функции, тем больше % CPU потребляет эта функция относительно других. Конечно же, самой “жирной” является app.Run, внутри которой порядка 70% от общей нагрузки на CPU всей программой занимает рекурсивный обход директории /home и лишь 30% — обработчик событий.
Результат куда более приятный, чем у inotify, где программа забирала 36.5% (!) мощности одного логического потока. Но следует учитывать, что это лишь оценочное нагрузочное тестирование, которое показывает лишь ориентировочные цифры, отличающиеся от реальных боевых условий. Несмотря на это, результаты показывают, что fanotify лучше справляется с нагрузками.
“Проскальзывание” событий в случае с огромными директориями.
Старый эксперимент: допустим, у нас имеется директория под контролем целостности. Мы берём объёмную папку и копируем её внутрь первой. Предварительно не забудем увеличить лимиты. Тестовую объёмную папку наполним большим количеством объектов:
# увеличиваем лимиты fanotifysudo sysctl fs.fanotify.max_queued_events=300000sudo sysctl fs.fanotify.max_user_marks=1000000# проверяем размер тестовой директорииfind fixture_200k | wc -l
Что делать с результатами эксперимента? В случае с inotify в коде был специальный контейнер, который хранил в себе соответствия “watch descriptor —> путь”. Количество скопированных файлов сравнивалось с размером контейнера. Сейчас ситуация иная: функция fanotify_mark не даёт никаких дескрипторов, а возвращает статус, сигнализирующий о том, удалось ли добавить объект под наблюдение или нет. Поступать будем так:
-
Сгенерируем “плоскую” директорию ‘huge_dir’ внушительного размера (скрипт для генерации: https://github.com/Daniel-Ager-Okker/fanotify-article-materials/blob/main/7.security_metadata_events_loss/gentree/gentree.py)
-
Запустим тестовую модифицированную программу, которая следит за контрольной директорией и складывает имена новых объектов в map, чтобы нивелировать избыточность (код: https://github.com/Daniel-Ager-Okker/fanotify-article-materials/blob/main/7.security_metadata_events_loss/util/util.go)
-
Сравним размер дерева ‘huge_dir’ с размером контейнера
Тестовая огромная директория имеет количество 200 тысяч элементов:
find fixture_200k | wc -l# вывод: 200000
Размер контейнера составил 200001 элементов (200 тысяч внутренних элементов + 1 сама директория ‘huge_dir’ как объект)
#------------<...>------------#unique child names seen: 200001queue overflow events: 0listen error: context canceled
Выходит, что проскальзывания не было, все события были последовательно получены и обработаны.
Выводы
В данной статье мы рассмотрели все проблемы нативного использования fanotify в контексте сравнения с inotify. Чтобы подвести итог, давайте взглянем на таблицу:
|
Проблема |
inotify |
fanotify |
Примечание |
|---|---|---|---|
|
Неполнота метаданных |
не хватает информации о том, какая программа (какой процесс ОС) служит причиной возникновения события; нет возможности узнать, какой пользователь нарушил целостность |
есть возможность по pid узнать путь к программе-инициатору события и имя пользователя |
— |
|
Избыточность событий |
зачастую вместо одного события наблюдается сразу несколько |
такая же картина с “лишними” событиями |
избыточность не является причиной внутреннего устройства inotify/fanotify, а определяется механизмами работы редакторов с файлами |
|
Проблема идентификации |
после удаления объекта и его повторного создания отслеживание прекращается |
создание нового объекта на месте старого по такому же пути также не учитывается инструментом |
вероятнее всего, причина кроется в разных значениях f_handle (идентификатор объекта файловой системы в ядре Linux) до удаления и после создания вновь |
|
Отсутствие рекурсивности |
её нет вообще, нужна собственная реализация |
возможно использование флагов FAN_MARK_MOUNT и FAN_MARK_FILESYSTEM, но важно учитывать правила рекурсивности, которые эти флаги накладывают |
|
|
Ограничения по количеству отслеживаемых объектов |
собственную реализацию рекурсивности следует внимательно проектировать с учётом ограничений max_user_watches, max_user_instances, max_queued_events вкупе с нагрузкой на CPU |
(предположительно) есть возможность увеличивать лимиты max_user_marks, max_user_groups и max_queued_events без риска резкого роста потребления CPU |
вопрос нагрузочного тестирования fanotify с учётом увеличенных верхних пределов требует более детальной проработки |
|
“Проскальзывание” событий |
некоторая часть может не обрабатываться инструментом, пролетая незамеченными |
(предположительно) все события учитываются без потерь |
|
Итог: fanotify — более привлекательный инструмент, полностью закрывающий два недостатка (неполнота метаданных и рекурсивность) и частично избавляющий от проблем лимитов и “проскальзывания” (не берусь утверждать без отдельного глубокого анализа).
Напоследок: так ли все оптимистично?
Всегда ли правдивы метаданные, которые предоставляет fanotify?
В начале статьи мы разобрались, как правильно определять имя пользователя и программы, нарушивших целостность.
Нюанс #1. Провернём такой трюк. За основу возьмём программу, которая мониторила регулярный файл. Будем вносить в него изменения при помощи редактора nano:
# 1.Изменение первоеnano test.txt# внесли изменения, Ctrl+O, Ctrl+X# 2.Изменение второеsudo nano test.txt# внесли изменения, Ctrl+O, Ctrl+X
Первый шаг — изменение без привилегий, второй — с применением sudo. В логе программы видим:
Got event: pid=133518 mask=modification fd=-1Changed path: /home/agirre/fanotify-article-materials/2.security_metadata_reg_file/test.txtCaused by user: agirre # стандартный пользователь agirreCaused by program: /usr/bin/nanoGot event: pid=133518 mask=modification fd=-1Changed path: /home/agirre/fanotify-article-materials/2.security_metadata_reg_file/test.txtCaused by user: agirre # стандартный пользователь agirreCaused by program: /usr/bin/nanoGot event: pid=133651 mask=modification fd=-1Changed path: /home/agirre/fanotify-article-materials/2.security_metadata_reg_file/test.txtCaused by user: root # а здесь уже root!Caused by program: /usr/bin/nanoGot event: pid=133651 mask=modification fd=-1Changed path: /home/agirre/fanotify-article-materials/2.security_metadata_reg_file/test.txtCaused by user: root # а здесь уже root!Caused by program: /usr/bin/nano
Использование sudo все портит. Наша программа довольно простая, она заточена под определение user name по pid. Применение sudo порождает создание нового процесса, в котором происходит смена реального UID на 0 (root):
Удивляться тут нечему, такое устройство работы. Минус в том, что это некоторое замазывание следов, так как в ходе расследования остаётся тайной, а кто именно из пользователей, обладающих привилегиями, инициировал событие.
Нюанс не так критичен, потому что направо и налево root-а не раздают.
Нюанс №2. Сделаем следующее. Напишем простой обёрточный скрипт на Python, который будет с помощью bash менять файл:
#!/usr/bin/env python3import osimport timefile = "test.txt"os.system(f"echo 'Hello World' > {file}") # изменение файла из-под pythontime.sleep(10) # специально ждем, чтобы fanotify успел обработать событие
Запускаем его и видим в логах:
Got event: pid=146130 mask=modification fd=-1Changed path: /home/agirre/fanotify-article-materials/2.security_metadata_reg_file/test.txtCaused by user: agirreCaused by program: /usr/bin/dashCaused by program: /usr/bin/dash? Но ведь был запущен исполняемый python-скрипт! Дело в том, что os.system порождает новый процесс, поэтому все логично, под этим os.system использовалась оболочка /usr/bin/dash. Таких цепочек можно придумать большое множество.
Caused by program: /usr/bin/dash? Но ведь был запущен исполняемый python-скрипт! Дело в том, что os.system порождает новый процесс, поэтому все логично, под этим os.system использовалась оболочка /usr/bin/dash. Таких цепочек можно придумать большое множество.
Чтобы данную цепочку разрешить, нужно улучшать программу, добавляя дополнительную логику определения pid с точки зрения наличия родительского процесса (ppid). Это приведёт к увеличению времени обработки. События же генерируются в разы быстрее, чем программа успевает их обрабатывать, в итоге возникает два очень серьёзных риска:
-
переполнение очереди (пока программа занимается обработкой старых событий, приходит крайне много новых)
-
“окно слепоты”; в данном статье “проскальзывания” не наблюдалось, но это лишь частный случай; когда событий будет настолько много, что программа не будет успевать их обработать, новым уже не хватит места в очереди, в результате чего они будут проигнорированы (“проскальзывание”)
Нюанс №3. Гонка с pid. Что будет, если убрать time.sleep из python-скриптов?
Возьмём вот такой скрипт:
#!/usr/bin/env python3import osimport timefile = "test.txt"os.system(f"echo 'Hello World' > {file}")
В результате видим:
Got event: pid=158120 mask=modification fd=-1Changed path: /home/agirre/fanotify-article-materials/2.security_metadata_reg_file/test.txtProblem while getting name of user, who causes event: can't read /proc/158120/status: open /proc/158120/status: no such file or directoryProblem while getting path to the program the user used: can't get path for PID 158120: readlink /proc/158120/exe: no such file or directory
Смысл в том, что события приходят гораздо быстрее, чем мы успеваем их обработать. Поэтому к моменту, когда программа начала использовать pid для получения пользователя и программы, процесс с этим pid давно умер. Как итог — потеря необходимой для проведения расследования информации.
Нюанс №4. Запутывание через docker.
Что будет, если изменить подконтрольный файл из-под docker-контейнера? А что мы вообще должны ожидать?
Разберём вопрос с технической точки зрения. Опорой технологии docker являются cgroups и linux-namespaces, обе эти функции уже содержатся в ядре Linux. То есть по факту это логическим образом отделенные от хоста процессы. Именно логически, так как физически все они выполняются на том же хосте, на котором запускается контейнер. Выходит, что PID’ов два:
-
логический (тот, что внутри контейнера); его номер в контейнере, где открыт nano для редактирования, имеет значение 198:
-
настоящий (тот, что на хосте); его номер можно получить по логическому, он имеет значение 176601
#!/usr/bin/bash# bash-скрипт для определения реального PID по логическому (запуск с sudo)container_pid=$(docker inspect -f '{{.State.Pid}}' upbeat_booth)nano_pid_in_container=198grep -l "NSpid:.*$nano_pid_in_container" /proc/*/status 2>/dev/null | cut -d'/' -f3# Вывод: 176601
Какой же из этих двух (198 или 176601) мы должны увидеть в логах программы? Скорее всего настоящий, так как логического по факту не существует. Проверяем:
Got event: pid=176601 mask=modification fd=-1Changed path: /home/agirre/fanotify-article-materials/2.security_metadata_reg_file/test.txtCaused by user: rootCaused by program: /usr/bin/nano
И правда, видим 176601. Но вот user у нас root, а program - /usr/bin/nano. Не сопоставляется с ожиданиями, ведь мы запускали программу docker из-под agirre. Давайте собственноручно проверим содержимое директории /proc/176601.
# Самостоятельно проверяем, куда ведет symlink exereadlink /proc/176601/exe # ведет на /usr/bin/nano# Самостоятельно читаем status-файлnano /proc/176601/status# Видим:# Uid: 0 0 0 0
Инструмент fanotify ведёт себя ожидаемо: на хосте виден реальный pid (176601), а ложность в том, что по этому pid в /proc читаются учётные данные процесса внутри контейнера (root + nano), а не пользователь, который на хосте сделал docker exec. Не очень приятно, злоумышленник с доступом к docker запросто может запутать следы, модифицируя файлы из-под docker.
Заключительные выводы
Если смотреть на задачу контроля целостности комплексно, то можно смело сказать, что fanotify более мощный, но и более сложный инструмент. Он закрывает часть проблем inotify (метаданные по pid, рекурсия через FAN_MARK_MOUNT / FAN_MARK_FILESYSTEM, меньшая нагрузка на CPU при сопоставимом сценарии), но не снимает ответственность с прикладного слоя. Более того: часть рисков становится заметнее именно потому, что данных в событии больше, а конфигурация — сложнее.
При проектировании агента или сервиса на fanotify нужно иметь в виду следующие аспекты:
-
нормализация событий — свёртка шумных серий в одно бизнес-событие («файл X изменён») перед отправкой в SIEM
-
политика mark-ов — когда нужно писать свою логику рекурсивности, а когда достаточно использовать
FAN_MARK_MOUNT и FAN_MARK_FILESYSTEM,помня при этом про излишние события -
производительность — отдельный вопрос поиска баланса между верхними пределами, нагрузкой на CPU и скоростью обработки старых событий по сравнению с появлением новых
-
модель достоверности метаданных — следует понимать, что fanotify обладает своими недостатками, не являясь панацеей
Если в вашем проекте уже есть опыт с inotify и вы заглядываете на fanotify, то теперь вы знаете: fanotify даёт больше возможностей, но и больше обязанностей в плане разработки.
ссылка на оригинал статьи https://habr.com/ru/articles/1043972/