1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
|
#!/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)
|