Как стать автором
Обновить
Positive Technologies
Лидер результативной кибербезопасности

Учимся понимать события подсистемы аудита Linux

Уровень сложностиСредний
Время на прочтение35 мин
Количество просмотров26K

Приветствую всех любителей изучать новое. Меня зовут Рома, и я занимаюсь исследованием безопасности ОС Linux в экспертной лаборатории PT Expert Security Center.

В рамках инициативы нашей компании по обмену экспертными знаниями с сообществом я расскажу вам об известной многим администраторам системе журналирования в Linux-подобных ОС — подсистеме аудита Linux (auditd). При должной настройке она позволяет получать полную информацию о действиях, выполняемых в операционной системе.

Специалистам по информационной безопасности полезно уметь расшифровывать и обрабатывать события auditd для отслеживания потенциально вредоносной активности. В связи с этим нам потребовалось организовать для них экспертную поддержку в системе мониторинга событий ИБ и управления инцидентами MaxPatrol SIEM. При их изучении мы старались ответить на следующие вопросы:

  1. Связаны ли между собой записи в журнале событий? Если да, то каким образом?

  2. Чем отличаются записи с разными значениями type=XXX в журнале, и какую информацию может нести каждая из них?

  3. Почему события типа type=USER_* и type=CRED_* при одинаковом значении поля type могут нести различный набор полей и как их корректно нормализовывать в SIEM?

Давайте разберем особенности и подводные камни, с которыми пришлось столкнуться при изучении подсистемы аудита. Возможно, вы что-то уже знаете из официальной документации, например из man (справочных страниц Linux). Некоторые же особенности нигде не описаны, и их пришлось выяснять самостоятельно.

Я буду рассматривать актуальные на момент написания статьи версии исходного кода auditd: audit-kernel 6.4 и audit-userspace 3.1.2. В качестве примеров нормализованных событий приведены скриншоты из интерфейса MaxPatrol SIEM. Правила нормализации вы можете найти в нашем открытом репозитории Security Experts Community.

Для быстрой навигации

Внутреннее устройство

Подсистема аудита Linux — это полноценный компонент ядра, неразрывно связанный с ним.

Взаимодействие компонентов подсистемы аудита Linux
Взаимодействие компонентов подсистемы аудита 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() можно изменить их значения. Но в нашей практике таких примеров не было.

Нормализованное событие выглядит следующим образом.

Информация о событии в MaxPatrol SIEM
Информация о событии в MaxPatrol SIEM

Дополнительные записи наподобие 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

Всегда имеет значение start. Поле op в событиях auditd — это, как правило, сокращение слова operation.

ver

Версия службы auditd

format

Формат событий. Всего может быть два значения:

· raw: события будут представлены в базовом виде, без дополнительной обработки;

· enriched: события будут обогащены дополнительной информацией, например именами пользователей на основе их UID. Об этих обогащениях говорилось ранее.

kernel

Версия ядра Linux, в котором запущена подсистема аудита

pid

ID запущенного процесса auditd

uid

ID пользователя, от имени которого запущен процесс auditd

res

Результат операции. Всегда имеет значение success

Ниже приведен пример нормализованного события.

Информация о событии в MaxPatrol SIEM
Информация о событии в MaxPatrol SIEM

Что же касается события DAEMON_END, то, к сожалению, по завершении работы auditd оно не отправляется на удаленный сервер, поэтому на стороне SIEM-системы мы его не увидим. Но для общего понимания все же разберем его элементы:

Поле

Описание

op

Всегда имеет значение terminate

pid

ID процесса, отправившего сигнал о завершении службы

res

Всегда имеет значение success

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

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

Всегда имеет значение set, то есть это операция задания параметра

<function_name>

Новое значение параметра function_name. Таблица маппинга function_name и параметров из файла audit.rules будет приведена ниже.

old

Старое значение изменяемого параметра

res

Результат операции

Ниже приведена таблица маппинга параметров.

Параметр auditctl/audit.rules

Значение function_name

Описание

-b

audit_backlog_limit

Размер очереди для событий, ожидающих отправки в службу аудита

--backlog_wait_time

audit_backlog_wait_time

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

-e

audit_enabled

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

Возможные значения:

· 0: системные вызовы не проходят через подсистему аудита, не обрабатываются ей и, соответственно, не журналируются;

· 1: системные вызовы проходят через подсистему аудита, обрабатываются ей и журналируются (если настроены правила аудита для этих вызовов);

· 2: конфигурация службы аудита блокируется на время её работы. Снять блокировку можно только перезагрузив узел.

-f

audit_failure

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

Возможные значения:

· 0: ничего не делать — просто пропустить журналирование записи;

· 1: внести запись в журнал ядра с помощью printk;

· 2: вызвать так называемую «панику ядра» (kernel panic).

-r

audit_rate_limit

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

