
Приветствую всех любителей изучать новое. Меня зовут Рома, и я занимаюсь исследованием безопасности ОС Linux в экспертной лаборатории PT Expert Security Center.
В рамках инициативы нашей компании по обмену экспертными знаниями с сообществом я расскажу вам об известной многим администраторам системе журналирования в Linux-подобных ОС — подсистеме аудита Linux (auditd). При должной настройке она позволяет получать полную информацию о действиях, выполняемых в операционной системе.
Специалистам по информационной безопасности полезно уметь расшифровывать и обрабатывать события auditd для отслеживания потенциально вредоносной активности. В связи с этим нам потребовалось организовать для них экспертную поддержку в системе мониторинга событий ИБ и управления инцидентами MaxPatrol SIEM. При их изучении мы старались ответить на следующие вопросы:
Связаны ли между собой записи в журнале событий? Если да, то каким образом?
Чем отличаются записи с разными значениями
type=XXXв журнале, и какую информацию может нести каждая из них?Почему события типа
type=USER_*иtype=CRED_*при одинаковом значении поляtypeмогут нести различный набор полей и как их корректно нормализовывать в SIEM?
Давайте разберем особенности и подводные камни, с которыми пришлось столкнуться при изучении подсистемы аудита. Возможно, вы что-то уже знаете из официальной документации, например из man (справочных страниц Linux). Некоторые же особенности нигде не описаны, и их пришлось выяснять самостоятельно.
Я буду рассматривать актуальные на момент написания статьи версии исходного кода auditd: audit-kernel 6.4 и audit-userspace 3.1.2. В качестве примеров нормализованных событий приведены скриншоты из интерфейса MaxPatrol SIEM. Правила нормализации вы можете найти в нашем открытом репозитории Security Experts Community.
Для быстрой навигации
Внутреннее устройство
Подсистема аудита Linux — это полноценный компонент ядра, неразрывно связанный с ним.

Со стороны пользовательского пространства общение с ядром происходит через специально выделенный netlink-сокет. Используя официально поставляемые в пакете auditd утилиты, администратор узла может включать и отключать подсистему, устанавливать для нее правила аудита, анализировать статистику и т.п. Различные сторонние утилиты могут использовать API из библиотеки libaudit для формирования событий в формате auditd, которые затем попадают в единый журнал audit.log.
Каждое событие в журнале — это набор записей, содержащие разные фрагменты контекста.
type=SYSCALL msg=audit(1663937111.702:73702361): arch=c000003e syscall=59 success=yes exit=0 a0=7f90824cfbf8 a1=7f907a020608 a2=7f907d9de920 a3=7f90742fcb10 items=2 ppid=1736 pid=3202324 auid=4294967295 uid=996 gid=997 euid=996 suid=996 fsuid=996 egid=997 sgid=997 fsgid=997 tty=(none) ses=4294967295 comm="ps" exe="/usr/bin/ps" key="execve_rule" ARCH=x86_64 SYSCALL=execve AUID="unset" UID="git" GID="git" EUID="git" SUID="git" FSUID="git" EGID="git" SGID="git" FSGID="git"
type=EXECVE msg=audit(1663937111.702:73702361): argc=5 a0="ps" a1="-o" a2="rss=" a3="-p" a4="1736"
type=CWD msg=audit(1663937111.702:73702361) cwd="/opt/gitlab/sv/gitlab-exporter"
type=PROCTITLE msg=audit(1663937111.702:73702361): proctitle=7073002D6F007273733D002D700031373336
Они объединяются по числу-счетчику, идущему после времени возникновения события (73702361 в примере выше). Будем называть его audit_id. При запуске системы оно инициализируется значением “1”, а затем с каждым новым событием увеличивается на 1. При пересылке событий в MaxPatrol SIEM по этому числу удобно восстанавливать последовательность возникновения событий в системе.
В конце каждой записи добавляются атрибуты, имена которых записываются строчными буквами. Они являются результатом обогащения одноименных полей записи. Например, идентификаторы пользователей преобразуются в имена.
Пока что этих знаний должно быть достаточно для понимания дальнейшего материала. Теперь же перейдем к событиям.
События
События подсистемы аудита
Рассмотрим базовые события службы, относящиеся к ее работе.
LOGIN
Начнем обзор с события LOGIN, так как данные из него (значение полей auid и ses) присутствуют во всех событиях, возникающих в рамках сессии пользователя. Оно сигнализирует о том, что сессии пользователя в терминале назначен идентификатор.
Пример события:
type=LOGIN msg=audit(1705079372.663:35922): pid=1868146 uid=0 old-auid=4294967295 auid=1000 tty=(none) old-ses=4294967295 ses=24982 res=1 UID="root" OLD-AUID="unset" AUID="zabbix"
type=SYSCALL msg=audit(1693231661.663:35922): arch=c000003e syscall=1 success=yes exit=4 a0=3 a1=7ffea4955270 a2=4 a3=7f8fa0234371 items=0 ppid=1429800 pid=1868146 auid=1000 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=24982 comm="sshd" exe="/usr/sbin/sshd" key=(null) ARCH=x86_64 SYSCALL=write AUID="zabbix" UID="root" GID="root" EUID="root" SUID="root" FSUID="root" EGID="root" SGID="root" FSGID="root"
type=PROCTITLE msg=audit(1693231661.663:35922): proctitle=2F7573722F7362696E2F73736864002D44002D52
Функция формирования события выглядит следующим образом:
// https://github.com/linux-audit/audit-kernel/blob/6995e2de6891c724bfeb2db33d7b87775f913ad1/kernel/audit.c#L2308 static void audit_log_set_loginuid(kuid_t koldloginuid, kuid_t kloginuid, unsigned int oldsessionid, unsigned int sessionid, int rc) { struct audit_buffer *ab; uid_t uid, oldloginuid, loginuid; struct tty_struct *tty; if (!audit_enabled) return; ab = audit_log_start(audit_context(), GFP_KERNEL, AUDIT_LOGIN); if (!ab) return; uid = from_kuid(&init_user_ns, task_uid(current)); oldloginuid = from_kuid(&init_user_ns, koldloginuid); loginuid = from_kuid(&init_user_ns, kloginuid), tty = audit_get_tty(); audit_log_format(ab, "pid=%d uid=%u", task_tgid_nr(current), uid); audit_log_task_context(ab); audit_log_format(ab, " old-auid=%u auid=%u tty=%s old-ses=%u ses=%u res=%d", oldloginuid, loginuid, tty ? tty_name(tty) : "(none)", oldsessionid, sessionid, !rc ); audit_put_tty(tty); audit_log_end(ab); }
Если вы когда-нибудь захотите самостоятельно изучать исходные файлы auditd для понимания событий, обратите внимание на основные функции, участвующие в их формировании:
audit_log_start(). Функция, запускающая конструирование записи. Константа
AUDIT_*из ее аргументов задает значение поляtype.audit_log_format(). Функция для формирования тела записи. Их может быть несколько — каждая дополняет уже существующее содержимое. Если вы изучали какой-нибудь язык программирования, то знание механизма форматных строк поможет вам понять, что происходит в аргументах этой функции.
audit_log_session_info(). Эта функция не участвует непосредственно в формировании этого события. Но в последующих примерах она служит источником значений для полей
auidиses, которые назначаются в событииLOGIN.audit_log_task_context(). Функция для добавления поля
subjв запись. Оно содержит информацию о метке субъекта из системы мандатного разграничения доступа (например,unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023для SELinux).audit_log_end(). Функция, завершающая формирование записи.
Вернемся к событию. Для входящего пользователя задаются параметры auid и ses. Поле auid (audit uid) содержит идентификатор вошедшего пользователя из операционной системы. Его значение является сквозным для всех событий, возникающих в рамках текущей сессии. Если пользователь переключится на другого пользователя с помощью su или sudo, это не повлияет на значение auid в событиях. Поле ses при этом содержит назначенный уникальный числовой идентификатор сессии.
Old-варианты полей auid и ses, как правило, содержат системное значение 4294967295. Теоретически с помощью библиотечной функции audit_setloginuid() можно изменить их значения. Но в нашей практике таких примеров не было.
Нормализованное событие выглядит следующим образом.

