diff --git a/src/guard.c b/src/guard.c new file mode 100644 index 0000000..17035ac --- /dev/null +++ b/src/guard.c @@ -0,0 +1,644 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // Required for WIFEXITED and WEXITSTATUS +#include // Required for tolower + +// Include headers for security module checks +#ifdef __linux__ +#include +#include +#endif + +#if __has_include() +#define HAVE_SELINUX 1 +#include +#else +#undef HAVE_SELINUX +#endif + +#if __has_include() +#define HAVE_APPARMOR 1 +#include +#else +#undef HAVE_APPARMOR +#endif + +// Helper macros for logging to both syslog and stderr +#define log_info_both(fmt, ...) \ + do \ + { \ + syslog(LOG_INFO, fmt, ##__VA_ARGS__); \ + fprintf(stderr, "INFO: " fmt "\n", ##__VA_ARGS__); \ + fflush(stderr); \ + } while (0) + +#define log_warning_both(fmt, ...) \ + do \ + { \ + syslog(LOG_WARNING, fmt, ##__VA_ARGS__); \ + fprintf(stderr, "WARNING: " fmt "\n", ##__VA_ARGS__); \ + fflush(stderr); \ + } while (0) + +#define log_err_both(fmt, ...) \ + do \ + { \ + syslog(LOG_ERR, fmt, ##__VA_ARGS__); \ + fprintf(stderr, "ERROR: " fmt "\n", ##__VA_ARGS__); \ + fflush(stderr); \ + } while (0) + +#define log_debug_both(fmt, ...) \ + do \ + { \ + syslog(LOG_DEBUG, fmt, ##__VA_ARGS__); \ + fprintf(stderr, "DEBUG: " fmt "\n", ##__VA_ARGS__); \ + fflush(stderr); \ + } while (0) + +#define BUF_SIZE 8192 // Increased buffer size just in case +#define HASH_SIZE 65 +#define WHITELIST_FILE "/etc/smartguard/whitelist.json" + +// Function to check if an attribute exists, based on `user.trusted` +// Returns 1 for user.trusted=1 (trusted) +// Returns -1 for user.trusted=0 (explicitly untrusted) +// Returns 0 for no attribute or other attribute value (needs further checks) +int is_trusted_xattr(const char *path) +{ + char value[2] = {0}; + // Use getxattr to determine the exact size + ssize_t ret = getxattr(path, "user.trusted", value, sizeof(value)); + + if (ret < 0) + { + // Attribute does not exist or error (e.g., ENODATA for not found) + // Treat as not explicitly trusted or untrusted + return 0; + } + else if (ret == 1 && value[0] == '1') + { + // Found attribute, size 1, value is '1' + return 1; // Trusted + } + else if (ret == 1 && value[0] == '0') + { + // Found attribute, size 1, value is '0' + return -1; // Explicitly untrusted + } + else + { + // Attribute exists but is not '1' or '0' (e.g., different length, other value) + // Treat as not explicitly trusted or untrusted + syslog(LOG_WARNING, "Unexpected user.trusted xattr value for %s, size %zd, value '%s'", path, ret, value); + return 0; + } +} + +// Function to compute the SHA256 hash of a file +int compute_sha256(const char *path, char *output) +{ + unsigned char hash[SHA256_DIGEST_LENGTH]; + char buf[BUF_SIZE / 2]; // Smaller buffer for file reading + FILE *file = fopen(path, "rb"); + if (!file) + return -1; + + SHA256_CTX sha256; + SHA256_Init(&sha256); + size_t bytesRead = 0; + while ((bytesRead = fread(buf, 1, sizeof(buf), file))) + { + SHA256_Update(&sha256, buf, bytesRead); + } + fclose(file); + SHA256_Final(hash, &sha256); + + for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) + { + sprintf(output + (i * 2), "%02x", hash[i]); + } + output[64] = 0; + return 0; +} + +// Check if a hash is present in the whitelist +int is_hash_whitelisted(const char *hash) +{ + FILE *file = fopen(WHITELIST_FILE, "r"); + if (!file) + return 0; // Whitelist file not found or readable, treat as not whitelisted + + fseek(file, 0, SEEK_END); + long fsize = ftell(file); + rewind(file); + + char *data = malloc(fsize + 1); + if (!data) + { + fclose(file); + syslog(LOG_ERR, "Failed to allocate memory for whitelist"); + return 0; + } + fread(data, 1, fsize, file); + data[fsize] = 0; + fclose(file); + + struct json_object *parsed_json; + parsed_json = json_tokener_parse(data); + free(data); // Free allocated memory + data = NULL; // Prevent double free + + if (!parsed_json) + { + syslog(LOG_ERR, "Failed to parse whitelist JSON"); + return 0; + } + + int found = 0; + struct json_object *val; + // Use _ex version for safer check + if (json_object_object_get_ex(parsed_json, hash, &val)) + { + found = 1; + } + + json_object_put(parsed_json); // Decrement reference count + return found; +} + +// Add a hash to the whitelist +int add_hash_to_whitelist(const char *hash, const char *path) +{ + FILE *file = fopen(WHITELIST_FILE, "r+"); + struct json_object *parsed_json; + + if (file) + { + fseek(file, 0, SEEK_END); + long fsize = ftell(file); + rewind(file); + + char *data = malloc(fsize + 1); + if (!data) + { + fclose(file); + syslog(LOG_ERR, "Failed to allocate memory for reading whitelist to add hash"); + return -1; + } + fread(data, 1, fsize, file); + data[fsize] = 0; + + parsed_json = json_tokener_parse(data); + free(data); + data = NULL; + fclose(file); + } + else + { + // If file doesn't exist, create a new JSON object + parsed_json = json_object_new_object(); + } + + if (!parsed_json) + { + syslog(LOG_ERR, "Failed to create/parse JSON object for whitelist add"); + return -1; + } + + // Add the new hash and path + json_object_object_add(parsed_json, hash, json_object_new_string(path)); + + // Re-open file in write mode to truncate and write + file = fopen(WHITELIST_FILE, "w"); + if (!file) + { + syslog(LOG_ERR, "Failed to open whitelist file for writing: %s", strerror(errno)); + json_object_put(parsed_json); + return -1; + } + + // Write the updated JSON + fprintf(file, "%s", json_object_to_json_string_ext(parsed_json, JSON_C_TO_STRING_PRETTY)); + fclose(file); + + json_object_put(parsed_json); // Decrement reference count + return 0; +} + +// Function to prompt the user using zenity (GUI) +int prompt_user_gui(const char *path) +{ + char cmd[PATH_MAX + 128]; + // Ensure path is quoted in case it contains spaces or special characters + snprintf(cmd, sizeof(cmd), + "zenity --question --title='SmartGuard' --text='Untrusted executable detected:\\n%s\\nAllow execution?' --width=400", + path); // Added width for better formatting + + // Use WEXITSTATUS to get the actual exit code of zenity + int status = system(cmd); + if (status == -1) + { + syslog(LOG_ERR, "system() failed to run zenity: %s", strerror(errno)); + return 0; // Treat system error as denial + } + + // zenity returns 0 for Yes, 1 for No, >1 for errors + return (WIFEXITED(status) && WEXITSTATUS(status) == 0); // 0 for Yes, 1 for No +} + +// Function to prompt the user using the command line +int prompt_user_cli(const char *path) +{ + char response[10]; // Buffer for 'yes' or 'no' + newline + log_warning_both("Untrusted executable detected: %s", path); // Log the detection first + fprintf(stdout, "SmartGuard: Untrusted executable detected:\n%s\nAllow execution? (yes/no): ", path); + fflush(stdout); // Ensure prompt is displayed + + if (fgets(response, sizeof(response), stdin) == NULL) + { + // Error reading input + log_err_both("Failed to read user response from stdin: %s", strerror(errno)); + fprintf(stdout, "Error reading response. Denying execution.\n"); + return 0; // Deny on error + } + + // Remove trailing newline if present + response[strcspn(response, "\n")] = 0; + + // Convert response to lowercase for case-insensitive comparison + for (int i = 0; response[i]; i++) + { + response[i] = tolower(response[i]); + } + + // Check for 'yes' or 'y' + if (strcmp(response, "yes") == 0 || strcmp(response, "y") == 0) + { + fprintf(stdout, "Execution allowed by user.\n"); + return 1; // Allow + } + else + { + fprintf(stdout, "Execution denied by user.\n"); + return 0; // Deny + } +} + +int main() +{ + // Open syslog early + openlog("smartguard", LOG_PID | LOG_CONS, LOG_USER); + log_info_both("SmartGuard started."); + + // --- Security Module Checks --- +#ifdef HAVE_SELINUX + int selinux_enabled = is_selinux_enabled(); + if (selinux_enabled == 1) + { + log_info_both("SELinux is enabled. It may affect fanotify operations. Check audit logs if issues persist (e.g., 'sudo ausearch -m avc')."); + } + else if (selinux_enabled == 0) + { + log_info_both("SELinux is disabled."); + } + else + { + log_warning_both("Could not determine SELinux status."); + } +#else + log_info_both("SELinux development headers not found. Could not check SELinux status."); +#endif + +#ifdef HAVE_APPARMOR + int apparmor_enabled = aa_is_enabled(); + if (apparmor_enabled == 1) + { + log_info_both("AppArmor is enabled. It may affect fanotify operations. Check system logs (syslog/journald) if issues persist."); + } + else if (apparmor_enabled == 0) + { + log_info_both("AppArmor is disabled."); + } + else + { + log_warning_both("Could not determine AppArmor status."); + } +#else + log_info_both("AppArmor development headers not found. Could not check AppArmor status."); +#endif + // --- End Security Module Checks --- + + int fan_fd = fanotify_init(FAN_CLASS_PRE_CONTENT | FAN_NONBLOCK | FAN_CLOEXEC, + O_RDONLY | O_CLOEXEC); + + if (fan_fd < 0) + { + log_err_both("fanotify_init failed: %s", strerror(errno)); + perror("fanotify_init"); + exit(EXIT_FAILURE); + } + + // --- Kernel Capability Check: Check for FAN_OPEN_EXEC_PERM support --- + // Attempt to mark the root directory for a permission event. + // This will fail with EINVAL if CONFIG_FANOTIFY_ACCESS_PERMISSIONS is not set. + // Use FAN_MARK_ADD consistent with marking directories. + if (fanotify_mark(fan_fd, FAN_MARK_ADD, + FAN_OPEN_EXEC_PERM, AT_FDCWD, "/") < 0) // Mark root directory + { + if (errno == EINVAL) + { + // This specific error for FAN_OPEN_EXEC_PERM indicates missing kernel support + fprintf(stderr, "ERROR: Kernel does not support fanotify permission events (e.g., FAN_OPEN_EXEC_PERM).\n"); + fprintf(stderr, "ERROR: This feature requires CONFIG_FANOTIFY_ACCESS_PERMISSIONS=y in the kernel configuration.\n"); + syslog(LOG_ERR, "Kernel lacks FANOTIFY_ACCESS_PERMISSIONS support. Exiting."); + fflush(stderr); // Ensure error messages are flushed + close(fan_fd); // Close the fanotify fd before exiting + exit(EXIT_FAILURE); + } + else + { + // Log other potential errors during the capability check mark but don't exit + syslog(LOG_WARNING, "Initial fanotify permission mark on / failed with unexpected error: %s", strerror(errno)); + } + } + else + { + // If the capability check mark succeeds, we know permission events are *likely* supported globally. + // Immediately remove the temporary mark on root, we only needed to test capability. + if (fanotify_mark(fan_fd, FAN_MARK_REMOVE, // Use FAN_MARK_REMOVE with FAN_MARK_ADD + FAN_OPEN_EXEC_PERM, AT_FDCWD, "/") < 0) + { + syslog(LOG_WARNING, "Failed to remove temporary fanotify permission mark on /: %s", strerror(errno)); + // Non-fatal error, continue + } + log_info_both("Kernel supports fanotify permission events."); + } + // --- End Kernel Capability Check --- + + // --- Mark the root filesystem to cover all user homes --- + // This attempts to monitor all execution events on the primary filesystem. + // This strategy was proven to capture execution events reliably on your system, + // unlike marking directories directly. + // WARNING: Marking the root filesystem monitors ALL execution events within it, + // including system directories. This requires robust filtering logic to avoid + // interfering with system processes. + + // The root filesystem mount point is typically '/' + const char *root_mount_point = "/"; + + if (fanotify_mark(fan_fd, FAN_MARK_ADD | FAN_MARK_FILESYSTEM, // Use FAN_MARK_FILESYSTEM + FAN_OPEN_EXEC_PERM, AT_FDCWD, root_mount_point) < 0) + { + log_err_both("fanotify_mark failed on filesystem %s: %s", root_mount_point, strerror(errno)); + perror("fanotify_mark with FAN_OPEN_EXEC_PERM on filesystem"); + + // No fallback as requested. If marking the root filesystem fails, exit. + close(fan_fd); + exit(EXIT_FAILURE); + } + else + { + log_info_both("SmartGuard marking filesystem %s with FAN_OPEN_EXEC_PERM", root_mount_point); + log_warning_both("NOTE: Marking the root filesystem monitors ALL execution events within it."); + log_warning_both("Implement robust filtering for system binaries!"); + } + // --- End Mark root filesystem --- + + // --- Prompt User Decision --- + int (*prompt_func)(const char *); // Declare function pointer + if (getenv("DISPLAY") != NULL) + { + prompt_func = &prompt_user_gui; + log_info_both("Detected GUI environment, using graphical prompt."); + } + else + { + prompt_func = &prompt_user_cli; + log_info_both("No GUI environment detected, using command-line prompt."); + } + // --- End Prompt User Decision --- + + char buf[BUF_SIZE]; + struct fanotify_event_metadata *metadata; + + while (1) + { + ssize_t len = read(fan_fd, buf, sizeof(buf)); + if (len == -1 && errno != EAGAIN) + { + log_err_both("read from fanotify failed: %s", strerror(errno)); + perror("read"); + break; // Exit the loop on fatal read errors + } + if (len <= 0) // Handle non-blocking read returning no data + continue; + + log_debug_both("Read %zd bytes from fanotify fd", len); + + metadata = (struct fanotify_event_metadata *)buf; + while (FAN_EVENT_OK(metadata, len)) + { + log_debug_both("Processing metadata: event_len=%u, vers=%u, metadata_len=%u, mask=0x%x, fd=%d, pid=%d", + metadata->event_len, metadata->vers, metadata->metadata_len, + metadata->mask, metadata->fd, metadata->pid); + + if (metadata->vers != FANOTIFY_METADATA_VERSION) + { + fprintf(stderr, "ERROR: fanotify version mismatch\n"); + syslog(LOG_ERR, "fanotify version mismatch"); + // Attempt to close fd before exiting on mismatch + if (metadata->fd >= 0) + close(metadata->fd); + close(fan_fd); + exit(EXIT_FAILURE); + } + + log_debug_both("Event detected: mask = %x, fd = %d", metadata->mask, metadata->fd); + + // Initialize response to DENY by default + struct fanotify_response response = { + .fd = metadata->fd, + .response = FAN_DENY}; + + // Only process FAN_OPEN_EXEC_PERM events with valid FDs for permission checks + if (metadata->fd >= 0 && (metadata->mask & FAN_OPEN_EXEC_PERM)) // Ensure it's specifically the PERM event + { + char path[PATH_MAX]; + // Get path from the event's file descriptor + char link_path[64]; // Buffer for "/proc/self/fd/" plus a few digits + snprintf(link_path, sizeof(link_path), "/proc/self/fd/%d", metadata->fd); + + ssize_t path_len = readlink(link_path, path, sizeof(path) - 1); // Use readlink for /proc/self/fd + + if (path_len > 0) // Check if readlink wrote anything + { + path[path_len] = '\0'; // Null-terminate the path + // For paths obtained via readlink(/proc/self/fd/...), they are usually absolute + // No extra realpath needed unless dealing with weird cases (like deleted files) + } + else if (path_len == -1) // Handle readlink failure + { + log_warning_both("Failed to get path for fd %d using readlink(/proc/self/fd/...): %s", metadata->fd, strerror(errno)); + strcpy(path, "unknown_path"); // Fallback placeholder + } + else + { // path_len == 0 (empty path? unlikely for exec) + log_warning_both("readlink for fd %d returned empty path.", metadata->fd); + strcpy(path, "unknown_path"); // Fallback placeholder + } + + log_info_both("Accessing file: %s", path); + + // --- Filtering Logic --- + // Automatically FAN_ALLOW execution for paths within trusted system directories. + // This is essential when marking the entire filesystem. + int auto_allow = 0; + // Expanded list of common trusted system paths + if (strncmp(path, "/bin/", 5) == 0 || + strncmp(path, "/usr/bin/", 9) == 0 || + strncmp(path, "/sbin/", 6) == 0 || + strncmp(path, "/usr/sbin/", 10) == 0 || + strncmp(path, "/lib/", 5) == 0 || + strncmp(path, "/usr/lib/", 9) == 0 || + strncmp(path, "/lib32/", 7) == 0 || // 32-bit libs + strncmp(path, "/usr/lib32/", 11) == 0 || + strncmp(path, "/lib64/", 7) == 0 || // 64-bit libs + strncmp(path, "/usr/lib64/", 11) == 0 || + strncmp(path, "/opt/", 5) == 0 || // Common for installed software (often trusted) + strncmp(path, "/usr/local/bin/", 15) == 0 || // Local admin installed binaries + strncmp(path, "/usr/local/sbin/", 16) == 0 || + strncmp(path, "/usr/local/lib/", 15) == 0 || + strncmp(path, "/usr/local/lib32/", 17) == 0 || + strncmp(path, "/usr/local/lib64/", 17) == 0 || + strncmp(path, "/snap/", 6) == 0 || // Snap packages + strncmp(path, "/var/lib/", 9) == 0 || // Some daemons + strncmp(path, "/etc/alternatives/", 18) == 0 || // Alternatives symlinks + strncmp(path, "/proc/", 6) == 0 || // Kernel/system pseudofs + strncmp(path, "/sys/", 5) == 0 || + strncmp(path, "/dev/", 5) == 0 || // Devices (unlikely to execute but good practice) + strncmp(path, "/tmp/", 5) == 0 || // /tmp can be tricky, often excluded from security checks + strncmp(path, "/var/run/", 9) == 0 || // Socket/pid files, sometimes executables here + strncmp(path, "/run/", 6) == 0) // Similar to /var/run + { + auto_allow = 1; + log_debug_both("Automatically allowed trusted path: %s", path); + } + // You might add checks for specific PIDs you trust or file ownership (e.g., root) as well if needed. + // Be careful with owner checks in user-writable trusted paths like /usr/local. + + if (auto_allow) + { + response.response = FAN_ALLOW; + } + // --- End Filtering Logic --- + else // Apply SmartGuard logic only to non-filtered (potentially untrusted) paths + { + log_info_both("Checking xattr for %s", path); + int trusted_status = is_trusted_xattr(path); + + if (trusted_status == 1) // Trusted (user.trusted=1) + { + response.response = FAN_ALLOW; + log_info_both("Allowed (xattr: 1): %s", path); + } + else if (trusted_status == -1) // Explicitly Untrusted (user.trusted=0) + { + // response.response is already FAN_DENY + log_warning_both("Denied (xattr: 0): %s", path); + } + else // No xattr, other xattr value, or error (trusted_status == 0) + { + // Fallback to hash check and user prompt + char hash[HASH_SIZE]; + // compute_sha256 handles file opening internally + if (compute_sha256(path, hash) == 0) + { + if (is_hash_whitelisted(hash)) + { + response.response = FAN_ALLOW; + log_info_both("Allowed (whitelist): %s", path); + } + else + { + // --- Call the selected prompt function --- + if (prompt_func(path)) // Use the function pointer + // --- End Call selected prompt function --- + { + response.response = FAN_ALLOW; + add_hash_to_whitelist(hash, path); + // Set trusted=1 after user approval and whitelisting + setxattr(path, "user.trusted", "1", 1, 0); + log_info_both("User allowed: %s", path); + } + else + { + log_warning_both("User denied: %s", path); + } + } + } + else + { + log_err_both("Failed to compute hash for %s: %s", path, strerror(errno)); + // Keep default FAN_DENY on hash computation failure + } + } + } // End else (not filtered) + + // For FAN_OPEN_EXEC_PERM events, we *must* write a response. + ssize_t bytes_written = write(fan_fd, &response, sizeof(response)); + if (bytes_written != sizeof(response)) + { + log_err_both("Partial or failed write for fanotify response (fd %d): %zd bytes written, expected %zu. Error: %s", + metadata->fd, bytes_written, sizeof(response), strerror(errno)); + } + } + else if (metadata->fd >= 0) // Events with FDs but not FAN_OPEN_EXEC_PERM + { + log_debug_both("Unhandled event type with fd: mask = %x, fd = %d", metadata->mask, metadata->fd); + } + + // IMPORTANT: Close the file descriptor *if* it was provided by the event. + // This must happen for *any* event with fd >= 0, regardless of mask, + // after you are done processing the event. + if (metadata->fd >= 0) + { + close(metadata->fd); + } + + // Move to the next event in the buffer + metadata = FAN_EVENT_NEXT(metadata, len); + } + } + + // This part is typically not reached in the infinite loop + close(fan_fd); + closelog(); + return 0; // Should not be reached +} + +// Compile with: +// gcc -o smartguard smartguard.c -ljson-c -lssl -lcrypto -lselinux -lapparmor +// Make sure to have the development packages for libselinux and libapparmor installed +// (e.g., libselinux1-dev libapparmor-dev on Debian/Ubuntu/Raspberry Pi OS) +// Run with: +// sudo ./smartguard +// Make sure to have the whitelist.json file in /etc/smartguard/ with proper permissions +// and ownership. The file should be a JSON object with hashes as keys and paths as values. \ No newline at end of file