#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (C) 2016 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # This script will take any number of trace files generated by strace(1) # and output a system call filtering policy suitable for use with Minijail. """Helper tool to generate a minijail seccomp filter from strace output.""" from __future__ import print_function import argparse import collections import re import sys NOTICE = """# Copyright (C) 2018 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ ALLOW = '1' # This ignores any leading PID tag and trailing , and extracts # the syscall name and the argument list. LINE_RE = re.compile(r'^\s*(?:\[[^]]*\]|\d+)?\s*([a-zA-Z0-9_]+)\(([^)<]*)') SOCKETCALLS = { 'accept', 'bind', 'connect', 'getpeername', 'getsockname', 'getsockopt', 'listen', 'recv', 'recvfrom', 'recvmsg', 'send', 'sendmsg', 'sendto', 'setsockopt', 'shutdown', 'socket', 'socketpair', } ArgInspectionEntry = collections.namedtuple('ArgInspectionEntry', ('arg_index', 'value_set')) def parse_args(argv): """Returns the parsed CLI arguments for this tool.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('traces', nargs='+', help='The strace logs.') return parser.parse_args(argv) def get_seccomp_bpf_filter(syscall, entry): """Return a minijail seccomp-bpf filter expression for the syscall.""" arg_index = entry.arg_index arg_values = entry.value_set atoms = [] if syscall in ('mmap', 'mmap2', 'mprotect') and arg_index == 2: # See if there is at least one instance of any of these syscalls trying # to map memory with both PROT_EXEC and PROT_WRITE. If there isn't, we # can craft a concise expression to forbid this. write_and_exec = set(('PROT_EXEC', 'PROT_WRITE')) for arg_value in arg_values: if write_and_exec.issubset(set(p.strip() for p in arg_value.split('|'))): break else: atoms.extend(['arg2 in ~PROT_EXEC', 'arg2 in ~PROT_WRITE']) arg_values = set() atoms.extend('arg%d == %s' % (arg_index, arg_value) for arg_value in arg_values) return ' || '.join(atoms) def parse_trace_file(trace_filename, syscalls, arg_inspection): """Parses one file produced by strace.""" uses_socketcall = ('i386' in trace_filename or ('x86' in trace_filename and '64' not in trace_filename)) with open(trace_filename) as trace_file: for line in trace_file: matches = LINE_RE.match(line) if not matches: continue syscall, args = matches.groups() if uses_socketcall and syscall in SOCKETCALLS: syscall = 'socketcall' syscalls[syscall] += 1 args = [arg.strip() for arg in args.split(',')] if syscall in arg_inspection: arg_value = args[arg_inspection[syscall].arg_index] arg_inspection[syscall].value_set.add(arg_value) def main(argv): """Main entrypoint.""" opts = parse_args(argv) syscalls = collections.defaultdict(int) arg_inspection = { 'socket': ArgInspectionEntry(0, set([])), # int domain 'ioctl': ArgInspectionEntry(1, set([])), # int request 'prctl': ArgInspectionEntry(0, set([])), # int option 'mmap': ArgInspectionEntry(2, set([])), # int prot 'mmap2': ArgInspectionEntry(2, set([])), # int prot 'mprotect': ArgInspectionEntry(2, set([])), # int prot } for trace_filename in opts.traces: parse_trace_file(trace_filename, syscalls, arg_inspection) # Add the basic set if they are not yet present. basic_set = [ 'restart_syscall', 'exit', 'exit_group', 'rt_sigreturn', ] for basic_syscall in basic_set: if basic_syscall not in syscalls: syscalls[basic_syscall] = 1 # Sort the syscalls based on frequency. This way the calls that are used # more often come first which in turn speeds up the filter slightly. sorted_syscalls = list( x[0] for x in sorted(syscalls.items(), key=lambda pair: pair[1], reverse=True) ) print(NOTICE) for syscall in sorted_syscalls: if syscall in arg_inspection: arg_filter = get_seccomp_bpf_filter(syscall, arg_inspection[syscall]) else: arg_filter = ALLOW print('%s: %s' % (syscall, arg_filter)) if __name__ == '__main__': sys.exit(main(sys.argv[1:]))