1#!/usr/bin/env python
2# Copyright (c) 2011 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Makes sure that all EXE and DLL files in the provided directory were built
7correctly.
8
9In essense it runs a subset of BinScope tests ensuring that binaries have
10/NXCOMPAT, /DYNAMICBASE and /SAFESEH.
11"""
12
13from __future__ import print_function
14
15import json
16import os
17import optparse
18import sys
19
20# Find /third_party/pefile based on current directory and script path.
21sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..',
22                             'third_party', 'pefile'))
23import pefile
24
25PE_FILE_EXTENSIONS = ['.exe', '.dll']
26DYNAMICBASE_FLAG = 0x0040
27NXCOMPAT_FLAG = 0x0100
28NO_SEH_FLAG = 0x0400
29MACHINE_TYPE_AMD64 = 0x8664
30
31# Please do not add your file here without confirming that it indeed doesn't
32# require /NXCOMPAT and /DYNAMICBASE.  Contact cpu@chromium.org or your local
33# Windows guru for advice.
34EXCLUDED_FILES = [
35                  'crashpad_util_test_process_info_test_child.exe',
36                  'mini_installer.exe',
37                  'previous_version_mini_installer.exe',
38                  ]
39
40def IsPEFile(path):
41  return (os.path.isfile(path) and
42          os.path.splitext(path)[1].lower() in PE_FILE_EXTENSIONS and
43          os.path.basename(path) not in EXCLUDED_FILES)
44
45def main(options, args):
46  directory = args[0]
47  pe_total = 0
48  pe_passed = 0
49
50  failures = []
51
52  for file in os.listdir(directory):
53    path = os.path.abspath(os.path.join(directory, file))
54    if not IsPEFile(path):
55      continue
56    pe = pefile.PE(path, fast_load=True)
57    pe.parse_data_directories(directories=[
58        pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG']])
59    pe_total = pe_total + 1
60    success = True
61
62    # Check for /DYNAMICBASE.
63    if pe.OPTIONAL_HEADER.DllCharacteristics & DYNAMICBASE_FLAG:
64      if options.verbose:
65        print("Checking %s for /DYNAMICBASE... PASS" % path)
66    else:
67      success = False
68      print("Checking %s for /DYNAMICBASE... FAIL" % path)
69
70    # Check for /NXCOMPAT.
71    if pe.OPTIONAL_HEADER.DllCharacteristics & NXCOMPAT_FLAG:
72      if options.verbose:
73        print("Checking %s for /NXCOMPAT... PASS" % path)
74    else:
75      success = False
76      print("Checking %s for /NXCOMPAT... FAIL" % path)
77
78    # Check for /SAFESEH. Binaries should meet one of the following
79    # criteria:
80    #   1) Have no SEH table as indicated by the DLL characteristics
81    #   2) Have a LOAD_CONFIG section containing a valid SEH table
82    #   3) Be a 64-bit binary, in which case /SAFESEH isn't required
83    #
84    # Refer to the following MSDN article for more information:
85    # http://msdn.microsoft.com/en-us/library/9a89h429.aspx
86    if (pe.OPTIONAL_HEADER.DllCharacteristics & NO_SEH_FLAG or
87        (hasattr(pe, "DIRECTORY_ENTRY_LOAD_CONFIG") and
88         pe.DIRECTORY_ENTRY_LOAD_CONFIG.struct.SEHandlerCount > 0 and
89         pe.DIRECTORY_ENTRY_LOAD_CONFIG.struct.SEHandlerTable != 0) or
90        pe.FILE_HEADER.Machine == MACHINE_TYPE_AMD64):
91      if options.verbose:
92        print("Checking %s for /SAFESEH... PASS" % path)
93    else:
94      success = False
95      print("Checking %s for /SAFESEH... FAIL" % path)
96
97    # ASLR is weakened on Windows 64-bit when the ImageBase is below 4GB
98    # (because the loader will never be rebase the image above 4GB).
99    if pe.FILE_HEADER.Machine == MACHINE_TYPE_AMD64:
100      if pe.OPTIONAL_HEADER.ImageBase <= 0xFFFFFFFF:
101        print("Checking %s ImageBase (0x%X < 4GB)... FAIL" %
102              (path, pe.OPTIONAL_HEADER.ImageBase))
103        success = False
104      elif options.verbose:
105        print("Checking %s ImageBase (0x%X > 4GB)... PASS" %
106              (path, pe.OPTIONAL_HEADER.ImageBase))
107
108    # Update tally.
109    if success:
110      pe_passed = pe_passed + 1
111    else:
112      failures.append(path)
113
114  print("Result: %d files found, %d files passed" % (pe_total, pe_passed))
115
116  if options.json:
117    with open(options.json, 'w') as f:
118      json.dump(failures, f)
119
120  if pe_passed != pe_total:
121    sys.exit(1)
122
123if __name__ == '__main__':
124  usage = "Usage: %prog [options] DIRECTORY"
125  option_parser = optparse.OptionParser(usage=usage)
126  option_parser.add_option("-v", "--verbose", action="store_true",
127                           default=False, help="Print debug logging")
128  option_parser.add_option("--json", help="Path to JSON output file")
129  options, args = option_parser.parse_args()
130  if not args:
131    option_parser.print_help()
132    sys.exit(0)
133  main(options, args)
134