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 @@
|
||||||
"""
|
"""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 = [
|
||||||
|
|
14
ostools.py
14
ostools.py
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue