#!/usr/bin/env python3 # Copyright (c) 2011 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """Makes sure that all EXE and DLL files in the provided directory were built correctly. In essense it runs a subset of BinScope tests ensuring that binaries have /NXCOMPAT, /DYNAMICBASE and /SAFESEH. """ import json import os import optparse import sys REPO_ROOT = os.path.join(os.path.dirname(__file__), '..', '..') FILES_CFG = os.path.join(REPO_ROOT, 'chrome', 'tools', 'build', 'win', 'FILES.cfg') PEFILE_DIR = os.path.join(REPO_ROOT, 'third_party', 'pefile_py3') sys.path.append(PEFILE_DIR) import pefile PE_FILE_EXTENSIONS = ['.exe', '.dll'] # https://docs.microsoft.com/en-us/windows/win32/debug/pe-format DYNAMICBASE_FLAG = 0x0040 NXCOMPAT_FLAG = 0x0100 NO_SEH_FLAG = 0x0400 GUARD_CF_FLAG = 0x4000 MACHINE_TYPE_AMD64 = 0x8664 CETCOMPAT_BIT = 0 # offset in extended dll characteristics # Please do not add your file here without confirming that it indeed doesn't # require /NXCOMPAT and /DYNAMICBASE. Contact //sandbox/win/OWNERS or your # local Windows guru for advice. EXCLUDED_FILES = [ 'crashpad_util_test_process_info_test_child.exe', 'mini_installer.exe', 'previous_version_mini_installer.exe', ] # PE files that are otherwise included but for which /cetcompat is not required. CETCOMPAT_NOT_REQUIRED = [ 'chrome_proxy.exe', 'chrome_pwa_launcher.exe', 'elevation_service.exe', 'nacl64.exe', 'notification_helper.exe', ] def IsPEFile(path): return (os.path.isfile(path) and os.path.splitext(path)[1].lower() in PE_FILE_EXTENSIONS and os.path.basename(path) not in EXCLUDED_FILES) def IsBitSet(data, bit_idx): return 0 != data[int(bit_idx / 8)] & (1 << (bit_idx % 8)) def IsCetExpected(path): return os.path.basename(path) not in CETCOMPAT_NOT_REQUIRED def main(options, args): directory = args[0] pe_total = 0 pe_passed = 0 failures = [] # Load FILES.cfg - it is a python file setting a FILES variable. exec_globals = {'__builtins__': None} with open(FILES_CFG, encoding="utf-8") as f: code = compile(f.read(), FILES_CFG, 'exec') exec(code, exec_globals) files_cfg = exec_globals['FILES'] # Determines whether a specified file is in the 'default' # filegroup - which means it's shipped with Chrome. def IsInDefaultFileGroup(path): for fileobj in files_cfg: if fileobj['filename'] == os.path.basename(path): if 'default' in fileobj.get('filegroup', {}): return True return False for file in os.listdir(directory): path = os.path.abspath(os.path.join(directory, file)) if not IsPEFile(path): continue pe = pefile.PE(path, fast_load=True) pe.parse_data_directories(directories=[ pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG'], pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_DEBUG'] ]) pe_total = pe_total + 1 success = True # Check for /DYNAMICBASE. if pe.OPTIONAL_HEADER.DllCharacteristics & DYNAMICBASE_FLAG: if options.verbose: print("Checking %s for /DYNAMICBASE... PASS" % path) else: success = False print("Checking %s for /DYNAMICBASE... FAIL" % path) # Check for /NXCOMPAT. if pe.OPTIONAL_HEADER.DllCharacteristics & NXCOMPAT_FLAG: if options.verbose: print("Checking %s for /NXCOMPAT... PASS" % path) else: success = False print("Checking %s for /NXCOMPAT... FAIL" % path) # Check for /SAFESEH. Binaries should meet one of the following # criteria: # 1) Have no SEH table as indicated by the DLL characteristics # 2) Have a LOAD_CONFIG section containing a valid SEH table # 3) Be a 64-bit binary, in which case /SAFESEH isn't required # # Refer to the following MSDN article for more information: # http://msdn.microsoft.com/en-us/library/9a89h429.aspx if (pe.OPTIONAL_HEADER.DllCharacteristics & NO_SEH_FLAG or (hasattr(pe, "DIRECTORY_ENTRY_LOAD_CONFIG") and pe.DIRECTORY_ENTRY_LOAD_CONFIG.struct.SEHandlerCount > 0 and pe.DIRECTORY_ENTRY_LOAD_CONFIG.struct.SEHandlerTable != 0) or pe.FILE_HEADER.Machine == MACHINE_TYPE_AMD64): if options.verbose: print("Checking %s for /SAFESEH... PASS" % path) else: success = False print("Checking %s for /SAFESEH... FAIL" % path) # ASLR is weakened on Windows 64-bit when the ImageBase is below 4GB # (because the loader will never be rebase the image above 4GB). if pe.FILE_HEADER.Machine == MACHINE_TYPE_AMD64: if pe.OPTIONAL_HEADER.ImageBase <= 0xFFFFFFFF: print("Checking %s ImageBase (0x%X < 4GB)... FAIL" % (path, pe.OPTIONAL_HEADER.ImageBase)) success = False elif options.verbose: print("Checking %s ImageBase (0x%X > 4GB)... PASS" % (path, pe.OPTIONAL_HEADER.ImageBase)) # Can only guarantee that files that are built by Chromium # are protected by /GUARD:CF. Some system libraries are not. if IsInDefaultFileGroup(path): # Check for /GUARD:CF. if pe.OPTIONAL_HEADER.DllCharacteristics & GUARD_CF_FLAG: if options.verbose: print("Checking %s for /GUARD:CF... PASS" % path) else: success = False print("Checking %s for /GUARD:CF... FAIL" % path) else: if options.verbose: print("Skipping check for /GUARD:CF for %s." % path) # Check cetcompat for x64 - debug directory type # IMAGE_DEBUG_TYPE_EX_DLLCHARACTERISTICS. if pe.FILE_HEADER.Machine == MACHINE_TYPE_AMD64: if IsInDefaultFileGroup(path) and IsCetExpected(path): found_cetcompat = False for dbg_ent in pe.DIRECTORY_ENTRY_DEBUG: if dbg_ent.struct.Type == pefile.DEBUG_TYPE[ 'IMAGE_DEBUG_TYPE_EX_DLLCHARACTERISTICS']: # pefile does not read this, so access the raw data. ex_dll_offset = dbg_ent.struct.PointerToRawData ex_dll_length = dbg_ent.struct.SizeOfData ex_dll_char_data = pe.__data__[ex_dll_offset:ex_dll_offset + ex_dll_length] if IsBitSet(ex_dll_char_data, CETCOMPAT_BIT): found_cetcompat = True break # only one ex_dllcharacteristics so can stop once seen. if found_cetcompat: if options.verbose: print("Checking %s for /CETCOMPAT... PASS" % path) else: success = False print("Checking %s for /CETCOMPAT... FAIL" % path) else: if options.verbose: print("Skipping check for /CETCOMPAT for %s." % path) # Update tally. if success: pe_passed = pe_passed + 1 else: failures.append(path) print("Result: %d files found, %d files passed" % (pe_total, pe_passed)) if options.json: with open(options.json, 'w') as f: json.dump(failures, f) if pe_passed != pe_total: sys.exit(1) if __name__ == '__main__': usage = "Usage: %prog [options] DIRECTORY" option_parser = optparse.OptionParser(usage=usage) option_parser.add_option("-v", "--verbose", action="store_true", default=False, help="Print debug logging") option_parser.add_option("--json", help="Path to JSON output file") options, args = option_parser.parse_args() if not args: option_parser.print_help() sys.exit(0) main(options, args)