All Articles

fanotify による ClamAV の On-Access スキャン - OSS で学ぶ AntiVirus on Linux -

突然ですが、11/2 から開催されている技術書典 17 のオンラインマーケットで、『A part of Anti-Virus -公開サンプルコードで学ぶ Windows filesystem minifilter driver -』という技術同人誌を無料頒布しています。

この本の中では、Windows 用の AntiVirus がリアルタイムファイルスキャン(On-Access スキャン)に利用するファイルシステムミニフィルタドライバーのしくみについて解説しています。

img

参考:A part of Anti-Virus:かえるのほんだな

上記は Magical WinDbg Vol 1/2 と同じく、諸般の事情で無料公開していますので、ぜひお気軽に購入いただければと思います。

本記事では、上記の書籍の番外編的な形で Linux 用 AntiVirus ソフトウェアのリアルタイムファイルスキャン(On-Access スキャン) に利用しているカーネルフレームワークである fanotify について簡単に解説していきます。

Linux 用の ClamAV は Linux カーネルがサポートしている fanotify を使用して On-Access スキャンを実装しています。

主要な商用 Linux 用 AntiVirus ソフトウェアは ClamAV と同じく On-Access スキャンの実装に fanotify を使用しているため、ClamAV の実装をソースコードレベルで理解することはそれらの商用 AntiVirus の動作を理解する上でも有用かと思います。

参考:fanotify(7) - Linux manual page

参考:On-Access Scanning - ClamAV Documentation

この記事では、ClamAV を通して fanotify を利用した Linux 用 AntiVirus の On-Access スキャンの挙動についてざっくりまとめていこうと思っています。

本書では ClamAV 0.104 のソースコードを参照しています。

もくじ

fanotify のテストプログラムを作る

ClamAV の実装を確認する前に、そもそも fanotify がどのように動作するのかを把握するためのテストプログラムを作ってみました。

fanotify とは

fanotify は Linux カーネル 2.6.13 から存在していた inotify というファイルシステムイベントをモニタリングするフレームワークと同様に、システム内のファイルシステムイベントを通知することができ、さらにその操作を許可したり拒否したりすることができる機能です。(さらにその前身として dnotify という機能が存在していたらしいです。)

参考:Linux file system notification subsystem

fanotify は Linux カーネル 2.6.36 で追加された後、2.6.37 で有効化されました。

その後、現在までにいくつかの機能追加やバグ修正などが行われているようです。

手持ちの Linux カーネルに関連する書籍を漁った限り、fanotify についてはほとんど記載がありませんでしたが、LWN.net の 2009 年のメールマガジンを読むと、fanotify は AntiVirus ソフトウェアベンダーや顧客からの要望でOn-Access スキャンを合理的に実装可能な方法を用意するために追加された機能であることがわかります。

So it’s back to that time. I’m not quite sure how to present fanotify. I can start sending patches (they are available), but this message is just going to be a re-into, what questions and problems are still out there?

Long ago the anti-malware vendors started asking the community for a reasonable way to do on access file scanning, historically they have used syscall table rewrites and binary LSM hook hacks to get their information.

Customers and Linux users keep demanding this stuff and in an effort give them a supportable method to use these products I have been working to develop fanotify.

fanotify provides two things:

  1. a new notification system, sorta like inotify, only instead of an arbitrary ‘watch descriptor’ which userspace has to know how to map back to an object on the filesystem, fanotify provides an open read-only fd back to the original object. It should be noted that the set of fanotify events is much smaller than the set of inotify events.
  2. an access system in which processes may be blocked until the fanotify userspace listener has decided if the operation should be allowed.

参考:fanotify: the fscking all notification system [LWN.net]

なお、fanotify はあくまで AntiVirus ソフトウェアによる On-Access スキャンのためのインターフェースとして実装されており、fanotify 自身が Malware の検出やブロックを行うものではありません。

fanotify の実装に関わる議論については以下のスレッドなども参考になりました。

参考:Re: [malware-list] scanner interface proposal was: [TALPA] Intro to a linux interface for on access scanning

参考:TALPA - a threat model? well sorta.

fanotify のサンプルプログラムを作る

今回は fanotify に関する理解を深めるため、テスト用のプログラムを作成して検証してみることにしました。

以下は、以下のマニュアルで紹介されているサンプルコードを少しカスタマイズしたコードです。

参考:Ubuntu Manpage: fanotify - ファイルシステムイベントを監視する

/* Define _GNU_SOURCE, Otherwise we don't get O_LARGEFILE */
#define _GNU_SOURCE

#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <poll.h>
#include <errno.h>
#include <limits.h>
#include <sys/stat.h>
#include <sys/signalfd.h>
#include <fcntl.h>

#include <linux/fanotify.h>

/* Structure to keep track of monitored directories */
typedef struct
{
    /* Path of the directory */
    char *path;
} monitored_t;

/* Size of buffer to use when reading fanotify events */
#define FANOTIFY_BUFFER_SIZE 8192

/* Enumerate list of FDs to poll */
enum
{
    FD_POLL_SIGNAL = 0,
    FD_POLL_FANOTIFY,
    FD_POLL_MAX
};

/* Setup fanotify notifications (FAN) mask. All these defined in fanotify.h. */
static uint64_t event_mask =
    (FAN_ACCESS |         /* File accessed */
     FAN_MODIFY |         /* File modified */
     FAN_CLOSE_WRITE |    /* Writtable file closed */
     FAN_CLOSE_NOWRITE |  /* Unwrittable file closed */
     FAN_OPEN |           /* File was opened */
     FAN_ONDIR |          /* We want to be reported of events in the directory */
     FAN_EVENT_ON_CHILD); /* We want to be reported of events in files of the directory */

/* Array of directories being monitored */
static monitored_t *monitors;
static int n_monitors;

static char *
get_program_name_from_pid(int pid,
                          char *buffer,
                          size_t buffer_size)
{
    int fd;
    ssize_t len;
    char *aux;

    /* Try to get program name by PID */
    sprintf(buffer, "/proc/%d/cmdline", pid);
    if ((fd = open(buffer, O_RDONLY)) < 0)
        return NULL;

    /* Read file contents into buffer */
    if ((len = read(fd, buffer, buffer_size - 1)) <= 0)
    {
        close(fd);
        return NULL;
    }
    close(fd);

    buffer[len] = '\0';
    aux = strstr(buffer, "^@");
    if (aux)
        *aux = '\0';

    return buffer;
}

