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