
Приветствую всех любителей изучать новое. Меня зовут Рома, и я занимаюсь исследованием безопасности ОС 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=c000003e
syscall=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_64
SYSCALL=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=c000003e
syscall=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_64
SYSCALL=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=7f90742fcb10
items=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=2
ppid=1736 pid=3202324auid=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"
Более подробно об идентификаторах можно почитать на сайте. Ниже приведена краткая информация:
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=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"
Часто встречаемые форматы значений приведены ниже:
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=3202324
auid=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=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"
Поле 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=4294967295
comm="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 другие программы, использующие подсистему аудита в качестве основной или дополнительной системы журналирования.

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