Дополнительные записи наподобие SYSCALL рассмотрим чуть позже при обсуждении событий CONFIG_CHANGE.
DAEMON_START / DAEMON_END
События этих типов уведомляют о запуске и остановке службы аудита соответственно.
Примеры событий:
type=DAEMON_START msg=audit(1695054308.076:7647): op=start ver=3.0.7 format=enriched kernel=5.4.0-137-generic auid=4294967295 pid=751 uid=0 ses=4294967295 subj=unconfined res=success AUID="unset" UID="root"
<...>
type=DAEMON_END msg=audit(1695054400.076:7648): op=terminate auid=0 pid=1 subj= res=success AUID="root"
Особенность событий DAEMON_* заключается в том, что при запуске системы их audit_id выбирается случайным образом — отдельно от основного счетчика.
// https://github.com/linux-audit/audit-userspace/blob/572eb7d4fe926e7c1c52166d08e78af54877cbc5/src/auditd.c#L306 if (seq_num == 0) { srandom(time(NULL)); seq_num = random() % 10000; // задание случайного значения } else seq_num++; // Write event into netlink area like normal events if (gettimeofday(&tv, NULL) == 0) { e->reply.len = snprintf((char *)e->reply.msg.data, DMSG_SIZE, "audit(%lu.%03u:%u): %s", tv.tv_sec, (unsigned)(tv.tv_usec/1000), seq_num, str ); } else { e->reply.len = snprintf((char *)e->reply.msg.data, DMSG_SIZE, "audit(%lu.%03d:%u): %s", (unsigned long)time(NULL), 0, seq_num, str ); }
Пример двух подряд идущих событий при запуске системы:
type=DAEMON_START msg=audit(1695054308.076:7647): op=start ver=3.0.7 format=enriched kernel=5.4.0-137-generic auid=4294967295 pid=751 uid=0 ses=4294967295 subj=unconfined res=success AUID="unset" UID="root"
type=CONFIG_CHANGE msg=audit(1695054308.241:5): audit_backlog_limit=8192 old=64 auid=4294967295 ses=4294967295 subj=system_u:system_r:unconfined_service_t:s0 res=1 AUID="unset"
Для дальнейших событий DAEMON_* это число также увеличивается на единицу, но итоговая нумерация всего потока получается неупорядоченной. В чем заключается сакральный смысл отдельной нумерации таких событий — для меня осталось загадкой.
Разберем информацию из события DAEMON_START:
Поле | Описание |
op | Всегда имеет значение |
ver | Версия службы auditd |
format | Формат событий. Всего может быть два значения: · raw: события будут представлены в базовом виде, без дополнительной обработки; · enriched: события будут обогащены дополнительной информацией, например именами пользователей на основе их UID. Об этих обогащениях говорилось ранее. |
kernel | Версия ядра Linux, в котором запущена подсистема аудита |
pid | ID запущенного процесса auditd |
uid | ID пользователя, от имени которого запущен процесс auditd |
res | Результат операции. Всегда имеет значение |
Ниже приведен пример нормализованного события.

Что же касается события DAEMON_END, то, к сожалению, по завершении работы auditd оно не отправляется на удаленный сервер, поэтому на стороне SIEM-системы мы его не увидим. Но для общего понимания все же разберем его элементы:
Поле | Описание |
op | Всегда имеет значение |
pid | ID процесса, отправившего сигнал о завершении службы |
res | Всегда имеет значение |
Как мы видим, особых подробностей событие в себе не несет. Поэтому его можно скорее рассматривать как маркер произошедшего, нежели пытаться что-то анализировать на его основе.
CONFIG_CHANGE
События этого типа сигнализируют об изменениях конфигурации auditd, включая задание параметров службы и управление правилами аудита.
Параметрами и правилами можно управлять в реальном времени с помощью утилиты auditctl, либо можно прописать все команды в файле audit.rules, а затем применить его с помощью той же auditctl или перезапустить службу auditd.
Начнем с простого — с события задания параметра. Рассмотрим следующий пример:
type=CONFIG_CHANGE msg=audit(1704970843.772:2557): op=set audit_backlog_limit=8192 old=8192 auid=4294967295 ses=4294967295 res=1 AUID="unset"
Функция формирования события выглядит следующим образом:
// https://github.com/linux-audit/audit-kernel/blob/6995e2de6891c724bfeb2db33d7b87775f913ad1/kernel/audit.c#L384 static int audit_log_config_change(char *function_name, u32 new, u32 old, int allow_changes) { struct audit_buffer *ab; int rc = 0; ab = audit_log_start(audit_context(), GFP_KERNEL, AUDIT_CONFIG_CHANGE); if (unlikely(!ab)) return rc; audit_log_format(ab, "op=set %s=%u old=%u ", function_name, new, old); audit_log_session_info(ab); rc = audit_log_task_context(ab); if (rc) allow_changes = 0; audit_log_format(ab, " res=%d", allow_changes); audit_log_end(ab); return rc; }
Давайте посмотрим, из каких элементов состоит событие.
Поле | Описание |
op | Всегда имеет значение |
<function_name> | Новое значение параметра |
old | Старое значение изменяемого параметра |
res | Результат операции |
Ниже приведена таблица маппинга параметров.
Параметр auditctl/audit.rules | Значение function_name | Описание |
-b | audit_backlog_limit | Размер очереди для событий, ожидающих отправки в службу аудита |
--backlog_wait_time | audit_backlog_wait_time | Время ожидания, после которого ядро попытается отправить новые события службе аудита по достижении значения |
-e | audit_enabled | Флаг активности подсистемы аудита. Влияет только на обработку системных вызовов подсистемой аудита на стороне ядра. Возможные значения: · 0: системные вызовы не проходят через подсистему аудита, не обрабатываются ей и, соответственно, не журналируются; · 1: системные вызовы проходят через подсистему аудита, обрабатываются ей и журналируются (если настроены правила аудита для этих вызовов); · 2: конфигурация службы аудита блокируется на время её работы. Снять блокировку можно только перезагрузив узел. |
-f | audit_failure | Действие, которое ядро будет выполнять по достижении backlog_limit или в случае обнаружения ошибки и невозможности продолжения работы. Возможные значения: · 0: ничего не делать — просто пропустить журналирование записи; · 1: внести запись в журнал ядра с помощью printk; · 2: вызвать так называемую «панику ядра» (kernel panic). |
-r | audit_rate_limit | Максимальное количество записей в секунду, отправляемое ядром службе аудита |
Нормализованное событие имеет следующий вид.

