概要

第一回ハッカソン

段階的にゴールを設定しながら進めた.

調査

maliciousなファイルの実行を検知するために,exec系の検知が必要.

Linuxには,ファイルシステムのイベントを通知するfanotifyという機能があるので,これをうまく使えないか最初に考えた.

fanotifyexecの検知ができれば良さそうと思い,man 7 fanotifyを読んでみたものの,日本語のmanを読んだ限りではそれらしき設定は無かった.

<linux/fanotify.h>を読むと,FAN_OPEN_EXECというそれっぽい値が定義されていたので,コメントや英語のmanを読むと,ファイルの実行を検知出来る事がわかった.

PoC

fanotifyのmanには簡単なPoCが書かれていたので,それをベースにファイルの実行を記録するようなコードを書いた

#define _GNU_SOURCE     /* O_LARGEFILE の定義を得るために必要 */
#include <errno.h>
#include <err.h>
#include <fcntl.h>
#include <limits.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/fanotify.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

/* ファイルディスクリプター 'fd' から読み出しできる全 fanotify イベントを読み出す */

static void
handle_events(int fd)
{
    const struct fanotify_event_metadata *metadata;
    struct fanotify_event_metadata buf[200];
    ssize_t len;
    char path[PATH_MAX];
    ssize_t path_len;
    char procfd_path[PATH_MAX];
    struct fanotify_response response;

    /* fanotify ファイルディスクリプターからイベントが読み出せる間はループする */

    for(;;) {

        /* イベントを読み出す */

        len = read(fd, (void *) &buf, sizeof(buf));
        if (len == -1 && errno != EAGAIN) {
            perror("read");
            exit(EXIT_FAILURE);
        }

        /* 読み出せるデータの最後に達しているかチェックする */

        if (len <= 0)
            break;

        /* バッファーの最初のイベントを参照する */

        metadata = buf;

        /* バッファー内の全イベントを処理する */

        while (FAN_EVENT_OK(metadata, len)) {

            /* 実行時とコンパイル時の構造体が一致するか確認する */

            if (metadata->vers != FANOTIFY_METADATA_VERSION) {
                fprintf(stderr,
                        "Mismatch of fanotify metadata version.\n");
                exit(EXIT_FAILURE);
            }

            /* metadata->fd には、キューのオーバーフローを示す FAN_NOFD か、
               ファイルディスクリプター (負でない整数) のいずれかが入っている。
               ここではキューのオーバーフローは無視している。 */

            if (metadata->fd >= 0) {
                if (metadata->mask & FAN_OPEN_EXEC)
                    printf("FAN_OPEN_EXEC: ");
                /* アクセスされたファイルのパス名を取得し表示する */

                snprintf(procfd_path, sizeof(procfd_path),
                         "/proc/self/fd/%d", metadata->fd);
                path_len = readlink(procfd_path, path,
                                    sizeof(path) - 1);
                if (path_len == -1) {
                    perror("readlink");
                    exit(EXIT_FAILURE);
                }

                path[path_len] = '\0';
                printf("File %s\n", path);

                /* イベントのファイルディスクリプターをクローズする */
                close(metadata->fd);
            }

            /* 次のイベントに進む */

            metadata = FAN_EVENT_NEXT(metadata, len);
        }
    }
}

int
main(int argc, char *argv[])
{
    char buf;
    int fd, poll_num;
    nfds_t nfds;
    struct pollfd fds[2];

    /* マウントポイントが指定されたか確認する */

    if (argc != 2)
        return fprintf(stderr, "Usage: %s target\n", argv[0]), 1;

    printf("Press enter key to terminate.\n");

    /* fanotify API にアクセスするためのファイルディスクリプターを作成する */

    fd = fanotify_init(FAN_CLOEXEC | FAN_CLASS_PRE_CONTENT | FAN_NONBLOCK,
                       O_RDONLY | O_LARGEFILE);
    if (fd == -1)
        err(1, "fanotify_init");

    /* 指定されたマウントに対して以下を監視するようにマークを付ける:
       - ファイルのオープン前のアクセス許可イベント
       - 書き込み可能なファイルディスクリプターのクローズ後の
         通知イベント */
    int dirfd = open(argv[1], O_DIRECTORY);
    if (dirfd < 0)
        err(1, "open(%s):", argv[1]);

    if (fanotify_mark(fd, FAN_MARK_ADD | FAN_MARK_MOUNT,
                      FAN_OPEN_EXEC,
                      AT_FDCWD,
                      argv[1]) == -1) {
        perror("fanotify_mark");
        exit(EXIT_FAILURE);
    }

    /* ポーリングの準備 */

    nfds = 2;

    /* コンソールの入力 */

    fds[0].fd = STDIN_FILENO;
    fds[0].events = POLLIN;

    /* fanotify の入力 */

    fds[1].fd = fd;
    fds[1].events = POLLIN;

    /* イベントの発生を待つループ */

    printf("Listening for events.\n");

    while (1) {
        poll_num = poll(fds, nfds, -1);
        if (poll_num == -1) {
            if (errno == EINTR)     /* シグナルに割り込まれた場合 */
                continue;           /* poll() を再開する */

            perror("poll");         /* 予期しないエラー */
            exit(EXIT_FAILURE);
        }

        if (poll_num > 0) {
            if (fds[0].revents & POLLIN) {

                /* コンソールからの入力がある場合: 空の標準入力であれば終了 */

                while (read(STDIN_FILENO, &buf, 1) > 0 && buf != '\n')
                    continue;
                break;
            }

            if (fds[1].revents & POLLIN) {

                /* fanotify イベントがある場合 */

                handle_events(fd);
            }
        }
    }

    printf("Listening for events stopped.\n");
    exit(EXIT_SUCCESS);
}

demo

Rustで

検知できる事はわかったので,Rustで触れるようにしたい.

自分で新しくcrateを作るのも良いが,一から愚直なライブラリを作るのは心が折れるし,需要と供給が一致しない.

すでにある程度整備されているライブラリに機能を追加したほうが楽だし自分以外も使いやすい.

Rustでfanotifyを扱うcrateはいくつかあるが,fanotifyはかなり長いこと放置されており,望みが薄い.

そこでfanotify-rsを選んだ.

コードも割と読みやすそうな感じだし,何より最近のコミットが4ヶ月前だ.

最初の目標は,このcrateFAN_OPEN_EXEC等を使えるようにする事にした.

最初の目標

linux/fanotify.hを読みながら足りない定数を定義し,適宜コメントを書くだけなのですぐ終わった.

ビルドしようと思ったらwarningがいっぱい出てきたのでそこも修正しておいた.

PRはこれ

Cargo.tomlauthors,いまいちよくわかってないけどとりあえず自分の名前も書いておいた.

次へ

次回のハッカソン・ゼミへ向けて,やっておきたい(宿題とは別で,普通に好奇心からどうしても調べたい)事がある

等.

これらについてもまとめたいが,腹が減ったし他の事もやりたいので一旦抜けます.

FAN_OPEN_EXEC

patch

割と最近(v5.1)の変更で,このv5.1のタイミングでfanotifyは色々機能が追加されている.