/* * preload_audit.c - libc-level interposer: log every file/exec/socket call a * dynamically linked ELF makes, without ptrace. * * gcc -shared -fPIC -O2 -o libpreload_audit.so preload_audit.c -ldl * ELFAUDIT_LOG=/tmp/audit.log LD_PRELOAD=$PWD/libpreload_audit.so ./binary * * Why bother when strace exists: * * ~zero slowdown (no context switch per syscall), usable on load tests * * you see the *libc* view: fopen("cfg") before the loader resolves it * * works where ptrace is blocked (containers without CAP_SYS_PTRACE, * kernel.yama.ptrace_scope=2/3) * Limits, be honest about them: * * static binaries and setuid binaries ignore LD_PRELOAD * * Go binaries and anything issuing raw syscalls bypass libc entirely * * a program that wants to hide can detect and unset LD_PRELOAD */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include static int logfd = -1; static void audit_init(void) __attribute__((constructor)); static void audit_init(void) { const char *path = getenv("ELFAUDIT_LOG"); if (!path) path = "/tmp/elfaudit.log"; logfd = open(path, O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC, 0644); } static void emit(const char *kind, const char *detail, const char *result) { if (logfd < 0) return; struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); char line[2048]; int n = snprintf(line, sizeof line, "%ld.%06ld\t%d\t%s\t%s\t%s\n", (long)ts.tv_sec, ts.tv_nsec / 1000, (int)getpid(), kind, detail ? detail : "-", result ? result : "-"); if (n > 0) (void)!write(logfd, line, (size_t)n); } #define REAL(name) \ static typeof(name) *real_##name; \ if (!real_##name) real_##name = (typeof(name) *)dlsym(RTLD_NEXT, #name) /* ---- files ------------------------------------------------------------ */ int open(const char *path, int flags, ...) { mode_t mode = 0; if (flags & O_CREAT) { va_list ap; va_start(ap, flags); mode = va_arg(ap, int); va_end(ap); } REAL(open); int fd = real_open(path, flags, mode); emit((flags & (O_WRONLY | O_RDWR)) ? "open-write" : "open-read", path, fd < 0 ? "FAIL" : "ok"); return fd; } int open64(const char *path, int flags, ...) { mode_t mode = 0; if (flags & O_CREAT) { va_list ap; va_start(ap, flags); mode = va_arg(ap, int); va_end(ap); } REAL(open64); int fd = real_open64(path, flags, mode); emit((flags & (O_WRONLY | O_RDWR)) ? "open-write" : "open-read", path, fd < 0 ? "FAIL" : "ok"); return fd; } int openat(int dirfd, const char *path, int flags, ...) { mode_t mode = 0; if (flags & O_CREAT) { va_list ap; va_start(ap, flags); mode = va_arg(ap, int); va_end(ap); } REAL(openat); int fd = real_openat(dirfd, path, flags, mode); emit((flags & (O_WRONLY | O_RDWR)) ? "open-write" : "open-read", path, fd < 0 ? "FAIL" : "ok"); return fd; } FILE *fopen(const char *path, const char *mode) { REAL(fopen); FILE *f = real_fopen(path, mode); emit(strpbrk(mode, "wa+") ? "fopen-write" : "fopen-read", path, f ? "ok" : "FAIL"); return f; } FILE *fopen64(const char *path, const char *mode) { REAL(fopen64); FILE *f = real_fopen64(path, mode); emit(strpbrk(mode, "wa+") ? "fopen-write" : "fopen-read", path, f ? "ok" : "FAIL"); return f; } int unlink(const char *path) { REAL(unlink); int r = real_unlink(path); emit("unlink", path, r ? "FAIL" : "ok"); return r; } int rename(const char *o, const char *n) { REAL(rename); int r = real_rename(o, n); char b[1024]; snprintf(b, sizeof b, "%s -> %s", o, n); emit("rename", b, r ? "FAIL" : "ok"); return r; } int remove(const char *path) { REAL(remove); int r = real_remove(path); emit("remove", path, r ? "FAIL" : "ok"); return r; } /* ---- dynamic loading --------------------------------------------------- */ void *dlopen(const char *path, int flags) { REAL(dlopen); void *h = real_dlopen(path, flags); emit("dlopen", path ? path : "(self)", h ? "ok" : "FAIL"); return h; } /* ---- processes ---------------------------------------------------------- */ int execve(const char *path, char *const argv[], char *const envp[]) { char buf[1024]; size_t off = (size_t)snprintf(buf, sizeof buf, "%s", path); for (int i = 0; argv[i] && off < sizeof buf - 2 && i < 12; i++) off += (size_t)snprintf(buf + off, sizeof buf - off, " %s", argv[i]); emit("execve", buf, "about-to-exec"); REAL(execve); return real_execve(path, argv, envp); } int execvp(const char *file, char *const argv[]) { emit("execvp", file, "about-to-exec"); REAL(execvp); return real_execvp(file, argv); } int system(const char *cmd) { emit("system", cmd, "shell"); REAL(system); return real_system(cmd); } FILE *popen(const char *cmd, const char *type) { emit("popen", cmd, type); REAL(popen); return real_popen(cmd, type); } pid_t fork(void) { REAL(fork); pid_t p = real_fork(); if (p > 0) { char b[64]; snprintf(b, sizeof b, "child=%d", (int)p); emit("fork", b, "ok"); } return p; } /* ---- network ------------------------------------------------------------ */ int connect(int fd, const struct sockaddr *sa, socklen_t len) { char b[256] = "unknown"; if (sa && sa->sa_family == AF_INET) { const struct sockaddr_in *in = (const struct sockaddr_in *)sa; char ip[64]; inet_ntop(AF_INET, &in->sin_addr, ip, sizeof ip); snprintf(b, sizeof b, "%s:%d", ip, ntohs(in->sin_port)); } else if (sa && sa->sa_family == AF_INET6) { const struct sockaddr_in6 *in6 = (const struct sockaddr_in6 *)sa; char ip[64]; inet_ntop(AF_INET6, &in6->sin6_addr, ip, sizeof ip); snprintf(b, sizeof b, "[%s]:%d", ip, ntohs(in6->sin6_port)); } else if (sa && sa->sa_family == AF_UNIX) { snprintf(b, sizeof b, "unix:%s", ((const struct sockaddr_un *)sa)->sun_path); } REAL(connect); int r = real_connect(fd, sa, len); emit("connect", b, r ? "FAIL" : "ok"); return r; }