#!/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 <unfinished ...>, 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:]))
|