• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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('traces', nargs='+', help='The strace logs.')
66    return parser.parse_args(argv)
67
68
69def get_seccomp_bpf_filter(syscall, entry):
70    """Return a minijail seccomp-bpf filter expression for the syscall."""
71    arg_index = entry.arg_index
72    arg_values = entry.value_set
73    atoms = []
74    if syscall in ('mmap', 'mmap2', 'mprotect') and arg_index == 2:
75        # See if there is at least one instance of any of these syscalls trying
76        # to map memory with both PROT_EXEC and PROT_WRITE. If there isn't, we
77        # can craft a concise expression to forbid this.
78        write_and_exec = set(('PROT_EXEC', 'PROT_WRITE'))
79        for arg_value in arg_values:
80            if write_and_exec.issubset(set(p.strip() for p in
81                                           arg_value.split('|'))):
82                break
83        else:
84            atoms.extend(['arg2 in ~PROT_EXEC', 'arg2 in ~PROT_WRITE'])
85            arg_values = set()
86    atoms.extend('arg%d == %s' % (arg_index, arg_value)
87                 for arg_value in arg_values)
88    return ' || '.join(atoms)
89
90
91def parse_trace_file(trace_filename, syscalls, arg_inspection):
92    """Parses one file produced by strace."""
93    uses_socketcall = ('i386' in trace_filename or
94                       ('x86' in trace_filename and
95                        '64' not in trace_filename))
96
97    with open(trace_filename) as trace_file:
98        for line in trace_file:
99            matches = LINE_RE.match(line)
100            if not matches:
101                continue
102
103            syscall, args = matches.groups()
104            if uses_socketcall and syscall in SOCKETCALLS:
105                syscall = 'socketcall'
106
107            syscalls[syscall] += 1
108
109            args = [arg.strip() for arg in args.split(',')]
110
111            if syscall in arg_inspection:
112                arg_value = args[arg_inspection[syscall].arg_index]
113                arg_inspection[syscall].value_set.add(arg_value)
114
115
116def main(argv):
117    """Main entrypoint."""
118    opts = parse_args(argv)
119
120    syscalls = collections.defaultdict(int)
121
122    arg_inspection = {
123        'socket': ArgInspectionEntry(0, set([])),   # int domain
124        'ioctl': ArgInspectionEntry(1, set([])),    # int request
125        'prctl': ArgInspectionEntry(0, set([])),    # int option
126        'mmap': ArgInspectionEntry(2, set([])),     # int prot
127        'mmap2': ArgInspectionEntry(2, set([])),    # int prot
128        'mprotect': ArgInspectionEntry(2, set([])), # int prot
129    }
130
131    for trace_filename in opts.traces:
132        parse_trace_file(trace_filename, syscalls, arg_inspection)
133
134    # Add the basic set if they are not yet present.
135    basic_set = [
136        'restart_syscall', 'exit', 'exit_group', 'rt_sigreturn',
137    ]
138    for basic_syscall in basic_set:
139        if basic_syscall not in syscalls:
140            syscalls[basic_syscall] = 1
141
142    # Sort the syscalls based on frequency.  This way the calls that are used
143    # more often come first which in turn speeds up the filter slightly.
144    sorted_syscalls = list(
145        x[0] for x in sorted(syscalls.items(), key=lambda pair: pair[1],
146                             reverse=True)
147    )
148
149    print(NOTICE)
150
151    for syscall in sorted_syscalls:
152        if syscall in arg_inspection:
153            arg_filter = get_seccomp_bpf_filter(syscall, arg_inspection[syscall])
154        else:
155            arg_filter = ALLOW
156        print('%s: %s' % (syscall, arg_filter))
157
158
159if __name__ == '__main__':
160    sys.exit(main(sys.argv[1:]))
161