Пока все довольно прозрачно и просто для понимания. Перейдем к разбору событий управления правилами аудита.
Правила аудита можно разделить на два вида (подробнее см. в разделе RULE OPTIONS на сайте):
правила мониторинга файловых объектов (флаг
-wв auditctl или audit.rules) — для краткости будем называть их w-правилами;правила мониторинга системных вызовов (флаги
-a/Aв auditctl или audit.rules) — назовем их a-правилами.
Примеры правил обоих типов:
-a always,exit -F arch=b64 -S socket -F a0=0x2 -F key=socket_rule -w /home -p rwa -k home_access_rule
Событие установки a-правила:
type=CONFIG_CHANGE msg=audit(1704970843.303:2578): auid=4294967295 ses=4294967295 op=add_rule key="pt_siem_execve" list=4 res=1 AUID="unset"
type=SYSCALL msg=audit(1704970843.303:2578): arch=c000003e syscall=44 success=yes exit=1072 a0=3 a1=7ffdfffb9b80 a2=430 a3=0 items=0 ppid=1796367 pid=1796382 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="auditctl" exe="/usr/sbin/auditctl" key=(null) ARCH=x86_64 SYSCALL=sendto AUID="unset" UID="root" GID="root" EUID="root" SUID="root" FSUID="root" EGID="root" SGID="root" FSGID="root"
type=SOCKADDR msg=audit(1704970843.303:2578): saddr=100000000000000000000000 SADDR={ fam=netlink nlnk-fam=16 nlnk-pid=0 }
type=PROCTITLE msg=audit(1704970843.303:2578): proctitle=2F7362696E2F617564697463746C002D52002F6574632F61756469742F72756C65732E642F30302D7369656D2E72756C6573
Событие установки w-правила:
type=CONFIG_CHANGE msg=audit(1704970843.287:2597): auid=4294967295 ses=4294967295 op=add_rule key="pt_siem_etc_read" list=4 res=1 AUID="unset"
type=SYSCALL msg=audit(1704970843.287:2597): arch=c000003e syscall=44 success=yes exit=1088 a0=3 a1=7ffdfffb9b80 a2=440 a3=0 items=1 ppid=1796367 pid=1796382 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="auditctl" exe="/usr/sbin/auditctl" key=(null) ARCH=x86_64 SYSCALL=sendto AUID="unset" UID="root" GID="root" EUID="root" SUID="root" FSUID="root" EGID="root" SGID="root" FSGID="root"
type=SOCKADDR msg=audit(1704970843.287:2597): saddr=100000000000000000000000 SADDR={ fam=netlink nlnk-fam=16 nlnk-pid=0 }
type=CWD msg=audit(1704970843.287:2597): cwd="/"
type=PATH msg=audit(1704970843.287:2597): item=0 name="/etc/sudoers.d" inode=393298 dev=fd:00 mode=040755 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0 OUID="root" OGID="root"
type=PROCTITLE msg=audit(1704970843.287:2597): proctitle=2F7362696E2F617564697463746C002D52002F6574632F61756469742F61756469742E72756C6573
Функция, описывающая алгоритм построения записи CONFIG_CHANGE:
// https://github.com/linux-audit/audit-kernel/blob/6995e2de6891c724bfeb2db33d7b87775f913ad1/kernel/auditfilter.c#L1107 static void audit_log_rule_change(char *action, struct audit_krule *rule, int res) { struct audit_buffer *ab; if (!audit_enabled) return; ab = audit_log_start(audit_context(), GFP_KERNEL, AUDIT_CONFIG_CHANGE); if (!ab) return; audit_log_session_info(ab); audit_log_task_context(ab); audit_log_format(ab, " op=%s", action); // значения: add_rule или remove_rule audit_log_key(ab, rule->filterkey); // заполнение поля key audit_log_format(ab, " list=%d res=%d", rule->listnr, res); audit_log_end(ab); }
Рассмотрим только поля, добавляемые с помощью функций audit_log_format() и audit_log_key():
Поле | Описание |
op | Добавление (add_rule) или удаление (remove_rule) правила. |
key | Имя правила, задаваемое при его создании с помощью флагов -k или -F key=. Если оно не было задано, то будет установлено значение |
list | Значение константы, обозначающей атрибут |
res | Результат операции. |
Возможные значения поля list перечислены ниже:
// https://github.com/linux-audit/audit-kernel/blob/6995e2de6891c724bfeb2db33d7b87775f913ad1/include/uapi/linux/audit.h#L164 #define AUDIT_FILTER_USER 0x00 /* Apply rule to user-generated messages */ #define AUDIT_FILTER_TASK 0x01 /* Apply rule at task creation (not syscall) */ #define AUDIT_FILTER_ENTRY 0x02 /* Apply rule at syscall entry */ #define AUDIT_FILTER_WATCH 0x03 /* Apply rule to file system watches */ #define AUDIT_FILTER_EXIT 0x04 /* Apply rule at syscall exit */ #define AUDIT_FILTER_EXCLUDE 0x05 /* Apply rule before record creation */ #define AUDIT_FILTER_TYPE AUDIT_FILTER_EXCLUDE /* obsolete misleading naming */ #define AUDIT_FILTER_FS 0x06 /* Apply rule at __audit_inode_child */ #define AUDIT_NR_FILTERS 7 #define AUDIT_FILTER_PREPEND 0x10 /* Prepend to front of list */
Каждое w-правило вида «-w <путь_к_файловому_объекту> ...» является для auditd одним из следующих a-правил:
-a always,exit -S all -F dir=<путь_к_файловому_объекту> ... # если файловый объект является каталогом -a always,exit -S all -F path=<путь_к_файловому_объекту> ... # если файловый объект является обычным файлом
Чтобы убедиться в этом, можно посмотреть исходный код:
// https://github.com/linux-audit/audit-userspace/blob/572eb7d4fe926e7c1c52166d08e78af54877cbc5/src/auditctl.c#L1007 case 'w': if (add != AUDIT_FILTER_UNSET || del != AUDIT_FILTER_UNSET) { audit_msg(LOG_ERR, "watch option can't be given with a syscall"); retval = -1; } else if (optarg) { add = AUDIT_FILTER_EXIT; action = AUDIT_ALWAYS; _audit_syscalladded = 1; retval = audit_setup_watch_name(&rule_new, optarg); // вызывает audit_add_watch_dir() } else { audit_msg(LOG_ERR, "watch option needs a path"); retval = -1; } break; // https://github.com/linux-audit/audit-userspace/blob/572eb7d4fe926e7c1c52166d08e78af54877cbc5/lib/libaudit.c#L779 int audit_add_watch_dir(int type, struct audit_rule_data **rulep, const char *path) { size_t len = strlen(path); struct audit_rule_data *rule = *rulep; // <...> rule->flags = AUDIT_FILTER_EXIT; rule->action = AUDIT_ALWAYS; audit_rule_syscallbyname_data(rule, "all"); // -S all rule->field_count = 2; rule->fields[0] = type; // AUDIT_WATCH (-F path) или AUDIT_DIR (-F dir) rule->values[0] = len; rule->fieldflags[0] = AUDIT_EQUAL; rule->buflen = len; memcpy(&rule->buf[0], path, len); // Default to all permissions // По умолчанию задается -F perm=rwxa, но // администратор может изменить это значение (в отличие от остальных) rule->fields[1] = AUDIT_PERM; rule->fieldflags[1] = AUDIT_EQUAL; rule->values[1] = AUDIT_PERM_READ | AUDIT_PERM_WRITE | AUDIT_PERM_EXEC | AUDIT_PERM_ATTR; _audit_permadded = 1; return 0; }
Можно также настроить следующие три правила, а затем вывести содержимое конфигурации с помощью команды auditctl -l:
-w /home -p rwa -k home_access_rule -a always,exit -S all -F dir=/home -F perm=rwa -k home_access_rule_1 -a always,exit -S all -F path=/home -F perm=rwa -k home_access_rule_2
Вывод auditctl:
-w /home -p rwa -k home_access_rule -w /home -p rwa -k home_access_rule_1 -w /home -p rwa -k home_access_rule_2
Именно поэтому в событии установки w-правила указано значение list=4 (AUDIT_FILTER_EXIT). При этом константа AUDIT_FILTER_WATCH, которая, как кажется, могла бы отвечать за w-правила, в исходном коде нигде не используется.
В рассмотренных примерах можно также заметить, что, помимо CONFIG_CHANGE, появляются другие записи. Более подробно мы изучим их в последующих разделах, а сейчас поверхностно рассмотрим их в контексте CONFIG_CHANGE:
SYSCALL. Описывает системный вызов, связанный с установкой или удалением правила. В нашем случае это вызов sendto(), отвечающий за отправку данных в какое-то место назначения.
SOCKADDR. Информация о сокете netlink, о котором говорилось в начале статьи. Это то самое место назначения, в которое отправляет данные вызов sendto().
CWD. Рабочий каталог процесса, установившего правило.
PATH. Запись, характерная только для событий установки и удаления правил мониторинга файловых объектов. Содержит в себе информацию об этом объекте.
PROCTITLE. В нашем примере это поле содержит закодированную в HEX команду, с помощью которой выполнялась установка правила. Но не всегда в нем указывается именно команда — об этом поговорим ниже.
На практике эти дополнительные записи появляются не всегда. Но если появляются, они отлично дополняют общую картину.
Ниже приведен пример нормализованного события.

