From 485d5a4ea46dd90e228b3e4856b95a67a51d348c Mon Sep 17 00:00:00 2001 From: Eric Leblond Date: Wed, 20 Jul 2022 11:05:57 +0200 Subject: [PATCH] landlock: basic implementation This patch is adding support for Landlock, a Linux Security Module available since Linux 5.13. The concept is to prevent any file operation on directories where Suricata is not supposed to access. Landlock support is built by default if the header is present. The feature is disabled by default and need to be activated in the YAML to be active. Landlock documentation: https://docs.kernel.org/userspace-api/landlock.html Feature: #5479 --- configure.ac | 8 ++ src/Makefile.am | 2 + src/suricata.c | 4 + src/util-landlock.c | 275 ++++++++++++++++++++++++++++++++++++++++++++ src/util-landlock.h | 31 +++++ suricata.yaml.in | 14 +++ 6 files changed, 334 insertions(+) create mode 100644 src/util-landlock.c create mode 100644 src/util-landlock.h diff --git a/configure.ac b/configure.ac index 92c55d0cc8..05a18ee92a 100644 --- a/configure.ac +++ b/configure.ac @@ -377,6 +377,13 @@ AC_SUBST(SECLDFLAGS) ]) + #check for Landlock support + AC_CHECK_HEADERS([linux/landlock.h]) + enable_landlock="no" + if test "$ac_cv_header_linux_landlock_h" = "yes"; then + enable_landlock="yes" + fi + #check for plugin support AC_CHECK_HEADERS([dlfcn.h]) AC_MSG_CHECKING([for plugin support]) @@ -2513,6 +2520,7 @@ SURICATA_BUILD_CONF="Suricata Configuration: Hyperscan support: ${enable_hyperscan} Libnet support: ${enable_libnet} liblz4 support: ${enable_liblz4} + Landlock support: ${enable_landlock} Rust support: ${enable_rust} Rust strict mode: ${enable_rust_strict} diff --git a/src/Makefile.am b/src/Makefile.am index d2f23447d0..2760dfca77 100755 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -546,6 +546,7 @@ noinst_HEADERS = \ util-ioctl.h \ util-ip.h \ util-ja3.h \ + util-landlock.h \ util-logopenfile.h \ util-log-redis.h \ util-lua-common.h \ @@ -1132,6 +1133,7 @@ libsuricata_c_a_SOURCES = \ util-ioctl.c \ util-ip.c \ util-ja3.c \ + util-landlock.c \ util-logopenfile.c \ util-log-redis.c \ util-lua.c \ diff --git a/src/suricata.c b/src/suricata.c index 2842ff6b5e..3f2cdc1051 100644 --- a/src/suricata.c +++ b/src/suricata.c @@ -162,6 +162,8 @@ #include "util-daemon.h" #include "util-byte.h" #include "util-luajit.h" +#include "util-landlock.h" + #include "reputation.h" #include "output.h" @@ -2904,6 +2906,8 @@ int SuricataMain(int argc, char **argv) exit(EXIT_FAILURE); } + LandlockSandboxing(&suricata); + SCDropMainThreadCaps(suricata.userid, suricata.groupid); /* Re-enable coredumps after privileges are dropped. */ diff --git a/src/util-landlock.c b/src/util-landlock.c new file mode 100644 index 0000000000..cc1fadeac2 --- /dev/null +++ b/src/util-landlock.c @@ -0,0 +1,275 @@ +/* Copyright (C) 2022 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + * + * \author Eric Leblond + */ + +#include "suricata.h" +#include "util-conf.h" +#include "util-landlock.h" +#include "util-mem.h" + +#ifndef HAVE_LINUX_LANDLOCK_H + +void LandlockSandboxing(SCInstance *suri) +{ + return; +} + +#else /* HAVE_LINUX_LANDLOCK_H */ + +#include + +#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 + +#ifndef LANDLOCK_ACCESS_FS_REFER +#define LANDLOCK_ACCESS_FS_REFER (1ULL << 13) +#endif + +#define _LANDLOCK_ACCESS_FS_WRITE \ + (LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_REMOVE_DIR | \ + LANDLOCK_ACCESS_FS_REMOVE_FILE | LANDLOCK_ACCESS_FS_MAKE_CHAR | \ + LANDLOCK_ACCESS_FS_MAKE_DIR | LANDLOCK_ACCESS_FS_MAKE_REG | \ + LANDLOCK_ACCESS_FS_MAKE_SOCK | LANDLOCK_ACCESS_FS_MAKE_FIFO | \ + LANDLOCK_ACCESS_FS_MAKE_BLOCK | LANDLOCK_ACCESS_FS_MAKE_SYM | \ + LANDLOCK_ACCESS_FS_REFER) + +#define _LANDLOCK_ACCESS_FS_READ (LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR) + +#define _LANDLOCK_SURI_ACCESS_FS_WRITE \ + (LANDLOCK_ACCESS_FS_WRITE_FILE | LANDLOCK_ACCESS_FS_MAKE_DIR | LANDLOCK_ACCESS_FS_MAKE_REG | \ + LANDLOCK_ACCESS_FS_REMOVE_FILE | LANDLOCK_ACCESS_FS_MAKE_SOCK) + +struct landlock_ruleset { + int fd; + struct landlock_ruleset_attr attr; +}; + +static inline struct landlock_ruleset *LandlockCreateRuleset(void) +{ + struct landlock_ruleset *ruleset = SCCalloc(1, sizeof(struct landlock_ruleset)); + if (ruleset == NULL) { + SCLogError(SC_ERR_MEM_ALLOC, "Can't alloc landlock ruleset"); + return NULL; + } + + ruleset->attr.handled_access_fs = + _LANDLOCK_ACCESS_FS_READ | _LANDLOCK_ACCESS_FS_WRITE | LANDLOCK_ACCESS_FS_EXECUTE; + + int abi = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION); + if (abi < 0) { + SCFree(ruleset); + return NULL; + } + if (abi < 2) { + ruleset->attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER; + } + + ruleset->fd = landlock_create_ruleset(&ruleset->attr, sizeof(ruleset->attr), 0); + if (ruleset->fd < 0) { + SCFree(ruleset); + SCLogError(SC_ERR_CONF_YAML_ERROR, "Can't create landlock ruleset"); + return NULL; + } + return ruleset; +} + +static inline void LandlockEnforceRuleset(struct landlock_ruleset *ruleset) +{ + if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) { + SCLogError( + SC_ERR_CONF_YAML_ERROR, "Can't self restrict (prctl phase): %s", strerror(errno)); + return; + } + if (landlock_restrict_self(ruleset->fd, 0)) { + SCLogError(SC_ERR_CONF_YAML_ERROR, "Can't self restrict (landlock phase): %s", + strerror(errno)); + } +} + +static int LandlockSandboxingAddRule( + struct landlock_ruleset *ruleset, const char *directory, uint64_t permission) +{ + struct landlock_path_beneath_attr path_beneath = { + .allowed_access = permission & ruleset->attr.handled_access_fs, + }; + + int dir_fd = open(directory, O_PATH | O_CLOEXEC | O_DIRECTORY); + if (dir_fd == -1) { + SCLogError(SC_ERR_CONF_YAML_ERROR, "Can't open %s", directory); + return -1; + } + path_beneath.parent_fd = dir_fd; + + if (landlock_add_rule(ruleset->fd, LANDLOCK_RULE_PATH_BENEATH, &path_beneath, 0)) { + SCLogError(SC_ERR_CONF_YAML_ERROR, "Can't add write rule: %s", strerror(errno)); + close(dir_fd); + return -1; + } + + close(dir_fd); + return 0; +} + +static inline void LandlockSandboxingWritePath( + struct landlock_ruleset *ruleset, const char *directory) +{ + if (LandlockSandboxingAddRule(ruleset, directory, _LANDLOCK_SURI_ACCESS_FS_WRITE) == 0) { + SCLogConfig("Added write permission to '%s'", directory); + } +} + +static inline void LandlockSandboxingReadPath( + struct landlock_ruleset *ruleset, const char *directory) +{ + if (LandlockSandboxingAddRule(ruleset, directory, _LANDLOCK_ACCESS_FS_READ) == 0) { + SCLogConfig("Added read permission to '%s'", directory); + } +} + +void LandlockSandboxing(SCInstance *suri) +{ + /* Read configuration variable and exit if no enforcement */ + int conf_status; + ConfGetBool("security.landlock.enabled", &conf_status); + if (!conf_status) { + SCLogConfig("Landlock is not enabled in configuration"); + return; + } + struct landlock_ruleset *ruleset = LandlockCreateRuleset(); + if (ruleset == NULL) { + SCLogError(SC_ERR_NOT_SUPPORTED, "Kernel does not support Landlock"); + return; + } + + LandlockSandboxingWritePath(ruleset, ConfigGetLogDirectory()); + struct stat sb; + if (stat(ConfigGetDataDirectory(), &sb) == 0) { + LandlockSandboxingAddRule(ruleset, ConfigGetDataDirectory(), + _LANDLOCK_SURI_ACCESS_FS_WRITE | _LANDLOCK_ACCESS_FS_READ); + } + if (suri->run_mode == RUNMODE_PCAP_FILE) { + const char *pcap_file; + ConfGet("pcap-file.file", &pcap_file); + char *file_name = SCStrdup(pcap_file); + if (file_name != NULL) { + struct stat statbuf; + if (stat(file_name, &statbuf) != -1) { + if (S_ISDIR(statbuf.st_mode)) { + LandlockSandboxingReadPath(ruleset, file_name); + } else { + LandlockSandboxingReadPath(ruleset, dirname(file_name)); + } + } else { + SCLogError(SC_ERR_OPENING_FILE, "Can't open pcap file"); + } + SCFree(file_name); + } + } + if (suri->sig_file) { + char *file_name = SCStrdup(suri->sig_file); + if (file_name != NULL) { + LandlockSandboxingReadPath(ruleset, dirname(file_name)); + SCFree(file_name); + } + } + if (suri->pid_filename) { + char *file_name = SCStrdup(suri->pid_filename); + if (file_name != NULL) { + LandlockSandboxingWritePath(ruleset, dirname(file_name)); + SCFree(file_name); + } + } + if (ConfUnixSocketIsEnable()) { + const char *socketname; + if (ConfGet("unix-command.filename", &socketname) == 1) { + if (PathIsAbsolute(socketname)) { + char *file_name = SCStrdup(socketname); + if (file_name != NULL) { + LandlockSandboxingWritePath(ruleset, dirname(file_name)); + SCFree(file_name); + } + } else { + LandlockSandboxingWritePath(ruleset, LOCAL_STATE_DIR "/run/suricata/"); + } + } else { + LandlockSandboxingWritePath(ruleset, LOCAL_STATE_DIR "/run/suricata/"); + } + } + if (suri->sig_file_exclusive == FALSE) { + const char *rule_path; + ConfGet("default-rule-path", &rule_path); + if (rule_path) { + LandlockSandboxingReadPath(ruleset, rule_path); + } + } + + ConfNode *read_dirs = ConfGetNode("security.landlock.directories.read"); + if (read_dirs) { + if (!ConfNodeIsSequence(read_dirs)) { + SCLogWarning(SC_ERR_INVALID_ARGUMENT, + "Invalid security.landlock.directories.read configuration section: " + "expected a list of directory names."); + } else { + ConfNode *directory; + TAILQ_FOREACH (directory, &read_dirs->head, next) { + LandlockSandboxingReadPath(ruleset, directory->val); + } + } + } + ConfNode *write_dirs = ConfGetNode("security.landlock.directories.write"); + if (write_dirs) { + if (!ConfNodeIsSequence(write_dirs)) { + SCLogWarning(SC_ERR_INVALID_ARGUMENT, + "Invalid security.landlock.directories.write configuration section: " + "expected a list of directory names."); + } else { + ConfNode *directory; + TAILQ_FOREACH (directory, &write_dirs->head, next) { + LandlockSandboxingWritePath(ruleset, directory->val); + } + } + } + LandlockEnforceRuleset(ruleset); + SCFree(ruleset); +} + +#endif /* HAVE_LINUX_LANDLOCK_H */ diff --git a/src/util-landlock.h b/src/util-landlock.h new file mode 100644 index 0000000000..5c090435c0 --- /dev/null +++ b/src/util-landlock.h @@ -0,0 +1,31 @@ +/* Copyright (C) 2022 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +/** + * \file + * + * \author Eric Leblond + */ + +#ifndef __UTIL_LANDLOCK_H__ +#define __UTIL_LANDLOCK_H__ + +#include "suricata.h" + +void LandlockSandboxing(SCInstance *suri); + +#endif /* __UTIL_LANDLOCK_H__ */ diff --git a/suricata.yaml.in b/suricata.yaml.in index 7f3ad5b5ca..bb8712cd20 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -1097,6 +1097,20 @@ asn1-max-frames: 256 # user: suri # group: suri +security: + # Use landlock security module under Linux + landlock: + enabled: no + directories: + #write: + # - @e_rundir@ + # /usr and /etc folders are added to read list to allow + # file magic to be used. + read: + - /usr/ + - /etc/ + - @e_sysconfdir@ + # Some logging modules will use that name in event as identifier. The default # value is the hostname #sensor-name: suricata