From 200cd7878c60b9c81dfdd95a2d9d3381e853cba6 Mon Sep 17 00:00:00 2001 From: Albert S Date: Sun, 13 Oct 2019 17:57:12 +0200 Subject: [PATCH] Initial commit --- README.md | 42 ++++++ qssb.h | 401 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 443 insertions(+) create mode 100644 README.md create mode 100644 qssb.h diff --git a/README.md b/README.md new file mode 100644 index 0000000..868f2ad --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +qssb (quite simple sandbox) +=========================== +qssb.h is a simple header only library for easy sandboxing of +applications. + +It aims to provide an interface to avoid the annoying details that +using Seccomp and Linux Namespaces requires. + +Features +======== +Systemcall filtering, restricting file system access, dropping +privileges, isolating the application from the network, etc. + +Requirements +============ +Kernel x.y.z. + +Status +====== +No release yet, API is unstable. + +Documentation +============= +To be written + +Examples +======== +Real world project: cgit sandboxed: https://git.quitesimple.org/cgitsb + + +Contributing +============ +Contributations are very welcome. Options: +1) Pull-Request: github.com/quitesimpleorg/qssb +2) Mail to qssb at quitesimple.org with instructions +on where to pull the changes. +3) Mailing a classic patch. + +License +======= +ISC + diff --git a/qssb.h b/qssb.h new file mode 100644 index 0000000..04641dd --- /dev/null +++ b/qssb.h @@ -0,0 +1,401 @@ +#ifndef QSSB_H +#define QSSB_H +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//TODO: stolen from kernel samples/seccomp, GPLv2...? +#define ALLOW \ + BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW) +#define DENY \ + BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL) +#define SYSCALL(nr, jt) \ + BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, (nr), 0, 1), jt + +#define LOAD_SYSCALL_NR \ + BPF_STMT(BPF_LD+BPF_W+BPF_ABS, \ + offsetof(struct seccomp_data, nr)) + +#define QSSB_ISOLATE_NETWORK 1<<1 + +#define QSSB_LOG_ERROR(...) fprintf(stderr, __VA_ARGS__) + +/* Policy tells qssb what to do */ +struct qssb_policy +{ + int drop_caps; + int preserve_cwd; + int not_dumpable; + int no_new_privs; + int namespace_options; + int syscall_default_policy; + int *blacklisted_syscalls; + int *allowed_syscalls; + char *chroot_target_path; + char *chdir_path; + char **readonly_paths; + char **writable_paths; +}; + + +/* Creates the default policy + * Must be freed using qssb_free_policy + * @returns: default policy */ +struct qssb_policy *qssb_init_policy() +{ + struct qssb_policy *result = calloc(1, sizeof(struct qssb_policy)); + result->drop_caps = 1; + result->not_dumpable = 1; + result->no_new_privs = 1; + result->namespace_options = 0; + result->chdir_path = "/"; + result->chroot_target_path = NULL; + result->readonly_paths = NULL; + result->writable_paths = NULL; + return result; +} + + + +/* 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) +{ + char path[PATH_MAX + 2] = { 0 }; + snprintf(path, sizeof(path), "%s/", p); + + char *begin = path; + char *end = begin+1; + + while(*end) + { + if(*end == '/') + { + *end = 0; + if(mkdir(begin, mode) < 0) + { + if(errno == EEXIST) + { + //TODO: stat, test if it is a directory, if not, err + } + else + { + QSSB_LOG_ERROR("Failed to create directory for chroot: %s\n", begin); + return -1; + } + } + *end = '/'; + ++end; + while(*end == '/') + { + ++end; + } + } + else + { + ++end; + } + } + return 0; +} + + +/* 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, char **paths, unsigned long flags) +{ + if(paths == NULL) + { + return 0; + } + + char *path = *paths; + while(path != NULL) + { + char path_inside_chroot[PATH_MAX + 1]; + snprintf(path_inside_chroot, sizeof(path_inside_chroot), "%s/%s", chroot_target_path, path); + int ret = mkdir_structure(path_inside_chroot, 0700); + if(ret < 0) + { + QSSB_LOG_ERROR("Error creating directory structure while mounting paths to chroot. %s\n", strerror(errno)); + return ret; + } + + ret = mount(path, path_inside_chroot, NULL, flags, NULL); + if(ret < 0 ) + { + QSSB_LOG_ERROR("Error: Failed to mount %s to %s: %s\n", path, path_inside_chroot, strerror(errno)); + return ret; + } + + path = *(++paths); + } + + return 0; +} + +/* Ends the policy as best as possible. */ +/* TODO: can this function do actually anything useful?*/ +static int qssb_end_policy(struct qssb_policy *ctxt) +{ + +} + +/* + * Frees the memory taken by a qssb_policy object + */ +static void qssb_free_policy(struct qssb_policy *ctxt) +{ + free(ctxt); +} + +/* Enters the user and mount namespaces */ +static int enter_namespaces() +{ + int ret = unshare(CLONE_NEWUSER); + if(ret == -1) + { + QSSB_LOG_ERROR("Error: Failed to unshare user namespaces: %s\n", strerror(errno)); + return ret; + } + + uid_t current_uid = getuid(); + gid_t current_gid = getgid(); + + //TODO: check errors + FILE *fp = fopen("/proc/self/setgroups", "w"); + fprintf(fp, "deny"); + fclose(fp); + + fp = fopen("/proc/self/uid_map", "w"); + fprintf(fp, "0 %i", current_uid); + fclose(fp); + + fp = fopen("/proc/self/gid_map", "w"); + fprintf(fp, "0 %i", current_gid); + fclose(fp); + + ret = unshare(CLONE_NEWNS); + if(ret == -1) + { + QSSB_LOG_ERROR("Error: Failed to unshare mount namespaces: %s\n", strerror(errno)); + return ret; + } + + return 0; +} + +/* Drops all capabiltiies held by the process + * + * @returns: 0 on sucess, -1 on error +*/ +static int drop_caps() +{ + int cap = 0; + int res = 0; + while((res = prctl(PR_CAPBSET_DROP, cap, 0, 0, 0)) == 0) + { + ++cap; + } + + if(res == -1 && errno != EINVAL) + { + QSSB_LOG_ERROR("Failed to drop the capability bounding set!"); + return -errno; + } + + //TODO: systems that are not 64 bit + struct __user_cap_header_struct h = { 0 }; + h.pid = 0; + h.version = _LINUX_CAPABILITY_VERSION_3; + struct __user_cap_data_struct drop[2]; + drop[0].effective = 0; + drop[0].permitted = 0; + drop[0].inheritable = 0; + drop[1].effective = 0; + drop[1].permitted = 0; + drop[1].inheritable = 0; + if(capset(&h, drop) == -1) + { + QSSB_LOG_ERROR("Failed to drop capabilities: %s\n", strerror(errno));; + return -errno; + } + return 0; +} + +/* + * 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, int per_syscall, int default_action) +{ + struct sock_filter filter[1024] = + { + LOAD_SYSCALL_NR, + }; + + int current_filter_index = 1; + while(*syscalls != -1) + { + struct sock_filter syscall = BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, *syscalls, 0, 1); + struct sock_filter action = BPF_STMT(BPF_RET+BPF_K, per_syscall); + filter[current_filter_index++] = syscall; + filter[current_filter_index++] = action; + + ++syscalls; + } + + struct sock_filter da = BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL); + filter[current_filter_index] = da; + + struct sock_fprog prog = { + .len = current_filter_index + 1, + .filter = filter, + }; + + if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == -1) + { + QSSB_LOG_ERROR("prctl SET_SECCOMP %s\n", strerror(errno)); + return -1; + } + + return 0; +} + +/* + * Blacklists the specified systemcalls. + * + * syscalls: array of system calls numbers. -1 must be the last entry. + */ +static int seccomp_enable_blacklist(int *syscalls) +{ + return seccomp_enable(syscalls, SECCOMP_RET_KILL, SECCOMP_RET_ALLOW); +} + +/* + * Blacklists the specified systemcalls. + * + * syscalls: array of system calls numbers. -1 must be the last entry. + */ +static int seccomp_enable_whitelist(int *syscalls) +{ + return seccomp_enable(syscalls, SECCOMP_RET_ALLOW, SECCOMP_RET_KILL); +} + +/* Enables the specified qssb_policy. + * + * 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) + { + QSSB_LOG_ERROR("Error: Cannot mix blacklisted and whitelisted systemcalls\n"); + return -EINVAL; + } + + + if(policy->chroot_target_path == NULL) + { + policy->chroot_target_path = "/tmp/.TODOIMPLEMENT"; //TODO: implement + } + + if(enter_namespaces() < 0) + { + QSSB_LOG_ERROR("Error while trying to enter namespaces\n"); + return -1; + } + + if(policy->readonly_paths != NULL || policy->writable_paths != NULL) + { + if(mount_to_chroot(policy->chroot_target_path, policy->readonly_paths, MS_BIND | MS_RDONLY) < 0) + { + return -1; + } + + if(mount_to_chroot(policy->chroot_target_path, policy->writable_paths, MS_BIND) < 0) + { + return -1; + } + + if(chroot(policy->chroot_target_path) < 0) + { + QSSB_LOG_ERROR("chroot: failed to enter %s\n", policy->chroot_target_path); + return -1; + } + } + + if(chdir(policy->chdir_path) < 0) + { + return -1; + } + + if(policy->drop_caps) + { + drop_caps(); + } + + if(policy->not_dumpable) + { + if(prctl(PR_SET_DUMPABLE, 0) == -1) + { + QSSB_LOG_ERROR("prctl: PR_SET_DUMPABLE failed\n"); + return -1; + } + } + + if(policy->no_new_privs) + { + if(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) + { + QSSB_LOG_ERROR("prctl: PR_SET_NO_NEW_PRIVS failed: %s\n", strerror(errno)); + return -1; + } + } + + if(policy->allowed_syscalls != NULL) + { + if(seccomp_enable_whitelist(policy->allowed_syscalls) <0) + { + QSSB_LOG_ERROR("seccomp_enable_whitelist failed\n"); + return -1; + } + } + + if(policy->blacklisted_syscalls != NULL) + { + if(seccomp_enable_blacklist(policy->blacklisted_syscalls) <0) + { + QSSB_LOG_ERROR("seccomp_enable_blacklist failed\n"); + return -1; + } + } + + return 0; +} +#endif