static char *
get_file_path_from_fd(int fd,
                      char *buffer,
                      size_t buffer_size)
{
    ssize_t len;

    if (fd <= 0)
        return NULL;

    sprintf(buffer, "/proc/self/fd/%d", fd);
    if ((len = readlink(buffer, buffer, buffer_size - 1)) < 0)
        return NULL;

    buffer[len] = '\0';
    return buffer;
}

static void event_process(struct fanotify_event_metadata *event)
{
    char path[PATH_MAX];

    printf("Received event in path '%s'",
           get_file_path_from_fd(event->fd,
                                 path,
                                 PATH_MAX)
               ? path
               : "unknown");
    printf(" pid=%d (%s): \n",
           event->pid,
           (get_program_name_from_pid(event->pid,
                                      path,
                                      PATH_MAX)
                ? path
                : "unknown"));

    if (event->mask & FAN_OPEN)
        printf("\tFAN_OPEN\n");
    if (event->mask & FAN_ACCESS)
        printf("\tFAN_ACCESS\n");
    if (event->mask & FAN_MODIFY)
        printf("\tFAN_MODIFY\n");
    if (event->mask & FAN_CLOSE_WRITE)
        printf("\tFAN_CLOSE_WRITE\n");
    if (event->mask & FAN_CLOSE_NOWRITE)
        printf("\tFAN_CLOSE_NOWRITE\n");
    fflush(stdout);

    close(event->fd);
}

static void
shutdown_fanotify(int fanotify_fd)
{
    int i;

    for (i = 0; i < n_monitors; ++i)
    {
        /* Remove the mark, using same event mask as when creating it */
        fanotify_mark(fanotify_fd,
                      FAN_MARK_REMOVE,
                      event_mask,
                      AT_FDCWD,
                      monitors[i].path);
        free(monitors[i].path);
    }
    free(monitors);
    close(fanotify_fd);
}

static int
initialize_fanotify(int argc,
                    const char **argv)
{
    int i;
    int fanotify_fd;

    /* Create new fanotify device */
    if ((fanotify_fd = fanotify_init(FAN_CLASS_CONTENT | FAN_UNLIMITED_QUEUE | FAN_UNLIMITED_MARKS,
                                     O_RDONLY | O_LARGEFILE)) < 0)
    {
        fprintf(stderr,
                "Couldn't setup new fanotify device: %s\n",
                strerror(errno));
        return -1;
    }

    /* Allocate array of monitor setups */
    n_monitors = argc - 1;
    monitors = malloc(n_monitors * sizeof(monitored_t));

    /* Loop all input directories, setting up marks */
    for (i = 0; i < n_monitors; ++i)
    {
        monitors[i].path = strdup(argv[i + 1]);
        /* Add new fanotify mark */
        if (fanotify_mark(fanotify_fd,
                          FAN_MARK_ADD | FAN_MARK_MOUNT,
                          event_mask,
                          AT_FDCWD,
                          monitors[i].path) < 0)
        {
            fprintf(stderr,
                    "Couldn't add monitor in directory '%s': '%s'\n",
                    monitors[i].path,
                    strerror(errno));
            return -1;
        }

        printf("Started monitoring directory '%s'...\n",
               monitors[i].path);
    }

    return fanotify_fd;
}

static void
shutdown_signals(int signal_fd)
{
    close(signal_fd);
}

static int
initialize_signals(void)
{
    int signal_fd;
    sigset_t sigmask;

    /* We want to handle SIGINT and SIGTERM in the signal_fd, so we block them. */
    sigemptyset(&sigmask);
    sigaddset(&sigmask, SIGINT);
    sigaddset(&sigmask, SIGTERM);

    if (sigprocmask(SIG_BLOCK, &sigmask, NULL) < 0)
    {
        fprintf(stderr,
                "Couldn't block signals: '%s'\n",
                strerror(errno));
        return -1;
    }

    /* Get new FD to read signals from it */
    if ((signal_fd = signalfd(-1, &sigmask, 0)) < 0)
    {
        fprintf(stderr,
                "Couldn't setup signal FD: '%s'\n",
                strerror(errno));
        return -1;
    }

    return signal_fd;
}

