Compare commits

...

12 Commits

Author SHA1 Message Date
946492c28e qssb_free_policy(): free path policies 2021-05-22 20:05:31 +02:00
ad9c391e3f QSSB_FS_ALLOW_WRITE does not imply ALLOW_READ anymore
Landlock can handle write access without it implying read access,
in contrast to the existing bind mounts solution. Hence, remove
ALLOW_READ from ALLOW_WRITE bitmask.
2021-05-22 20:05:31 +02:00
fcebed557c Add qssb_append_path_polic{ies,y}: Convenience function to add path policies 2021-05-22 20:05:25 +02:00
bb02e40101 Begin landlock support 2021-05-15 23:30:05 +02:00
7e2d4139cb Begin check_policy_sanity(): Checks whether policy is reasonable
Issue: #3
2021-05-09 12:59:58 +02:00
6e6812e13d Introduce mount_path_policies_to_chroot option, changing path_policy enforcement logic
Previously, we needed chroot and bind mounts to enforce path_policies. Therefore,
in the presence of path policies, we had to explicitly create a chroot
dir.

With the coming landlock support, this is not required anymore.

However, one might still want to chroot and bind mount flags. But
path policies don't dictate that anymore.
2021-05-09 12:59:58 +02:00
edf144bbc7 Allow overriding HAVE_LANDLOCK irrespectible of kernel verison 2021-05-09 12:59:58 +02:00
67e1afc904 Remove unused policy flag QSSB_FS_ALLOW_NOTHING 2021-05-09 12:59:58 +02:00
2c94fe8225 qssb_path_policy: rename 'mountpoint' to 'path', make 'policy' unsigned 2021-05-09 12:59:58 +02:00
4674638e9a Add landlock policy flags if landlock is supported 2021-05-09 12:59:58 +02:00
8697fd8b84 qssb.h: Add copyright header 2021-05-09 10:02:31 +02:00
ed6a2a1067 Rename general QSSB_MOUNT* flags to QSSB_FS* 2021-05-09 09:35:17 +02:00

390
qssb.h
View File

