• 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('--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