Системные вызовы: SYSCALL
Рассмотрим события, поступающие непосредственно от операционной системы. Они представляют собой информацию о выполнении различных системных вызовов.
Системные вызовы — это API, который операционная система предоставляет пользователю для общения с ядром. Они предназначены для самых различных операций, в том числе для управления файловыми объектами, процессами, сетевыми соединениями и т п.
Изучение событий начнем с записи SYSCALL. Рассмотрим каждую группу полей более подробно, чем в других событиях.
Архитектура
Сначала поговорим о полях arch/ARCH.
arch=c000003e
syscall=59 success=yes exit=0 a0=7f90824cfbf8 a1=7f907a020608 a2=7f907d9de920 a3=7f90742fcb10 items=2 ppid=1736 pid=3202324 auid=4294967295 uid=996 gid=997 euid=996 suid=996 fsuid=996 egid=997 sgid=997 fsgid=997 tty=(none) ses=4294967295 comm="ps" exe="/usr/bin/ps" key="execve_rule"ARCH=x86_64SYSCALL=execve AUID="unset" UID="git" GID="git" EUID="git" SUID="git" FSUID="git" EGID="git" SGID="git" FSGID="git"
Оно описывает архитектуру системного вызова. Позволяет корректно расшифровать код вызова, представленный в полеsyscall, так как один и тот же вызов в разных архитектурах имеет разные значения кодов.
Ниже для наглядности приведены отрывки исходного кода с константами, формирующими значения поля arch. Поле ARCH содержит удобочитаемое значение архитектуры, поэтому вручную расшифровывать arch не обязательно.
// https://github.com/linux-audit/audit-kernel/blob/6995e2de6891c724bfeb2db33d7b87775f913ad1/include/uapi/linux/audit.h#L384 #define __AUDIT_ARCH_64BIT 0x80000000 #define __AUDIT_ARCH_LE 0x40000000 #define AUDIT_ARCH_AARCH64 (EM_AARCH64|__AUDIT_ARCH_64BIT|__AUDIT_ARCH_LE) #define AUDIT_ARCH_ALPHA (EM_ALPHA|__AUDIT_ARCH_64BIT|__AUDIT_ARCH_LE) #define AUDIT_ARCH_ARCOMPACT (EM_ARCOMPACT|__AUDIT_ARCH_LE) #define AUDIT_ARCH_ARCOMPACTBE (EM_ARCOMPACT) #define AUDIT_ARCH_ARCV2 (EM_ARCV2|__AUDIT_ARCH_LE) #define AUDIT_ARCH_ARCV2BE (EM_ARCV2) #define AUDIT_ARCH_ARM (EM_ARM|__AUDIT_ARCH_LE) // <...> // https://github.com/linux-audit/audit-kernel/blob/6995e2de6891c724bfeb2db33d7b87775f913ad1/include/uapi/linux/elf-em.h#L5 #define EM_NONE 0 #define EM_M32 1 #define EM_SPARC 2 #define EM_386 3 #define EM_68K 4 #define EM_88K 5 #define EM_486 6 /* Perhaps disused */ #define EM_860 7 #define EM_MIPS 8 /* MIPS R3000 (officially, big-endian only) */ #define EM_MIPS_RS3_LE 10 /* MIPS R3000 little-endian */ #define EM_MIPS_RS4_BE 10 /* MIPS R4000 big-endian */ #define EM_PARISC 15 /* HPPA */ #define EM_SPARC32PLUS 18 /* Sun's "v8plus" */ #define EM_PPC 20 /* PowerPC */ // <...>
При нормализации мы используем это поле для самостоятельного преобразования кодов вызовов в их имена (на тот случай, если версия auditd не поддерживает обогащение событий или если оно не настроено).

Структура вызова
Изучим информацию о системном вызове:
arch=c000003esyscall=59 success=yes exit=0 a0=7f90824cfbf8 a1=7f907a020608 a2=7f907d9de920 a3=7f90742fcb10items=2 ppid=1736 pid=3202324 auid=4294967295 uid=996 gid=997 euid=996 suid=996 fsuid=996 egid=997 sgid=997 fsgid=997 tty=(none) ses=4294967295 comm="ps" exe="/usr/bin/ps" key="execve_rule" ARCH=x86_64SYSCALL=execveAUID="unset" UID="git" GID="git" EUID="git" SUID="git" FSUID="git" EGID="git" SGID="git" FSGID="git"
Лучше всего делать это, опираясь на соответствующие man-страницы, например в нашем случае — на страницу о вызове execve().

