From 2278df1493e064c197913e49b5d1935942d83448 Mon Sep 17 00:00:00 2001 From: daniel Date: Tue, 6 May 2025 16:57:32 -0700 Subject: initial import --- src/fanotify.c | 210 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 src/fanotify.c (limited to 'src/fanotify.c') diff --git a/src/fanotify.c b/src/fanotify.c new file mode 100644 index 0000000..869cc49 --- /dev/null +++ b/src/fanotify.c @@ -0,0 +1,210 @@ + +#define _GNU_SOURCE + +#include +#include +#include + +#include + +#include "json.h" +#include "error.h" +#include "output.h" +#include "fanotify.h" +#include "hash_ledger.h" +#include "time_common.h" + +// set up fanotify descriptor. returns initialized descriptor on +// success, -1 on error +int setup_fanotify(void) { + int fan_fd = fanotify_init(FAN_CLASS_PRE_CONTENT | FAN_CLOEXEC | FAN_NONBLOCK, + O_RDONLY | O_CLOEXEC | O_LARGEFILE); + + if (fan_fd < 0) { + error("fanotify_init failed: %s", strerror(errno)); + return -1; + } + + // TODO mark all mount points instead of / + if (fanotify_mark(fan_fd, + FAN_MARK_ADD | FAN_MARK_MOUNT, + FAN_OPEN_EXEC | FAN_OPEN_EXEC_PERM, + AT_FDCWD, + "/") < 0) { + error("fanotify_mark failed: %s", strerror(errno)); + close(fan_fd); + return -1; + } + + // set non-blocking + int flags = fcntl(fan_fd, F_GETFL, 0); + fcntl(fan_fd, F_SETFL, flags | O_NONBLOCK); + + return fan_fd; +} + +typedef struct { + struct hash_entry *entry; + pattern_table_t *patterns; + size_t match_count; +} match_context_t; + +static void match_callback(const struct ac_match *m, void *user_data) { + match_context_t *ctx = user_data; + + //printf("fanotify matched: %s at offset %zu\n", m->id, m->offset); + + if (ctx->entry->matched_pattern_count < MAX_MATCHED_PATTERNS) { + ctx->entry->matched_patterns[ctx->entry->matched_pattern_count++] = m->id; + } + + pattern_t *p = pattern_table_find(ctx->patterns, m->id); + if (p && p->match_count < MAX_MATCHED_PATTERNS) { + p->match_offsets[p->match_count++] = m->offset; + } +} + +// TODO handle old style FID-based events as well as old style FD +// TODO hash-based allow and blocklists +void select_fanotify(int fan_fd, agent_context_t *ctx) { + struct fanotify_event_metadata buf[4096]; + ssize_t len = read(fan_fd, buf, sizeof(buf)); + if (len < 0) { + if (errno != EAGAIN) { + error("fanotify read: %s", strerror(errno)); + } + + return; + } + + struct fanotify_event_metadata *metadata; + for (metadata = buf; FAN_EVENT_OK(metadata, len); metadata = FAN_EVENT_NEXT(metadata, len)) { + if (metadata->mask & FAN_OPEN_EXEC_PERM) { + struct fanotify_response response; + response.fd = metadata->fd; + response.response = FAN_ALLOW; + + char path[PATH_MAX]; + snprintf(path, sizeof(path), "/proc/self/fd/%d", metadata->fd); + + char target[PATH_MAX]; + ssize_t len = readlink(path, target, sizeof(target) - 1); + if (len >= 0) { + target[len] = '\0'; + } else { + error("readlink: %s", strerror(errno)); + } + + // hash_ledger_find(ctx->hash_ledger, target) + // - if not in ledger, create and scan. + // - if in ledger + // - scan if too much time passed, scan again + // - if verdict is block, send FAN_DENY and emit log w/ rule matches + // - if verdict is quarantine, block and quarantine + // = if verdict is allow, allow it. + + struct hash_entry *he = hash_ledger_find(ctx->hash_ledger, target); + if (!he) { + struct stat sb; + stat(target, &sb); + he = hash_ledger_add_or_update(ctx->hash_ledger, target, &sb); + } + + scan_verdict_t verdict = VERDICT_INFORMATIONAL; + if (he->last_scanned == 0 || time(NULL) - he->last_scanned > 600) { + match_context_t mctx = { + .entry = he, + .patterns = &ctx->rules.patterns, + .match_count = 0 + }; + for (size_t i = 0; i < ctx->rules.patterns.count; i++) { + ctx->rules.patterns.patterns[i].match_count = 0; + } + pattern_table_clear_matches(&ctx->rules.patterns); + + ac_match_path(ctx->ac, target, match_callback, &mctx); + //ac_match_fd(ctx->ac, metadata->fd, match_callback, &mctx); + + he->matched_rule_count = 0; + he->matched_pattern_count = 0; + + for (size_t i = 0; i < ctx->rules.rule_count; i++) { + const rule_t *r = &ctx->rules.rules[i]; + + if (evaluate_rule(r, &ctx->rules.patterns)) { + //printf("fanotify match: %s\n", r->id); + if (he->matched_rule_count < MAX_MATCHED_RULES) { + he->matched_rules[he->matched_rule_count++] = r->id; + } + + if (r->action == RULE_BLOCK) { + verdict = VERDICT_BLOCK; + break; + } else if (r->action == RULE_QUARANTINE && verdict < VERDICT_QUARANTINE) { + verdict = VERDICT_QUARANTINE; + } else if (r->action == RULE_ALLOW && verdict < VERDICT_ALLOW) { + verdict = VERDICT_ALLOW; + } + } + } + + he->verdict = verdict; + he->last_scanned = time(NULL); + he->scan_count++; + } + + switch (he->verdict) { + case VERDICT_BLOCK: + case VERDICT_QUARANTINE: + response.response = FAN_DENY; // deny blocked/quarantined files. + break; + default: + response.response = FAN_ALLOW; // fail open + break; + } + + // TODO emit log for rule matches w/ allow action, not only block/quarantine + if (response.response == FAN_DENY) { + json_t buf = {0}; + json_init(&buf); + json_start_object(&buf); + + json_add_double(&buf, "timestamp", timestamp()); + json_add_string(&buf, "hostname", ctx->hostname); + json_add_string(&buf, "event_type", "fanotify_block"); + json_add_string(&buf, "verdict", + he->verdict == VERDICT_BLOCK ? "block" : + he->verdict == VERDICT_QUARANTINE ? "quarantine" : + he->verdict == VERDICT_ALLOW ? "allow" : "informational"); + + // TODO filesize + json_add_string(&buf, "md5", he->md5); + json_add_string(&buf, "sha256", he->sha256); + json_add_double(&buf, "entropy", he->entropy); + + json_add_array_start(&buf, "matched_rules"); + for (size_t i = 0; i < he->matched_rule_count; i++) { + json_array_add_string(&buf, he->matched_rules[i]); + } + json_end_array(&buf); + + json_end_object(&buf); + output(json_get(&buf)); + json_free(&buf); + } + + //printf("\n\n\nfanotify: fd: %d, pid: %d, mask: %lld, target: %s (%s) verdict: %d\n\n\n", metadata->fd, metadata->pid, metadata->mask, target, he->md5, he->verdict); + + if (write(fan_fd, &response, sizeof(response)) < 0) { + error("fanotify write response: %s", strerror(errno)); + } + } else if (metadata->mask & FAN_OPEN_EXEC) { + // TODO separate branch for FAN_OPEN_EXEC if PERM isn't + // available--prefer this over proc_connector EXEC events + } + + if (metadata->fd >= 0) { + close(metadata->fd); + } + } +} -- cgit v1.2.3