1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3# 4# Copyright (C) 2016 The Android Open Source Project 5# 6# Licensed under the Apache License, Version 2.0 (the "License"); 7# you may not use this file except in compliance with the License. 8# You may obtain a copy of the License at 9# 10# http://www.apache.org/licenses/LICENSE-2.0 11# 12# Unless required by applicable law or agreed to in writing, software 13# distributed under the License is distributed on an "AS IS" BASIS, 14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15# See the License for the specific language governing permissions and 16# limitations under the License. 17# 18# This script will take any number of trace files generated by strace(1) 19# and output a system call filtering policy suitable for use with Minijail. 20 21"""Helper tool to generate a minijail seccomp filter from strace output.""" 22 23from __future__ import print_function 24 25import argparse 26import collections 27import re 28import sys 29 30 31NOTICE = """# Copyright (C) 2018 The Android Open Source Project 32# 33# Licensed under the Apache License, Version 2.0 (the "License"); 34# you may not use this file except in compliance with the License. 35# You may obtain a copy of the License at 36# 37# http://www.apache.org/licenses/LICENSE-2.0 38# 39# Unless required by applicable law or agreed to in writing, software 40# distributed under the License is distributed on an "AS IS" BASIS, 41# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 42# See the License for the specific language governing permissions and 43# limitations under the License. 44""" 45 46ALLOW = '1' 47 48# This ignores any leading PID tag and trailing <unfinished ...>, and extracts 49# the syscall name and the argument list. 50LINE_RE = re.compile(r'^\s*(?:\[[^]]*\]|\d+)?\s*([a-zA-Z0-9_]+)\(([^)<]*)') 51 52SOCKETCALLS = { 53 'accept', 'bind', 'connect', 'getpeername', 'getsockname', 'getsockopt', 54 'listen', 'recv', 'recvfrom', 'recvmsg', 'send', 'sendmsg', 'sendto', 55 'setsockopt', 'shutdown', 'socket', 'socketpair', 56} 57 58ArgInspectionEntry = collections.namedtuple('ArgInspectionEntry', 59 ('arg_index', 'value_set')) 60 61 62def parse_args(argv): 63 """Returns the parsed CLI arguments for this tool.""" 64 parser = argparse.ArgumentParser(description=__doc__) 65 parser.add_argument('--frequency', nargs='?', type=argparse.FileType('w'), 66 help='frequency file') 67 parser.add_argument('--policy', nargs='?', type=argparse.FileType('w'), 68 default=sys.stdout, help='policy file') 69 parser.add_argument('traces', nargs='+', help='The strace logs.') 70 return parser.parse_args(argv) 71 72 73def get_seccomp_bpf_filter(syscall, entry): 74 """Return a minijail seccomp-bpf filter expression for the syscall.""" 75 arg_index = entry.arg_index 76 arg_values = entry.value_set 77 atoms = [] 78 if syscall in ('mmap', 'mmap2', 'mprotect') and arg_index == 2: 79 # See if there is at least one instance of any of these syscalls trying 80 # to map memory with both PROT_EXEC and PROT_WRITE. If there isn't, we 81 # can craft a concise expression to forbid this. 82 write_and_exec = set(('PROT_EXEC', 'PROT_WRITE')) 83 for arg_value in arg_values: 84 if write_and_exec.issubset(set(p.strip() for p in 85 arg_value.split('|'))): 86 break 87 else: 88 atoms.extend(['arg2 in ~PROT_EXEC', 'arg2 in ~PROT_WRITE']) 89 arg_values = set() 90 atoms.extend('arg%d == %s' % (arg_index, arg_value) 91 for arg_value in arg_values) 92 return ' || '.join(atoms) 93 94 95def parse_trace_file(trace_filename, syscalls, arg_inspection): 96 """Parses one file produced by strace.""" 97 uses_socketcall = ('i386' in trace_filename or 98 ('x86' in trace_filename and 99 '64' not in trace_filename)) 100 101 with open(trace_filename) as trace_file: 102 for line in trace_file: 103 matches = LINE_RE.match(line) 104 if not matches: 105 continue 106 107 syscall, args = matches.groups() 108 if uses_socketcall and syscall in SOCKETCALLS: 109 syscall = 'socketcall' 110 111 syscalls[syscall] += 1 112 113 args = [arg.strip() for arg in args.split(',')] 114 115 if syscall in arg_inspection: 116 arg_value = args[arg_inspection[syscall].arg_index] 117 arg_inspection[syscall].value_set.add(arg_value) 118 119 120def main(argv=None): 121 """Main entrypoint.""" 122 123 if argv is None: 124 argv = sys.argv[1:] 125 126 opts = parse_args(argv) 127 128 syscalls = collections.defaultdict(int) 129 130 arg_inspection = { 131 'socket': ArgInspectionEntry(0, set([])), # int domain 132 'ioctl': ArgInspectionEntry(1, set([])), # int request 133 'prctl': ArgInspectionEntry(0, set([])), # int option 134 'mmap': ArgInspectionEntry(2, set([])), # int prot 135 'mmap2': ArgInspectionEntry(2, set([])), # int prot 136 'mprotect': ArgInspectionEntry(2, set([])), # int prot 137 } 138 139 for trace_filename in opts.traces: 140 parse_trace_file(trace_filename, syscalls, arg_inspection) 141 142 # Add the basic set if they are not yet present. 143 basic_set = [ 144 'restart_syscall', 'exit', 'exit_group', 'rt_sigreturn', 145 ] 146 for basic_syscall in basic_set: 147 if basic_syscall not in syscalls: 148 syscalls[basic_syscall] = 1 149 150 # If a frequency file isn't used then sort the syscalls based on frequency 151 # to make the common case fast (by checking frequent calls earlier). 152 # Otherwise, sort alphabetically to make it easier for humans to see which 153 # calls are in use (and if necessary manually add a new syscall to the 154 # list). 155 if opts.frequency is None: 156 sorted_syscalls = list( 157 x[0] for x in sorted(syscalls.items(), key=lambda pair: pair[1], 158 reverse=True) 159 ) 160 else: 161 sorted_syscalls = list( 162 x[0] for x in sorted(syscalls.items(), key=lambda pair: pair[0]) 163 ) 164 165 print(NOTICE, file=opts.policy) 166 if opts.frequency is not None: 167 print(NOTICE, file=opts.frequency) 168 169 for syscall in sorted_syscalls: 170 if syscall in arg_inspection: 171 arg_filter = get_seccomp_bpf_filter(syscall, arg_inspection[syscall]) 172 else: 173 arg_filter = ALLOW 174 print('%s: %s' % (syscall, arg_filter), file=opts.policy) 175 if opts.frequency is not None: 176 print('%s: %s' % (syscall, syscalls[syscall]), 177 file=opts.frequency) 178 179if __name__ == '__main__': 180 sys.exit(main(sys.argv[1:])) 181