Check security features of an ELF file.

def checksec_app(_parser, _, args):  # pragma: no cover
    """
    Check security features of an ELF file.
    """

    import sys
    import argparse
    import csv
    import os.path

    def checksec(elf, path, fortifiable_funcs):
        relro = 0
        nx = False
        pie = 0
        rpath = False
        runpath = False

        for header in elf.program_headers:
            if header.type == ELF.ProgramHeader.Type.gnu_relro:
                relro = 1
            elif header.type == ELF.ProgramHeader.Type.gnu_stack:
                if not header.flags & ELF.ProgramHeader.Flags.x:
                    nx = True

        if elf.type == ELF.Type.shared:
            pie = 1

        for entry in elf.dynamic_section_entries:
            if entry.type == ELF.DynamicSectionEntry.Type.bind_now and relro == 1:
                relro = 2
            elif entry.type == ELF.DynamicSectionEntry.Type.flags and \
                    entry.value & ELF.DynamicSectionEntry.Flags.bind_now:
                relro = 2
            elif entry.type == ELF.DynamicSectionEntry.Type.flags_1 and \
                    entry.value & ELF.DynamicSectionEntry.Flags_1.now:
                relro = 2
            elif entry.type == ELF.DynamicSectionEntry.Type.debug and pie == 1:
                pie = 2
            elif entry.type == ELF.DynamicSectionEntry.Type.rpath:
                rpath = True
            elif entry.type == ELF.DynamicSectionEntry.Type.runpath:
                runpath = True

        rtl_symbol_names = set(
            symbol.name
            for symbol in elf.symbols
            if symbol.name and symbol.shndx == ELF.Symbol.SpecialSection.undef
        )

        fortified = fortifiable_funcs & rtl_symbol_names
        unfortified = fortifiable_funcs & set('__%s_chk' % symbol_name for symbol_name in rtl_symbol_names)

        canary = '__stack_chk_fail' in rtl_symbol_names

        return {
            'path': path,
            'relro': relro,
            'nx': nx,
            'pie': pie,
            'rpath': rpath,
            'runpath': runpath,
            'canary': canary,
            'fortified': len(fortified),
            'unfortified': len(unfortified),
            'fortifiable': len(fortified | unfortified),
        }

    def check_paths(paths, fortifiable_funcs):
        for path in paths:
            if os.path.isdir(path):
                for data in check_paths(
                        (os.path.join(path, fn) for fn in os.listdir(path) if fn not in ('.', '..')),
                        fortifiable_funcs,
                ):
                    yield data
            else:
                try:
                    elf = ELF(path)
                except:
                    continue

                yield checksec(elf, path, fortifiable_funcs)

    parser = argparse.ArgumentParser(
        prog=_parser.prog,
        description=_parser.description,
    )
    parser.add_argument('path', nargs='+', help='ELF file to check security features of')
    parser.add_argument(
        '-f', '--format',
        dest='format',
        choices=['text', 'csv'],
        default='text',
        help='set output format'
    )
    parser.add_argument(
        '-l', '--libc',
        dest='libc',
        help='path to the applicable libc.so'
    )
    args = parser.parse_args(args)

    if args.libc:
        libc = ELF(args.libc)
        fortifiable_funcs = set([
            symbol.name
            for symbol in libc.symbols
            if symbol.name.startswith('__') and symbol.name.endswith('_chk')
        ])
    else:
        fortifiable_funcs = set('''__wctomb_chk __wcsncat_chk __mbstowcs_chk __strncpy_chk __syslog_chk __mempcpy_chk
                                   __fprintf_chk __recvfrom_chk __readlinkat_chk __wcsncpy_chk __fread_chk
                                   __getlogin_r_chk __vfwprintf_chk __recv_chk __strncat_chk __printf_chk __confstr_chk
                                   __pread_chk __ppoll_chk __ptsname_r_chk __wcscat_chk __snprintf_chk __vwprintf_chk
                                   __memset_chk __memmove_chk __gets_chk __fgetws_unlocked_chk __asprintf_chk __poll_chk
                                   __fdelt_chk __fgets_unlocked_chk __strcat_chk __vsyslog_chk __stpcpy_chk
                                   __vdprintf_chk __strcpy_chk __obstack_printf_chk __getwd_chk __pread64_chk
                                   __wcpcpy_chk __fread_unlocked_chk __dprintf_chk __fgets_chk __wcpncpy_chk
                                   __obstack_vprintf_chk __wprintf_chk __getgroups_chk __wcscpy_chk __vfprintf_chk
                                   __fgetws_chk __vswprintf_chk __ttyname_r_chk __mbsrtowcs_chk
                                   __wmempcpy_chk __wcsrtombs_chk __fwprintf_chk __read_chk __getcwd_chk __vsnprintf_chk
                                   __memcpy_chk __wmemmove_chk __vasprintf_chk __sprintf_chk __vprintf_chk
                                   __mbsnrtowcs_chk __wcrtomb_chk __realpath_chk __vsprintf_chk __wcsnrtombs_chk
                                   __gethostname_chk __swprintf_chk __readlink_chk __wmemset_chk __getdomainname_chk
                                   __wmemcpy_chk __longjmp_chk __stpncpy_chk __wcstombs_chk'''.split())

    if args.format == 'text':
        print('RELRO    CANARY  NX   PIE  RPATH  RUNPATH  FORTIFIED  PATH')
        for data in check_paths(args.path, fortifiable_funcs):
            print('{:7}  {:6}  {:3}  {:3}  {:5}  {:7}  {:>9}  {}'.format(
                ('No', 'Partial', 'Full')[data['relro']],
                'Yes' if data['canary'] else 'No',
                'Yes' if data['nx'] else 'No',
                ('No', 'DSO', 'Yes')[data['pie']],
                'Yes' if data['rpath'] else 'No',
                'Yes' if data['runpath'] else 'No',
                '{}/{}/{}'.format(data['fortified'], data['unfortified'], data['fortifiable']),
                data['path']
            ))
    else:
        writer = csv.writer(sys.stdout)
        writer.writerow(['path', 'relro', 'canary', 'nx', 'pie', 'rpath', 'runpath', 'fortified', 'unfortified',
                         'fortifiable'])
        for data in check_paths(args.path, fortifiable_funcs):
            writer.writerow([
                data['path'],
                ('no', 'partial', 'full')[data['relro']],
                'yes' if data['canary'] else 'no',
                'yes' if data['nx'] else 'no',
                ('no', 'dso', 'yes')[data['pie']],
                'yes' if data['rpath'] else 'no',
                'yes' if data['runpath'] else 'no',
                data['fortified'],
                data['unfortified'],
                data['fortifiable'],
            ])