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