From 0e1d50c8f0faba9cf50a2e5c90f5e8e82e90e4b3 Mon Sep 17 00:00:00 2001 From: Tom Smeding Date: Mon, 17 Aug 2020 23:50:52 +0200 Subject: Initial working version --- .gitignore | 4 + Makefile | 48 ++++++ README.md | 9 ++ lib/libintercept.c | 195 ++++++++++++++++++++++++ lib/libintercept.h | 10 ++ main.c | 423 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 689 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 lib/libintercept.c create mode 100644 lib/libintercept.h create mode 100644 main.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..46c2bce --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +gen/ +obj/ +exec-intercept +log.txt diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f63a25a --- /dev/null +++ b/Makefile @@ -0,0 +1,48 @@ +CC = gcc +CFLAGS = -Wall -Wextra -std=c11 -O2 -g +LDFLAGS = +LDFLAGS_LIB = $(LDFLAGS) -ldl +TARGET = exec-intercept + +OBJDIR = obj +GENDIR = gen + +SOURCES := $(sort $(wildcard *.c) $(GENDIR)/libintercept.so.c) +HEADERS_LIB := $(wildcard lib/*.h) +HEADERS := $(sort $(wildcard *.h) $(HEADERS_LIB) $(GENDIR)/libintercept.so.h) + +.PHONY: all clean + +all: $(TARGET) + +clean: + @echo "Cleaning" + @rm -f $(TARGET) + @rm -rf $(OBJDIR) $(GENDIR) + + +$(OBJDIR)/lib/libintercept.so: lib/libintercept.c $(HEADERS_LIB) + @mkdir -p $(dir $@) + @echo "CCLD -o $@" + @$(CC) $(CFLAGS) -fPIC -shared -o $@ $< $(LDFLAGS_LIB) + +$(GENDIR)/libintercept.so.c: $(OBJDIR)/lib/libintercept.so + @mkdir -p $(dir $@) + @echo "XXD -i $<" + @cd $(dir $<) && xxd -i $(notdir $<) >$(abspath $@) + +$(GENDIR)/libintercept.so.h: $(GENDIR)/libintercept.so.c + @echo "SED -o $@" + @sed -n 's/^\(.*\) =.*/extern \1;/p' $< >$@ + +$(OBJDIR)/%.o: %.c $(HEADERS) | $(OBJDIR) + @mkdir -p $(dir $@) + @echo "CC $<" + @$(CC) $(CFLAGS) -c -o $@ $< + +$(TARGET): $(patsubst %.c,$(OBJDIR)/%.o,$(SOURCES)) | $(OBJDIR) + @echo "LD -o $@" + @$(CC) -o $@ $^ $(LDFLAGS) + +$(OBJDIR): + @mkdir -p $(OBJDIR) diff --git a/README.md b/README.md new file mode 100644 index 0000000..63073aa --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# exec-intercept + +This is like [bear](https://github.com/rizsotto/Bear), except it's not specialised to C-like compiler invocations. + +This tool intercepts all `execve` calls, logs them to a file, then executes the +`execve` calls normally. The same is done for `execve` calls of processes thus +started. + +The tool currently only works on Linux. diff --git a/lib/libintercept.c b/lib/libintercept.c new file mode 100644 index 0000000..8503308 --- /dev/null +++ b/lib/libintercept.c @@ -0,0 +1,195 @@ +#define _GNU_SOURCE // RTLD_NEXT +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // posix_spawn +#include +#include +#include "libintercept.h" + + +// Returns whether successful +static bool sendall(int sock, const char *buffer, size_t length) { + size_t cursor = 0; + while (cursor < length) { + ssize_t nw = send(sock, buffer + cursor, length - cursor, 0); + if (nw <= 0) return false; + cursor += nw; + } + return true; +} + +static void try_transmit_invocation(const char *pathname, char *const argv[]) { + fprintf(stderr, "try_transmit_invocation: %s ...\n", pathname); + + const char *socketpath = getenv(COMM_SOCKET_ENVVAR); + if (socketpath == NULL) { + fprintf(stderr, " socket path not given\n"); + return; + } + + // The SOCK_CLOEXEC option is technically unnecessary, but let's be careful. + int sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (sock < 0) { + fprintf(stderr, " cannot create socket: %s\n", strerror(errno)); + return; + } + + struct sockaddr_un addr; + memset(&addr, 0, sizeof addr); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, socketpath, sizeof(addr.sun_path) - 1); + + int ret = connect(sock, (const struct sockaddr*)&addr, sizeof addr); + if (ret < 0) { + fprintf(stderr, " cannot connect to socket\n"); + return; + } + + size_t nargs = 0; + while (argv[nargs] != NULL) nargs++; + + bool ok = false; + if (!sendall(sock, (const char*)&nargs, 8)) goto cleanup; + if (!sendall(sock, pathname, strlen(pathname) + 1)) goto cleanup; + for (size_t i = 0; i < nargs; i++) { + if (!sendall(sock, argv[i], strlen(argv[i]) + 1)) goto cleanup; + } + + ok = true; + +cleanup: + if (!ok) fprintf(stderr, " failed to write to socket\n"); + close(sock); +} + +__attribute__((constructor)) +static void constructor(void) { + fprintf(stderr, "constructor...\n"); +} + +// Abridged from glibc posix/execl.c, LGPL copyright FSF +int execl(const char *pathname, const char *arg, ...) { + size_t argc; + va_list ap; + va_start(ap, arg); + for (argc = 1; va_arg(ap, const char*) && argc < INT_MAX; argc++) {} + va_end(ap); + if (argc >= INT_MAX) { + errno = E2BIG; + return -1; + } + + char *argv[argc + 1]; + argv[0] = (char*)arg; + va_start(ap, arg); + for (size_t i = 1; i < argc; i++) argv[i] = va_arg(ap, char*); + argv[argc] = NULL; + va_end(ap); + + try_transmit_invocation(pathname, argv); + + int (*real_execv)(const char*, char *const[]) = dlsym(RTLD_NEXT, "execv"); + return real_execv(pathname, argv); +} + +int execlp(const char *file, const char *arg, ...) { + size_t argc; + va_list ap; + va_start(ap, arg); + for (argc = 1; va_arg(ap, const char*) && argc < INT_MAX; argc++) {} + va_end(ap); + if (argc >= INT_MAX) { + errno = E2BIG; + return -1; + } + + char *argv[argc + 1]; + argv[0] = (char*)arg; + va_start(ap, arg); + for (size_t i = 1; i < argc; i++) argv[i] = va_arg(ap, char*); + argv[argc] = NULL; + va_end(ap); + + try_transmit_invocation(file, argv); + + int (*real_execvp)(const char*, char *const[]) = dlsym(RTLD_NEXT, "execvp"); + return real_execvp(file, argv); +} + +int execle(const char *pathname, const char *arg, ...) { + size_t argc; + va_list ap; + va_start(ap, arg); + for (argc = 1; va_arg(ap, const char*) && argc < INT_MAX; argc++) {} + char *const *envp = va_arg(ap, char *const *); + va_end(ap); + if (argc >= INT_MAX) { + errno = E2BIG; + return -1; + } + + char *argv[argc + 1]; + argv[0] = (char*)arg; + va_start(ap, arg); + for (size_t i = 1; i < argc; i++) argv[i] = va_arg(ap, char*); + argv[argc] = NULL; + va_end(ap); + + try_transmit_invocation(pathname, argv); + + int (*real_execvpe)(const char*, char *const[], char *const[]) = dlsym(RTLD_NEXT, "execvpe"); + return real_execvpe(pathname, argv, envp); +} + +int execv(const char *pathname, char *const argv[]) { + try_transmit_invocation(pathname, argv); + + int (*real_execv)(const char*, char *const[]) = dlsym(RTLD_NEXT, "execv"); + return real_execv(pathname, argv); +} + +int execvp(const char *file, char *const argv[]) { + try_transmit_invocation(file, argv); + + int (*real_execvp)(const char*, char *const[]) = dlsym(RTLD_NEXT, "execvp"); + return real_execvp(file, argv); +} + +int execve(const char *pathname, char *const argv[], char *const envp[]) { + try_transmit_invocation(pathname, argv); + + int (*real_execve)(const char*, char *const[], char *const[]) = dlsym(RTLD_NEXT, "execve"); + return real_execve(pathname, argv, envp); +} + +int execvpe(const char *file, char *const argv[], char *const envp[]) { + try_transmit_invocation(file, argv); + + int (*real_execvpe)(const char*, char *const[], char *const[]) = dlsym(RTLD_NEXT, "execvpe"); + return real_execvpe(file, argv, envp); +} + +int posix_spawn( + pid_t *pid, const char *path, + const posix_spawn_file_actions_t *file_actions, + const posix_spawnattr_t *attrp, + char *const argv[], char *const envp[] +) { + try_transmit_invocation(path, argv); + + int (*real_posix_spawn)( + pid_t*, const char*, + const posix_spawn_file_actions_t*, + const posix_spawnattr_t*, + char *const[], char *const[] + ) = dlsym(RTLD_NEXT, "posix_spawn"); + return real_posix_spawn(pid, path, file_actions, attrp, argv, envp); +} diff --git a/lib/libintercept.h b/lib/libintercept.h new file mode 100644 index 0000000..0efb3a8 --- /dev/null +++ b/lib/libintercept.h @@ -0,0 +1,10 @@ +#pragma once + + +#define COMM_SOCKET_ENVVAR "EXEC_INTERCEPT_SOCKET" + +// Socket protocol: the client process sends one message per execve invocation. +// Format: +// - Number of arguments (8 byte little-endian unsigned integer) +// - Program path (null-terminated string) +// - Arguments (null-terminated strings) diff --git a/main.c b/main.c new file mode 100644 index 0000000..650dde5 --- /dev/null +++ b/main.c @@ -0,0 +1,423 @@ +#define _POSIX_C_SOURCE 200809L // mkdtemp +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "gen/libintercept.so.h" +#include "lib/libintercept.h" + +#ifndef __linux__ +#error This program currently only works on Linux +#endif + + +static void* mallocerr(size_t num) { + void *ptr = malloc(num); + if (ptr == NULL) { + fprintf(stderr, "Cannot allocate memory\n"); + exit(1); + } + return ptr; +} + +// Returns whether successful +static bool write_library(const char *path) { + FILE *f = fopen(path, "w"); + if (!f) { + fprintf(stderr, "Could not place library in temporary directory\n"); + return false; + } + + fwrite(libintercept_so, 1, libintercept_so_len, f); + fclose(f); + if (ferror(f) != 0) { + fprintf(stderr, "Could not write to temporary directory\n"); + return false; + } + + return true; +} + +// Returns socket, or -1 on error +static int create_socket(const char *path) { + int sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if (sock < 0) { + perror("Cannot create unix socket: socket"); + return -1; + } + + struct sockaddr_un addr; + memset(&addr, 0, sizeof addr); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, path, sizeof addr.sun_path - 1); + fprintf(stderr, "Binding to <%s>\n", addr.sun_path); + + if (bind(sock, (const struct sockaddr*)&addr, sizeof addr) < 0) { + perror("Cannot create unix socket: bind"); + close(sock); + return -1; + } + + if (listen(sock, 10) < 0) { + perror("listen"); + close(sock); + unlink(path); + return -1; + } + + return sock; +} + +struct paths { + char *tempdir; + char *libpath; + char *socketpath; + int socket; +}; + +// returns {NULL} on failure +static struct paths create_tempfiles(void) { + const char *template = "/tmp/exec-intercept.XXXXXX"; + const char *libsuffix = "/libintercept.so"; + const char *socketsuffix = "/exec-intercept.socket"; + + char *tempdir = mallocerr(strlen(template) + 1); + memcpy(tempdir, template, strlen(template) + 1); + + if (mkdtemp(tempdir) == NULL) { + perror("Cannot create temporary directory"); + exit(1); + } + + char *libpath = mallocerr(strlen(template) + strlen(libsuffix) + 1); + memcpy(libpath, tempdir, strlen(template)); + memcpy(libpath + strlen(template), libsuffix, strlen(libsuffix) + 1); + + if (!write_library(libpath)) { + free(libpath); + rmdir(tempdir); + free(tempdir); + return (struct paths){NULL, NULL, NULL, -1}; + } + + char *socketpath = mallocerr(strlen(template) + strlen(socketsuffix) + 1); + memcpy(socketpath, tempdir, strlen(template)); + memcpy(socketpath + strlen(template), socketsuffix, strlen(socketsuffix) + 1); + + int socket = create_socket(socketpath); + if (socket < 0) { + free(socketpath); + unlink(libpath); + free(libpath); + rmdir(tempdir); + free(tempdir); + return (struct paths){NULL, NULL, NULL, -1}; + } + + return (struct paths){tempdir, libpath, socketpath, socket}; +} + +static void cleanup_tempfiles(struct paths info) { + close(info.socket); + unlink(info.socketpath); + free(info.socketpath); + unlink(info.libpath); + free(info.libpath); + rmdir(info.tempdir); + free(info.tempdir); +} + +// Returns PID of child, or -1 on error +static pid_t start_child(char **argv) { + pid_t pid = fork(); + if (pid < 0) { + perror("fork"); + return -1; + } + + if (pid == 0) { + // Use execvp, which finds the executable in PATH + execvp(argv[0], argv); + perror("execvp"); + exit(1); + } + + return pid; +} + +struct message_buffer { + size_t cap, len; + char *buffer; +}; + +// If .buffer is NULL, memory allocation failed +static struct message_buffer message_buffer_alloc(void) { + struct message_buffer mb; + mb.cap = 1024; + mb.len = 0; + mb.buffer = malloc(mb.cap); + if (mb.buffer == NULL) { + fprintf(stderr, "Cannot allocate memory\n"); + return mb; + } + return mb; +} + +// Returns length of (string + '\0'), or 0 if '\0' is not found +static size_t consume_string(const char *buffer, size_t available) { + for (size_t i = 0; i < available; i++) { + if (buffer[i] == '\0') return i + 1; + } + return 0; +} + +static bool shell_safe(char c) { + return ('+' <= c && c <= ':') || + c == '=' || + ('@' <= c && c <= '[') || + (']' <= c && c <= '_') || + ('a' <= c && c <= 'z'); +} + +static void write_shell_escaped(FILE *logfile, const char *str, size_t length) { + if (length == 0) { + fprintf(logfile, "''"); + return; + } + + bool safe = true; + for (size_t i = 0; i < length; i++) { + if (!shell_safe(str[i])) { + safe = false; + break; + } + } + + if (safe) { + fwrite(str, 1, length, logfile); + return; + } + + int quotemode = 0; // 0=none, 1='', 2="", 3=$'' + for (size_t i = 0; i < length; i++) { + if (str[i] < ' ' || str[i] > '~') { + if (quotemode == 0) fprintf(logfile, "$'"); + else if (quotemode == 1) fprintf(logfile, "'$'"); + else if (quotemode == 2) fprintf(logfile, "\"$'"); + quotemode = 3; + fprintf(logfile, "\\x%c%c", + "0123456789abcdef"[str[i] / 16], "0123456789abcdef"[str[i] % 16]); + } else if (str[i] == '\'') { + if (quotemode == 0) fputc('"', logfile); + else if (quotemode == 1 || quotemode == 3) fprintf(logfile, "'\""); + quotemode = 2; + fputc('\'', logfile); + } else { + if (quotemode == 0) fputc('\'', logfile); + else if (quotemode == 2) fprintf(logfile, "\"'"); + else if (quotemode == 3) fprintf(logfile, "''"); + quotemode = 1; + fputc(str[i], logfile); + } + } + if (quotemode == 1 || quotemode == 3) fputc('\'', logfile); + else if (quotemode == 2) fputc('"', logfile); +} + +// Returns amount consumed, which may be 0 +static size_t parse_write_entry(const char *buffer, size_t available, FILE *logfile) { + if (available < 8) return 0; + const size_t nargs = *(const size_t*)buffer; + + size_t cursor = 8; + for (size_t i = 0; i < nargs + 1; i++) { + const size_t len = consume_string(buffer + cursor, available - cursor); + if (len == 0) return 0; + cursor += len; + } + + // All null bytes were found, so let's log the full message + cursor = 8; + for (size_t i = 0; i < nargs + 1; i++) { + const size_t len = consume_string(buffer + cursor, available - cursor); + if (i != 1) { // skip argv[0] + if (i > 0) fputc(' ', logfile); + write_shell_escaped(logfile, buffer + cursor, len - 1); + } + cursor += len; + } + + fputc('\n', logfile); + + return cursor; +} + +// Returns whether successful +static bool message_buffer_add_data( + struct message_buffer *mb, const char *data, size_t length, + FILE *logfile) { + if (mb->len + length > mb->cap) { + do mb->cap *= 2; + while (mb->len + length > mb->cap); + mb->buffer = realloc(mb->buffer, mb->cap); + if (mb->buffer == NULL) return false; + } + + memcpy(mb->buffer + mb->len, data, length); + mb->len += length; + + size_t nconsumed = parse_write_entry(mb->buffer, mb->len, logfile); + if (nconsumed > 0) { + memmove(mb->buffer, mb->buffer + nconsumed, mb->len - nconsumed); + mb->len -= nconsumed; + } + return true; +} + +static void message_buffer_free(struct message_buffer mb) { + free(mb.buffer); +} + +static void message_loop(int listensock, pid_t child_pid, FILE *logfile) { + // pfds[0] is always listensock + size_t conn_cap = 4, conn_len = 1; + + struct pollfd *pfds = mallocerr(conn_cap * sizeof(struct pollfd)); + pfds[0].fd = listensock; + pfds[0].events = POLLIN; + + // mbufs[0] is always unused + struct message_buffer *mbufs = mallocerr(conn_cap * sizeof(struct message_buffer)); + + while (true) { + int ret = poll(pfds, conn_len, -1); + if (ret < 0 && errno != EINTR) { + perror("poll"); + goto cleanup; + } + + if (pfds[0].revents & POLLIN) { + int sock = accept(listensock, NULL, NULL); + if (sock >= 0) { + if (conn_len == conn_cap) { + conn_cap *= 2; + pfds = realloc(pfds, conn_cap * sizeof(struct pollfd)); + mbufs = realloc(mbufs, conn_cap * sizeof(struct message_buffer)); + if (!pfds || !mbufs) { + fprintf(stderr, "Cannot allocate memory!\n"); + exit(1); + } + } + pfds[conn_len].fd = sock; + pfds[conn_len].events = POLLIN; + mbufs[conn_len] = message_buffer_alloc(); + conn_len++; + } + } + + for (size_t i = 1; i < conn_len; i++) { + if (!(pfds[i].revents & POLLIN)) continue; + + char buf[1024]; + ssize_t nr = read(pfds[i].fd, buf, sizeof buf); + if (nr < 0 && errno != EINTR && errno != ECONNRESET) { + perror("read"); + close(pfds[i].fd); + message_buffer_free(mbufs[i]); + if (i < conn_len - 1) { + pfds[i] = pfds[conn_len - 1]; + mbufs[i] = mbufs[conn_len - 1]; + } + conn_len--; + } + + if (nr > 0) { + message_buffer_add_data(&mbufs[i], buf, (size_t)nr, logfile); + continue; // see if there's more data before wait()'ing on the child + } + } + + int status; + pid_t waitret = waitpid(child_pid, &status, WNOHANG); + if (waitret < 0) { + perror("waitpid"); + goto cleanup; + } + + if (waitret > 0 && WIFEXITED(status)) { + goto cleanup; + } + } + +cleanup: + for (size_t i = 1; i < conn_len; i++) { + close(pfds[i].fd); + message_buffer_free(mbufs[i]); + } +} + +static void signal_handler_nop(int sig) { + (void)sig; +} + +int main(int argc, char **argv) { + if (argc < 4) { + fprintf(stderr, + "Usage: %s -o \n" + "Captures (and passes through) all execve calls in the command,\n" + "and logs them to the specified file. The commands in the log\n" + "file are shell-escaped in bash syntax.\n", + argv[0]); + return 1; + } + + const char *logfname = argv[2]; + + if (getenv("LD_LIBRARY_PATH") != NULL || getenv("LD_PRELOAD") != NULL) { + fprintf(stderr, + "This program manipulates the dynamic loader environment\n" + "variables LD_LIBRARY_PATH and LD_PRELOAD; having them set\n" + "while running this program is insecure. Please unset them\n" + "first.\n"); + return 1; + } + + if (signal(SIGCHLD, signal_handler_nop) == SIG_ERR) { + perror("signal"); + return 1; + } + + struct paths paths = create_tempfiles(); + if (paths.tempdir == NULL) return 1; + FILE *logfile = NULL; + + // This new environment only applies to new child processes + if (setenv("LD_PRELOAD", paths.libpath, 1) < 0 || + setenv(COMM_SOCKET_ENVVAR, paths.socketpath, 1) < 0) { + perror("setenv"); + goto cleanup; + } + + logfile = fopen(logfname, "w"); + if (logfile == NULL) { + fprintf(stderr, "Cannot open log file <%s>\n", logfname); + goto cleanup; + } + + pid_t child_pid = start_child(argv + 3); + if (child_pid == -1) { + goto cleanup; + } + + message_loop(paths.socket, child_pid, logfile); + +cleanup: + if (logfile != NULL) fclose(logfile); + cleanup_tempfiles(paths); +} -- cgit v1.2.3-54-g00ecf