From 496502b15045d589ca1fd0c90c57d557998a9ba0 Mon Sep 17 00:00:00 2001 From: Tyler Hicks Date: Wed, 23 Apr 2014 15:40:20 -0500 Subject: [PATCH] utils: Basic support for ptrace rules Bug: https://bugs.launchpad.net/bugs/1300317 This patch does bare bones parsing of ptrace rules and stores the raw strings for writing them out later. It is meant to be a simple change to prevent aa.py from emitting a traceback when encountering ptrace rules. Signed-off-by: Tyler Hicks Acked-by: Steve Beattie Acked-By: Christian Boltz --- utils/apparmor/aa.py | 49 ++++++++++++++++++++ utils/apparmor/rules.py | 12 +++++ utils/test/test-ptrace_parse.py | 53 +++++++++++++++++++++ utils/test/test-regex_matches.py | 79 ++++++++++++++++++++++++++++++++ 4 files changed, 193 insertions(+) create mode 100644 utils/test/test-ptrace_parse.py diff --git a/utils/apparmor/aa.py b/utils/apparmor/aa.py index 87eb7b00e..8c1e3742a 100644 --- a/utils/apparmor/aa.py +++ b/utils/apparmor/aa.py @@ -2625,6 +2625,7 @@ RE_NETWORK_FAMILY = re.compile('\s+(\S+)\s*,$') RE_PROFILE_DBUS = re.compile('^\s*(audit\s+)?(allow\s+|deny\s+)?(dbus[^#]*\s*,)\s*(#.*)?$') RE_PROFILE_MOUNT = re.compile('^\s*(audit\s+)?(allow\s+|deny\s+)?((mount|remount|umount)[^#]*\s*,)\s*(#.*)?$') RE_PROFILE_SIGNAL = re.compile('^\s*(audit\s+)?(allow\s+|deny\s+)?((signal)[^#]*\s*,)\s*(#.*)?$') +RE_PROFILE_PTRACE = re.compile('^\s*(audit\s+)?(allow\s+|deny\s+)?((ptrace)[^#]*\s*,)\s*(#.*)?$') # match anything that's not " or #, or matching quotes with anything except quotes inside __re_no_or_quoted_hash = '([^#"]|"[^"]*")*' @@ -2704,6 +2705,7 @@ def parse_profile_data(data, file, do_include): profile_data[profile][hat]['allow']['dbus'] = list() profile_data[profile][hat]['allow']['mount'] = list() profile_data[profile][hat]['allow']['signal'] = list() + profile_data[profile][hat]['allow']['ptrace'] = list() # Save the initial comment if initial_comment: profile_data[profile][hat]['initial_comment'] = initial_comment @@ -3086,6 +3088,28 @@ def parse_profile_data(data, file, do_include): signal_rules.append(signal_rule) profile_data[profile][hat][allow]['signal'] = signal_rules + elif RE_PROFILE_PTRACE.search(line): + matches = RE_PROFILE_PTRACE.search(line).groups() + + if not profile: + raise AppArmorException(_('Syntax Error: Unexpected ptrace entry found in file: %s line: %s') % (file, lineno + 1)) + + audit = False + if matches[0]: + audit = True + allow = 'allow' + if matches[1] and matches[1].strip() == 'deny': + allow = 'deny' + ptrace = matches[2].strip() + + ptrace_rule = parse_ptrace_rule(ptrace) + ptrace_rule.audit = audit + ptrace_rule.deny = (allow == 'deny') + + ptrace_rules = profile_data[profile][hat][allow].get('ptrace', list()) + ptrace_rules.append(ptrace_rule) + profile_data[profile][hat][allow]['ptrace'] = ptrace_rules + elif RE_PROFILE_CHANGE_HAT.search(line): matches = RE_PROFILE_CHANGE_HAT.search(line).groups() @@ -3188,6 +3212,10 @@ def parse_signal_rule(line): # XXX Do real parsing here return aarules.Raw_Signal_Rule(line) +def parse_ptrace_rule(line): + # XXX Do real parsing here + return aarules.Raw_Ptrace_Rule(line) + def separate_vars(vs): """Returns a list of all the values for a variable""" data = [] @@ -3435,6 +3463,24 @@ def write_signal(prof_data, depth): data += write_signal_rules(prof_data, depth, 'allow') return data +def write_ptrace_rules(prof_data, depth, allow): + pre = ' ' * depth + data = [] + + # no ptrace rules, so return + if not prof_data[allow].get('ptrace', False): + return data + + for ptrace_rule in prof_data[allow]['ptrace']: + data.append('%s%s' % (pre, ptrace_rule.serialize())) + data.append('') + return data + +def write_ptrace(prof_data, depth): + data = write_ptrace_rules(prof_data, depth, 'deny') + data += write_ptrace_rules(prof_data, depth, 'allow') + return data + def write_link_rules(prof_data, depth, allow): pre = ' ' * depth data = [] @@ -3542,6 +3588,7 @@ def write_rules(prof_data, depth): data += write_dbus(prof_data, depth) data += write_mount(prof_data, depth) data += write_signal(prof_data, depth) + data += write_ptrace(prof_data, depth) data += write_links(prof_data, depth) data += write_paths(prof_data, depth) data += write_change_profile(prof_data, depth) @@ -3692,6 +3739,7 @@ def serialize_profile_from_old_profile(profile_data, name, options): 'dbus': write_dbus, 'mount': write_mount, 'signal': write_signal, + 'ptrace': write_ptrace, 'link': write_links, 'path': write_paths, 'change_profile': write_change_profile, @@ -3785,6 +3833,7 @@ def serialize_profile_from_old_profile(profile_data, name, options): data += write_dbus(write_prof_data[name], depth) data += write_mount(write_prof_data[name], depth) data += write_signal(write_prof_data[name], depth) + data += write_ptrace(write_prof_data[name], depth) data += write_links(write_prof_data[name], depth) data += write_paths(write_prof_data[name], depth) data += write_change_profile(write_prof_data[name], depth) diff --git a/utils/apparmor/rules.py b/utils/apparmor/rules.py index 64f8e544c..5b93e1eb9 100644 --- a/utils/apparmor/rules.py +++ b/utils/apparmor/rules.py @@ -79,3 +79,15 @@ class Raw_Signal_Rule(object): return "%s%s%s" % ('audit ' if self.audit else '', 'deny ' if self.deny else '', self.rule) + +class Raw_Ptrace_Rule(object): + audit = False + deny = False + + def __init__(self, rule): + self.rule = rule + + def serialize(self): + return "%s%s%s" % ('audit ' if self.audit else '', + 'deny ' if self.deny else '', + self.rule) diff --git a/utils/test/test-ptrace_parse.py b/utils/test/test-ptrace_parse.py new file mode 100644 index 000000000..a2ce6f1ca --- /dev/null +++ b/utils/test/test-ptrace_parse.py @@ -0,0 +1,53 @@ +#! /usr/bin/env python +# ------------------------------------------------------------------ +# +# Copyright (C) 2014 Canonical Ltd. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of version 2 of the GNU General Public +# License published by the Free Software Foundation. +# +# ------------------------------------------------------------------ + +import apparmor.aa as aa +import unittest + +class AAParsePtraceTest(unittest.TestCase): + + def _test_parse_ptrace_rule(self, rule): + ptrace = aa.parse_ptrace_rule(rule) + self.assertEqual(rule, ptrace.serialize(), + 'ptrace object returned "%s", expected "%s"' % (ptrace.serialize(), rule)) + + def test_parse_plain_ptrace_rule(self): + self._test_parse_ptrace_rule('ptrace,') + + def test_parse_readby_ptrace_rule(self): + self._test_parse_ptrace_rule('ptrace (readby),') + + def test_parse_trace_ptrace_rule(self): + self._test_parse_ptrace_rule('ptrace (trace),') + + def test_parse_trace_read_ptrace_rule(self): + self._test_parse_ptrace_rule('ptrace (trace read),') + + def test_parse_r_ptrace_rule(self): + self._test_parse_ptrace_rule('ptrace r,') + + def test_parse_w_ptrace_rule(self): + self._test_parse_ptrace_rule('ptrace w,') + + def test_parse_rw_ptrace_rule(self): + self._test_parse_ptrace_rule('ptrace rw,') + + def test_parse_peer_1_ptrace_rule(self): + self._test_parse_ptrace_rule('ptrace read peer=foo,') + + def test_parse_peer_2_ptrace_rule(self): + self._test_parse_ptrace_rule('ptrace (trace read) peer=/usr/bin/bar,') + + def test_parse_peer_3_ptrace_rule(self): + self._test_parse_ptrace_rule('ptrace wr peer=/sbin/baz,') + +if __name__ == '__main__': + unittest.main() diff --git a/utils/test/test-regex_matches.py b/utils/test/test-regex_matches.py index 7096a50cf..3118c359d 100644 --- a/utils/test/test-regex_matches.py +++ b/utils/test/test-regex_matches.py @@ -48,6 +48,11 @@ regex_has_comma_testcases = [ ('signal (send, receive)%s', 'embedded parens signal 01'), ('signal (send, receive) set=(hup, quit)%s', 'embedded parens signal 02'), + ('ptrace%s', 'bare ptrace'), + ('ptrace trace%s', 'simple ptrace'), + ('ptrace (tracedby, readby)%s', 'embedded parens ptrace 01'), + ('ptrace (trace) peer=/usr/bin/foo%s', 'embedded parens ptrace 02'), + # the following fail due to inadequacies in the regex # ('dbus (r, w, %s', 'incomplete dbus action'), # ('member="{Hello,AddMatch,RemoveMatch, %s', 'incomplete {} regex'), # also invalid policy @@ -106,6 +111,8 @@ regex_split_comment_testcases = [ ('file /tmp/foo rw, # read-write', ('file /tmp/foo rw, ', '# read-write')), ('signal, # comment', ('signal, ', '# comment')), ('signal receive set=(usr1 usr2) peer=foo,', False), + ('ptrace, # comment', ('ptrace, ', '# comment')), + ('ptrace (trace read) peer=/usr/bin/foo,', False), ] def setup_split_comment_testcases(): @@ -365,6 +372,77 @@ class AARegexSignal(unittest.TestCase): self.assertEqual(parsed, rule, 'Expected signal rule "%s", got "%s"' % (rule, parsed)) +class AARegexPtrace(unittest.TestCase): + '''Tests for RE_PROFILE_PTRACE''' + + def test_bare_ptrace_01(self): + '''test ' ptrace,' ''' + + rule = 'ptrace,' + line = ' %s' % rule + result = aa.RE_PROFILE_PTRACE.search(line) + self.assertTrue(result, 'Couldn\'t find ptrace rule in "%s"' % line) + parsed = result.groups()[2].strip() + self.assertEqual(parsed, rule, 'Expected ptrace rule "%s", got "%s"' + % (rule, parsed)) + + def test_bare_ptrace_02(self): + '''test ' audit ptrace,' ''' + + rule = 'ptrace,' + line = ' audit %s' % rule + result = aa.RE_PROFILE_PTRACE.search(line) + self.assertTrue(result, 'Couldn\'t find ptrace rule in "%s"' % line) + self.assertTrue(result.groups()[0], 'Couldn\'t find audit modifier in "%s"' % line) + parsed = result.groups()[2].strip() + self.assertEqual(parsed, rule, 'Expected ptrace rule "%s", got "%s"' + % (rule, parsed)) + + def test_simple_ptrace_01(self): + '''test ' ptrace trace,' ''' + + rule = 'ptrace trace,' + line = ' %s' % rule + result = aa.RE_PROFILE_PTRACE.search(line) + self.assertTrue(result, 'Couldn\'t find ptrace rule in "%s"' % line) + parsed = result.groups()[2].strip() + self.assertEqual(parsed, rule, 'Expected ptrace rule "%s", got "%s"' + % (rule, parsed)) + + def test_simple_ptrace_02(self): + '''test ' ptrace (tracedby, readby),' ''' + + rule = 'ptrace (tracedby, readby),' + line = ' %s' % rule + result = aa.RE_PROFILE_PTRACE.search(line) + self.assertTrue(result, 'Couldn\'t find ptrace rule in "%s"' % line) + parsed = result.groups()[2].strip() + self.assertEqual(parsed, rule, 'Expected ptrace rule "%s", got "%s"' + % (rule, parsed)) + + def test_simple_ptrace_03(self): + '''test ' audit ptrace (read),' ''' + + rule = 'ptrace (read),' + line = ' audit %s' % rule + result = aa.RE_PROFILE_PTRACE.search(line) + self.assertTrue(result, 'Couldn\'t find ptrace rule in "%s"' % line) + self.assertTrue(result.groups()[0], 'Couldn\'t find audit modifier in "%s"' % line) + parsed = result.groups()[2].strip() + self.assertEqual(parsed, rule, 'Expected ptrace rule "%s", got "%s"' + % (rule, parsed)) + + def test_peer_ptrace_01(self): + '''test ' ptrace trace peer=/usr/sbin/daemon,' ''' + + rule = 'ptrace trace peer=/usr/sbin/daemon,' + line = ' %s' % rule + result = aa.RE_PROFILE_PTRACE.search(line) + self.assertTrue(result, 'Couldn\'t find ptrace rule in "%s"' % line) + parsed = result.groups()[2].strip() + self.assertEqual(parsed, rule, 'Expected ptrace rule "%s", got "%s"' + % (rule, parsed)) + if __name__ == '__main__': verbosity = 2 @@ -378,6 +456,7 @@ if __name__ == '__main__': test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(AARegexPath)) test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(AARegexFile)) test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(AARegexSignal)) + test_suite.addTest(unittest.TestLoader().loadTestsFromTestCase(AARegexPtrace)) result = unittest.TextTestRunner(verbosity=verbosity).run(test_suite) if not result.wasSuccessful(): exit(1)