#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); } } }