Рассмотрим поля вызова по очереди:
syscall/SYSCALL. Код вызова и его имя. Коды вызовов для каждой архитектуры можно посмотреть на сайте.
success. Успешность выполнения системного вызова в целом.
exit. Значение, возвращаемое системным вызовом после выполнения. При успешном завершении оно зависит от самого системного вызова (см. раздел RETURN VALUE на man-странице вызова). В случае ошибки значение будет отрицательным, а само число будет обозначать код ошибки (см. раздел ERRORS на man-странице). Стоит отметить, что одному коду ошибки могут соответствовать разные ситуации, как в случае с
EACCESSв примере выше. Но в самом событии такой конкретной информации нет, поэтому приходится обходиться только общими описаниями из вывода команды errno.a0-a3. Аргументы системного вызова. Всегда представлены в виде числовых значений в HEX-кодировке. Поля, соответствующие аргументам с типом char *, например, содержат просто адреса в памяти и практической пользы не несут. Число аргументов в событии всегда фиксировано, из чего следуют интересные особенности:
если вызов принимает больше четырех аргументов, то пятый и последующие аргументы мы не увидим;
если вызов принимает меньше четырех аргументов, то остается загадкой, что в событии подразумевается под остальными (в нашем примере —
a3).
На этапе нормализации вся эта информация формирует основу события: задаются действие, тип объекта, успешность выполнения и, при наличии, причина ошибки.
Например, для события
arch=c000003esyscall=257 success=no exit=-2 a0=ffffff9c a1=55eeafd2dc20 a2=0 a3=0items=1 ppid=1 pid=4102158 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295 comm="atop" exe="/usr/bin/atop" subj=unconfined key=(null) ARCH=x86_64SYSCALL=openatAUID="unset" UID="root" GID="root" EUID="root" SUID="root" FSUID="root" EGID="root" SGID="root" FSGID="root"
на стороне MaxPatrol SIEM получаем «скелет» нормализованного события (в формате JSON):
{ "action": "access", "object": "file_object", "status": "failure", "category.generic": "File System Object", "category.high": "System Management", "category.low": "Manipulation", "msgid": "openat", "object.property": "flags", "object.state": "r", "object.value": "O_RDONLY", "reason": "No such file or directory" }
Остальная информация из SYSCALL и других записей будет служить дополнительным источником контекста для событий.
Пути к файлам: SYSCALL и PATH
Это самая нетривиальная часть события с точки зрения анализа, поэтому разберем ее шаг за шагом.
type=SYSCALL msg=audit(1704970843.702:73702361): arch=c000003e syscall=59 success=yes exit=0 a0=7f90824cfbf8 a1=7f907a020608 a2=7f907d9de920 a3=7f90742fcb10items=2ppid=1736 pid=3202324 auid=4294967295 uid=996 gid=997 euid=996 suid=996 fsuid=996 egid=997 sgid=997 fsgid=997 tty=(none) ses=4294967295 comm="ps" exe="/usr/bin/ps" key="execve_rule" ARCH=x86_64 SYSCALL=execve AUID="unset" UID="git" GID="git" EUID="git" SUID="git" FSUID="git" EGID="git" SGID="git" FSGID="git"
type=PATH msg=audit(1704970843.702:73702361):item=0name="/bin/ps" inode=1445245 dev=fd:00 mode=0100755 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0 OUID="root" OGID="root"
type=PATH msg=audit(1704970843.702:73702361):item=1name="/lib64/ld-linux-x86-64.so.2" inode=1442616 dev=fd:00 mode=0100755 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0 OUID="root" OGID="root"
Значение поля items в записи SYSCALL должно совпадать с количеством записей PATH. В локальных журналах это условие всегда выполняется. Если на стороне MaxPatrol SIEM есть расхождения, значит имеются проблемы с доставкой событий, что не позволит восстановить полный контекст события.
Рассмотрим запись PATH подробнее. Некоторую часть значений содержащихся в ней полей можно соотнести с выводом команды ls для конкретного файла. Этот вывод знакомым многим, кто так или иначе взаимодействовал с Linux.

Стоит отметить, что значение в поле name в записи может быть закодировано в формате HEX, если в нем есть управляющие или непечатаемые символы, двойные кавычки или пробелы. За это отвечает функция audit_log_n_untrustedstring(), которая также используется при обработке строковых значений некоторых других полей различных типов записей.
// https://github.com/linux-audit/audit-kernel/blob/v6.4/kernel/audit.c#L2102 void audit_log_n_untrustedstring(struct audit_buffer *ab, const char *string, size_t len) { if (audit_string_contains_control(string, len)) audit_log_n_hex(ab, string, len); else audit_log_n_string(ab, string, len); }
При этом закодированное значение не заключается в двойные кавычки.
Поле mode проще расшифровывать с конца. Последние три цифры обозначают права доступа к файлу в восьмеричной системе. Четвертая цифра с конца обозначает дополнительные права доступа, которые чуть менее известны обычному пользователю: suid-бит, sgid-бит и sticky-бит. Оставшиеся цифры — тип файлового объекта. Маппинг их значений можно посмотреть в таблице:
mode (event) | mode (ls, 1-й символ) | Описание |
01 | p | Именованный канал (pipe) |
02 | c | Символьное устройство (char device) |
04 | d | Папка (directory) |
06 | b | Блочное устройство (block device) |
010 | - | Обычный файл (regular file) |
012 | l | Символическая ссылка (symlink) |
014 | s | Сокет (socket) |
Поле dev содержит номер устройства, на котором хранится файл (если сам файл не является устройством). Поле rdev содержит номер устройства, если файловый объект имеет тип device. Поля cap_* описывают так называемые capabilities для файлового объекта. В рамках этой статьи мы не будем погружаться в их особенности. Подробнее про них можно почитать на сайте. Поле nametype (в некоторых дистрибутивах — objtype) обозначает тип файлового объекта внутри контекста события:
// https://github.com/linux-audit/audit-kernel/blob/6995e2de6891c724bfeb2db33d7b87775f913ad1/include/linux/audit.h#L135 #define AUDIT_TYPE_UNKNOWN 0 /* we don't know yet */ #define AUDIT_TYPE_NORMAL 1 /* a "normal" audit record */ #define AUDIT_TYPE_PARENT 2 /* a parent audit record */ #define AUDIT_TYPE_CHILD_DELETE 3 /* a child being deleted */ #define AUDIT_TYPE_CHILD_CREATE 4 /* a child being created */
Более подробное описание типов файловых объектов приведено ниже:
UNKNOWN. Файловый объект неизвестен системе (например, его не существует). При
nametype=UNKNOWNбудут отсутствовать поля начиная сinodeи заканчиваяrdev.NORMAL. Просто файловый объект. Как правило это исполняемый файл или файл, у которого изменяются атрибуты.
PARENT. Файловый объект является родительским для одного из представленных в событии.
DELETE. Файловый объект, удаляемый при выполнении вызова.
CREATE. Файловый объект, создаваемый при выполнении вызова.
Для понимания взаимосвязей между записями PATH необходимо погрузиться в исходный код ядра, и в частности — подсистемы аудита. Немного изучив его, выяснится, что системные вызовы являются некоторыми «обертками» над функциями. Для удобства их изучения существует сайт, на котором соотнесены системные вызовы и ссылки на исходный код с их реализацией. Например, вызовы rename(), renameat() и renameat2(), отвечающие за перемещение и переименование файловых объектов, являются обертками над функцией do_renameat2().
// https://github.com/linux-audit/audit-kernel/blob/6995e2de6891c724bfeb2db33d7b87775f913ad1/fs/namei.c#L4888 static int do_renameat2(int olddfd, const char __user *oldname, int newdfd, const char __user *newname, unsigned int flags) { // <...> } SYSCALL_DEFINE5(renameat2, int, olddfd, const char __user *, oldname, int, newdfd, const char __user *, newname, unsigned int, flags) { return do_renameat2(olddfd, oldname, newdfd, newname, flags); } SYSCALL_DEFINE4(renameat, int, olddfd, const char __user *, oldname, int, newdfd, const char __user *, newname) { return do_renameat2(olddfd, oldname, newdfd, newname, 0); } SYSCALL_DEFINE2(rename, const char __user *, oldname, const char __user *, newname) { return do_renameat2(AT_FDCWD, oldname, AT_FDCWD, newname, 0); }
Если попытаться выполнить трассировку функции с помощью bpftrace, получим следующий результат:
// [root@localhost test_dir] mv ../test_dir_2 ./test_dir_22 do_renameat2(olddfd: -100, oldname: "../test_dir_2", newdfd: -100, newname: "./test_dir_22", flags: 1) { // filename_parentat(old) audit_alloc_name(audit_context(), type: 0) { // [item=0] nametype=UNKNOWN } __audit_inode(filename: "../test_dir_2", dentry: [262146] "root", flags: 1) { // [item=0] nametype=UNKNOWN -> nametype=PARENT inode=262146 } // filename_parentat(new) audit_alloc_name(audit_context(), type: 0) { // [item=1] nametype=UNKNOWN } __audit_inode(filename: "./test_dir_22", dentry: [262852] "test_dir", flags: 1) { // [item=1] nametype=UNKNOWN -> nametype=PARENT inode=262852 } vfs_rename(old_dir_inode: 262146, old_dentry: [262906] "test_dir_2", new_dir_inode: 262852, new_dentry: [0] "test_dir_22", ... , flags: 1) { may_delete(dir_inode: 262146, child_dentry: [262906] "test_dir_2", ...) { __audit_inode_child(parent_inode: 262146, dentry: [262906] "test_dir_2", type: 3) { audit_alloc_name(audit_context(), type: 3) { // [item=2] nametype=DELETE inode=262906 } } } // may_create() __audit_inode_child(parent_inode: 262852, dentry: [0] "test_dir_22", type: 4) { audit_alloc_name(audit_context(), type: 4) { // [item=3] nametype=CREATE } } // fsnotify_move() для ситуации без ошибок __audit_inode_child(parent_inode: 262852, dentry: [262906] "test_dir_22", type: 4) { // [item=3] nametype=CREATE -> nametype=CREATE inode=262906 } } } -> 0
Комментариями внутри do_renameat2() я пометил функции, которые приносят в PATH те или иные значения полей, а также сами значения.
Получаемые записи PATH:
type=PATH msg=audit(1704970843.024:221): item=0 name="../" inode=262146 dev=fd:00 mode=040700 ouid=0 ogid=0 rdev=00:00 nametype=PARENT cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0
type=PATH msg=audit(1704970843.024:221): item=1 name="./" inode=262852 dev=fd:00 mode=040755 ouid=0 ogid=0 rdev=00:00 nametype=PARENT cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0
type=PATH msg=audit(1704970843.024:221): item=2 name="../test_dir_2" inode=262906 dev=fd:00 mode=040755 ouid=0 ogid=0 rdev=00:00 nametype=DELETE cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0
type=PATH msg=audit(1704970843.024:221): item=3 name="./test_dir_22" inode=262906 dev=fd:00 mode=040755 ouid=0 ogid=0 rdev=00:00 nametype=CREATE cap_fp=0 cap_fi=0 cap_fe=0 cap_fver=0 cap_frootid=0
Не буду погружаться в подробное описание таких функций для формирования PATH, как __audit_inode(). Взамен этого я подготовил готовые схемы формирования записей PATH для особо сложных системных вызовов. Найти их можно в нашем открытом репозитории Security Experts Community. Ниже приведен пример для do_rename2():