Нормализованное событие имеет следующий вид.

Информация о событии в MaxPatrol SIEM
Информация о событии в MaxPatrol SIEM

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

Правила аудита можно разделить на два вида (подробнее см. в разделе 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=. Если оно не было задано, то будет установлено значение (null).

list

Значение константы, обозначающей атрибут list из правила аудита (см. раздел RULE OPTIONS на сайте).

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 команду, с помощью которой выполнялась установка правила. Но не всегда в нем указывается именно команда — об этом поговорим ниже.

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

Ниже приведен пример нормализованного события.

Информация о событии в MaxPatrol SIEM
Информация о событии в MaxPatrol SIEM

Системные вызовы: 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_64 SYSCALL=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 не поддерживает обогащение событий или если оно не настроено).

Пример правила нормализации syscall_attributes_modify
Пример правила нормализации syscall_attributes_modify

Структура вызова

Изучим информацию о системном вызове:

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"

Лучше всего делать это, опираясь на соответствующие man-страницы, например в нашем случае — на страницу о вызове execve().

Страница man

Рассмотрим поля вызова по очереди:

  • 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=0 items=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=openat AUID="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=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=PATH msg=audit(1704970843.702:73702361): item=0 name="/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=1 name="/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.

Слева направо: inode - mode – … - OUID - OGID – … – … - name
Слева направо: inode - mode – … - OUID - OGID – … – … - name

Стоит отметить, что значение в поле 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():

Пример для функции do_renameat2()
Пример для функции do_renameat2()

Идентификаторы процесса

В этом разделе рассмотрим идентификаторы процесса и пользователей, которые с ним связаны.

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"

Более подробно об идентификаторах можно почитать на сайте. Ниже приведена краткая информация:

  • 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=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"

Эти поля уже рассматривались в разделе 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:

Информация о событии в MaxPatrol SIEM
Информация о событии в MaxPatrol SIEM

Если же запущенный скрипт начнет выполнять другие системные вызовы (например, обращаться к файлам), то у нас в распоряжении будет только поле 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

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

Примеры заголовков из вывода команды ps
Примеры заголовков из вывода команды ps

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

Информация о процессе в событии в MaxPatrol SIEM
Информация о процессе в событии в MaxPatrol SIEM

Системные вызовы: CWD

Запись CWD содержит единственное поле cwd с полным путем к текущему рабочему каталогу процесса, выполнившего системный вызов.

type=CWD msg=audit(1704970843.287:209): cwd="/home/cm/ansible-role"

Относительные пути в записях PATH в некоторых случаях считаются относительно cwd. Пример такого случая приведен ниже.

Информация о событии в MaxPatrol SIEM
Информация о событии в MaxPatrol SIEM

Системные вызовы: 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
Семейство сокета: AF_INET
Байты по порядку:
· 2 байта: порт (0035h = 53)
· 4 байта: IP-адрес (0A003465h = 10.0.52.101)
· 8 байт: нулевые

saddr: 0A000009000000002A0DD6C10000001C000000000000004E00000000
Семейство сокета: AF_INET6
Байты по порядку:
· 2 байта: порт (0009h = 9)
· 4 байта: нулевые
· 16 байт: IP-адрес (2A0DD6C10000001C000000000000004Eh = 2a0d:d6c1:0:1c::4e)
· 4 байта: нулевые

saddr: 0A00A6780000000000000000000000000000FFFF0A7EFF0300000000
Семейство сокета: AF_INET6 (кейс с адресом IPv4, преобразованным в IPv6)
Байты по порядку:
· 2 байта: порт (A678h = 42616)
· 4 байта: нулевые
· 16 байт: IP-адрес (00000000000000000000FFFF0A7EFF03h = 10.126.255.3)
· 4 байта: нулевые

saddr: 01002F6373692F6373692E736F636B00
Семейство сокета: AF_UNIX
Все байты: путь к сокету (2F6373692F6373692E736F636B00h = /csi/csi.sock)

Направление соединения зависит непосредственно от системного вызова, к которому относится запись SOCKADDR. Ниже приведен пример для вызова connect():

Информация о событии в MaxPatrol SIEM
Информация о событии в MaxPatrol SIEM

События стороннего ПО (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;
	}
}

Ниже приведен пример нормализованного события.

Информация о событии в MaxPatrol SIEM
Информация о событии в MaxPatrol SIEM

Стоит отметить, что дистрибутивы, основанные на 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 другие программы, использующие подсистему аудита в качестве основной или дополнительной системы журналирования.

Пример с результатами поиска
Пример с результатами поиска

Заключение

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

Теги:
Хабы:
Всего голосов 5: ↑5 и ↓0+5
Комментарии1

Публикации

Информация

Сайт
www.ptsecurity.com
Дата регистрации
Дата основания
2002
Численность
1 001–5 000 человек
Местоположение
Россия