Linux audit ログの調べ物 - libaudit を試す

Linux の audit ログを 内部の実装も調べもせず 何となくな知識まま扱っていたので、いっちょ整頓してみようと調べ物をしていた。カーネルのソースを読んだり、libaudit のソースを読んだり。

kernel.org

github.com

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() を呼び出すのは一緒で、扱う文字列のフォーマットに差異があるだけっぽい

参考

rheb.hatenablog.com

5ecure.medium.com

security-plus-data-science.blogspot.com