Home | History | Annotate | Download | only in security_ASLR
      1 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """A test verifying Address Space Layout Randomization
      6 
      7 Uses system calls to get important pids and then gets information about
      8 the pids in /proc/<pid>/maps. Restarts the tested processes and reads
      9 information about them again. If ASLR is enabled, memory mappings should
     10 change.
     11 """
     12 
     13 from autotest_lib.client.bin import test
     14 from autotest_lib.client.bin import utils
     15 from autotest_lib.client.common_lib import error
     16 from autotest_lib.client.cros import upstart
     17 
     18 import logging
     19 import time
     20 import pprint
     21 import re
     22 
     23 def _pidof(exe_name):
     24     """Returns the PID of the first process with the given name."""
     25     pid = utils.system_output('pidof %s' % exe_name, ignore_status=True).strip()
     26     if len(pid.split()) > 1:
     27         pid = pid.split()[0]
     28 
     29     return pid
     30 
     31 
     32 class Process(object):
     33     """Holds information about a process.
     34 
     35     Stores basic information about a process. This class is a base for
     36     UpstartProcess and SystemdProcess declared below.
     37 
     38     Attributes:
     39         _name: String name of process.
     40         _service_name: Name of the service corresponding to the process.
     41         _parent: String name of process's parent. Defaults to None.
     42     """
     43 
     44     _START_POLL_INTERVAL_SECONDS = 1
     45     _START_TIMEOUT = 30
     46 
     47     def __init__(self, name, service_name, parent=None):
     48         self._name = name
     49         self._service_name = service_name
     50         self._parent = parent
     51 
     52     def get_name(self):
     53         return self._name
     54 
     55     def get_pid(self):
     56         """Gets pid of process, waiting for it if not found.
     57 
     58         Raises:
     59             error.TestFail: corresponding process is not found.
     60         """
     61         retries = 0
     62         ps_results = ""
     63         while retries < self._START_TIMEOUT:
     64             if self._parent is None:
     65                 ps_results = _pidof(self._name)
     66             else:
     67                 ppid = _pidof(self._parent)
     68                 get_pid_command = ('ps -C %s -o pid,ppid | grep " %s$"'
     69                     ' | awk \'{print $1}\'') % (self._name, ppid)
     70                 ps_results = utils.system_output(get_pid_command).strip()
     71 
     72             if ps_results != "":
     73                 return ps_results
     74 
     75             # The process could not be found. We then sleep, hoping the
     76             # process is just slow to initially start.
     77             time.sleep(self._START_POLL_INTERVAL_SECONDS)
     78             retries += 1
     79 
     80         # We never saw the process, so abort with details on who was missing.
     81         raise error.TestFail('Never saw a pid for "%s"' % (self._name))
     82 
     83 
     84 class UpstartProcess(Process):
     85     """Represents an Upstart service."""
     86 
     87     def exists(self):
     88         """Checks if the service is present in Upstart configuration."""
     89         return upstart.has_service(self._service_name)
     90 
     91     def restart(self):
     92         """Restarts the process via initctl."""
     93         utils.system('initctl restart %s' % self._service_name)
     94 
     95 class SystemdProcess(Process):
     96     """Represents an systemd service."""
     97 
     98     def exists(self):
     99         """Checks if the service is present in systemd configuration."""
    100         cmd = 'systemctl show -p LoadState %s.service' % self._service_name
    101         output = utils.system_output(cmd, ignore_status=True).strip()
    102         return output == 'LoadState=loaded'
    103 
    104     def restart(self):
    105         """Restarts the process via systemctl."""
    106         utils.system('systemctl restart %s' % self._service_name)
    107 
    108 
    109 class Mapping(object):
    110     """Holds information about a process's address mapping.
    111 
    112     Stores information about one memory mapping for a process.
    113 
    114     Attributes:
    115         _name: String name of process/memory occupying the location.
    116         _start: String containing memory address range start.
    117     """
    118     def __init__(self, name, start):
    119         self._start = start
    120         self._name = name
    121 
    122     def set_start(self, new_value):
    123         self._start = new_value
    124 
    125     def get_start(self):
    126         return self._start
    127 
    128     def __repr__(self):
    129         return "<mapping %s %s>" % (self._name, self._start)
    130 
    131 
    132 class security_ASLR(test.test):
    133     """Runs ASLR tests
    134 
    135     See top document comments for more information.
    136 
    137     Attributes:
    138         version: Current version of the test.
    139     """
    140     version = 1
    141 
    142     _TEST_ITERATION_COUNT = 5
    143 
    144     _ASAN_SYMBOL = "__asan_init"
    145 
    146     # 'update_engine' should at least be present on all boards.
    147     _PROCESS_LIST = [UpstartProcess('chrome', 'ui', parent='session_manager'),
    148                      UpstartProcess('debugd', 'debugd'),
    149                      UpstartProcess('update_engine', 'update-engine'),
    150                      SystemdProcess('update_engine', 'update-engine')]
    151 
    152 
    153     def get_processes_to_test(self):
    154         """Gets processes to test for main function.
    155 
    156         Called by run_once to get processes for this program to test.
    157         Filters binaries that actually exist on the system.
    158         This has to be a method because it constructs process objects.
    159 
    160         Returns:
    161             A list of process objects to be tested (see below for
    162             definition of process class).
    163         """
    164         return [p for p in self._PROCESS_LIST if p.exists()]
    165 
    166 
    167     def running_on_asan(self):
    168         """Returns whether we're running on ASan."""
    169         # -q, --quiet         * Only output 'bad' things
    170         # -F, --format <arg>  * Use specified format for output
    171         # -g, --gmatch        * Use regex rather than string compare (with -s)
    172         # -s, --symbol <arg>  * Find a specified symbol
    173         scanelf_command = "scanelf -qF'%s#F'"
    174         scanelf_command += " -gs %s `which debugd`" % self._ASAN_SYMBOL
    175         symbol = utils.system_output(scanelf_command)
    176         logging.debug("running_on_asan(): symbol: '%s', _ASAN_SYMBOL: '%s'",
    177                       symbol, self._ASAN_SYMBOL)
    178         return symbol != ""
    179 
    180 
    181     def test_randomization(self, process):
    182         """Tests ASLR of a single process.
    183 
    184         This is the main test function for the program. It creates data
    185         structures out of useful information from sampling /proc/<pid>/maps
    186         after restarting the process and then compares address starting
    187         locations of all executable, stack, and heap memory from each iteration.
    188 
    189         @param process: a process object representing the process to be tested.
    190 
    191         Returns:
    192             A dict containing a Boolean for whether or not the test passed
    193             and a list of string messages about passing/failing cases.
    194         """
    195         test_result = dict([('pass', True), ('results', []), ('cases', dict())])
    196         name = process.get_name()
    197         mappings = list()
    198         pid = -1
    199         for i in range(self._TEST_ITERATION_COUNT):
    200             new_pid = process.get_pid()
    201             if pid == new_pid:
    202                 raise error.TestFail(
    203                     'Service "%s" retained PID %d after restart.' % (name, pid))
    204             pid = new_pid
    205             mappings.append(self.map(pid))
    206             process.restart()
    207         logging.debug('Complete mappings dump for process %s:\n%s',
    208                       name, pprint.pformat(mappings, 4))
    209 
    210         initial_map = mappings[0]
    211         for i, mapping in enumerate(mappings[1:]):
    212             logging.debug('Iteration %d', i)
    213             for key in mapping.iterkeys():
    214                 # Set default case result to fail, pass when an address change
    215                 # occurs.
    216                 if not test_result['cases'].has_key(key):
    217                     test_result['cases'][key] = dict([('pass', False),
    218                             ('number', 0),
    219                             ('total', self._TEST_ITERATION_COUNT)])
    220                 was_same = (initial_map.has_key(key) and
    221                         initial_map[key].get_start() ==
    222                         mapping[key].get_start())
    223                 if was_same:
    224                     logging.debug("Bad: %s address didn't change", key)
    225                 else:
    226                     logging.debug('Good: %s address changed', key)
    227                     test_result['cases'][key]['number'] += 1
    228                     test_result['cases'][key]['pass'] = True
    229         for case, result in test_result['cases'].iteritems():
    230             if result['pass']:
    231                 test_result['results'].append( '[PASS] Address for %s '
    232                         'successfully changed' % case)
    233             else:
    234                 test_result['results'].append('[FAIL] Address for %s had '
    235                         'deterministic value: %s' % (case,
    236                         mappings[0][case].get_start()))
    237             test_result['pass'] = test_result['pass'] and result['pass']
    238         return test_result
    239 
    240 
    241     def map(self, pid):
    242         """Creates data structure from table in /proc/<pid>/maps.
    243 
    244         Gets all data from /proc/<pid>/maps, parses each entry, and saves
    245         entries corresponding to executable, stack, or heap memory into
    246         a dictionary.
    247 
    248         @param pid: a string containing the pid to be tested.
    249 
    250         Returns:
    251             A dict mapping names to mapping objects (see above for mapping
    252             definition).
    253         """
    254         memory_map = dict()
    255         maps_file = open("/proc/%s/maps" % pid)
    256         for maps_line in maps_file:
    257             result = self.parse_result(maps_line)
    258             if result is None:
    259                 continue
    260             name = result['name']
    261             start = result['start']
    262             perms = result['perms']
    263             is_memory = name == '[heap]' or name == '[stack]'
    264             is_useful = re.search('x', perms) is not None or is_memory
    265             if not is_useful:
    266                 continue
    267             if not name in memory_map:
    268                 memory_map[name] = Mapping(name, start)
    269             elif memory_map[name].get_start() < start:
    270                 memory_map[name].set_start(start)
    271         return memory_map
    272 
    273 
    274     def parse_result(self, result):
    275         """Builds dictionary from columns of a line of /proc/<pid>/maps
    276 
    277         Uses regular expressions to determine column separations. Puts
    278         column data into a dict mapping column names to their string values.
    279 
    280         @param result: one line of /proc/<pid>/maps as a string, for any <pid>.
    281 
    282         Returns:
    283             None if the regular expression wasn't matched. Otherwise:
    284             A dict of string column names mapped to their string values.
    285             For example:
    286 
    287         {'start': '9e981700000', 'end': '9e981800000', 'perms': 'rwxp',
    288             'something': '00000000', 'major': '00', 'minor': '00', 'inode':
    289             '00'}
    290         """
    291         # Build regex to parse one line of proc maps table.
    292         memory = r'(?P<start>\w+)-(?P<end>\w+)'
    293         perms = r'(?P<perms>(r|-)(w|-)(x|-)(s|p))'
    294         something = r'(?P<something>\w+)'
    295         devices = r'(?P<major>\w+):(?P<minor>\w+)'
    296         inode = r'(?P<inode>[0-9]+)'
    297         name = r'(?P<name>([a-zA-Z0-9/]+|\[heap\]|\[stack\]))'
    298         regex = r'%s +%s +%s +%s +%s +%s' % (memory, perms, something,
    299             devices, inode, name)
    300         found_match = re.match(regex, result)
    301         if found_match is None:
    302             return None
    303         parsed_result = found_match.groupdict()
    304         return parsed_result
    305 
    306 
    307     def run_once(self):
    308         """Main function.
    309 
    310         Called when test is run. Gets processes to test and calls test on
    311         them.
    312 
    313         Raises:
    314             error.TestFail if any processes' memory mapping addresses are the
    315             same after restarting.
    316         """
    317 
    318         if self.running_on_asan():
    319             logging.warning("security_ASLR is not available on ASan.")
    320             return
    321 
    322         processes = self.get_processes_to_test()
    323         # If we don't find any of the processes we wanted to test, we fail.
    324         if len(processes) == 0:
    325             proc_names = ", ".join([p.get_name() for p in self._PROCESS_LIST])
    326             raise error.TestFail(
    327                 'Could not find any of "%s" processes to test' % proc_names)
    328 
    329         aslr_enabled = True
    330         full_results = dict()
    331         for process in processes:
    332             test_results = self.test_randomization(process)
    333             full_results[process.get_name()] = test_results['results']
    334             if not test_results['pass']:
    335                 aslr_enabled = False
    336 
    337         logging.debug('SUMMARY:')
    338         for process_name, results in full_results.iteritems():
    339             logging.debug('Results for %s:', process_name)
    340             for result in results:
    341                 logging.debug(result)
    342 
    343         if not aslr_enabled:
    344             raise error.TestFail('One or more processes had deterministic '
    345                     'memory mappings')
    346