From 7dafe38c72081ae4611065dfa226c6c16be98a6a Mon Sep 17 00:00:00 2001 From: Dpeta <69427753+Dpeta@users.noreply.github.com> Date: Sun, 29 Jan 2023 17:29:17 +0100 Subject: [PATCH] Split seccomp filters off into low-risk call blacklist and optional whitelist, also set no_new_privs bit on Linux. --- libseccomp.py | 115 ++++++++++++++++++++++++++++++++++++++++++++------ ostools.py | 14 +++--- pesterchum.py | 47 ++++++++++++++++----- 3 files changed, 146 insertions(+), 30 deletions(-) diff --git a/libseccomp.py b/libseccomp.py index ac91f72..0d79e73 100644 --- a/libseccomp.py +++ b/libseccomp.py @@ -1,12 +1,14 @@ -""" -Applies a seccomp filter on Linux via libseccomp's Python bindings. -Has some security benefits, but since Python and Qt use many calls -and are pretty high-level, things are prone to breaking. +"""Functions for Applying a seccomp filter on Linux. +This prevents the process from using certain system calls, which has some security benefits. +Since Python and Qt use many calls and are pretty high-level, things are prone to breaking though. Certain features like opening links almost always break. -Libseccomp's Python bindings aren't available on the pypi, check your distro's -package manager for python-libseccomp (Arch) or python3-seccomp (Debian). +Uses libseccomp's Python bindings, which aren't available on the pypi. +Check your distro's package manager for python-libseccomp (Arch) or python3-seccomp (Debian). + +For info on system calls referencing software that uses seccomp like firejail/flatpak is useful. +Bindings documentation: https://github.com/seccomp/libseccomp/blob/main/src/python/seccomp.pyx """ import os import logging @@ -20,8 +22,36 @@ except ImportError: pesterchum_log = logging.getLogger("pchumLogger") -def activate_seccomp(): - """Sets the process into seccomp filter mode.""" +def load_seccomp_blacklist(): + """Applies a selective seccomp filter only disallows certain risky calls. + + Should be less likely to cause issues than a full-on whitelist.""" + if seccomp is None: + pesterchum_log.warning( + "Failed to import seccomp, verify you have" + " python-libseccomp (Arch) or python3-seccomp (Debian) installed" + " and aren't running a pyinstaller build." + ) + return + # Allows all calls by default. + sec_filter = seccomp.SyscallFilter(defaction=seccomp.ALLOW) + + # Deny all socket domains other than AF_UNIX and and AF_INET. + sec_filter.add_rule(seccomp.ERRNO(1), "socket", seccomp.Arg(0, seccomp.LT, 1)) + sec_filter.add_rule(seccomp.ERRNO(1), "socket", seccomp.Arg(0, seccomp.GT, 2)) + + # Fully deny these calls. + for call in CALL_BLACKLIST: + try: + sec_filter.add_rule(seccomp.ERRNO(1), call) + except RuntimeError: + pesterchum_log.warning("Failed to load deny '%s' call seccomp rule.", call) + + sec_filter.load() + + +def load_seccomp_whitelist(): + """Applies a restrictive seccomp filter that disallows most calls by default.""" if seccomp is None: pesterchum_log.error( "Failed to import seccomp, verify you have" @@ -32,8 +62,11 @@ def activate_seccomp(): # Violation gives "Operation not permitted". sec_filter = seccomp.SyscallFilter(defaction=seccomp.ERRNO(1)) # Full access calls - for call in PCHUM_SYSTEM_CALLS: - sec_filter.add_rule(seccomp.ALLOW, call) + for call in CALL_WHITELIST: + try: + sec_filter.add_rule(seccomp.ALLOW, call) + except RuntimeError: + pesterchum_log.warning("Failed to load allow '%s' call seccomp rule.", call) # Allow only UNIX and INET sockets, see the linux manual and source on socket for reference. # Arg(0, seccomp.EQ, 1) means argument 0 must be equal to 1, 1 being the value of AF_UNIX. @@ -48,7 +81,7 @@ def activate_seccomp(): seccomp.ALLOW, "tgkill", seccomp.Arg(1, seccomp.EQ, threading.get_native_id()) ) - # Allow openat as along as it's not in R+W mode. + # Allow openat as long as it's not in R+W mode. # We can't really lock down open/openat further without breaking everything, # even though it's one of the most important calls to lock down. # Could probably allow breaking out of the sandbox in the case of full-on RCE/ACE. @@ -58,8 +91,8 @@ def activate_seccomp(): # Required for Pesterchum to function normally. -# Pesterchum doesn't call most of these directly, there's a lot of abstraction with Python and Qt. -PCHUM_SYSTEM_CALLS = [ +# We don't call most of these directly, there's a lot of abstraction with Python and Qt. +CALL_WHITELIST = [ "access", # Files "brk", # Required "clone3", # Required @@ -73,7 +106,7 @@ PCHUM_SYSTEM_CALLS = [ "ftruncate", # Required "futex", # Required "getcwd", # Get working directory - "getdents64", # Files? Required. + "getdents", # Files? Required. "getgid", # Audio "getpeername", # Connect "getpid", # Audio @@ -109,6 +142,60 @@ PCHUM_SYSTEM_CALLS = [ "write", # Required ] +# Blacklists of calls we should be able to safely deny. +# Setuid might be useful to drop privileges. +SETUID = [ + "setgid", + "setgroups", + "setregid", + "setresgid", + "setresuid", + "setreuid", + "setuid", +] +SYSTEM = [ + "acct", + "bpf", + "capset", + "chown", + "chroot", + "fanotify_init", + "fsconfig", + "fsmount", + "fsopen", + "fspick", + "kexec_file_load", + "kexec_load", + "lookup_dcookie", + "mount", + "move_mount", + "nfsservctl", + "open_by_handle_at", + "open_tree", + "perf_event_open", + "personality", + "pidfd_getfd", + "pivot_root", + "pivot_root", + "process_vm_readv", + "process_vm_writev", + "ptrace", # <-- Important + "quotactl", + "reboot", + "rtas", + "s390_runtime_instr", + "setdomainname", + "setfsuid", + "sethostname", + "swapoff", + "swapon", + "sys_debug_setcontext", + "umount", + "umount2", + "vhangup", +] +CALL_BLACKLIST = SETUID + SYSTEM + """ # Optional EXTRA_CALLS = [ diff --git a/ostools.py b/ostools.py index e598184..a667657 100644 --- a/ostools.py +++ b/ostools.py @@ -26,17 +26,19 @@ def isOSXBundle(): def isRoot(): - """Return True if running with elevated privileges.""" - # Windows + """Return True if running as root on Linux/Mac/Misc""" + if hasattr(os, "getuid"): + return not os.getuid() # 0 if root + return False + + +def isAdmin(): + """Return True if running as Admin on Windows.""" try: if isWin32(): return ctypes.windll.shell32.IsUserAnAdmin() == 1 except OSError as win_issue: print(win_issue) - # Unix - if hasattr(os, "getuid"): - return not os.getuid() # 0 if root - # Just assume it's fine otherwise ig return False diff --git a/pesterchum.py b/pesterchum.py index 1f941d5..7460a4d 100755 --- a/pesterchum.py +++ b/pesterchum.py @@ -9,6 +9,7 @@ import random import re import time import json +import ctypes # Set working directory if os.path.dirname(sys.argv[0]): @@ -110,7 +111,7 @@ parser.add_argument( ) if ostools.isLinux(): parser.add_argument( - "--seccomp", + "--strict-seccomp", action="store_true", help=( "Restrict the system calls Pesterchum is allowed to make via seccomp." @@ -1383,6 +1384,9 @@ class PesterWindow(MovingWindow): # Silly guy prevention pt. 2 # We really shouldn't run as root. self.root_check() + # Set no_new_privs bit on Linux. + if ostools.isLinux(): + self.set_no_new_privs() # karxi: For the record, these are set via commandline arguments. By # default, they aren't usable any other way - you can't set them via @@ -1693,9 +1697,15 @@ class PesterWindow(MovingWindow): self.lastCheckPing = None # Activate seccomp on Linux if enabled - if "seccomp" in options: - if options["seccomp"]: - libseccomp.activate_seccomp() + if ostools.isLinux(): + try: + libseccomp.load_seccomp_blacklist() # Load blacklist always + if "strict-seccomp" in options: + if options["strict-seccomp"]: + libseccomp.load_seccomp_whitelist() # Load whitelist if enabled + except RuntimeError: + # We probably tried to interact with a call not available on this kernel. + PchumLog.exception("") @QtCore.pyqtSlot(QString, QString) def updateMsg(self, ver, url): @@ -1723,11 +1733,11 @@ class PesterWindow(MovingWindow): def root_check(self): """Raise a warning message box if Pesterchum has admin/root privileges.""" - if ostools.isRoot(): + if ostools.isRoot() or ostools.isAdmin(): msgbox = QtWidgets.QMessageBox() msg = ( "Running with elevated privileges, " - "this is potentially a security risk." + "this is a security risk and may break certain features." "\nThere is no valid reason to run Pesterchum as an administrator or as root." "\n\nQuit?" ) @@ -1751,10 +1761,27 @@ class PesterWindow(MovingWindow): self.app.quit() # Optional sys.exit() + def set_no_new_privs(self): + """Set no_new_privs bit on Linux, disallows gaining more privileges. + + For info see: https://www.kernel.org/doc/html/latest/userspace-api/no_new_privs.html + """ + try: + libc = ctypes.CDLL(None) + # 38 is PR_SET_NO_NEW_PRIVS, see prctl.h in Linux kernel. + # To test, use PR_GET_NO_NEW_PRIVS: libc.prctl(39, 0, 0, 0, 0) + libc.prctl(38, 1, 0, 0, 0) + # Seems to work; strace output with PR_GET_NO_NEW_PRIVS calls: + # prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0) = 0 + # prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) = 0 + # prctl(PR_GET_NO_NEW_PRIVS, 0, 0, 0, 0) = 1 + except (ctypes.ArgumentError, OSError): + # Exception is usually not raised even when the call fails. + PchumLog.exception("Failed to set no_new_privs bit.") + @QtCore.pyqtSlot() def checkPing(self): - """Check if server is alive on app level, - this function is called every 15sec""" + """Check if server is alive on app level, this function is called every 15sec""" # Return without irc if not hasattr(self.parent, "irc"): self.lastCheckPing = None @@ -4569,8 +4596,8 @@ class MainProgram(QtCore.QObject): if args.nohonk: options["honk"] = False if ostools.isLinux(): - if args.seccomp: - options["seccomp"] = True + if args.strict_seccomp: + options["strict-seccomp"] = True except Exception as e: print(e)