Идентификаторы процесса
В этом разделе рассмотрим идентификаторы процесса и пользователей, которые с ним связаны.
arch=c000003e syscall=59 success=yes exit=0 a0=7f90824cfbf8 a1=7f907a020608 a2=7f907d9de920 a3=7f90742fcb10 items=2ppid=1736 pid=3202324auid=4294967295uid=996 gid=997 euid=996 suid=996 fsuid=996 egid=997 sgid=997 fsgid=997tty=(none) ses=4294967295 comm="ps" exe="/usr/bin/ps" key="execve_rule" ARCH=x86_64 SYSCALL=execve AUID="unset"UID="git" GID="git" EUID="git" SUID="git" FSUID="git" EGID="git" SGID="git" FSGID="git"
Более подробно об идентификаторах можно почитать на сайте. Ниже приведена краткая информация:
PID/PPID: идентификаторы процесса и родительского процесса соответственно.
UID/GID: реальные (real) идентификаторы пользователя и группы. Определяют хозяина процесса.
EUID/EGID: действующие (effective) идентификаторы пользователя и группы. Определяют текущие полномочия процесса.
SUID/SGID: сохраненные set-user-ID и set-group-ID пользователя-владельца исполняемого файла. Используются для хранения начальных значений EUID/EGID, задаваемых при запуске файлов с установленными битами set-user и set-group.
FSUID/FSGID: идентификаторы, специфичные для Linux; не представляют интереса для понимания общих принципов.
Терминал
Это поле содержит информацию о терминале пользователя, выполнившего системный вызов.
arch=c000003e syscall=59 success=yes exit=0 a0=7f90824cfbf8 a1=7f907a020608 a2=7f907d9de920 a3=7f90742fcb10 items=2 ppid=1736 pid=3202324 auid=4294967295 uid=996 gid=997 euid=996 suid=996 fsuid=996 egid=997 sgid=997 fsgid=997tty=(none)ses=4294967295 comm="ps" exe="/usr/bin/ps" key="execve_rule" ARCH=x86_64 SYSCALL=execve AUID="unset" UID="git" GID="git" EUID="git" SUID="git" FSUID="git" EGID="git" SGID="git" FSGID="git"
Часто встречаемые форматы значений приведены ниже:
ttyX: физический терминал (то есть пользователь буквально сидит за монитором, подключенным к узлу).
pts/X (или /dev/pts/X): терминал, который эмулируется какой-либо программой (например, SSH). Командные оболочки, запускаемые в графических оболочках, также являются pts-терминалами.
(none): обозначает отсутствие терминала. Такое значение устанавливается для вызовов, выполняемых системными процессами.
Информация о сессии
Поле, содержащее информацию о сессии пользователя, выполнившего системный вызов.
arch=c000003e syscall=59 success=yes exit=0 a0=7f90824cfbf8 a1=7f907a020608 a2=7f907d9de920 a3=7f90742fcb10 items=2 ppid=1736 pid=3202324auid=4294967295uid=996 gid=997 euid=996 suid=996 fsuid=996 egid=997 sgid=997 fsgid=997 tty=(none)ses=4294967295comm="ps" exe="/usr/bin/ps" key="execve_rule" ARCH=x86_64 SYSCALL=execve AUID="unset" UID="git" GID="git" EUID="git" SUID="git" FSUID="git" EGID="git" SGID="git" FSGID="git"
Эти поля уже рассматривались в разделе LOGIN.
Дополнительная информация о процессе
Несколько полей содержат информацию об исполняемом файле процесса.
arch=c000003e syscall=59 success=yes exit=0 a0=7f90824cfbf8 a1=7f907a020608 a2=7f907d9de920 a3=7f90742fcb10 items=2 ppid=1736 pid=3202324 auid=4294967295 uid=996 gid=997 euid=996 suid=996 fsuid=996 egid=997 sgid=997 fsgid=997 tty=(none) ses=4294967295comm="ps" exe="/usr/bin/ps"key="execve_rule" ARCH=x86_64 SYSCALL=execve AUID="unset" UID="git" GID="git" EUID="git" SUID="git" FSUID="git" EGID="git" SGID="git" FSGID="git"
Поле comm, как правило, содержит имя исполняемого файла процесса, хотя его значение можно задать вручную. Длина значения ограничена 16 символами. В операционной системе его можно посмотреть в файле /proc/<pid>/comm .
Поле exe содержит полный путь к исполняемому файлу. В операционной системе это значение можно посмотреть по ссылке /proc/<pid>/exe (через readlink).
arch=c000003e syscall=59 success=yes exit=0 a0=5653ad1e69e0 a1=5653ad0ace80 a2=5653ad0c2100 a3=7fff8dd2e010 items=3 ppid=388 pid=958 auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) ses=4294967295comm="cached_setup_te" exe="/usr/bin/dash"subj=unconfined key="execve_rule" ARCH=x86_64 SYSCALL=execve AUID="unset" UID="root" GID="root" EUID="root" SUID="root" FSUID="root" EGID="root" SGID="root" FSGID="root"
Однако при выполнении скриптов в этом поле будет содержаться путь к интерпретатору, а не к скрипту. Для событий запуска скрипта в MaxPatrol SIEM мы получаем полный путь до него из нулевой записи PATH:

Если же запущенный скрипт начнет выполнять другие системные вызовы (например, обращаться к файлам), то у нас в распоряжении будет только поле exe. А оно, как мы выяснили, содержит не вполне корректную информацию. В таких случаях необходимо перепроверять ее, обращаясь к событию запуска процесса.
Правило аудита
Это поле содержит имя правила аудита, обнаружившего соответствующий системный вызов (оно задается в файле audit.rules или при выполнении auditctl с флагами -k или -F key=).
arch=c000003e syscall=59 success=yes exit=0 a0=7f90824cfbf8 a1=7f907a020608 a2=7f907d9de920 a3=7f90742fcb10 items=2 ppid=1736 pid=3202324 auid=4294967295 uid=996 gid=997 euid=996 suid=996 fsuid=996 egid=997 sgid=997 fsgid=997 tty=(none) ses=4294967295 comm="ps" exe="/usr/bin/ps"key="execve_rule"ARCH=x86_64 SYSCALL=execve AUID="unset" UID="git" GID="git" EUID="git" SUID="git" FSUID="git" EGID="git" SGID="git" FSGID="git"
Системные вызовы: PROCTITLE
Запись PROCTITLE содержит единственное поле proctitle с заголовком процесса, выполнившего системный вызов. Максимальная длина значения ограничена 128 символами (без учета кодирования в HEX).
type=PROCTITLE msg=audit(1704970843.702:73702361): proctitle=7073002D6F007273733D002D700031373336
По умолчанию в этом поле указывается команда запуска процесса. Однако стоит отметить, что значение заголовка может устанавливаться самим процессом, поэтому спешить с однозначными выводами при его парсинге явно не стоит.

В своих правилах нормализаций мы записываем значение этого поля в *.process.meta, чтобы отделить его от команды запуска:

Системные вызовы: CWD
Запись CWD содержит единственное поле cwd с полным путем к текущему рабочему каталогу процесса, выполнившего системный вызов.
type=CWD msg=audit(1704970843.287:209): cwd="/home/cm/ansible-role"
Относительные пути в записях PATH в некоторых случаях считаются относительно cwd. Пример такого случая приведен ниже.

Системные вызовы: EXECVE
Запись EXECVE содержит команду запуска процесса. Она присутствует только в событиях, относящихся к вызовам семейства exec().
type=EXECVE msg=audit(1704970843.702:73702361): argc=5 a0="ps" a1="-o" a2="rss=" a3="-p" a4="1736"
Если аргументов много, то записей EXECVE может быть несколько (пример события для удобства сокращен). Поле argc, содержащее количество аргументов, при этом будет указано только в первой записи.
type=EXECVE msg=audit(1704970843.734:38026): argc=10001 a0="docker" a1="run" a2="--name" <...> a125_len=58 a125[0]="input/2023-01-21/002935bd-fc0d-42fc-b37f-52c4970d760e.json"
type=EXECVE msg=audit(1704970843.734:38026): a126="input/2023-01-21/00297f08-8df5-482b-a37a-c3674f57652c.json" <...> a238_len=58 a238[0]="input/2023-01-21/00661c59-0511-4b4e-ab6e-4a6298517bfa.json"
Аргументы в a-полях также могут разбиваться. При этом в a*_len будет указана длина аргумента, а в a*[i] — его части.
// https://github.com/linux-audit/audit-kernel/blob/6995e2de6891c724bfeb2db33d7b87775f913ad1/kernel/auditsc.c#L1236 len_tmp = 0; if (require_data || (iter > 0) || ((len_abuf + sizeof(abuf)) > len_rem)) { // с разбивкой аргумента if (iter == 0) { len_tmp += snprintf(&abuf[len_tmp], sizeof(abuf) - len_tmp, " a%d_len=%lu", arg, len_full ); } len_tmp += snprintf(&abuf[len_tmp], sizeof(abuf) - len_tmp, " a%d[%d]=", arg, iter++ ); } else // без разбивки len_tmp += snprintf(&abuf[len_tmp], sizeof(abuf) - len_tmp, " a%d=", arg ); WARN_ON(len_tmp >= sizeof(abuf)); abuf[sizeof(abuf) - 1] = '\0'; /* log the arg in the audit record */ audit_log_format(*ab, "%s", abuf);
Ситуация с PROCTITLE и EXECVE очень напоминает ситуацию с полем exe из записи SYSCALL и полем name из записи PATH при запуске скриптов. В событии запуска процесса содержится более корректная информация о процессе (путь к исполняемому файлу и команду запуска). Другие же события, связанные с этим процессом, ее не содержат. Поэтому для восстановления полной картины, как говорилось выше, приходится обращаться к событию запуска процесса.
Системные вызовы: SOCKADDR
Запись SOCKADDR содержит информацию о сокете и присутствует только в событиях, связанных с операциями над ним.
type=SOCKADDR msg=audit(1704970843.702:7777): saddr=020001BB0A0AC0E80000000000000000 SADDR={ saddr_fam=inet laddr=10.10.192.232 lport=443 }
Поле saddr содержит информацию о сокете в байтах, закодированных в HEX. В его обогащенном «собрате» SADDR эта информация представлена в более удобном виде.
Если по какой-то причине в событии отсутствует обогащение, можно расшифровать поле saddr самостоятельно. Под семейство сокета выделены первые два байта, но используется только первый. Значения констант можно узнать из определенного файла исходного кода.
// https://github.com/linux-audit/audit-kernel/blob/6995e2de6891c724bfeb2db33d7b87775f913ad1/include/linux/socket.h#L187 #define AF_UNSPEC 0 #define AF_UNIX 1 /* Unix domain sockets */ #define AF_LOCAL 1 /* POSIX name for AF_UNIX */ #define AF_INET 2 /* Internet IP Protocol */ #define AF_AX25 3 /* Amateur Radio AX.25 */ #define AF_IPX 4 /* Novell IPX */ // <...>
Интерпретация значений дальнейших байтов зависит от семейства сокета.
Варианты saddr и их интерпретация |
saddr: 020000350A0034650000000000000000 |
saddr: 0A000009000000002A0DD6C10000001C000000000000004E00000000 |
saddr: 0A00A6780000000000000000000000000000FFFF0A7EFF0300000000 |
saddr: 01002F6373692F6373692E736F636B00 |
Направление соединения зависит непосредственно от системного вызова, к которому относится запись SOCKADDR. Ниже приведен пример для вызова connect():

