Linux の audit ログを 内部の実装も調べもせず 何となくな知識まま扱っていたので、いっちょ整頓してみようと調べ物をしていた。カーネルのソースを読んだり、libaudit のソースを読んだり。
audit ログはカーネル内部で生成されるもの ( 例えばシステムコールの監査 ) と、ユーザ空間から書き込むものとあるが ( 例えば pam モジュール )、後者は libaudit を使ってログを出せる。
Proof of Concept
libaudit での audit ログ書き出しは割と簡単なコードで扱える。試しに小さいコードを書いた。
#include <stdio.h> #include <libaudit.h> int main() { int fd = audit_open(); if (fd == -1) { perror("failed to audit_open"); return 1; } if (audit_log_user_message(fd, AUDIT_USYS_CONFIG, "audit_log_user_message", "example.com", NULL, NULL, 1) < 0) { perror("failed to audit_log_user_message"); audit_close(fd); return 1; } audit_close(fd); return 0; }
root ユーザで実行すると /var/log/audit/audit.log に以下のようなログが残る
type=USYS_CONFIG msg=audit(1728036027.707:474): pid=6119 uid=0 auid=1000 ses=48 subj=unconfined msg='audit_log_user_message exe="/vagrant/main" hostname=example.com addr=93.184.215.14 terminal=pts/2 res=success'UID="root" AUID="vagrant"
type の指定は適当である。
audit_open(3)
audit ログを書き出すためのファイルデスクリプタを返す関数であるが、socket(PF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_AUDIT);
なソケットを扱っている。
/* * This function opens a connection to the kernel's audit * subsystem. You must be root for the call to succeed. On error, * a negative value is returned. On success, the file descriptor is * returned - which can be 0 or higher. */ int audit_open(void) { int fd = socket(PF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_AUDIT); if (fd < 0) { if (errno == EINVAL || errno == EPROTONOSUPPORT || errno == EAFNOSUPPORT) audit_msg(LOG_ERR, "Error - audit support not in kernel"); else audit_msg(LOG_ERR, "Error opening audit netlink socket (%s)", strerror(errno)); } return fd; }
netlink と audit のアーキテクチャは、検索すれば色々な文章が見つかるのでそちらを参考にされたし
audit_close(3)
audit_open(3) で得たファイルデスクリプタを close(2) する。すごい、エラーハンドリングがない!
void audit_close(int fd) { if (fd >= 0) close(fd); }
(カーネルのバグがない限りは ... ) netlink のソケットの close(2) は必ず成功するんだろうかな?
audit_log_user_message(3)
audit ログに書き出す文字列を snprintf(3) で組み立てて、libaudit 内部の audit_send_user_message() を呼び出す。 (最終的に netlink のソケットに書き出す。この辺は深追いししすぎになるので触れない )
以下のような処理が入ってるあたりが面白かった
- 引数の addr の 名前解決 (DNS ) をしてくれる
- audit_log_user_message(3) を呼び出したプロセスを実行するバイナリ名を解決してくれる
- audit_log_user_message(3) を呼び出したプロセスに付いた TTY 名を解決してくれる
/* * This function will log a message to the audit system using a predefined * message format. This function should be used by all console apps that do * not manipulate accounts or groups. * * audit_fd - The fd returned by audit_open * type - type of message, ex: AUDIT_USER, AUDIT_USYS_CONFIG, AUDIT_USER_LOGIN * message - the message being sent * hostname - the hostname if known * addr - The network address of the user * tty - The tty of the user * result - 1 is "success" and 0 is "failed" * * It returns the sequence number which is > 0 on success or <= 0 on error. */ int audit_log_user_message(int audit_fd, int type, const char *message, const char *hostname, const char *addr, const char *tty, int result) { char buf[MAX_AUDIT_MESSAGE_LENGTH]; char addrbuf[INET6_ADDRSTRLEN]; static char exename[PATH_MAX*2]=""; char ttyname[TTY_PATH]; const char *success; int ret; if (audit_fd < 0) return 0; if (result) success = "success"; else success = "failed"; /* If hostname is empty string, make it NULL ptr */ if (hostname && *hostname == 0) hostname = NULL; /* See if we can deduce addr */ addrbuf[0] = 0; if (addr == NULL || strlen(addr) == 0) _resolve_addr(addrbuf, hostname); else strncat(addrbuf, addr, sizeof(addrbuf)-1); /* Fill in exec name if needed */ if (exename[0] == 0) _get_exename(exename, sizeof(exename)); /* Fill in tty if needed */ if (tty == NULL) tty = _get_tty(ttyname, TTY_PATH); else if (*tty == 0) tty = NULL; /* Get the local name if we have a real tty */ if (hostname == NULL && tty) hostname = _get_hostname(tty); snprintf(buf, sizeof(buf), "%s exe=%s hostname=%s addr=%s terminal=%s res=%s", message, exename, hostname ? hostname : "?", addrbuf, tty ? tty : "?", success ); errno = 0; ret = audit_send_user_message( audit_fd, type, HIDE_IT, buf ); if ((ret < 1) && errno == 0) errno = ret; return ret; }
audit_log_user_message(3) 以外にも audit ログを書き出す関数はあるが、内部で audit_send_user_message() を呼び出すのは一緒で、扱う文字列のフォーマットに差異があるだけっぽい
- audit_log_user_comm_message(3) - Linux man page,
- audit_log_acct_message(3) - Linux man page
- audit_log_user_avc_message(3) - Linux man page
- audit_log_semanage_message(3) - Linux man page