Add src/guard.c

This commit is contained in:
2025-04-22 02:29:33 -07:00
parent ee5eea99fa
commit 167271002e
+644
View File
@@ -0,0 +1,644 @@
#define _GNU_SOURCE
#include <fcntl.h>
#include <limits.h>
#include <linux/fanotify.h>
#include <openssl/sha.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/fanotify.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <syslog.h>
#include <unistd.h>
#include <errno.h>
#include <libgen.h>
#include <sys/xattr.h>
#include <json-c/json.h>
#include <sys/wait.h> // Required for WIFEXITED and WEXITSTATUS
#include <ctype.h> // Required for tolower
// Include headers for security module checks
#ifdef __linux__
#include <sys/statfs.h>
#include <sys/utsname.h>
#endif
#if __has_include(<selinux/selinux.h>)
#define HAVE_SELINUX 1
#include <selinux/selinux.h>
#else
#undef HAVE_SELINUX
#endif
#if __has_include(<libapparmor.h>)
#define HAVE_APPARMOR 1
#include <libapparmor.h>
#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.