События стороннего ПО (USER_*, CRED_*, etc)
Помимо журналирования событий от операционной системы, подсистема аудита способна на стороне ядра принимать события из пользовательского пространства, оборачивать их в свой формат и возвращать обратно для записи в журнал.
Пример события входа в систему от OpenSSH:
type=USER_LOGIN msg=audit(1705079375.303:37516): pid=1868146 uid=0 auid=1000 ses=24982 msg='op=login id=1000 exe="/usr/sbin/sshd" hostname=10.125.3.2 addr=10.125.3.2 terminal=/dev/pts/0 res=success' UID="root" AUID="zabbix" ID="zabbix"
Функция приема события на стороне подсистемы аудита:
// https://github.com/linux-audit/audit-kernel/blob/6995e2de6891c724bfeb2db33d7b87775f913ad1/kernel/audit.c#L1364 case AUDIT_FIRST_USER_MSG ... AUDIT_LAST_USER_MSG: case AUDIT_FIRST_USER_MSG2 ... AUDIT_LAST_USER_MSG2: // <...> err = audit_filter(msg_type, AUDIT_FILTER_USER); if (err == 1) { char *str = data; // <...> // По сути - audit_log_common_recv_msg(), // добавляет в начало события несколько полей audit_log_user_recv_msg(&ab, msg_type); if (msg_type != AUDIT_USER_TTY) { str[data_len - 1] = '\0'; // Содержимое события audit_log_format(ab, " msg='%.*s'", AUDIT_MESSAGE_TEXT_MAX, str); } else { audit_log_format(ab, " data="); if (data_len > 0 && str[data_len - 1] == '\0') data_len--; audit_log_n_untrustedstring(ab, str, data_len); } audit_log_end(ab); } break; // https://github.com/linux-audit/audit-kernel/blob/6995e2de6891c724bfeb2db33d7b87775f913ad1/kernel/audit.c#L1076 static void audit_log_common_recv_msg(struct audit_context *context, struct audit_buffer **ab, u16 msg_type) { uid_t uid = from_kuid(&init_user_ns, current_uid()); pid_t pid = task_tgid_nr(current); // <...> *ab = audit_log_start(context, GFP_KERNEL, msg_type); if (unlikely(!*ab)) return; // Добавляемые в начало события поля audit_log_format(*ab, "pid=%d uid=%u ", pid, uid); audit_log_session_info(*ab); audit_log_task_context(*ab); }
Поля в начале события содержат информацию о процессе, приславшем событие, о пользователе, от чьего имени он запущен, и о сессии. Тело события содержится в поле msg. Оно формируется с помощью функций, предоставляемых библиотекой libaudit.h.
// https://github.com/linux-audit/audit-userspace/blob/572eb7d4fe926e7c1c52166d08e78af54877cbc5/lib/libaudit.h#L710 extern int audit_log_user_message(int audit_fd, int type, const char *message, const char *hostname, const char *addr, const char *tty, int result); extern int audit_log_user_comm_message(int audit_fd, int type, const char *message, const char *comm, const char *hostname, const char *addr, const char *tty, int result); extern int audit_log_acct_message(int audit_fd, int type, const char *pgname, const char *op, const char *name, unsigned int id, const char *host, const char *addr, const char *tty, int result); extern int audit_log_user_avc_message(int audit_fd, int type, const char *message, const char *hostname, const char *addr, const char *tty, uid_t uid); extern int audit_log_semanage_message(int audit_fd, int type, const char *pgname, const char *op, const char *name, unsigned int id, const char *new_seuser, const char *new_role, const char *new_range, const char *old_seuser, const char *old_role, const char *old_range, const char *host, const char *addr, const char *tty, int result); extern int audit_log_user_command(int audit_fd, int type, const char *command, const char *tty, int result); // https://github.com/linux-audit/audit-userspace/blob/572eb7d4fe926e7c1c52166d08e78af54877cbc5/lib/audit_logging.c#L480 // Пример для audit_log_acct_message() if (name && id == -1) { if (audit_value_needs_encoding(name, len)) { audit_encode_value(user, name, len); format = "op=%s acct=%s exe=%s hostname=%s addr=%s terminal=%s res=%s"; } else format = "op=%s acct=\"%s\" exe=%s hostname=%s addr=%s terminal=%s res=%s"; snprintf(buf, sizeof(buf), format, op, user, exename, host ? host : "?", addrbuf, tty ? tty : "?", success ); } else snprintf(buf, sizeof(buf), "op=%s id=%u exe=%s hostname=%s addr=%s terminal=%s res=%s", op, id, exename, host ? host : "?", addrbuf, tty ? tty : "?", success ); // Отправление содержимого события в ядро ret = audit_send_user_message(audit_fd, type, REAL_ERR, buf);
Для корректной нормализации событий от различного ПО, использующего формат auditd, необходимо отдельно изучать исходный код каждого из них.
Пример использования библиотеки libaudit в OpenSSH:
// https://github.com/openssh/openssh-portable/blob/V_9_4_P1/audit-linux.c #include <libaudit.h> #include "log.h" #include "audit.h" // <...> int linux_audit_record_event(int uid, const char *username, const char *hostname, const char *ip, const char *ttyn, int success) { int audit_fd, rc, saved_errno; // <...> rc = audit_log_acct_message(audit_fd, AUDIT_USER_LOGIN, NULL, "login", username ? username : "(unknown)", username == NULL ? uid : -1, hostname, ip, ttyn, success ); // <...> return rc >= 0; } void audit_event(struct ssh *ssh, ssh_audit_event_t event) { switch(event) { case SSH_AUTH_SUCCESS: case SSH_CONNECTION_CLOSE: case SSH_NOLOGIN: case SSH_LOGIN_EXCEED_MAXTRIES: case SSH_LOGIN_ROOT_DENIED: break; case SSH_AUTH_FAIL_NONE: case SSH_AUTH_FAIL_PASSWD: case SSH_AUTH_FAIL_KBDINT: case SSH_AUTH_FAIL_PUBKEY: case SSH_AUTH_FAIL_HOSTBASED: case SSH_AUTH_FAIL_GSSAPI: case SSH_INVALID_USER: linux_audit_record_event(-1, audit_username(), NULL, ssh_remote_ipaddr(ssh), "sshd", 0); break; default: debug("%s: unhandled event %d", __func__, event); break; } }
Ниже приведен пример нормализованного события.

Стоит отметить, что дистрибутивы, основанные на Red Hat, часто содержат патчи для стандартных системных утилит, значительно расширяющие набор событий для подсистемы аудита. В качестве примера можно сравнить события открытия сессии SSH из официального репозитория OpenSSH и события из пакета для CentOS Stream 8:
// https://github.com/openssh/openssh-portable/blob/daa5b2d869ee5a16f3ef9035aa0ad3c70cf4028e/audit-linux.c#L86 void audit_session_open(struct logininfo *li) { if (linux_audit_record_event(li->uid, NULL, li->hostname, NULL, li->line, 1) == 0) fatal("linux_audit_write_entry failed: %s", strerror(errno)); } // https://gitlab.com/redhat/centos-stream/rpms/openssh/-/blame/c8s/openssh-7.6p1-audit.patch#L487 void audit_session_open(struct logininfo *li) { - if (linux_audit_record_event(li->uid, NULL, li->hostname, NULL, - li->line, 1) == 0) - fatal("linux_audit_write_entry failed: %s", strerror(errno)); + if (!user_login_count++) + linux_audit_user_logxxx(li->uid, NULL, li->hostname, + li->line, 1, AUDIT_USER_LOGIN); + linux_audit_user_logxxx(li->uid, NULL, li->hostname, + li->line, 1, AUDIT_USER_START); }
Можно увидеть, что вместо одного события USER_LOGIN на Red-Hat-подобных ОС при входе пользователя будет два события: USER_LOGIN и USER_START. На такие моменты стоит обращать внимание при анализе событий, так как содержимое записей также может отличаться в различных дистрибутивах.
Среди стандартных системных утилит, для которых есть отдельные события в журнале подсистемы аудита, можно выделить следующие:
Shadow Utils — стандартные системные утилиты для управления пользователями и группами;
Pluggable Authentication Modules (PAM) — стандартные модули аутентификации;
Systemd — подсистема инициализации и управления службами.
Ради интереса вы можете самостоятельно поискать на GitHub другие программы, использующие подсистему аудита в качестве основной или дополнительной системы журналирования.

Заключение
На этом, пожалуй, можно завершить рассказ о событиях подсистемы аудита. Я постарался осветить все ее базовые особенности, необходимые для корректной интерпретации большинства событий системы и их последующей нормализации. Если вы готовы поделиться интересными или неочевидными с точки зрения интерпретации кейсами событий — пишите их в комментариях. Будем углублять наши знания вместе.
