Skip to content

Commit a092970

Browse files
committed
Merge pull request #6854
579b863 devtools: Add security-check.py (Wladimir J. van der Laan)
2 parents 0fbfc51 + 579b863 commit a092970

File tree

3 files changed

+268
-3
lines changed

3 files changed

+268
-3
lines changed

contrib/devtools/security-check.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#!/usr/bin/python2
2+
'''
3+
Perform basic ELF security checks on a series of executables.
4+
Exit status will be 0 if succesful, and the program will be silent.
5+
Otherwise the exit status will be 1 and it will log which executables failed which checks.
6+
Needs `readelf` (for ELF) and `objdump` (for PE).
7+
'''
8+
from __future__ import division,print_function
9+
import subprocess
10+
import sys
11+
import os
12+
13+
READELF_CMD = os.getenv('READELF', '/usr/bin/readelf')
14+
OBJDUMP_CMD = os.getenv('OBJDUMP', '/usr/bin/objdump')
15+
16+
def check_ELF_PIE(executable):
17+
'''
18+
Check for position independent executable (PIE), allowing for address space randomization.
19+
'''
20+
p = subprocess.Popen([READELF_CMD, '-h', '-W', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
21+
(stdout, stderr) = p.communicate()
22+
if p.returncode:
23+
raise IOError('Error opening file')
24+
25+
ok = False
26+
for line in stdout.split('\n'):
27+
line = line.split()
28+
if len(line)>=2 and line[0] == 'Type:' and line[1] == 'DYN':
29+
ok = True
30+
return ok
31+
32+
def get_ELF_program_headers(executable):
33+
'''Return type and flags for ELF program headers'''
34+
p = subprocess.Popen([READELF_CMD, '-l', '-W', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
35+
(stdout, stderr) = p.communicate()
36+
if p.returncode:
37+
raise IOError('Error opening file')
38+
in_headers = False
39+
count = 0
40+
headers = []
41+
for line in stdout.split('\n'):
42+
if line.startswith('Program Headers:'):
43+
in_headers = True
44+
if line == '':
45+
in_headers = False
46+
if in_headers:
47+
if count == 1: # header line
48+
ofs_typ = line.find('Type')
49+
ofs_offset = line.find('Offset')
50+
ofs_flags = line.find('Flg')
51+
ofs_align = line.find('Align')
52+
if ofs_typ == -1 or ofs_offset == -1 or ofs_flags == -1 or ofs_align == -1:
53+
raise ValueError('Cannot parse elfread -lW output')
54+
elif count > 1:
55+
typ = line[ofs_typ:ofs_offset].rstrip()
56+
flags = line[ofs_flags:ofs_align].rstrip()
57+
headers.append((typ, flags))
58+
count += 1
59+
return headers
60+
61+
def check_ELF_NX(executable):
62+
'''
63+
Check that no sections are writable and executable (including the stack)
64+
'''
65+
have_wx = False
66+
have_gnu_stack = False
67+
for (typ, flags) in get_ELF_program_headers(executable):
68+
if typ == 'GNU_STACK':
69+
have_gnu_stack = True
70+
if 'W' in flags and 'E' in flags: # section is both writable and executable
71+
have_wx = True
72+
return have_gnu_stack and not have_wx
73+
74+
def check_ELF_RELRO(executable):
75+
'''
76+
Check for read-only relocations.
77+
GNU_RELRO program header must exist
78+
Dynamic section must have BIND_NOW flag
79+
'''
80+
have_gnu_relro = False
81+
for (typ, flags) in get_ELF_program_headers(executable):
82+
# Note: not checking flags == 'R': here as linkers set the permission differently
83+
# This does not affect security: the permission flags of the GNU_RELRO program header are ignored, the PT_LOAD header determines the effective permissions.
84+
# However, the dynamic linker need to write to this area so these are RW.
85+
# Glibc itself takes care of mprotecting this area R after relocations are finished.
86+
# See also http://permalink.gmane.org/gmane.comp.gnu.binutils/71347
87+
if typ == 'GNU_RELRO':
88+
have_gnu_relro = True
89+
90+
have_bindnow = False
91+
p = subprocess.Popen([READELF_CMD, '-d', '-W', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
92+
(stdout, stderr) = p.communicate()
93+
if p.returncode:
94+
raise IOError('Error opening file')
95+
for line in stdout.split('\n'):
96+
tokens = line.split()
97+
if len(tokens)>1 and tokens[1] == '(BIND_NOW)':
98+
have_bindnow = True
99+
return have_gnu_relro and have_bindnow
100+
101+
def check_ELF_Canary(executable):
102+
'''
103+
Check for use of stack canary
104+
'''
105+
p = subprocess.Popen([READELF_CMD, '--dyn-syms', '-W', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
106+
(stdout, stderr) = p.communicate()
107+
if p.returncode:
108+
raise IOError('Error opening file')
109+
ok = False
110+
for line in stdout.split('\n'):
111+
if '__stack_chk_fail' in line:
112+
ok = True
113+
return ok
114+
115+
def get_PE_dll_characteristics(executable):
116+
'''
117+
Get PE DllCharacteristics bits
118+
'''
119+
p = subprocess.Popen([OBJDUMP_CMD, '-x', executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
120+
(stdout, stderr) = p.communicate()
121+
if p.returncode:
122+
raise IOError('Error opening file')
123+
for line in stdout.split('\n'):
124+
tokens = line.split()
125+
if len(tokens)>=2 and tokens[0] == 'DllCharacteristics':
126+
return int(tokens[1],16)
127+
return 0
128+
129+
130+
def check_PE_PIE(executable):
131+
'''PIE: DllCharacteristics bit 0x40 signifies dynamicbase (ASLR)'''
132+
return bool(get_PE_dll_characteristics(executable) & 0x40)
133+
134+
def check_PE_NX(executable):
135+
'''NX: DllCharacteristics bit 0x100 signifies nxcompat (DEP)'''
136+
return bool(get_PE_dll_characteristics(executable) & 0x100)
137+
138+
CHECKS = {
139+
'ELF': [
140+
('PIE', check_ELF_PIE),
141+
('NX', check_ELF_NX),
142+
('RELRO', check_ELF_RELRO),
143+
('Canary', check_ELF_Canary)
144+
],
145+
'PE': [
146+
('PIE', check_PE_PIE),
147+
('NX', check_PE_NX)
148+
]
149+
}
150+
151+
def identify_executable(executable):
152+
with open(filename, 'rb') as f:
153+
magic = f.read(4)
154+
if magic.startswith(b'MZ'):
155+
return 'PE'
156+
elif magic.startswith(b'\x7fELF'):
157+
return 'ELF'
158+
return None
159+
160+
if __name__ == '__main__':
161+
retval = 0
162+
for filename in sys.argv[1:]:
163+
try:
164+
etype = identify_executable(filename)
165+
if etype is None:
166+
print('%s: unknown format' % filename)
167+
retval = 1
168+
continue
169+
170+
failed = []
171+
for (name, func) in CHECKS[etype]:
172+
if not func(filename):
173+
failed.append(name)
174+
if failed:
175+
print('%s: failed %s' % (filename, ' '.join(failed)))
176+
retval = 1
177+
except IOError:
178+
print('%s: cannot open' % filename)
179+
retval = 1
180+
exit(retval)
181+

contrib/devtools/symbol-check.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#!/usr/bin/python
1+
#!/usr/bin/python2
22
# Copyright (c) 2014 Wladimir J. van der Laan
33
# Distributed under the MIT software license, see the accompanying
44
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
@@ -15,6 +15,7 @@
1515
import subprocess
1616
import re
1717
import sys
18+
import os
1819

1920
# Debian 6.0.9 (Squeeze) has:
2021
#
@@ -45,8 +46,10 @@
4546
IGNORE_EXPORTS = {
4647
'_edata', '_end', '_init', '__bss_start', '_fini'
4748
}
48-
READELF_CMD = '/usr/bin/readelf'
49-
CPPFILT_CMD = '/usr/bin/c++filt'
49+
READELF_CMD = os.getenv('READELF', '/usr/bin/readelf')
50+
CPPFILT_CMD = os.getenv('CPPFILT', '/usr/bin/c++filt')
51+
# Allowed NEEDED libraries
52+
ALLOWED_LIBRARIES = {'librt.so.1','libpthread.so.0','libanl.so.1','libm.so.6','libgcc_s.so.1','libc.so.6','ld-linux-x86-64.so.2'}
5053

5154
class CPPFilt(object):
5255
'''
@@ -98,6 +101,22 @@ def check_version(max_versions, version):
98101
return False
99102
return ver <= max_versions[lib]
100103

104+
def read_libraries(filename):
105+
p = subprocess.Popen([READELF_CMD, '-d', '-W', filename], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
106+
(stdout, stderr) = p.communicate()
107+
if p.returncode:
108+
raise IOError('Error opening file')
109+
libraries = []
110+
for line in stdout.split('\n'):
111+
tokens = line.split()
112+
if len(tokens)>2 and tokens[1] == '(NEEDED)':
113+
match = re.match('^Shared library: \[(.*)\]$', ' '.join(tokens[2:]))
114+
if match:
115+
libraries.append(match.group(1))
116+
else:
117+
raise ValueError('Unparseable (NEEDED) specification')
118+
return libraries
119+
101120
if __name__ == '__main__':
102121
cppfilt = CPPFilt()
103122
retval = 0
@@ -113,6 +132,11 @@ def check_version(max_versions, version):
113132
continue
114133
print('%s: export of symbol %s not allowed' % (filename, cppfilt(sym)))
115134
retval = 1
135+
# Check dependency libraries
136+
for library_name in read_libraries(filename):
137+
if library_name not in ALLOWED_LIBRARIES:
138+
print('%s: NEEDED library %s is not allowed' % (filename, library_name))
139+
retval = 1
116140

117141
exit(retval)
118142

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
#!/usr/bin/python2
2+
'''
3+
Test script for security-check.py
4+
'''
5+
from __future__ import division,print_function
6+
import subprocess
7+
import sys
8+
import unittest
9+
10+
def write_testcode(filename):
11+
with open(filename, 'w') as f:
12+
f.write('''
13+
#include <stdio.h>
14+
int main()
15+
{
16+
printf("the quick brown fox jumps over the lazy god\\n");
17+
return 0;
18+
}
19+
''')
20+
21+
def call_security_check(cc, source, executable, options):
22+
subprocess.check_call([cc,source,'-o',executable] + options)
23+
p = subprocess.Popen(['./security-check.py',executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
24+
(stdout, stderr) = p.communicate()
25+
return (p.returncode, stdout.rstrip())
26+
27+
class TestSecurityChecks(unittest.TestCase):
28+
def test_ELF(self):
29+
source = 'test1.c'
30+
executable = 'test1'
31+
cc = 'gcc'
32+
write_testcode(source)
33+
34+
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-zexecstack','-fno-stack-protector','-Wl,-znorelro']),
35+
(1, executable+': failed PIE NX RELRO Canary'))
36+
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-znoexecstack','-fno-stack-protector','-Wl,-znorelro']),
37+
(1, executable+': failed PIE RELRO Canary'))
38+
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-znoexecstack','-fstack-protector-all','-Wl,-znorelro']),
39+
(1, executable+': failed PIE RELRO'))
40+
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-znoexecstack','-fstack-protector-all','-Wl,-znorelro','-pie','-fPIE']),
41+
(1, executable+': failed RELRO'))
42+
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,-znoexecstack','-fstack-protector-all','-Wl,-zrelro','-Wl,-z,now','-pie','-fPIE']),
43+
(0, ''))
44+
45+
def test_PE(self):
46+
source = 'test1.c'
47+
executable = 'test1.exe'
48+
cc = 'i686-w64-mingw32-gcc'
49+
write_testcode(source)
50+
51+
self.assertEqual(call_security_check(cc, source, executable, []),
52+
(1, executable+': failed PIE NX'))
53+
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,--nxcompat']),
54+
(1, executable+': failed PIE'))
55+
self.assertEqual(call_security_check(cc, source, executable, ['-Wl,--nxcompat','-Wl,--dynamicbase']),
56+
(0, ''))
57+
58+
if __name__ == '__main__':
59+
unittest.main()
60+

0 commit comments

Comments
 (0)