@ -1,3 +1,19 @@
/*
* Copyright (c) 2021 Albert S. <mail at quitesimple dot org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#ifndef QSSB_H
#define QSSB_H
@ -13,6 +29,7 @@
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/random.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
@ -20,11 +37,25 @@
#include <linux/limits.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/version.h>
#include <sys/capability.h>
#include <stddef.h>
#include <inttypes.h>
#include <asm/unistd.h>
#ifndef HAVE_LANDLOCK
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,13,0)
/* TODO: Hopefully a fair assumption. But we need to runtime checks */
#define HAVE_LANDLOCK = 1
#endif
#endif
#if HAVE_LANDLOCK == 1
#include <linux/landlock.h>
#if LANDLOCK_CREATE_RULESET_VERSION != (1U << 0)
#error "This landlock ABI version is not supported by qssb (yet)"
#endif
#endif
//TODO: stolen from kernel samples/seccomp, GPLv2...?
#define ALLOW \
BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW)
@ -51,17 +82,55 @@
#define QSSB_SYS(x) (__NR_##x)
//TODO: implement
#define QSSB_MOUNT_ALLOW_NOTHING 0 //explicit rule
#define QSSB_MOUNT_ALLOW_READ 1<<0
#define QSSB_MOUNT_ALLOW_WRITE (1<<1) | QSSB_MOUNT_ALLOW_READ
#define QSSB_MOUNT_ALLOW_EXEC 1<<2
#define QSSB_MOUNT_ALLOW_DEV 1<<3
#define QSSB_MOUNT_ALLOW_SETUID 1<<4
#define QSSB_FS_ALLOW_READ 1<<0
#define QSSB_FS_ALLOW_WRITE (1<<1)
#define QSSB_FS_ALLOW_EXEC 1<<2
#define QSSB_FS_ALLOW_DEV 1<<3
#define QSSB_FS_ALLOW_SETUID 1<<4
//don't mount recursive
#define QSSB_MOUNT_NOT_REC 1<<5
#if HAVE_LANDLOCK == 1
#define QSSB_FS_ALLOW_REMOVE_DIR (1 << 7)
#define QSSB_FS_ALLOW_REMOVE_FILE (1 << 8)
#define QSSB_FS_ALLOW_MAKE_CHAR (1 << 9)
#define QSSB_FS_ALLOW_MAKE_DIR (1 << 10)
#define QSSB_FS_ALLOW_MAKE_REG (1 << 11)
#define QSSB_FS_ALLOW_MAKE_SOCK (1 << 12)
#define QSSB_FS_ALLOW_MAKE_FIFO (1 << 13)
#define QSSB_FS_ALLOW_MAKE_BLOCK (1 << 14)
#define QSSB_FS_ALLOW_MAKE_SYM (1 << 15)
#define QSSB_FS_ALLOW_WRITE_FILE (1 << 16)
#define QSSB_FS_ALLOW_READ_DIR (1 << 17)
#define QSSB_FS_ALLOW_REMOVE (1 << 18)
#ifndef landlock_create_ruleset
static inline int landlock_create_ruleset(
const struct landlock_ruleset_attr *const attr,
const size_t size, const __u32 flags)
{
return syscall(__NR_landlock_create_ruleset, attr, size, flags);
}
#endif
#ifndef landlock_add_rule
static inline int landlock_add_rule(const int ruleset_fd,
const enum landlock_rule_type rule_type,
const void *const rule_attr, const __u32 flags)
{
return syscall(__NR_landlock_add_rule, ruleset_fd, rule_type,
rule_attr, flags);
}
#endif
#ifndef landlock_restrict_self
static inline int landlock_restrict_self(const int ruleset_fd,
const __u32 flags)
{
return syscall(__NR_landlock_restrict_self, ruleset_fd, flags);
}
#endif
#endif
/* Most exploits have more need for those syscalls than the
* exploited programs. In cases they are needed, this list should be
@ -85,8 +154,8 @@ static int default_blacklisted_syscals[] = {
struct qssb_path_policy
{
const char *mountpoint;
int policy;
const char *path;
unsigned int policy;
struct qssb_path_policy *next;
};
@ -100,13 +169,18 @@ struct qssb_policy
int no_new_privs;
int namespace_options;
int syscall_default_policy;
/* Bind mounts all paths in path_policies into the chroot and applies
non-landlock policies */
int mount_path_policies_to_chroot;
int *blacklisted_syscalls;
int *allowed_syscalls;
char chroot_target_path[PATH_MAX];
const char *chdir_path;
struct qssb_path_policy *path_policies;
};
/* Do not manually add policies here, use qssb_append_path_polic*() */
struct qssb_path_policy *path_policies;
struct qssb_path_policy **path_policies_tail;
};
/* Creates the default policy
* Must be freed using qssb_free_policy
@ -120,11 +194,46 @@ struct qssb_policy *qssb_init_policy()
result->no_new_privs = 1;
result->namespace_options = QSSB_UNSHARE_MOUNT | QSSB_UNSHARE_USER;
result->chdir_path = NULL;
result->mount_path_policies_to_chroot = 0;
result->chroot_target_path[0] = '\0';
result->path_policies = NULL;
result->path_policies_tail = &(result->path_policies);
return result;
}
int qssb_append_path_policies(struct qssb_policy *qssb_policy, unsigned int path_policy, ...)
{
va_list args;
const char *path;
va_start(args, path_policy);
path = va_arg(args, char*);
while(path != NULL)
{
struct qssb_path_policy *newpolicy = calloc(1, sizeof(struct qssb_path_policy));
if(newpolicy == NULL)
{
QSSB_LOG_ERROR("Failed to allocate memory for path policy\n");
return -1;
}
newpolicy->path = path;
newpolicy->policy = path_policy;
newpolicy->next = NULL;
*(qssb_policy->path_policies_tail) = newpolicy;
qssb_policy->path_policies_tail = &(newpolicy->next);
path = va_arg(args, char*);
}
va_end(args);
return 0;
}
int qssb_append_path_policy(struct qssb_policy *qssb_policy, unsigned int path_policy, const char *path)
{
return qssb_append_path_policies(qssb_policy, path_policy, path, NULL);
}
/*
* Fills buffer with random characters a-z.
@ -150,8 +259,8 @@ int random_string(char *buffer, size_t buffer_length)
}
/* Creates a directory and all necessary parent directories
*
/* Creates a directory and all necessary parent directories
*
* @returns: 0 on success, -ERRNO on failure
* */
static int mkdir_structure(const char *p, mode_t mode)
@ -172,7 +281,7 @@ static int mkdir_structure(const char *p, mode_t mode)
char *begin = path;
char *end = begin+1;
while(*end)
{
if(*end == '/')
@ -205,34 +314,32 @@ static int mkdir_structure(const char *p, mode_t mode)
return 0;
}
/* @returns: argument for mount(2) flags */
static int get_policy_mount_flags(struct qssb_path_policy *policy)
{
int result = 0;
if( (policy->policy & QSSB_MOUNT_ALLOW_DEV) == 0)
if( (policy->policy & QSSB_FS_ALLOW_DEV) == 0)
{
result |= MS_NODEV;
}
if( (policy->policy & QSSB_MOUNT_ALLOW_EXEC) == 0)
if( (policy->policy & QSSB_FS_ALLOW_EXEC) == 0)
{
result |= MS_NOEXEC;
}
if( (policy->policy & QSSB_MOUNT_ALLOW_SETUID) == 0)
if( (policy->policy & QSSB_FS_ALLOW_SETUID) == 0)
{
result |= MS_NOSUID;
}
if( ((policy->policy) & (QSSB_MOUNT_ALLOW_WRITE)) == QSSB_MOUNT_ALLOW_READ)
if( (policy->policy & QSSB_FS_ALLOW_WRITE) == 0)
{
result |= MS_RDONLY;
}
if( !(policy->policy & QSSB_MOUNT_NOT_REC))
if( (policy->policy & QSSB_MOUNT_NOT_REC) == 0)
{
result |= MS_REC;
}
@ -241,7 +348,7 @@ static int get_policy_mount_flags(struct qssb_path_policy *policy)
/* Helper to mount directories into the chroot path "chroot_target_path"
* Paths will be created if necessary
* @returns: 0 on sucess, -ERRNO on failure */
static int mount_to_chroot(const char *chroot_target_path, struct qssb_path_policy *path_policy)
{
@ -249,7 +356,7 @@ static int mount_to_chroot(const char *chroot_target_path, struct qssb_path_poli
{
char path_inside_chroot[PATH_MAX];
int written = snprintf(path_inside_chroot, sizeof(path_inside_chroot), "%s/%s", chroot_target_path, path_policy->mountpoint);
int written = snprintf(path_inside_chroot, sizeof(path_inside_chroot), "%s/%s", chroot_target_path, path_policy->path);
if(written < 0)
{
QSSB_LOG_ERROR("qssb: mount_to_chroot: Error during path concatination\n");
@ -273,12 +380,12 @@ static int mount_to_chroot(const char *chroot_target_path, struct qssb_path_poli
mount_flags |= MS_BIND;
if(path_policy->policy & QSSB_MOUNT_ALLOW_READ)
if(path_policy->policy & QSSB_FS_ALLOW_READ || path_policy->policy & QSSB_FS_ALLOW_WRITE)
{
ret = mount(path_policy->mountpoint, path_inside_chroot, NULL, mount_flags, NULL);
ret = mount(path_policy->path, path_inside_chroot, NULL, mount_flags, NULL);
if(ret < 0 )
{
QSSB_LOG_ERROR("Error: Failed to mount %s to %s: %s\n", path_policy->mountpoint, path_inside_chroot, strerror(errno));
QSSB_LOG_ERROR("Error: Failed to mount %s to %s: %s\n", path_policy->path, path_inside_chroot, strerror(errno));
return ret;
}
@ -308,6 +415,13 @@ int qssb_end_policy(struct qssb_policy *ctxt)
*/
void qssb_free_policy(struct qssb_policy *ctxt)
{
struct qssb_path_policy *current = ctxt->path_policies;
while(current)
{
struct qssb_path_policy *tmp = current;
current = current->next;
free(tmp);
}
free(ctxt);
}
@ -333,7 +447,7 @@ static int enter_namespaces(int namespace_options)
fp = fopen("/proc/self/uid_map", "w");
fprintf(fp, "0 %i", current_uid);
fclose(fp);
fclose(fp);
fp = fopen("/proc/self/gid_map", "w");
fprintf(fp, "0 %i", current_gid);
@ -360,11 +474,11 @@ static int enter_namespaces(int namespace_options)
}
}
return 0;
return 0;
}
/* Drops all capabiltiies held by the process
*
/* Drops all capabiltiies held by the process
*
* @returns: 0 on sucess, -1 on error
*/
static int drop_caps()
@ -403,11 +517,11 @@ static int drop_caps()
/*
* Enables the per_syscall seccomp action for system calls
*
*
* syscalls: array of system calls numbers. -1 must be the last entry.
* per_syscall: action to apply for each system call
* default_action: the default action at the end
*
*
* @returns: 0 on success, -1 on error
*/
static int seccomp_enable(int *syscalls, unsigned int per_syscall, unsigned int default_action)
@ -425,7 +539,7 @@ static int seccomp_enable(int *syscalls, unsigned int per_syscall, unsigned int
struct sock_filter action = BPF_STMT(BPF_RET+BPF_K, per_syscall);
filter[current_filter_index++] = syscall;
filter[current_filter_index++] = action;
++syscalls;
}
@ -438,7 +552,7 @@ static int seccomp_enable(int *syscalls, unsigned int per_syscall, unsigned int
.filter = filter,
};
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1)
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1)
{
QSSB_LOG_ERROR("prctl SET_SECCOMP %s\n", strerror(errno));
return -1;
@ -449,7 +563,7 @@ static int seccomp_enable(int *syscalls, unsigned int per_syscall, unsigned int
/*
* Blacklists the specified systemcalls.
*
*
* syscalls: array of system calls numbers. -1 must be the last entry.
*/
static int seccomp_enable_blacklist(int *syscalls)
@ -459,7 +573,7 @@ static int seccomp_enable_blacklist(int *syscalls)
/*
* Blacklists the specified systemcalls.
*
*
* syscalls: array of system calls numbers. -1 must be the last entry.
*/
static int seccomp_enable_whitelist(int *syscalls)
@ -467,17 +581,175 @@ static int seccomp_enable_whitelist(int *syscalls)
return seccomp_enable(syscalls, SECCOMP_RET_ALLOW, SECCOMP_RET_KILL);
}
#if HAVE_LANDLOCK == 1
static unsigned int qssb_flags_to_landlock(unsigned int flags)
{
unsigned int result = 0;
if(flags & QSSB_FS_ALLOW_DEV)
{
result |= LANDLOCK_ACCESS_FS_MAKE_BLOCK;
result |= LANDLOCK_ACCESS_FS_MAKE_CHAR;
}
if(flags & QSSB_FS_ALLOW_MAKE_BLOCK)
{
result |= LANDLOCK_ACCESS_FS_MAKE_BLOCK;
}
if(flags & QSSB_FS_ALLOW_MAKE_CHAR)
{
result |= LANDLOCK_ACCESS_FS_MAKE_CHAR;
}
if(flags & QSSB_FS_ALLOW_MAKE_DIR)
{
result |= LANDLOCK_ACCESS_FS_MAKE_DIR;
}
if(flags & QSSB_FS_ALLOW_MAKE_FIFO)
{
result |= LANDLOCK_ACCESS_FS_MAKE_FIFO;
}
if(flags & QSSB_FS_ALLOW_MAKE_REG)
{
result |= LANDLOCK_ACCESS_FS_MAKE_REG;
}
if(flags & QSSB_FS_ALLOW_MAKE_SOCK)
{
result |= LANDLOCK_ACCESS_FS_MAKE_SOCK;
}
if(flags & QSSB_FS_ALLOW_MAKE_SYM)
{
result |= LANDLOCK_ACCESS_FS_MAKE_SYM;
}
if(flags & QSSB_FS_ALLOW_READ)
{
result |= LANDLOCK_ACCESS_FS_READ_FILE;
result |= LANDLOCK_ACCESS_FS_READ_DIR;
}
if(flags & QSSB_FS_ALLOW_REMOVE)
{
result |= LANDLOCK_ACCESS_FS_REMOVE_DIR;
result |= LANDLOCK_ACCESS_FS_REMOVE_FILE;
}
if(flags & QSSB_FS_ALLOW_REMOVE_DIR)
{
result |= LANDLOCK_ACCESS_FS_REMOVE_DIR;
}
if(flags & QSSB_FS_ALLOW_REMOVE_FILE)
{
result |= LANDLOCK_ACCESS_FS_REMOVE_FILE;
}
if(flags & QSSB_FS_ALLOW_EXEC)
{
result |= LANDLOCK_ACCESS_FS_EXECUTE;
}
if(flags & QSSB_FS_ALLOW_WRITE)
{
result |= LANDLOCK_ACCESS_FS_MAKE_REG;
result |= LANDLOCK_ACCESS_FS_WRITE_FILE;
}
if(flags & QSSB_FS_ALLOW_WRITE_FILE)
{
result |= LANDLOCK_ACCESS_FS_WRITE_FILE;
}
if(flags & QSSB_FS_ALLOW_READ_DIR)
{
result |= LANDLOCK_ACCESS_FS_READ_DIR;
}
return result;
}
static int landlock_prepare_ruleset(struct qssb_path_policy *policies)
{
int ruleset_fd = -1;
struct landlock_ruleset_attr ruleset_attr;
/* We here want the maximum possible ruleset, so set the var to the max possible bitmask.
Stolen/Adapted from: [linux src]/security/landlock/limits.h
*/
ruleset_attr.handled_access_fs = ((LANDLOCK_ACCESS_FS_MAKE_SYM << 1) - 1);
ruleset_fd = landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0);
if (ruleset_fd < 0)
{
QSSB_LOG_ERROR("Failed to create landlock ruleset");
return -1;
}
struct qssb_path_policy *policy = policies;
while(policy != NULL)
{
struct landlock_path_beneath_attr path_beneath;
path_beneath.parent_fd = open(policy->path, O_PATH | O_CLOEXEC);
if(path_beneath.parent_fd < 0)
{
QSSB_LOG_ERROR("Failed to open policy path %s while preparing landlock ruleset\n", policy->path);
close(ruleset_fd);
return path_beneath.parent_fd;
}
path_beneath.allowed_access = qssb_flags_to_landlock(policy->policy);
int ret = landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0);
if(ret)
{
QSSB_LOG_ERROR("Failed to update ruleset while processsing policy path %s\n", policy->path);
close(ruleset_fd);
return ret;
}
policy = policy->next;
}
return ruleset_fd;
}
#endif
/* Checks for illogical or dangerous combinations */
static int check_policy_sanity(struct qssb_policy *policy)
{
if(policy->blacklisted_syscalls != NULL && policy->allowed_syscalls != NULL)
{
QSSB_LOG_ERROR("Error: Cannot mix blacklisted and whitelisted systemcalls\n");
return -EINVAL;
}
if(policy->mount_path_policies_to_chroot == 1)
{
if(policy->path_policies == NULL)
{
QSSB_LOG_ERROR("Cannot mount path policies to chroot if non are given\n");
return -1;
}
if(!(policy->namespace_options & QSSB_UNSHARE_MOUNT))
{
QSSB_LOG_ERROR("mount_path_policies_to_chroot = 1 requires unsharing mount namespace\n");
return -1;
}
}
if(policy->no_new_privs != 1)
{
if(policy->blacklisted_syscalls != NULL || policy->allowed_syscalls != NULL)
{
QSSB_LOG_ERROR("no_new_privs = 1 is required for seccomp filtering!\n");
return -1;
}
}
if(policy->path_policies != NULL && policy->mount_path_policies_to_chroot != 1)
{
#if HAVE_LANDLOCK != 1
QSSB_LOG_ERROR("Path policies cannot be enforced! System needs landlock support or set mount_path_policies_to_chroot = 1\n");
return -1;
#endif
}
return 0;
}
/* Enables the specified qssb_policy.
*
* The calling process is supposed *TO BE WRITTEN* if
*
* The calling process is supposed *TO BE WRITTEN* if
* this function fails.
* @returns: 0 on sucess, <0 on error
*/
int qssb_enable_policy(struct qssb_policy *policy)
{
if(policy->blacklisted_syscalls != NULL && policy->allowed_syscalls != NULL)
if(check_policy_sanity(policy) != 0)
{
QSSB_LOG_ERROR("Error: Cannot mix blacklisted and whitelisted systemcalls\n");
QSSB_LOG_ERROR("Error: Policy sanity check failed. Cannot apply policy!\n");
return -EINVAL;
}
@ -487,7 +759,7 @@ int qssb_enable_policy(struct qssb_policy *policy)
return -1;
}
if(policy->path_policies != NULL)
if(policy->mount_path_policies_to_chroot && policy->path_policies != NULL)
{
if(*policy->chroot_target_path == '\0')
{
@ -515,21 +787,37 @@ int qssb_enable_policy(struct qssb_policy *policy)
if(mount_to_chroot(policy->chroot_target_path, policy->path_policies) < 0)
{
QSSB_LOG_ERROR("mount_to_chroot: setup of path policies failed\n");
QSSB_LOG_ERROR("mount_to_chroot: bind mounting of path policies failed\n");
return -1;
}
}
if(*policy->chroot_target_path != '\0')
{
if(chroot(policy->chroot_target_path) < 0)
{
QSSB_LOG_ERROR("chroot: failed to enter %s\n", policy->chroot_target_path);
return -1;
}
}
if(policy->chdir_path == NULL)
#if HAVE_LANDLOCK == 1
int landlock_ruleset_fd = -1;
if(policy->path_policies != NULL)
{
landlock_ruleset_fd = landlock_prepare_ruleset(policy->path_policies);
if(landlock_ruleset_fd < 0)
{
policy->chdir_path = "/";
QSSB_LOG_ERROR("landlock_prepare_ruleset: Failed to prepare landlock ruleset: %s\n", strerror(errno));
return -1;
}
}
#endif
if(policy->chdir_path == NULL)
{
policy->chdir_path = "/";
}
if(policy->chdir_path != NULL && chdir(policy->chdir_path) < 0)
{
@ -561,9 +849,19 @@ int qssb_enable_policy(struct qssb_policy *policy)
{
QSSB_LOG_ERROR("prctl: PR_SET_NO_NEW_PRIVS failed: %s\n", strerror(errno));
return -1;
}
}
}
#if HAVE_LANDLOCK == 1
if (policy->path_policies != NULL && landlock_restrict_self(landlock_ruleset_fd, 0) != 0)
{
perror("Failed to enforce ruleset");
close(landlock_ruleset_fd);
return -1;
}
close(landlock_ruleset_fd);
#endif
if(policy->allowed_syscalls != NULL)
{
if(seccomp_enable_whitelist(policy->allowed_syscalls) <0)