/
githubmirror
/
libcap-ng
Обзор
Документация
Войти
/
githubmirror
/
libcap-ng
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
utils/cap-audit/cap_audit.c
597 строк
17 KB
Steve Grubb
cap-audit: preserve PATH fallback for target execution
11 авг 2026, 23:18
11 авг 2026, 23:18
db746f5
Код
Авторство
О чём код?
// SPDX-License-Identifier: GPL-2.0-or-later /* * cap-audit - Trace a target process to discover required capabilities. * Copyright (c) 2026 Red Hat Inc. * All Rights Reserved. * * This software may be freely redistributed and/or modified under the * terms of the GNU General Public License as published by the Free * Software Foundation; either version 2, or (at your option) any * later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * Authors: * Steve Grubb <sgrubb@redhat.com> * Portions of this code were made with codex 5.2 */ #include "cap_audit.h" #include "cap_audit_path.h" #include <bpf/bpf.h> #include <errno.h> #include <libaudit.h> #include <limits.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/resource.h> #include <sys/wait.h> #include <unistd.h> /* * Overview: * cap-audit launches a target application, traces that process tree using * eBPF hooks, and reports which Linux capabilities were actually exercised. * The userspace side performs three major jobs: * * (1) prepare the runtime environment by checking our own capabilities and * raising rlimits; * (2) coordinate with the eBPF program by registering the target PID before * exec() and consuming capability check events from the ring buffer; and * (3) analyze collected data to present required, conditional, and denied * capabilities in human and machine-readable formats. * * When the tool observes a capset syscall from the initial PID, it splits * capability accounting into initialization and operational phases. The * initialization phase covers all capability checks from process start to * the first capset. The operational phase covers everything after. This * separation allows the tool to distinguish capabilities needed for one-time * setup (binding privileged ports, chroot, loading restricted configuration) * from capabilities needed for ongoing operation. Recommendations for * programmatic capability dropping use only the operational set, while * deployment recommendations (file capabilities, systemd, containers) use * the union since the process must start with sufficient capabilities for * initialization. Programs that never call capset produce an undifferentiated * report identical to previous versions. * * Core design problem: * when tracing as root, many capability checks come from kernel-internal * work under the same PID rather than from app logic. The tool uses a * two-layer noise filter pipeline, split between BPF and userspace, to * separate real requirements from incidental checks. * * Layer 1 - pre-exec noise (BPF phase gate): * after fork() but before execve() completes, the child is still this * auditor image. PATH lookup, directory traversal, and exec machinery can * trigger checks like DAC_READ_SEARCH/SYS_ADMIN/SETPCAP that are unrelated * to the target app. The BPF side tracks PID phases and suppresses all * capability events until sched_process_exec transitions the PID from * phase 1 (pre-exec) to phase 2 (post-exec). * * Layer 2 - userspace always-noise filter: * handle_cap_event() unconditionally drops two classes of capability * checks that never represent real application requirements. First, * execve-triggered SYS_ADMIN and SETPCAP checks are kernel-internal * credential-transition noise. Second, cap-audit identifies the known * advisory memory overcommit probe by combining syscall, capability, and * CAP_OPT_NOAUDIT: cap_vm_enough_memory checks CAP_SYS_ADMIN on * brk/mmap/mprotect/mremap and the operation succeeds regardless of the * result. CAP_OPT_NOAUDIT means "do not audit", not "advisory", so it * is only a confirming signal here. Other enforcement checks also use * CAP_OPT_NOAUDIT to avoid audit spam, and must still be reported. The * syscall + capability match identifies the specific advisory call site, * while CAP_OPT_NOAUDIT confirms that the event followed the advisory path * rather than a security-gating path on the same syscall. * * Layer 3 - final drain shutdown backstop: * after waitpid() reports that the initial PID has exited, cap-audit does a * short final ring-buffer drain before analyzing results. That drain should * stay conservative because queued late SYS_ADMIN/SETPCAP checks from the * exiting interpreter/runtime are more likely to be teardown chatter than a * meaningful application requirement. The shutdown backstop is intentionally * limited to the initial PID and only applies during that final drain. * * PID filtering remains central: parent registers the child immediately after * fork(), BPF follows forks/exits to keep the target set precise, and each * event carries capability, syscall context, namespace info, and result for * per-capability aggregation. * * Parent/child startup is synchronized with a pipe. The child blocks in * read(sync_pipe) until the parent has inserted its PID in target_pids and * written a go byte. That ordering prevents missed events and prevents events * from arriving before the PID is registered. */ struct audit_state state; int audit_machine = -1; static char **prepend_execstart_argv(const service_config_t *cfg, char **args, int arg_count) __attribute_malloc__ __attr_dealloc_free __attr_access ((__read_only__, 1)) __attr_access ((__read_only__, 2)) __wur; static int has_service_suffix(const char *path) __attr_access ((__read_only__, 1)) __attribute_pure__; static void sig_handler(int sig __attribute__((unused))) { state.stop = 1; } static int set_memlock_rlimit(void) { struct rlimit rlim_new = { .rlim_cur = RLIM_INFINITY, .rlim_max = RLIM_INFINITY, }; if (setrlimit(RLIMIT_MEMLOCK, &rlim_new) < 0) { fprintf(stderr, "Error: Failed to raise memlock rlimit: %s\n", errno == EPERM ? "insufficient privileges" : strerror(errno)); return -1; } return 0; } static int init_capng(void) { capng_clear(CAPNG_SELECT_BOTH); if (capng_get_caps_process() != 0) { fprintf(stderr, "Error: Failed to get process capabilities\n"); return -1; } return 0; } static char **prepend_execstart_argv(const service_config_t *cfg, char **args, int arg_count) { char **argv; int i; argv = calloc((size_t)arg_count + 2, sizeof(char *)); if (!argv) return NULL; argv[0] = cfg->exec_argv[0]; for (i = 0; i < arg_count; i++) argv[i + 1] = args[i]; argv[arg_count + 1] = NULL; return argv; } static int has_service_suffix(const char *path) { size_t len; const char suffix[] = ".service"; size_t suffix_len = sizeof(suffix) - 1; if (!path) return 0; len = strlen(path); return len >= suffix_len && strcmp(path + len - suffix_len, suffix) == 0; } static int check_auditor_caps(void) { if (!capng_have_capability(CAPNG_EFFECTIVE, CAP_BPF) && !capng_have_capability(CAPNG_EFFECTIVE, CAP_SYS_ADMIN)) { fprintf( stderr, "Error: Need CAP_BPF or CAP_SYS_ADMIN to run auditor\n"); return -1; } if (!capng_have_capability(CAPNG_EFFECTIVE, CAP_PERFMON) && !capng_have_capability(CAPNG_EFFECTIVE, CAP_SYS_ADMIN)) { fprintf(stderr, "Error: Need CAP_PERFMON or CAP_SYS_ADMIN for " "perf events\n"); return -1; } if (!capng_have_capability(CAPNG_EFFECTIVE, CAP_SYS_PTRACE)) fprintf(stderr, "Warning: CAP_SYS_PTRACE not available, stack " "traces may be limited\n"); return 0; } static int set_target_pid(pid_t pid) { int map_fd; __u8 val = 1; map_fd = bpf_map__fd(state.skel->maps.target_pids); if (map_fd < 0) { fprintf(stderr, "Error: Failed to get target_pids map fd\n"); return -1; } if (bpf_map_update_elem(map_fd, &pid, &val, BPF_ANY) != 0) { fprintf(stderr, "Error: Failed to register target PID %d: %s\n", pid, strerror(errno)); return -1; } if (state.verbose) printf("[*] Registered PID %d for tracing\n", pid); return 0; } static void usage(FILE *out, const char *prog) { fprintf(out, "Usage: %s [options] -- command [args...]\n", prog); fprintf(out, " %s --service FILE [-- command-or-args...]\n", prog); fprintf(out, "Options:\n"); fprintf(out, " -h, --help Show this help message\n"); fprintf(out, " -v, --verbose Verbose output\n"); fprintf(out, " -j, --json JSON output\n"); fprintf(out, " -y, --yaml YAML output\n"); fprintf(out, " -s, --service FILE Simulate a systemd service unit\n"); } #ifndef CAP_AUDIT_NO_MAIN int main(int argc, char **argv) { int err; int arg_idx; pid_t child; pid_t ret_pid; int wstatus; char *target_path = NULL; char **service_target_argv = NULL; int separator_seen = 0; if (argc < 2) { usage(stderr, argv[0]); return 1; } arg_idx = 1; while (arg_idx < argc && argv[arg_idx][0] == '-') { if (!strcmp(argv[arg_idx], "-h") || !strcmp(argv[arg_idx], "--help")) { usage(stdout, argv[0]); return 0; } else if (!strcmp(argv[arg_idx], "-v") || !strcmp(argv[arg_idx], "--verbose")) state.verbose = 1; else if (!strcmp(argv[arg_idx], "-j") || !strcmp(argv[arg_idx], "--json")) state.json_output = 1; else if (!strcmp(argv[arg_idx], "-y") || !strcmp(argv[arg_idx], "--yaml")) state.yaml_output = 1; else if (!strcmp(argv[arg_idx], "-s") || !strcmp(argv[arg_idx], "--service")) { char *service_file; arg_idx++; if (arg_idx >= argc) { fprintf(stderr, "Error: --service requires a file\n"); usage(stderr, argv[0]); return 1; } service_file = strdup(argv[arg_idx]); if (!service_file) return 1; free(state.service_file); state.service_file = service_file; } else if (!strcmp(argv[arg_idx], "--")) { separator_seen = 1; arg_idx++; break; } else { fprintf(stderr, "Error: Unknown option '%s'\n", argv[arg_idx]); usage(stderr, argv[0]); return 1; } arg_idx++; } if (state.service_file) { state.service_cfg = calloc(1, sizeof(*state.service_cfg)); if (!state.service_cfg) goto err_target_path; if (parse_service_file(state.service_file, state.service_cfg) != 0) goto err_target_path; if (!separator_seen && arg_idx < argc) { fprintf(stderr, "Error: --service command overrides require --\n"); usage(stderr, argv[0]); goto err_target_path; } if (separator_seen && arg_idx < argc) { if (argv[arg_idx][0] == '-') { if (!state.service_cfg->exec_argv) { fprintf(stderr, "Error: service has no ExecStart executable for argument override\n"); goto err_target_path; } service_target_argv = prepend_execstart_argv(state.service_cfg, &argv[arg_idx], argc - arg_idx); if (!service_target_argv) goto err_target_path; state.target_argv = service_target_argv; } else { state.target_argv = &argv[arg_idx]; } } else { if (!state.service_cfg->exec_argv) { fprintf(stderr, "Error: service has no ExecStart command\n"); goto err_target_path; } state.target_argv = state.service_cfg->exec_argv; } } else if (arg_idx >= argc) { fprintf(stderr, "Error: No command specified\n"); usage(stderr, argv[0]); return 1; } else { state.target_argv = &argv[arg_idx]; } /* * Fail before capability checks, BPF setup, or fork/exec tracing so * a bad target command is reported directly by cap-audit. */ target_path = resolve_target_command(state.target_argv[0]); if (!target_path) { fprintf(stderr, "Error: target is not executable: %s\n", state.target_argv[0]); if (!state.service_file && has_service_suffix(state.target_argv[0])) fprintf(stderr, "Hint: to audit a systemd unit, use --service FILE\n"); goto err_target_path; } if (init_capng() != 0) goto err_target_path; if (check_auditor_caps() != 0) goto err_target_path; if (set_memlock_rlimit() != 0) goto err_target_path; state.app.exe = strdup(state.target_argv[0]); state.app.prog_type = UNSUPPORTED; if (audit_machine < 0) audit_machine = audit_detect_machine(); if (audit_machine < 0) { fprintf(stderr, "Error: unable to determine hardware architecture for syscall lookup. Exiting.\n"); free(state.app.exe); goto err_target_path; } state.app.execve_nr = audit_name_to_syscall("execve", audit_machine); state.app.mmap_nr = audit_name_to_syscall("mmap", audit_machine); state.app.brk_nr = audit_name_to_syscall("brk", audit_machine); state.app.mprotect_nr = audit_name_to_syscall("mprotect", audit_machine); state.app.mremap_nr = audit_name_to_syscall("mremap", audit_machine); state.app.capset_nr = audit_name_to_syscall("capset", audit_machine); state.skel = cap_audit_bpf__open_and_load(); if (!state.skel) { fprintf(stderr, "Error: Failed to load BPF program: %s\n", strerror(errno)); free(state.app.exe); goto err_target_path; } err = cap_audit_bpf__attach(state.skel); if (err) { fprintf(stderr, "Error: Failed to attach BPF programs: %s\n", strerror(-err)); cap_audit_bpf__destroy(state.skel); free(state.app.exe); goto err_target_path; } state.rb = ring_buffer__new(bpf_map__fd(state.skel->maps.cap_events), handle_cap_event, NULL, NULL); if (!state.rb) { fprintf(stderr, "Error: Failed to create ring buffer: %s\n", strerror(errno)); cap_audit_bpf__destroy(state.skel); free(state.app.exe); goto err_target_path; } printf("[*] Capability auditor started\n"); if (pipe(state.sync_pipe) != 0) { fprintf(stderr, "Error: pipe failed: %s\n", strerror(errno)); ring_buffer__free(state.rb); cap_audit_bpf__destroy(state.skel); free(state.app.exe); goto err_target_path; } child = fork(); if (child == 0) { char sync_byte; ssize_t bytes; close(state.sync_pipe[1]); bytes = read(state.sync_pipe[0], &sync_byte, 1); if (bytes != 1) { if (bytes < 0) perror("read"); else fprintf(stderr, "Error: failed to sync with parent\n"); close(state.sync_pipe[0]); exit(1); } close(state.sync_pipe[0]); if (state.service_cfg && apply_service_config(state.service_cfg) != 0) _exit(1); /* * The earlier path check is only diagnostic. Repeat lookup here * so execvp() preserves PATH fallback if a candidate changes. */ execvp(state.target_argv[0], state.target_argv); perror("execvp"); exit(1); } else if (child < 0) { fprintf(stderr, "Error: fork failed: %s\n", strerror(errno)); close(state.sync_pipe[0]); close(state.sync_pipe[1]); ring_buffer__free(state.rb); cap_audit_bpf__destroy(state.skel); free(state.app.exe); goto err_target_path; } state.app.pid = child; close(state.sync_pipe[0]); if (set_target_pid(child) != 0) { close(state.sync_pipe[1]); kill(child, SIGKILL); waitpid(child, NULL, 0); ring_buffer__free(state.rb); cap_audit_bpf__destroy(state.skel); free(state.app.exe); goto err_target_path; } if (write(state.sync_pipe[1], "1", 1) != 1) { fprintf(stderr, "Error: write failed: %s\n", strerror(errno)); close(state.sync_pipe[1]); kill(child, SIGKILL); waitpid(child, NULL, 0); ring_buffer__free(state.rb); cap_audit_bpf__destroy(state.skel); free(state.app.exe); goto err_target_path; } close(state.sync_pipe[1]); { char resolved[PATH_MAX]; if (resolve_target_exe(child, resolved, sizeof(resolved)) == 0) { char *resolved_dup = strdup(resolved); if (resolved_dup) { free(state.app.exe); state.app.exe = resolved_dup; } state.app.prog_type = classify_app(state.app.exe); } else { fprintf(stderr, "Warning: unable to resolve target path\n"); } } read_system_state(&state.app); inspect_target_file_caps(child); if (state.app.prog_type == PYTHON && state.verbose) printf("[*] Script interpreter: %s\n", state.app.exe); if (state.service_cfg && state.verbose) print_service_config(state.service_cfg); printf("[*] Tracing application: %s (PID %d)\n", state.app.exe, child); printf("[*] Press Ctrl-C to stop\n\n"); signal(SIGINT, sig_handler); signal(SIGTERM, sig_handler); while (!state.stop) { err = ring_buffer__poll(state.rb, 100); if (err < 0 && err != -EINTR) { fprintf(stderr, "Error polling ring buffer: %s\n", strerror(-err)); break; } ret_pid = waitpid(child, &wstatus, WNOHANG); if (ret_pid == child) { if (WIFEXITED(wstatus)) printf( "\n[*] Application exited with status %d\n", WEXITSTATUS(wstatus)); else if (WIFSIGNALED(wstatus)) printf( "\n[*] Application terminated by signal %d\n", WTERMSIG(wstatus)); break; } } printf("[*] Analyzing results...\n"); state.shutting_down = 1; usleep(100000); ring_buffer__poll(state.rb, 0); if (state.json_output) output_json(); else if (state.yaml_output) output_yaml(); else analyze_capabilities(); ring_buffer__free(state.rb); cap_audit_bpf__destroy(state.skel); free(state.app.exe); free(target_path); free(service_target_argv); if (state.service_cfg) { free_config(state.service_cfg); free(state.service_cfg); } free(state.service_file); for (int i = 0; i <= CAP_LAST_CAP; i++) { if (state.app.checks[i].reason) free(state.app.checks[i].reason); if (state.app.checks[i].op_reason) free(state.app.checks[i].op_reason); if (state.app.checks[i].denied_syscalls) free(state.app.checks[i].denied_syscalls); } return 0; err_target_path: free(target_path); free(service_target_argv); if (state.service_cfg) { free_config(state.service_cfg); free(state.service_cfg); } free(state.service_file); return 1; } #endif