Skip to content

Commit

Permalink
cptbox: Add writable and PID-specific procfs file whitelists
Browse files Browse the repository at this point in the history
  • Loading branch information
Xyene committed Jul 14, 2019
1 parent 6e85d1b commit 7e29cff
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 19 deletions.
59 changes: 42 additions & 17 deletions dmoj/cptbox/chroot.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@


class CHROOTSecurity(dict):
def __init__(self, filesystem, writable=(1, 2)):
def __init__(self, read_fs, write_fs=None, writable=(1, 2)):
super(CHROOTSecurity, self).__init__()
self.fs_jail = re.compile('|'.join(filesystem) if filesystem else '^')
self.read_fs = read_fs
self.write_fs = write_fs
self.read_fs_jail = {}
self.write_fs_jail = {}

self._writable = list(writable)

if sys.platform.startswith('freebsd'):
Expand All @@ -28,18 +32,18 @@ def __init__(self, filesystem, writable=(1, 2)):
self.update({
# Deny with report
sys_openat: self.check_file_access_at('openat', is_open=True),
sys_faccessat: self.check_file_access_at('faccessat'),
sys_open: self.check_file_access('open', 0, is_open=True),
sys_faccessat: self.check_file_access_at('faccessat'),
sys_access: self.check_file_access('access', 0),
sys_mkdir: self.check_file_access('mkdir', 0),
sys_unlink: self.check_file_access('unlink', 0),
sys_readlink: self.check_file_access('readlink', 0),
sys_readlinkat: self.check_file_access_at('readlinkat'),
sys_stat: self.check_file_access('stat', 0),
sys_stat64: self.check_file_access('stat64', 0),
sys_lstat: self.check_file_access('lstat', 0),
sys_lstat64: self.check_file_access('lstat64', 0),
sys_fstatat: self.check_file_access_at('fstatat'),
sys_mkdir: ACCESS_EPERM,
sys_unlink: ACCESS_EPERM,
sys_tgkill: self.do_kill,
sys_kill: self.do_kill,
sys_prctl: self.do_prctl,
Expand Down Expand Up @@ -172,21 +176,21 @@ def __init__(self, filesystem, writable=(1, 2)):
sys_minherit: ALLOW,
})

def check_open_flags(self, flags):
disallowed_flags = [os.O_WRONLY, os.O_RDWR, os.O_TRUNC]
def is_write_flags(self, flags):
write_flags = [os.O_WRONLY, os.O_RDWR, os.O_TRUNC]

for flag in disallowed_flags:
for flag in write_flags:
if flags & flag:
return False
return True

return True
return False

def check_file_access(self, syscall, argument, is_open=False):
def check(debugger):
file_ptr = getattr(debugger, 'uarg%d' % argument)
file = debugger.readstr(file_ptr)
file, accessible = self._file_access_check(file, debugger, debugger.uarg0 if is_open else None)
if accessible and (not is_open or self.check_open_flags(debugger.uarg1)):
file, accessible = self._file_access_check(file, debugger, is_open)
if accessible:
return True

log.info('Denied access via syscall %s: %s', syscall, file)
Expand All @@ -196,22 +200,43 @@ def check(debugger):
def check_file_access_at(self, syscall, is_open=False):
def check(debugger):
file = debugger.readstr(debugger.uarg1)
file, accessible = self._file_access_check(file, debugger, debugger.uarg0 if is_open else None,
dirfd=debugger.arg0, flag_reg=2)
if accessible and (not is_open or self.check_open_flags(debugger.uarg2)):
file, accessible = self._file_access_check(file, debugger, is_open, dirfd=debugger.arg0, flag_reg=2)
if accessible:
return True

log.info('Denied access via syscall %s: %s', syscall, file)
return ACCESS_ENOENT(debugger)
return check

def _file_access_check(self, rel_file, debugger, orig_uarg0=None, flag_reg=1, dirfd=AT_FDCWD):
def _get_fs_jail(self, debugger, is_write):
# The only syscalls that can ever have is_write=True are open and
# openat: if we ever want to support syscalls like unlink or mkdir,
# this behaviour will need to be changed. Currently, they result in
# an unconditional EPERM.
jail = self.write_fs_jail if is_write else self.read_fs_jail
fs = jail.get(debugger.pid)

if fs is None:
fs_parts = self.write_fs if is_write else self.read_fs
if fs_parts:
fs_re = '|'.join(map(lambda p: p.format(pid=debugger.pid), fs_parts))
else:
fs_re = '(?!)' # Disallow accessing everything by default.

fs = re.compile(fs_re)
jail[debugger.pid] = fs

return fs

def _file_access_check(self, rel_file, debugger, is_open, flag_reg=1, dirfd=AT_FDCWD):
try:
file = self.get_full_path(debugger, rel_file, dirfd)
except UnicodeDecodeError:
log.exception('Unicode decoding error while opening relative to %d: %r', dirfd, rel_file)
return '(undecodable)', False
if self.fs_jail.match(file) is None:

is_write = is_open and self.is_write_flags(getattr(debugger, 'uarg%d' % flag_reg))
if self._get_fs_jail(debugger, is_write).match(file) is None:
return file, False
return file, True

Expand Down
10 changes: 8 additions & 2 deletions dmoj/executors/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ def launch(self, *args, **kwargs):
'/usr/(?!home)', '/lib(?:32|64)?/', '/opt/', '/etc$',
'/etc/(?:localtime|timezone|nsswitch.conf|resolv.conf|passwd|malloc.conf)$',
'/usr$', '/tmp$', '/$']
BASE_WRITE_FILESYSTEM = ['/dev/stdout$', '/dev/stderr$', '/dev/null$']

if 'freebsd' in sys.platform:
BASE_FILESYSTEM += [r'/etc/s?pwd\.db$', '/dev/hv_tsc$']
Expand All @@ -67,7 +68,8 @@ def launch(self, *args, **kwargs):
BASE_FILESYSTEM += [r'/etc/libmap\.conf$', r'/var/run/ld-elf\.so\.hints$']
else:
# Linux and kFreeBSD mounts linux-style procfs.
BASE_FILESYSTEM += ['/proc$', '/proc/self/(?:maps|exe|auxv)$', '/proc/self$',
BASE_FILESYSTEM += ['/proc$', '/proc/(?:self|{pid})/(?:maps|exe|auxv)$',
'/proc/(?:self|{pid})$',
'/proc/(?:meminfo|stat|cpuinfo|filesystems|xen|uptime)$',
'/proc/sys/vm/overcommit_memory$']

Expand All @@ -80,6 +82,7 @@ class PlatformExecutorMixin(object):
data_grace = 0
personality = 0x0040000 # ADDR_NO_RANDOMIZE
fs = []
write_fs = []
syscalls = []

def _add_syscalls(self, sec):
Expand All @@ -94,14 +97,17 @@ def _add_syscalls(self, sec):
def get_security(self, launch_kwargs=None):
if CHROOTSecurity is None:
raise NotImplementedError('No security manager on Windows')
sec = CHROOTSecurity(self.get_fs())
sec = CHROOTSecurity(self.get_fs(), write_fs=self.get_write_fs())
return self._add_syscalls(sec)

def get_fs(self):
name = self.get_executor_name()
fs = BASE_FILESYSTEM + self.fs + env.get('extra_fs', {}).get(name, []) + [re.escape(self._dir)]
return fs

def get_write_fs(self):
return BASE_WRITE_FILESYSTEM + self.write_fs

def get_allowed_syscalls(self):
return self.syscalls

Expand Down

0 comments on commit 7e29cff

Please sign in to comment.