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:
parent
a4caa2065d
commit
7dafe38c72
3 changed files with 146 additions and 30 deletions
115
libseccomp.py
115
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 = [
|
||||
|
|
14
ostools.py
14
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
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue