[OLPC Security] Some code
Michael Stone
michael at laptop.org
Mon Nov 12 22:44:55 EST 2007
Marcus,
Thanks for the code drop. I've merged your code into the 'permissions'
branch in the
users/mstone/security
repo. I'll try it out as soon as I have the opportunity.
Michael
On Sat, Nov 10, 2007 at 02:17:01PM -0500, Marcus Leech wrote:
> Michael:
>
> I've attached code that I've been working on for 'use-camera' and
> 'use-microphone', as well as resource limiting.
> I also attached a permissions.info file that I've been using with the
> Web activity.
>
> I'm off to Atlanta for one week starting tomorrow. I'll likely still
> hop on #olpc while I'm there.
>
> I still don't have any kind of space on dev.laptop.org -- perhaps you
> should ply your roommate with more drink :-)
>
> I talked to my management yesterday, and my half-time engagement with
> OLPC has to end by the end of January, or
> perhaps February. I'm disappointed, but I hope to visit you guys
> sometime around Dec 15 or so.
> #!/usr/bin/env python
>
> import os, sys
> from os.path import exists, join, dirname, isdir, isabs
> import pwd
> import shutil
> import subprocess
> import grp
> import resource
>
> from rainbow import util, permissions
>
> USAGE = """
>
> NAME:
>
> rainbow-inject
>
> Purpose:
>
> rainbow-inject is a security-wrapper for Sugar Activities. Its purpose is to
> launch them in a low-privilege environment so that, if they are exploited,
> they are able to do less harm than would be the case if they ran with the
> full authority of the XO's human operator.
>
> This isolation is accomplished by running each activity as a separate user
> from a set of users that is known, at system installation time, to have
> tightly restricted access to the file-system.
>
> The environment that rainbow-inject creates for each activity it starts is
> controlled by an activity-specific security-record created by the
> rainbow-install-bundle.
>
> Environment Variables:
>
> SUGAR_BUNDLE_PATH : MUST contain an absolute path to a bundle to launch.
>
> SUGAR_BUNDLE_PATH : MUST contain the unique bundle-id of the bundle at
> SUGAR_BUNDLE_PATH.
>
> RAINBOW_DIR : Optional; defaults to /var/rainbow
> This directory is used by rainbow-inject for all its
> run-time data: currently, this means uid/gid allocation
> data and for storing persistent activity-data storage
>
> RAINBOW_STRACE_LOG : Optional; defaults to Nothing.
> If set, rainbow-inject will cause strace to write a
> complete log to the specified path.
>
> Arguments:
>
> Any arguments will be exec'ed verbatim after rainbow-inject properly
> configures its environment.
>
> File Descriptors:
>
> Open file-descriptors will be passed unmodified to the activity being
> launched. However, for the foreseeable future, rainbow-inject _will_ print
> trace information to descriptor 1 and may print error messages to descriptor
> 2 for debugging purposes.
>
> Return Codes:
>
> 1: An unhandled exception arose.
>
> """
>
> def log_file(filelike):
> def log(msg, *args):
> if len(args):
> filelike.write(msg % args + '\n')
> else:
> filelike.write(msg + '\n')
> return log
>
> def strace(log, log_path, argv, env):
> # cjb thinks we should really be using SystemTap here and refers us to
> # wiki:SystemTap and #83 <MS>
> log('applying strace')
> args = ['/usr/bin/strace', '-f', '-o', log_path]
> for k, v in env.iteritems():
> args.append('-E')
> args.append('%s=%s' % (k, v))
> args.extend(argv)
>
> return args, {}
>
> def grab_home(log, spool, bundle_id):
> uid = util.grab_uid(join(spool, 'uidpool'))
> gid = util.grab_gid(join(spool, 'gidpool'), join(spool, 'gid'), bundle_id)
>
> assert not exists('/home/%d' % uid)
> gid_cmd = ['/usr/sbin/groupadd', '-o', '-g', str(gid), str(gid)]
> log('adding group: %s', ' '.join(gid_cmd))
> try:
> subprocess.check_call(gid_cmd)
> except subprocess.CalledProcessError, e:
> if e.returncode is not 9: # we don't care if the group already exists
> raise e
>
> uid_cmd = ['/usr/sbin/useradd', '-m', '-u', str(uid), '-g', str(gid),
> '-c', '%s.%d' % (bundle_id, uid), str(uid)]
> log('adding user: %s', ' '.join(uid_cmd))
> subprocess.check_call(uid_cmd)
>
> home = '/home/%d' % uid
> os.chown(home, 0, gid)
> os.chmod(home, 0750)
>
> return (uid, gid, home)
>
> def configure_scratch_and_home(log, spool, bundle_id, uid, gid, home):
> scratch_root = join(spool, 'scratch', bundle_id)
> util.make_dirs(scratch_root, 0, 0, 0755)
>
> pw = pwd.getpwnam('olpc')
> olpc_uid = pw.pw_uid
>
> log('making data, conf, and tmp dirs in %s', scratch_root)
> for frag in ('data', 'conf'):
> path = join(scratch_root, frag)
> util.make_dirs(path, olpc_uid, gid, 06770)
> os.chown(path, olpc_uid, gid)
> os.chmod(path, 06770)
> os.symlink(path, join(home, frag))
>
> # TODO: Handle old activity versions' config data
>
> util.make_dirs(join(home, 'tmp'), uid, gid, 0700)
> util.mount('tmpfs', join(home, 'tmp'), 'tmpfs', 0, "size=1M,nr_inodes=1024")
> # /home/<uid> is 700 <uid>.<gid> so tmp is already somewhat protected
>
> xauth = join(home, '.Xauthority')
> shutil.copy('/home/olpc/.Xauthority', xauth)
> os.chown(xauth, uid, gid)
>
> for frag in ['session.info', 'config']:
> path = join('.sugar/default/', frag)
> util.make_dirs(dirname(join(home, path)), uid, gid, 0700)
> shutil.copy(join('/home/olpc', path),
> join(home, path))
> os.chown(join(home, path), uid, gid)
>
> def launch(log, home, uid, gid, argv, env, pset):
> env['USER'] = str(uid)
> env['HOME'] = home
> env['XAUTHORITY'] = join(home, '.Xauthority')
> env['SUGAR_ACTIVITY_ROOT'] = home
> env['TMPDIR'] = join(home, 'tmp')
>
> # Check for use-camera, and use-micropphone
> # Add appropriate groups from /etc/group
> groups = []
> gset = False
> if pset != None:
>
> if pset.has_permission('use-camera'):
> groups.append(grp.getgrnam('camera')[2])
> log ('Turning on camera access')
> gset = True
>
> if pset.has_permission('use-microphone'):
> log ('Turning on microphone access')
> groups.append(grp.getgrnam('microp')[2])
> gset = True
>
> #
> # Set appropriate group membership(s), depending on requested permissions
> # List of memberships in 'groups'
> #
> if gset == True:
> os.setgroups(groups)
>
> log('dropping privilege to (%d, %d)', uid, gid)
> os.setgid(gid)
> os.setuid(uid)
>
> # Limit various resources
> # Must be done *after* setting uid/gid
> # This should come from the permissions.info file, but this is OK
> # for now.
>
> try:
> p = pset.permission_params('lim_nofile')
> if p != None:
> x = int(float(p[0]))
> y = int(float(x*1.10))
> log('Setting RLIMIT_NOFILE to %d,%d', x, y)
> resource.setrlimit(resource.RLIMIT_NOFILE, (x,y))
>
> p = pset.permission_params('lim_fsize')
> if p != None:
> x = int(float(p[0]))
> y = int(float(x*1.10))
> log('Setting RLIMIT_FSIZE to %d,%d', x, y)
> resource.setrlimit(resource.RLIMIT_FSIZE, (x,y))
>
>
> p = pset.permission_params('lim_mem')
> if p != None:
> x = int(float(p[0]))
> y = int(float(x*1.10))
> log('Setting RLIMIT_AS to %d,%d', x, y)
> resource.setrlimit (resource.RLIMIT_AS, (int(200e6),int(210e6)))
>
> p = pset.permission_params('lim_nproc')
> if p != None:
> x = int(float(p[0]))
> y = int(float(x*1.10))
> log('Setting RLIMIT_NPROC to %d,%d', x, y)
> resource.setrlimit (resource.RLIMIT_NPROC, (x,y))
>
> except:
> pass
>
> log('chdir to %s' % env['SUGAR_BUNDLE_PATH'])
> os.chdir(env['SUGAR_BUNDLE_PATH'])
>
> # Since we've dropped privilege, there's nothing to verify about strace_log
> strace_log = env.get('RAINBOW_STRACE_LOG')
> if strace_log:
> argv, env = strace(strace_log, argv, env)
>
> log('about to execve\nargv: %s\nenv: %s', argv, env)
> for root, dirs, files in os.walk("/proc/self/fd"):
> for name in files:
> fd = int(name)
> if (fd >= 3):
> try:
> os.close(fd)
> except:
> pass
> os.execvpe(argv[0], argv, env)
>
> def verify_bundle_path(path):
> assert path and os.access(path, os.F_OK | os.R_OK) and isabs(path)
> return path
>
> def verify_bundle_id(bundle_id):
> assert bundle_id and all(s.isalnum() for s in bundle_id.split('.'))
> return bundle_id
>
> def verify_argv(argv):
> assert argv and len(argv) > 1
> return argv
>
> def rwx_dir(path):
> return os.access(path, os.F_OK | os.R_OK | os.W_OK | os.X_OK) \
> and isdir(path)
>
> def verify_spool(spool):
> assert spool
> for frag in ('uidpool', 'gidpool', 'gid', 'scratch'):
> assert rwx_dir(join(spool, frag))
>
> return spool
>
> def verify_home(home, uid, gid):
> saved_uid = os.getuid()
> saved_gid = os.getgid()
> os.setregid(-1, gid)
> os.setreuid(-1, uid)
> for frag in ('conf', 'data', 'tmp'):
> assert rwx_dir(join(home, frag))
> os.setreuid(-1, saved_uid)
> os.setregid(-1, saved_gid)
>
> def run(log, env, argv, bundle_path, bundle_id, spool_dir, pset):
> # Note: exceptions are intended to bubble up to the caller and should
> # terminate execution.
> bundle_path = verify_bundle_path(bundle_path)
> bundle_id = verify_bundle_id(bundle_id)
> argv = verify_argv(argv)
> spool_dir = verify_spool(spool_dir)
>
> (uid, gid, home) = grab_home(log, spool_dir, bundle_id)
> configure_scratch_and_home(log, spool_dir, bundle_id, uid, gid, home)
>
> verify_home(home, uid, gid)
>
>
> launch(log, home, uid, gid, argv, env, pset)
>
> def main():
> run(log_file(sys.stdout),
> os.environ,
> sys.argv,
> os.environ.get('SUGAR_BUNDLE_PATH'),
> os.environ.get('SUGAR_BUNDLE_ID'),
> (os.environ.get('RAINBOW_DIR') or '/var/rainbow'), None)
>
> if __name__ == '__main__':
> main()
> import os
> import sys
> import traceback
> from signal import SIGCHLD
> from optparse import OptionParser
>
> import gobject
>
> import dbus
> import dbus.service
> import dbus.mainloop.glib
>
> from rainbow import util, inject, permissions
> from rainbow.util.linux.clone import clone, CLONE_NEWNS
>
> def log(msg, *args):
> if len(args):
> print msg % args
> else:
> print msg
>
> class Rainbow(dbus.service.Object):
> """The Rainbow security service."""
> SERVICE_NAME = 'org.laptop.security.Rainbow'
> INTERFACE_NAME = 'org.laptop.security.Rainbow'
>
> _foreground_xid = None
> _id_map = {}
>
> def __init__(self, bus_or_name):
> dbus.service.Object.__init__(self, bus_or_name, '/')
>
> @dbus.service.method(INTERFACE_NAME,
> in_signature='s',
> out_signature='s')
> def echo(self, msg):
> return msg
>
> @dbus.service.method('org.laptop.security.DBus',
> in_signature='si',
> out_signature='b')
> def CheckCanOwn(self, service_name, xid):
> """Check the the given xid is allowed to bind to the bus with the given
> name.
> """
>
> # XXX: This should be checking that the thing on the other side is really
> # the session bus. <NPK>
>
> return service_name.startswith('org.laptop.Container%s'%xid)
>
> @dbus.service.method(INTERFACE_NAME,
> in_signature='s')
> def ChangeActivity(self, activity_id):
> """Sets CPU throttling on the foreground and background activities as needed."""
> pass
>
> @dbus.service.method(INTERFACE_NAME,
> in_signature='sa{ss}asss',
> async_callbacks=('success_cont', 'error_cont'))
> def CreateActivity(self, log_path, env, argv, bundle_path, bundle_id,
> success_cont, error_cont):
>
> # Reap zombies
> try:
> while os.waitpid(-1,os.WNOHANG) != (0,0):
> pass
>
> except OSError:
> pass
>
> #
> # Try to pick up the capabilities/permissions from the
> # bundle, if there isn't a permissions.info file, no
> # big deal, the app just doesn't get extra permissions
> #
> pset = None
> permpath = os.path.join(bundle_path, "permissions.info")
>
> perm_worked = True
> try:
> f = open(permpath)
> pset = permissions.PermissionSet(f)
> f.close()
> except:
> perm_worked = False
> pset = None
> pass
>
> try:
> child_pid = clone(CLONE_NEWNS | SIGCHLD)
> if not child_pid:
> # XXX: I _do not_ yet understand which fds I need to close when.
> # For example - should the D-Bus FD needs to be open to send
> # error messages? If so, when does it get closed?
> #
> # patchvonbraun: fixed in inject.py, by querying /proc/self/fd
> # to determine open FDs, and closing any that are >= 3
> #
> # This means that they're closed at the very last possible
> # moment before calling execvpe() in inject.run
> try:
> log_fd = os.open(log_path, os.O_WRONLY)
> os.dup2(log_fd, 1)
> os.dup2(log_fd, 2)
> ret = inject.run(log, env, argv, bundle_path, bundle_id,
> '/var/rainbow', pset)
> except Exception, e:
> util.trace()
> error_cont(e)
> finally:
> os._exit(255)
>
>
> except Exception, e:
> util.trace()
> error_cont(e)
>
> def run():
> """Start the Rainbow DBus service."""
> parser = OptionParser(version='0.1')
> parser.add_option('-f', '--config-file', default='/etc/rainbow/rainbow.conf',
> help='The configuration file for Rainbow.')
> parser.add_option('-d', '--daemon', action='store_true', default=False,
> help='Really daemonize; e.g. detach from the controlling terminal.')
> opts, args = parser.parse_args()
>
> # General FS initialization.
> util.make_dirs('/var/log/rainbow', 0, 0, 0777)
>
> if opts.daemon:
> util.daemon(0, 1)
> open('/var/run/rainbow.pid', 'w').write(str(os.getpid()))
> sys.stdout = open('/var/log/rainbow/stdout', 'w', 0)
> #sys.stderr = open('/var/log/rainbow/stderr', 'w', 0)
> sys.stderr = sys.stdout
>
> dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
>
> bus = dbus.SystemBus()
> name = dbus.service.BusName(Rainbow.SERVICE_NAME, bus)
> Rainbow(name)
>
> print 'Service running mainloop.'
> mainloop = gobject.MainLoop()
> mainloop.run()
>
> def main():
> run()
>
> if __name__ == '__main__':
> main()
> use-camera
> use-microphone
> lim_nofile 20
> lim_mem 190e6
> lim_nproc 8
> lim_fsize 10e6
> DOMAINS = {'network': ('via', 'to', 'port', 'rate', 'burst', 'connection-rate',
> 'transfer-limit', 'bind-port'),
> 'use-microphone': (),
> 'use-camera': (),
> 'play-background-sound': (),
> 'quota': ('limit'),
> 'lim_nofile': ('@NUM@'),
> 'lim_mem': ('@NUM@'),
> 'lim_nproc': ('@NUM@'),
> 'lim_fsize': ('@NUM@'),
> 'document_read_ro': ('type')
> }
>
> class PermissionSet(object):
> def __init__(self, fp=None):
> self._permissions = {}
> self._network_permissions = []
>
> for line in fp:
> line = line.lower().strip()
> if not line or line.startswith('#'):
> continue
>
> fields = line.split()
> if not fields[0] in DOMAINS:
> print "Unknown permissions domain: [%s]" % fields[0]
> continue
>
> for field in fields[1:]:
> if '@NUM@' in DOMAINS[fields[0]]:
> try:
> float(fields[1])
> except:
> print "Expecting numeric value in domain [%s] (%s)" % (fields[0], fields[1])
> continue
> else:
> key, value = field.split(':')
> if not key in DOMAINS[fields[0]]:
> print "Unknown flag [%s] in domain [%s]" % (key, fields[0])
> continue
>
> if fields[0] == 'network':
> self._network_permissions.append(fields[1:])
> else:
> self._permissions[fields[0]] = fields[1:] or True
>
> def has_permission(self, domain, key=None, value=None):
> "Fails for network, since it doesn't make sense to query those perms"
> if domain not in self._permissions:
> return False
> elif key and value:
> for permission in self._permissions[domain]:
> this_key, these_values = permission.split(':')
> if not key == this_key:
> continue
> these_values = these_values.split(',')
> if value in these_values:
> return True
> elif self._permissions[domain] == True:
> return True
> else:
> return False
>
> def permission_params(self, domain):
> if domain not in self._permissions:
> return None
> return(self._permissions[domain])
>
>
> if __name__ == '__main__':
> import cStringIO as stringio
>
> perms = """
> network via:ipv4,ipv6 to:pgp.mit.edu,laptop.org port:80 rate:100Kb/s burst:1Mb
> network via:ipv4 port:25 connection-rate:10/min transfer-limit:8Mb/hr
> network via:ipv6 bind-port:1400
>
> # Comment
> use-microphone
> use-camera
>
> # Comment, yay
> play-background-sound
>
> document_read_ro type:text/plain
>
> quota limit:15Mb
> """
>
> permfp = stringio.StringIO(perms)
> permset = PermissionSet(permfp)
> print permset._permissions
> print permset._network_permissions
More information about the Security
mailing list