Split seccomp filters off into low-risk call blacklist and optional whitelist, also set no_new_privs bit on Linux.

This commit is contained in:
Dpeta 2023-01-29 17:29:17 +01:00
parent a4caa2065d
commit 7dafe38c72
3 changed files with 146 additions and 30 deletions

View file

@ -1,12 +1,14 @@
""" """Functions for Applying a seccomp filter on Linux.
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.
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. Certain features like opening links almost always break.
Libseccomp's Python bindings aren't available on the pypi, check your distro's Uses libseccomp's Python bindings, which aren't available on the pypi.
package manager for python-libseccomp (Arch) or python3-seccomp (Debian). 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 os
import logging import logging
@ -20,8 +22,36 @@ except ImportError:
pesterchum_log = logging.getLogger("pchumLogger") pesterchum_log = logging.getLogger("pchumLogger")
def activate_seccomp(): def load_seccomp_blacklist():
"""Sets the process into seccomp filter mode.""" """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: if seccomp is None:
pesterchum_log.error( pesterchum_log.error(
"Failed to import seccomp, verify you have" "Failed to import seccomp, verify you have"
@ -32,8 +62,11 @@ def activate_seccomp():
# Violation gives "Operation not permitted". # Violation gives "Operation not permitted".
sec_filter = seccomp.SyscallFilter(defaction=seccomp.ERRNO(1)) sec_filter = seccomp.SyscallFilter(defaction=seccomp.ERRNO(1))
# Full access calls # Full access calls
for call in PCHUM_SYSTEM_CALLS: for call in CALL_WHITELIST:
sec_filter.add_rule(seccomp.ALLOW, call) 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. # 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. # 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()) 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, # 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. # 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. # 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. # Required for Pesterchum to function normally.
# Pesterchum doesn't call most of these directly, there's a lot of abstraction with Python and Qt. # We don't call most of these directly, there's a lot of abstraction with Python and Qt.
PCHUM_SYSTEM_CALLS = [ CALL_WHITELIST = [
"access", # Files "access", # Files
"brk", # Required "brk", # Required
"clone3", # Required "clone3", # Required
@ -73,7 +106,7 @@ PCHUM_SYSTEM_CALLS = [
"ftruncate", # Required "ftruncate", # Required
"futex", # Required "futex", # Required
"getcwd", # Get working directory "getcwd", # Get working directory
"getdents64", # Files? Required. "getdents", # Files? Required.
"getgid", # Audio "getgid", # Audio
"getpeername", # Connect "getpeername", # Connect
"getpid", # Audio "getpid", # Audio
@ -109,6 +142,60 @@ PCHUM_SYSTEM_CALLS = [
"write", # Required "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 # Optional
EXTRA_CALLS = [ EXTRA_CALLS = [

View file

@ -26,17 +26,19 @@ def isOSXBundle():
def isRoot(): def isRoot():
"""Return True if running with elevated privileges.""" """Return True if running as root on Linux/Mac/Misc"""
# Windows if hasattr(os, "getuid"):
return not os.getuid() # 0 if root
return False
def isAdmin():
"""Return True if running as Admin on Windows."""
try: try:
if isWin32(): if isWin32():
return ctypes.windll.shell32.IsUserAnAdmin() == 1 return ctypes.windll.shell32.IsUserAnAdmin() == 1
except OSError as win_issue: except OSError as win_issue:
print(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 return False

View file

@ -9,6 +9,7 @@ import random
import re import re
import time import time
import json import json
import ctypes
# Set working directory # Set working directory
if os.path.dirname(sys.argv[0]): if os.path.dirname(sys.argv[0]):
@ -110,7 +111,7 @@ parser.add_argument(
) )
if ostools.isLinux(): if ostools.isLinux():
parser.add_argument( parser.add_argument(
"--seccomp", "--strict-seccomp",
action="store_true", action="store_true",
help=( help=(
"Restrict the system calls Pesterchum is allowed to make via seccomp." "Restrict the system calls Pesterchum is allowed to make via seccomp."
@ -1383,6 +1384,9 @@ class PesterWindow(MovingWindow):
# Silly guy prevention pt. 2 # Silly guy prevention pt. 2
# We really shouldn't run as root. # We really shouldn't run as root.
self.root_check() 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 # 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 # default, they aren't usable any other way - you can't set them via
@ -1693,9 +1697,15 @@ class PesterWindow(MovingWindow):
self.lastCheckPing = None self.lastCheckPing = None
# Activate seccomp on Linux if enabled # Activate seccomp on Linux if enabled
if "seccomp" in options: if ostools.isLinux():
if options["seccomp"]: try:
libseccomp.activate_seccomp() 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) @QtCore.pyqtSlot(QString, QString)
def updateMsg(self, ver, url): def updateMsg(self, ver, url):
@ -1723,11 +1733,11 @@ class PesterWindow(MovingWindow):
def root_check(self): def root_check(self):
"""Raise a warning message box if Pesterchum has admin/root privileges.""" """Raise a warning message box if Pesterchum has admin/root privileges."""
if ostools.isRoot(): if ostools.isRoot() or ostools.isAdmin():
msgbox = QtWidgets.QMessageBox() msgbox = QtWidgets.QMessageBox()
msg = ( msg = (
"Running with elevated privileges, " "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." "\nThere is no valid reason to run Pesterchum as an administrator or as root."
"\n\nQuit?" "\n\nQuit?"
) )
@ -1751,10 +1761,27 @@ class PesterWindow(MovingWindow):
self.app.quit() # Optional self.app.quit() # Optional
sys.exit() 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() @QtCore.pyqtSlot()
def checkPing(self): def checkPing(self):
"""Check if server is alive on app level, """Check if server is alive on app level, this function is called every 15sec"""
this function is called every 15sec"""
# Return without irc # Return without irc
if not hasattr(self.parent, "irc"): if not hasattr(self.parent, "irc"):
self.lastCheckPing = None self.lastCheckPing = None
@ -4569,8 +4596,8 @@ class MainProgram(QtCore.QObject):
if args.nohonk: if args.nohonk:
options["honk"] = False options["honk"] = False
if ostools.isLinux(): if ostools.isLinux():
if args.seccomp: if args.strict_seccomp:
options["seccomp"] = True options["strict-seccomp"] = True
except Exception as e: except Exception as e:
print(e) print(e)