int main(int argc,
         const char **argv)
{
    int signal_fd;
    int fanotify_fd;
    struct pollfd fds[FD_POLL_MAX];

    /* Input arguments... */
    if (argc < 2)
    {
        fprintf(stderr, "Usage: %s directory1 [directory2 ...]\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* Initialize signals FD */
    if ((signal_fd = initialize_signals()) < 0)
    {
        fprintf(stderr, "Couldn't initialize signals\n");
        exit(EXIT_FAILURE);
    }

    /* Initialize fanotify FD and the marks */
    if ((fanotify_fd = initialize_fanotify(argc, argv)) < 0)
    {
        fprintf(stderr, "Couldn't initialize fanotify\n");
        exit(EXIT_FAILURE);
    }

    /* Setup polling */
    fds[FD_POLL_SIGNAL].fd = signal_fd;
    fds[FD_POLL_SIGNAL].events = POLLIN;
    fds[FD_POLL_FANOTIFY].fd = fanotify_fd;
    fds[FD_POLL_FANOTIFY].events = POLLIN;

    /* Now loop */
    for (;;)
    {
        /* Block until there is something to be read */
        if (poll(fds, FD_POLL_MAX, -1) < 0)
        {
            fprintf(stderr,
                    "Couldn't poll(): '%s'\n",
                    strerror(errno));
            exit(EXIT_FAILURE);
        }

        /* Signal received? */
        if (fds[FD_POLL_SIGNAL].revents & POLLIN)
        {
            struct signalfd_siginfo fdsi;

            if (read(fds[FD_POLL_SIGNAL].fd, &fdsi, sizeof(fdsi)) != sizeof(fdsi))
            {
                fprintf(stderr, "Couldn't read signal, wrong size read\n");
                exit(EXIT_FAILURE);
            }

            /* Break loop if we got the expected signal */
            if (fdsi.ssi_signo == SIGINT || fdsi.ssi_signo == SIGTERM)
            {
                break;
            }

            fprintf(stderr, "Received unexpected signal\n");
        }

        /* fanotify event received? */
        if (fds[FD_POLL_FANOTIFY].revents & POLLIN)
        {
            char buffer[FANOTIFY_BUFFER_SIZE];
            ssize_t length;

            /* Read from the FD. It will read all events available up to
             * the given buffer size. */
            if ((length = read(fds[FD_POLL_FANOTIFY].fd, buffer, FANOTIFY_BUFFER_SIZE)) > 0)
            {
                struct fanotify_event_metadata *metadata;

                metadata = (struct fanotify_event_metadata *)buffer;
                while (FAN_EVENT_OK(metadata, length))
                {
                    event_process(metadata);
                    if (metadata->fd > 0)
                        close(metadata->fd);
                    metadata = FAN_EVENT_NEXT(metadata, length);
                }
            }
        }
    }

    /* Clean exit */
    shutdown_fanotify(fanotify_fd);
    shutdown_signals(signal_fd);

    printf("Exiting fanotify example...\n");

    return EXIT_SUCCESS;
}

上記のサンプルについては本筋ではないので詳細な解説は行いませんが、ClamAV とほぼ同じ以下の設定で fanotify の初期化を行っています。

/* Setup fanotify notifications (FAN) mask. All these defined in fanotify.h. */
static uint64_t event_mask =
    (FAN_ACCESS |         /* File accessed */
     FAN_MODIFY |         /* File modified */
     FAN_CLOSE_WRITE |    /* Writtable file closed */
     FAN_CLOSE_NOWRITE |  /* Unwrittable file closed */
     FAN_OPEN |           /* File was opened */
     FAN_ONDIR |          /* We want to be reported of events in the directory */
     FAN_EVENT_ON_CHILD); /* We want to be reported of events in files of the directory */

fanotify_init(FAN_CLASS_CONTENT | FAN_UNLIMITED_QUEUE | FAN_UNLIMITED_MARKS, O_RDONLY | O_LARGEFILE)
    
fanotify_mark(fanotify_fd,
              FAN_MARK_ADD | FAN_MARK_MOUNT,
              event_mask,
              AT_FDCWD,
              monitors[i].path)

このソースコードをビルドしたプログラムに対してマウントポイントを指定して実行すると、指定したマウントポイント内のディレクトリやファイルアクセスが発生した場合に fanotify の通知を受け取ることができます。

image-20241027211958055

なお、ここでかなりややこしかったのが FAN_MARK_MOUNT の扱いです。

fanotify_mark 関数で FAN_MARK_MOUNT を指定した場合、fanotify は path 引数で受け取った「マウントポイント」内のすべてのディレクトリとファイルを監視対象とするようです。

この時監視対象になるのは「マウントポイント」であって「( Windows におけるフォルダーと同じ意味での)ディレクトリ」ではないようです。

私が初めに検証した環境では、 /home ディレクトリは /usr などの他のディレクトリと同じマウントポイント直下に存在していたため、/home/user などを監視対象に指定しても、/usr/file などの同じマウントポイント内のすべてのファイルへのアクセスイベントが通知されてしまいました。

実際に、以下の記事のような手順で /home ディレクトリを別のパーティションに格納した後、/etc/fstab をいじって /home に対して独自のマウントポイントを設定すると、fanotify で /home 配下のディレクトリやファイルへのアクセスのみを監視できるようになりました。

参考:Ubuntuで使用中のhomeディレクトリを別パーティションに移動する (LiveUSBなど不要) - kamocyc’s blog

なお、特定のディレクトリ、もしくはファイルのみの操作を監視したい場合は、FAN_MARK_MOUNT ではなく FAN_MARK_ADD だけを指定するとよさそうです。

特定のファイルの操作を禁止する

fanotify でファイルシステムイベントを通知できるようになったので、次は特定のファイルの操作のみを禁止するようにコードを実装しなおします。

修正箇所は少ないですが、作成したコードの全文を記載します。

/* Define _GNU_SOURCE, Otherwise we don't get O_LARGEFILE */
#define _GNU_SOURCE

#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <poll.h>
#include <errno.h>
#include <limits.h>
#include <sys/stat.h>
#include <sys/signalfd.h>
#include <fcntl.h>

#include <sys/fanotify.h>

/* Structure to keep track of monitored directories */
typedef struct
{
    /* Path of the directory */
    char *path;
} monitored_t;

/* Size of buffer to use when reading fanotify events */
#define FANOTIFY_BUFFER_SIZE 8192

/* Enumerate list of FDs to poll */
enum
{
    FD_POLL_SIGNAL = 0,
    FD_POLL_FANOTIFY,
    FD_POLL_MAX
};

/* Setup fanotify notifications (FAN) mask. All these defined in fanotify.h. */
static uint64_t event_mask =
    (FAN_OPEN_PERM |
     FAN_ACCESS |         /* File accessed */
     FAN_MODIFY |         /* File modified */
     FAN_CLOSE_WRITE |    /* Writtable file closed */
     FAN_CLOSE_NOWRITE |  /* Unwrittable file closed */
     FAN_OPEN |           /* File was opened */
     FAN_ONDIR |          /* We want to be reported of events in the directory */
     FAN_EVENT_ON_CHILD); /* We want to be reported of events in files of the directory */

/* Array of directories being monitored */
static monitored_t *monitors;
static int n_monitors;

static char *
get_program_name_from_pid(int pid,
                          char *buffer,
                          size_t buffer_size)
{
    int fd;
    ssize_t len;
    char *aux;

    /* Try to get program name by PID */
    sprintf(buffer, "/proc/%d/cmdline", pid);
    if ((fd = open(buffer, O_RDONLY)) < 0)
        return NULL;

    /* Read file contents into buffer */
    if ((len = read(fd, buffer, buffer_size - 1)) <= 0)
    {
        close(fd);
        return NULL;
    }
    close(fd);

    buffer[len] = '\0';
    aux = strstr(buffer, "^@");
    if (aux)
        *aux = '\0';

    return buffer;
}

static char *
get_file_path_from_fd(int fd,
                      char *buffer,
                      size_t buffer_size)
{
    ssize_t len;

    if (fd <= 0)
        return NULL;

    sprintf(buffer, "/proc/self/fd/%d", fd);
    if ((len = readlink(buffer, buffer, buffer_size - 1)) < 0)
        return NULL;

    buffer[len] = '\0';
    return buffer;
}

static void event_process(int fanotify_fd, struct fanotify_event_metadata *event)
{
    char path[PATH_MAX];

    printf("Received event in path '%s'",
           get_file_path_from_fd(event->fd,
                                 path,
                                 PATH_MAX)
               ? path
               : "unknown");
    printf(" pid=%d (%s): \n",
           event->pid,
           (get_program_name_from_pid(event->pid,
                                      path,
                                      PATH_MAX)
                ? path
                : "unknown"));

    if (event->mask & FAN_OPEN)
        printf("\tFAN_OPEN\n");
    if (event->mask & FAN_ACCESS)
        printf("\tFAN_ACCESS\n");
    if (event->mask & FAN_MODIFY)
        printf("\tFAN_MODIFY\n");
    if (event->mask & FAN_CLOSE_WRITE)
        printf("\tFAN_CLOSE_WRITE\n");
    if (event->mask & FAN_CLOSE_NOWRITE)
        printf("\tFAN_CLOSE_NOWRITE\n");
    fflush(stdout);

    struct fanotify_response response;
    response.fd = event->fd;

    if (strcmp(get_file_path_from_fd(event->fd,
                                     path,
                                     PATH_MAX),
               "/home/rana/eicar") == 0)
    {
        response.response = FAN_DENY;
    }
    else
    {
        response.response = FAN_ALLOW;
    }

    write(fanotify_fd, &response, sizeof(response));

    close(event->fd);
}

static void
shutdown_fanotify(int fanotify_fd)
{
    int i;

    for (i = 0; i < n_monitors; ++i)
    {
        /* Remove the mark, using same event mask as when creating it */
        fanotify_mark(fanotify_fd,
                      FAN_MARK_REMOVE,
                      event_mask,
                      AT_FDCWD,
                      monitors[i].path);
        free(monitors[i].path);
    }
    free(monitors);
    close(fanotify_fd);
}

static int
initialize_fanotify(int argc,
                    const char **argv)
{
    int i;
    int fanotify_fd;

    /* Create new fanotify device */
    if ((fanotify_fd = fanotify_init(FAN_CLASS_CONTENT | FAN_UNLIMITED_QUEUE | FAN_UNLIMITED_MARKS,
                                     O_RDONLY | O_LARGEFILE)) < 0)
    {
        fprintf(stderr,
                "Couldn't setup new fanotify device: %s\n",
                strerror(errno));
        return -1;
    }

    /* Allocate array of monitor setups */
    n_monitors = argc - 1;
    monitors = malloc(n_monitors * sizeof(monitored_t));

    /* Loop all input directories, setting up marks */
    for (i = 0; i < n_monitors; ++i)
    {
        monitors[i].path = strdup(argv[i + 1]);
        /* Add new fanotify mark */
        if (fanotify_mark(fanotify_fd,
                          FAN_MARK_ADD | FAN_MARK_MOUNT,
                          event_mask,
                          AT_FDCWD,
                          monitors[i].path) < 0)
        {
            fprintf(stderr,
                    "Couldn't add monitor in directory '%s': '%s'\n",
                    monitors[i].path,
                    strerror(errno));
            return -1;
        }

        printf("Started monitoring directory '%s'...\n",
               monitors[i].path);
    }

    return fanotify_fd;
}

static void
shutdown_signals(int signal_fd)
{
    close(signal_fd);
}

static int
initialize_signals(void)
{
    int signal_fd;
    sigset_t sigmask;

    /* We want to handle SIGINT and SIGTERM in the signal_fd, so we block them. */
    sigemptyset(&sigmask);
    sigaddset(&sigmask, SIGINT);
    sigaddset(&sigmask, SIGTERM);

    if (sigprocmask(SIG_BLOCK, &sigmask, NULL) < 0)
    {
        fprintf(stderr,
                "Couldn't block signals: '%s'\n",
                strerror(errno));
        return -1;
    }

    /* Get new FD to read signals from it */
    if ((signal_fd = signalfd(-1, &sigmask, 0)) < 0)
    {
        fprintf(stderr,
                "Couldn't setup signal FD: '%s'\n",
                strerror(errno));
        return -1;
    }

    return signal_fd;
}

int main(int argc,
         const char **argv)
{
    int signal_fd;
    int fanotify_fd;
    struct pollfd fds[FD_POLL_MAX];

    /* Input arguments... */
    if (argc < 2)
    {
        fprintf(stderr, "Usage: %s directory1 [directory2 ...]\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    /* Initialize signals FD */
    if ((signal_fd = initialize_signals()) < 0)
    {
        fprintf(stderr, "Couldn't initialize signals\n");
        exit(EXIT_FAILURE);
    }

    /* Initialize fanotify FD and the marks */
    if ((fanotify_fd = initialize_fanotify(argc, argv)) < 0)
    {
        fprintf(stderr, "Couldn't initialize fanotify\n");
        exit(EXIT_FAILURE);
    }

    /* Setup polling */
    fds[FD_POLL_SIGNAL].fd = signal_fd;
    fds[FD_POLL_SIGNAL].events = POLLIN;
    fds[FD_POLL_FANOTIFY].fd = fanotify_fd;
    fds[FD_POLL_FANOTIFY].events = POLLIN;

    /* Now loop */
    for (;;)
    {
        /* Block until there is something to be read */
        if (poll(fds, FD_POLL_MAX, -1) < 0)
        {
            fprintf(stderr,
                    "Couldn't poll(): '%s'\n",
                    strerror(errno));
            exit(EXIT_FAILURE);
        }

        /* Signal received? */
        if (fds[FD_POLL_SIGNAL].revents & POLLIN)
        {
            struct signalfd_siginfo fdsi;

            if (read(fds[FD_POLL_SIGNAL].fd, &fdsi, sizeof(fdsi)) != sizeof(fdsi))
            {
                fprintf(stderr, "Couldn't read signal, wrong size read\n");
                exit(EXIT_FAILURE);
            }

            /* Break loop if we got the expected signal */
            if (fdsi.ssi_signo == SIGINT || fdsi.ssi_signo == SIGTERM)
            {
                break;
            }

            fprintf(stderr, "Received unexpected signal\n");
        }

        /* fanotify event received? */
        if (fds[FD_POLL_FANOTIFY].revents & POLLIN)
        {
            char buffer[FANOTIFY_BUFFER_SIZE];
            ssize_t length;

            /* Read from the FD. It will read all events available up to
             * the given buffer size. */
            if ((length = read(fds[FD_POLL_FANOTIFY].fd, buffer, FANOTIFY_BUFFER_SIZE)) > 0)
            {
                struct fanotify_event_metadata *metadata;

                metadata = (struct fanotify_event_metadata *)buffer;
                while (FAN_EVENT_OK(metadata, length))
                {
                    event_process(fanotify_fd, metadata);
                    if (metadata->fd > 0)
                        close(metadata->fd);
                    metadata = FAN_EVENT_NEXT(metadata, length);
                }
            }
        }
    }

    /* Clean exit */
    shutdown_fanotify(fanotify_fd);
    shutdown_signals(signal_fd);

    printf("Exiting fanotify example...\n");

    return EXIT_SUCCESS;
}

最初のポイントは fanotify_mark 関数で使用する event_maskFAN_OPEN_PERM のビットを追記している点です。

static uint64_t event_mask =
    (FAN_OPEN_PERM |
     FAN_ACCESS |         /* File accessed */
     FAN_MODIFY |         /* File modified */
     FAN_CLOSE_WRITE |    /* Writtable file closed */
     FAN_CLOSE_NOWRITE |  /* Unwrittable file closed */
     FAN_OPEN |           /* File was opened */
     FAN_ONDIR |          /* We want to be reported of events in the directory */
     FAN_EVENT_ON_CHILD); /* We want to be reported of events in files of the directory */

これを追加すると、fanotify は fanotify_init で生成したファイルディスクリプタ経由で操作の許可または拒否などの通知を受け取るまで、対象の操作の実施を保留します。(タイムアウト値はデフォルトでは 5 秒らしい)

その後は、fanotify からファイルシステムイベントを受け取った際に、操作対象のファイルが /home/rana/eicar であるか否かをチェックし、特定のファイルの操作が行われた場合のみ fanotify_response 構造体の response メンバに FAN_DENY を設定します。

struct fanotify_response response;
response.fd = event->fd;

if (strcmp(get_file_path_from_fd(event->fd,
                                 path,
                                 PATH_MAX),
                                 "/home/rana/eicar") == 0)
{
    response.response = FAN_DENY;
}
else
{
    response.response = FAN_ALLOW;
}

write(fanotify_fd, &response, sizeof(response));

このプログラムを実行すると、以下のようにハードコードしたパスに存在するファイルにアクセスした場合のみ、そのアクセスが拒否されるようになります。

image-20241027224825657

clamonacc を読む

fanotify の使用方法についてざっくり理解できたので、ClamAV で On-Access スキャンを有効化した際に使用される clamonacc のコードを詳しく読むことにします。

clamonacc の main 関数の実装を確認する

ClamAV では On-Access スキャンは clamonacc と clamd の 2 つのプロセスが連動して実現しているようです。

clamd は libclamav を使用してウイルススキャンを行うためのマルチスレッドデーモンとして稼働しているプロセスですので、On-Access スキャンの挙動を確認するためにはまず clamonacc のコードを見ていくことにします。

初期化処理

clamonacc の main 関数ではまず、onas_init_context 関数を使用して onas_context の領域の初期化を行います。

struct onas_context *onas_init_context(void)
{
    struct onas_context *ctx = (struct onas_context *)cli_malloc(sizeof(struct onas_context));
    if (NULL == ctx) {
        return NULL;
    }

    memset(ctx, 0, sizeof(struct onas_context));
    return ctx;
}

ここでメモリの割り当てに使用されている cli_malloc は ClamAV 独自の関数ではありますが、単純に引数として受け取ったサイズを使用して malloc 関数によるメモリの割り当てを行う関数のようです。

#define CLI_MAX_ALLOCATION (182 * 1024 * 1024)

void *cli_malloc(size_t size)
{
    void *alloc;

    if (!size || size > CLI_MAX_ALLOCATION) {
        cli_errmsg("cli_malloc(): Attempt to allocate %lu bytes. Please report to https://github.com/Cisco-Talos/clamav/issues\n", (unsigned long int)size);
        return NULL;
    }

    alloc = malloc(size);

    if (!alloc) {
        perror("malloc_problem");
        cli_errmsg("cli_malloc(): Can't allocate memory (%lu bytes).\n", (unsigned long int)size);
        return NULL;
    } else
        return alloc;
}

ここで割り当てているコンテキストである onas_context 構造体にはスキャンに関連する様々な情報が含まれますが、現在のバージョンではそこまで活用されていなさそうです。

struct optstruct {
    char *name;
    char *cmd;
    char *strarg;
    long long numarg;
    int enabled;
    int active;
    int flags;
    int idx;
    struct optstruct *nextarg;
    struct optstruct *next;

    char **filename; /* cmdline */
};

struct onas_context {
    const struct optstruct *opts;
    const struct optstruct *clamdopts;

    int printinfected;
    int maxstream;

    uint32_t ddd_enabled;

    int fan_fd;
    uint64_t fan_mask;
    uint8_t retry_on_error;
    uint8_t retry_attempts;
    uint8_t deny_on_error;

    uint64_t sizelimit;
    uint64_t extinfo;

    int scantype;
    int isremote;
    int session;
    int timeout;

    int64_t portnum;

    int32_t maxthreads;
} __attribute__((packed));

コマンドライン引数の取得

続く数行では、clamonacc のコマンドライン引数をパースします。

ここで取得したコマンドライン引数はコンテキストの opts メンバに、さらに optparse 関数でパースした情報は clamdopts メンバに保存されます。

/* Parse out all our command line options */
opts = optparse(NULL, argc, argv, 1, OPT_CLAMONACC, OPT_CLAMSCAN, NULL);
if (opts == NULL) {
    logg("!Clamonacc: can't parse command line options\n");
    return 2;
}
ctx->opts = opts;

if (optget(opts, "verbose")->enabled) {
    mprintf_verbose = 1;
    logg_verbose    = 1;
}

/* And our config file options */
clamdopts = optparse(optget(opts, "config-file")->strarg, 0, NULL, 1, OPT_CLAMD, 0, NULL);
if (clamdopts == NULL) {
    logg("!Clamonacc: can't parse clamd configuration file %s\n", optget(opts, "config-file")->strarg);
    optfree((struct optstruct *)opts);
    return 2;
}
ctx->clamdopts = clamdopts;

これはどちらも optstruct 構造体へのポインタであり、リストととして管理されます。

image-20241025221022858

fanotify の登録を行う

次は startup_checks 関数による起動チェックです。

チェック結果に何らかの意味のある値が返ってくると、done セクションにジャンプしてクリーンアップ後にプロセスを終了するようです。

/* Make sure we're good to begin spinup */
ret = startup_checks(ctx);
if (ret) {
    if (ret == (int)CL_BREAK) {
        ret = 0;
    }
    goto done;
}

この startup_checks 関数の中では fanotify への登録が行われます。

#if defined(_GNU_SOURCE)
ctx->fan_fd = fanotify_init(FAN_CLASS_CONTENT | FAN_UNLIMITED_QUEUE | FAN_UNLIMITED_MARKS, O_LARGEFILE | O_RDONLY);
#else
ctx->fan_fd = fanotify_init(FAN_CLASS_CONTENT | FAN_UNLIMITED_QUEUE | FAN_UNLIMITED_MARKS, O_RDONLY);
#endif
if (ctx->fan_fd < 0) {
    logg("!Clamonacc: fanotify_init failed: %s\n", cli_strerror(errno, faerr, sizeof(faerr)));
    if (errno == EPERM) {
        logg("!Clamonacc: clamonacc must have elevated permissions ... exiting ...\n");
    }
    ret = 2;
    goto done;
}

fanotify への登録は fanotify_init 関数で行われます。

手元の環境でデバッグを行った際には、fanotify_init(FAN_CLASS_CONTENT | FAN_UNLIMITED_QUEUE | FAN_UNLIMITED_MARKS, O_LARGEFILE | O_RDONLY); の引数を使用して fanotify_init 関数を実行していることを確認できました。

image-20241025225143323

この関数は、fanotify グループを作成し、初期化する関数です。

fanotify_init 関数は、戻り値として作成したグループに関連づけられたイベントキューのファイルディスクリプタを返します。

clamonacc では、この戻り値はコンテキストの fan_fd メンバに保持されるようです。

参考:fanotify_init(2) - Linux manual page

fanotify_init 関数の第 1 引数である flag には notification classes の指定が含まれます。

clamonacc では一般に AntiVirus ソフトウェアに利用される FAN_CLASS_CONTENT を登録しています。

このクラスは、「ファイルにアクセスが発生したことを通知するイベント」と「ファイルに対するアクセス許可の決定イベント」を受信できるクラスです。

他に、fanotify のマークとイベントキューの上限を撤廃する FAN_UNLIMITED_QUEUE および FAN_UNLIMITED_MARKS の指定があります。

参考:fanotify(7) - Linux manual page

また、第 2 引数には O_LARGEFILE | O_RDONLY の指定が含まれます。

O_LARGEFILE は 2GB を超えるサイズのファイルの監視をサポートするもので、O_RDONLY は読み取りアクセスのみを許可する設定のようです。

fanotify_init 関数の呼び出し後は、fanotify_mark 関数による監視の登録を行います。

clamonacc では clamd の設定によって fanotify_mark 関数を呼び出す箇所が複数に分岐しますが、OnAccessMountPath のオプションでマウントポイントを指定している場合、onas_setup_fanotif 関数から呼び出された fanotif.c 内の以下のコードが実行されるようです。

image-20241101223410446

ここでは、fanotify_mark(onas_fan_fd, FAN_MARK_ADD | FAN_MARK_MOUNT, (*ctx)->fan_mask, (*ctx)->fan_fd, pt->strarg) を実行しており、pt->strarg の引数に OnAccessMountPath で指定したマウントポイントのパスが渡されます。

なお、ClamAV では、オプションとして OnAccessMountPath と fanotify でファイルアクセスのブロックを行うためのオプション OnAccessPrevention を併用することができません。

The OnAccessMountPath option uses a different fanotify api configuration which makes it incompatible with OnAccessIncludePath and the DDD System. Therefore, inotify watch-point limitations will not be a concern when using this option. Unfortunately, this also means that the following options cannot be used in conjunction with OnAccessMountPath:

OnAccessExtraScanning - is built around catching inotify events. OnAccessExcludePath - is built upon the DDD System. OnAccessPrevention - would lock up the system if / was selected for OnAccessMountPath. If you need OnAccessPrevention, you should use OnAccessIncludePath instead of OnAccessMountPath.

参考:On-Access Scanning - ClamAV Documentation

これはつまり、OnAccessIncludePath ではなく OnAccessMountPath を指定している場合は、ファイルが検出されてもアクセス自体はブロックされないことを意味します。

実際に clamonacc のソースコードを見ると、OnAccessMountPath が enabled の場合は FAN_OPEN_PERM ではなく FAN_OPEN の fanotify マスクが使用されることを確認できます。

ただし、今回はテストのため以下のようにソースコードを変更し、OnAccessMountPath を enabled に設定している場合でもファイルアクセスがブロックされるようにしています。

image-20241103155744015

ここまでに確認した onas_setup_fanotif 関数の処理が終了すると、onas_handle_signals 関数と onas_start_eloop 関数が呼び出され、On-Access スキャンの監視が行われるようになります。

/* Setup fanotify */
switch (onas_setup_fanotif(&ctx)) {
    case CL_SUCCESS:
        break;
    case CL_BREAK:
        ret = 0;
        goto done;
        break;
    case CL_EARG:
    default:
        mprintf("!Clamonacc: can't setup fanotify\n");
        ret = 2;
        goto done;
        break;
}

**
    
/* Setup signal handling */
g_ctx = ctx;
onas_handle_signals();

logg("*Clamonacc: beginning event loops\n");
/*  Kick off event loop(s) */
ret = onas_start_eloop(&ctx);

シグナルハンドリングの設定を行う

onas_handle_signals 関数では、以下のコードでシグナルハンドリングの設定を行います。

static void onas_handle_signals()
{
    sigset_t sigset;
    struct sigaction act;

    /* ignore all signals except SIGUSR1 */
    sigfillset(&sigset);
    sigdelset(&sigset, SIGUSR1);
    sigdelset(&sigset, SIGUSR2);
    /* The behavior of a process is undefined after it ignores a
	 * SIGFPE, SIGILL, SIGSEGV, or SIGBUS signal */
    sigdelset(&sigset, SIGFPE);
    sigdelset(&sigset, SIGILL);
    sigdelset(&sigset, SIGSEGV);
    sigdelset(&sigset, SIGINT);
    sigdelset(&sigset, SIGTERM);
#ifdef SIGBUS
    sigdelset(&sigset, SIGBUS);
#endif
    pthread_sigmask(SIG_SETMASK, &sigset, NULL);
    memset(&act, 0, sizeof(struct sigaction));
    act.sa_handler = onas_clamonacc_exit;
    sigfillset(&(act.sa_mask));
    sigaction(SIGUSR2, &act, NULL);
    sigaction(SIGTERM, &act, NULL);
    sigaction(SIGSEGV, &act, NULL);
    sigaction(SIGINT, &act, NULL);
}

sigfillset 関数で全てのシグナルが含まれるようにシグナルセットを初期化した後、sigdelset 関数で一部のシグナルをシグナルセットから削除しています。

さらにその後、新たに割り当てた act という sigaction 構造体の情報を使用し、SIGTERM などのシグナルを onas_clamonacc_exit 関数と紐づけています。

参考:sigfillset(3) manページ

実際に、kill コマンドで SIGTERM を発行した際の処理をデバッグすると、onas_clamonacc_exit 関数が呼び出されて処理を終了することを確認できます。

image-20241101230336454

スキャンイベントを待機する

ここまでの処理が完了した場合、最後にイベントループをトリガーする onas_start_eloop 関数が呼び出されます。

この関数の中では、さらに onas_fan_eloop 関数が実行されます。

この関数の実装(一部抜粋)は以下の通りです。

time_t start = time(NULL) - 30;

while (((bread = read((*ctx)->fan_fd, buf, sizeof(buf))) > 0) || (errno == EOVERFLOW || errno == EMFILE || errno == EACCES)) {

    switch (errno) {
        ***
    }

    fmd = (struct fanotify_event_metadata *)buf;
    while (FAN_EVENT_OK(fmd, bread)) {
        
        ***

        scan = 1;

        ***
    }
}

冒頭で検証に使ったサンプルコードと同じく、fanotify で登録したファイルディスクリプタから fanotify_event_metadata の情報を buf に読み出した後、FAN_EVENT_OK マクロのチェックを通し、scan フラグに 1 をセットしています。

この後さらにいくつかのチェックを行い、onas_scan_event 構造体の変数の初期化と情報の書き込みを行います。

onas_scan_event 構造体には、readlink 関数で取得したファイル名の情報などが書き込まれます。

struct onas_scan_event {
    const char *tcpaddr;
    int64_t portnum;
    char *pathname;
    int fan_fd;
#if defined(HAVE_SYS_FANOTIFY_H)
    struct fanotify_event_metadata *fmd;
#endif
    uint8_t retry_attempts;
    uint64_t sizelimit;
    int32_t scantype;
    int64_t maxstream;
    int64_t timeout;
    uint8_t bool_opts;
} __attribute((packed));


if (scan) {
    struct onas_scan_event *event_data;

    event_data = cli_calloc(1, sizeof(struct onas_scan_event));
    
    ***

    /* general mapping */
    onas_map_context_info_to_event_data(*ctx, &event_data);
    scan ? event_data->bool_opts |= ONAS_SCTH_B_SCAN : scan;

    /* fanotify specific stuffs */
    event_data->bool_opts |= ONAS_SCTH_B_FANOTIFY;
    event_data->fmd = cli_malloc(sizeof(struct fanotify_event_metadata));
    
    ***
           
    memcpy(event_data->fmd, fmd, sizeof(struct fanotify_event_metadata));
    event_data->pathname = cli_strdup(fname);
    
    ***

    logg("*ClamFanotif: attempting to feed consumer queue\n");

    /* feed consumer queue */
    if (CL_SUCCESS != onas_queue_event(event_data)) {
        close(fmd->fd);
        free(event_data->pathname);
        free(event_data->fmd);
        free(event_data);
        logg("!ClamFanotif: error occurred while feeding consumer queue ... \n");
        if ((*ctx)->retry_on_error) {
            err_cnt++;
            if (err_cnt < (*ctx)->retry_attempts) {
                logg("ClamFanotif: ... recovering ...\n");
                fmd = FAN_EVENT_NEXT(fmd, bread);
                continue;
            }
        }
        return 2;
    }
}

ここで作成された onas_scan_event 構造体の変数である event_data は、最後に onas_queue_event 関数に渡されます。

この関数の中ではまず、グローバル変数として定義されている pthread_mutex_t 構造体の変数 onas_queue_lock を使用してロックを取得します。

その後、同じくグローバル変数として登録されている g_onas_event_queue_tail をキューのリストに追加し、引数として受け取った event_data を書き込みます。

cl_error_t onas_queue_event(struct onas_scan_event *event_data)
{
    struct onas_event_queue_node *node = NULL;
    if (CL_EMEM == onas_new_event_queue_node(&node))
        return CL_EMEM;

    pthread_mutex_lock(&onas_queue_lock);
    node->next                                                            = g_onas_event_queue_tail;
    node->prev                                                            = g_onas_event_queue_tail->prev;
    ((struct onas_event_queue_node *)g_onas_event_queue_tail->prev)->next = node;
    g_onas_event_queue_tail->prev                                         = node;

    node->data = event_data;

    g_onas_event_queue.size++;

    pthread_cond_signal(&onas_scan_queue_empty_cond);
    pthread_mutex_unlock(&onas_queue_lock);

    return CL_SUCCESS;
}

ここではどうやら、スキャン対象の情報などが記録されたデータをキューに追加しているようです。

スキャンキューの監視

順番が前後しましたが、clamonacc.c の main 関数では、onas_setup_fanotif 関数による fanotify の登録を行う前に以下のコードが実行されます。

/* Setup our event queue */
ctx->maxthreads = optget(ctx->clamdopts, "OnAccessMaxThreads")->numarg;

switch (onas_scan_queue_start(&ctx)) {
    case CL_SUCCESS:
        break;
    case CL_BREAK:
    case CL_EARG:
    case CL_ECREAT:
    default:
        ret = 2;
        logg("!Clamonacc: can't setup event consumer queue\n");
        goto done;
        break;
}

ここで呼び出される onas_scan_queue_start 関数は pthread_create 関数を使って新しくスレッドを起動しています。

この時スレッドの start_routine として onas_scan_queue_th 関数がセットされています。

cl_error_t onas_scan_queue_start(struct onas_context **ctx)
{

    pthread_attr_t scan_queue_attr;
    int32_t thread_started = 1;

    if (!ctx || !*ctx) {
        logg("*ClamScanQueue: unable to start clamonacc. (bad context)\n");
        return CL_EARG;
    }

    if (pthread_attr_init(&scan_queue_attr)) {
        return CL_BREAK;
    }
    pthread_attr_setdetachstate(&scan_queue_attr, PTHREAD_CREATE_JOINABLE);
    thread_started = pthread_create(&scan_queue_pid, &scan_queue_attr, onas_scan_queue_th, *ctx);

    if (0 != thread_started) {
        /* Failed to create thread */
        logg("*ClamScanQueue: Unable to start event consumer queue thread ... \n");
        return CL_ECREAT;
    }

    return CL_SUCCESS;
}

上記の通り、onas_scan_queue_th 関数内では、sigfillset と sigdelset を使用してシグナルハンドリングの設定を行っています。

先に紹介した onas_handle_signals 関数の操作と一部重複しているように見えますが、この辺りの知識がなく、onas_scan_queue_th 側でも sigfillset を使用している理由は不明です。

デバッガを仕掛けてみると onas_handle_signals 関数 と onas_scan_queue_th 関数の呼び出し順序はまばらで、onas_handle_signals 関数の方が先に呼び出される場合もあるようですので、干渉しないのかは疑問です。

この実装の理由については今後判明したら追記します。

シグナルハンドリングの設定を行った後は、onas_init_event_queue 関数で onas_event_queue 構造体のリスト g_onas_event_queue をサイズ 0 で初期化した後に onas_consume_event 関数をループ内で実行します。

void *onas_scan_queue_th(void *arg)
{

    /* not a ton of use for context right now, but perhaps in the future we can pass in more options */
    struct onas_context *ctx = (struct onas_context *)arg;
    sigset_t sigset;
    int ret;

    /* ignore all signals except SIGUSR2 */
    sigfillset(&sigset);
    sigdelset(&sigset, SIGUSR2);
    /* The behavior of a process is undefined after it ignores a
	 * SIGFPE, SIGILL, SIGSEGV, or SIGBUS signal */
    sigdelset(&sigset, SIGFPE);
    sigdelset(&sigset, SIGILL);
    sigdelset(&sigset, SIGSEGV);
    sigdelset(&sigset, SIGTERM);
    sigdelset(&sigset, SIGINT);
#ifdef SIGBUS
    sigdelset(&sigset, SIGBUS);
#endif

    logg("*ClamScanQueue: initializing event queue consumer ... (%d) threads in thread pool\n", ctx->maxthreads);
    onas_init_event_queue();
    threadpool thpool = thpool_init(ctx->maxthreads);
    g_thpool          = thpool;

    /* loop w/ onas_consume_event until we die */
    pthread_cleanup_push(onas_scan_queue_exit, NULL);
    logg("*ClamScanQueue: waiting to consume events ...\n");
    do {
        onas_consume_event(thpool);
    } while (1);

    pthread_cleanup_pop(1);
}

onas_consume_event 関数は g_onas_event_queue のキューから要素を pop し、thpool_add_work 関数を使用してスレッドプールに onas_scan_worker の処理とともに渡されます。

参考:Pithikos/C-Thread-Pool: A minimal but powerful thread pool in ANSI C

static int onas_queue_is_b_empty()
{

    if (g_onas_event_queue.head->next == g_onas_event_queue.tail) {
        return 1;
    }

    return 0;
}


static int onas_consume_event(threadpool thpool)
{
    pthread_mutex_lock(&onas_queue_lock);

    while (onas_queue_is_b_empty()) {
        pthread_cond_wait(&onas_scan_queue_empty_cond, &onas_queue_lock);
    }

    struct onas_event_queue_node *popped_node = g_onas_event_queue_head->next;
    g_onas_event_queue_head->next             = g_onas_event_queue_head->next->next;
    g_onas_event_queue_head->next->prev       = g_onas_event_queue_head;
    g_onas_event_queue.size--;

    pthread_mutex_unlock(&onas_queue_lock);

    thpool_add_work(thpool, (void *)onas_scan_worker, (void *)popped_node->data);
    onas_destroy_event_queue_node(popped_node);

    return 1;
}

この onas_scan_worker 関数はいくつかの条件分岐を行いスキャンジョブを実行します。

今回テストに利用している環境では、キューから取り出したイベント情報を onas_scan_thread_handle_file 関数に渡す処理を行います。

image-20241102181444683

onas_scan_thread_handle_file 関数は、簡単なチェックやファイル情報を取得した後、初期化した infected 変数などを引数として onas_scan_thread_scanfile 関数に渡します。

ret = onas_scan_thread_scanfile(event_data, curr->fts_path, sb, &infected, &err, &ret_code);

onas_scan_thread_scanfile 関数は onas_scan -> onas_scan_safe -> onas_client_scan と関数を呼び出していき、最終的に clamd にスキャン要求を発行する onas_dsresult 関数を実行してスキャン結果を取得します。

onas_scan(event_data, fname, sb, infected, err, ret_code);onas_scan_safe(event_data, fname, sb, infected, err, ret_code);onas_client_scan(event_data->tcpaddr, event_data->portnum, event_data->scantype, event_data->maxstream,fname, fd, event_data->timeout, sb, infected, err, ret_code);if ((ret = onas_dsresult(curl, scantype, maxstream, fname, fd, timeout, &ret, err, ret_code)) >= 0) {
    *infected = ret;
}

onas_scan_thread_scanfile 関数では、最終的に clamd から受け取ったファイルのスキャン結果を元に、対象ファイルに対するアクセス可否を決定します。

もしスキャンの結果対象ファイルが Malware と判定された場合は、以下の箇所で fanotify_response 構造体の response メンバに FAN_DENY を設定します。

if (b_fanotify) {
    if ((*err && *ret_code && b_deny_on_error) || *infected) {
        res.response = FAN_DENY;
    }
}

これで、対象ファイルが検知された場合には fanotify によってファイルアクセスがブロックされます。

ちなみに、fanotify のアクセスマスクを FAN_OPEN_PERM に設定してしまうと fanotify のデバッグを上手く進めることができないので、今回は以下のように print デバッグを行うようにコードを改変しました。

image-20241103160512144

実際にテスト環境で Eicar を On-Access スキャンで検出させてみると、以下の通り上記の箇所のコードが実行され、FAN_DENY によるアクセス拒否が行われたことを確認できました。

image-20241103154210665

まとめ

オープンソースの Linux 用 AntiVirus である ClamAV を参考にしつつ、fanotify によるファイルアクセス制御と On-Access スキャンの実装についてまとめました。