Add src/guard.c
This commit is contained in:
+644
@@ -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.
|
||||
Reference in New Issue
Block a user