summaryrefslogtreecommitdiffstats
path: root/scripts/hardening-check.pl
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/hardening-check.pl')
-rwxr-xr-xscripts/hardening-check.pl684
1 files changed, 684 insertions, 0 deletions
diff --git a/scripts/hardening-check.pl b/scripts/hardening-check.pl
new file mode 100755
index 0000000..ad7f4e4
--- /dev/null
+++ b/scripts/hardening-check.pl
@@ -0,0 +1,684 @@
+#!/usr/bin/perl
+# Report the hardening characteristics of a set of binaries.
+# Copyright (C) 2009-2013 Kees Cook <kees@debian.org>
+# License: GPLv2 or newer
+use strict;
+use warnings;
+use Getopt::Long qw(:config no_ignore_case bundling);
+use Pod::Usage;
+use IPC::Open3;
+use Symbol qw(gensym);
+use Term::ANSIColor;
+use IO::Select;
+
+my $skip_pie = 0;
+my $skip_stackprotector = 0;
+my $skip_fortify = 0;
+my $skip_relro = 0;
+my $skip_bindnow = 0;
+my $skip_cfprotection = 0;
+my $report_functions = 0;
+my $find_libc_functions = 0;
+my $color = 0;
+my $lintian = 0;
+my $verbose = 0;
+my $debug = 0;
+my $quiet = 0;
+my $help = 0;
+my $man = 0;
+
+GetOptions(
+ "nopie|p+" => \$skip_pie,
+ "nostackprotector|s+" => \$skip_stackprotector,
+ "nofortify|f+" => \$skip_fortify,
+ "norelro|r+" => \$skip_relro,
+ "nobindnow|b+" => \$skip_bindnow,
+ "nocfprotection|x+" => \$skip_cfprotection,
+ "report-functions|R!" => \$report_functions,
+ "find-libc-functions|F!" => \$find_libc_functions,
+ "color|c!" => \$color,
+ "lintian|l!" => \$lintian,
+ "verbose|v!" => \$verbose,
+ "debug!" => \$debug,
+ "quiet|q!" => \$quiet,
+ "help|h|?" => \$help,
+ "man|H" => \$man,
+) or pod2usage(2);
+pod2usage(1) if $help;
+pod2usage(-exitstatus => 0, -verbose => 2, -noperldoc => 1) if $man;
+
+my $overall = 0;
+my $rc = 0;
+my $report = "";
+my @tags;
+my %libc = (
+ 'asprintf' => 1,
+ 'confstr' => 1,
+ 'dprintf' => 1,
+ 'fdelt' => 1,
+ 'fgets' => 1,
+ 'fgets_unlocked' => 1,
+ 'fgetws' => 1,
+ 'fgetws_unlocked' => 1,
+ 'fprintf' => 1,
+ 'fread' => 1,
+ 'fread_unlocked' => 1,
+ 'fwprintf' => 1,
+ 'getcwd' => 1,
+ 'getdomainname' => 1,
+ 'getgroups' => 1,
+ 'gethostname' => 1,
+ 'getlogin_r' => 1,
+ 'gets' => 1,
+ 'getwd' => 1,
+ 'longjmp' => 1,
+ 'mbsnrtowcs' => 1,
+ 'mbsrtowcs' => 1,
+ 'mbstowcs' => 1,
+ 'memcpy' => 1,
+ 'memmove' => 1,
+ 'mempcpy' => 1,
+ 'memset' => 1,
+ 'obstack_printf' => 1,
+ 'obstack_vprintf' => 1,
+ 'poll' => 1,
+ 'ppoll' => 1,
+ 'pread64' => 1,
+ 'pread' => 1,
+ 'printf' => 1,
+ 'ptsname_r' => 1,
+ 'read' => 1,
+ 'readlink' => 1,
+ 'readlinkat' => 1,
+ 'realpath' => 1,
+ 'recv' => 1,
+ 'recvfrom' => 1,
+ 'snprintf' => 1,
+ 'sprintf' => 1,
+ 'stpcpy' => 1,
+ 'stpncpy' => 1,
+ 'strcat' => 1,
+ 'strcpy' => 1,
+ 'strncat' => 1,
+ 'strncpy' => 1,
+ 'swprintf' => 1,
+ 'syslog' => 1,
+ 'ttyname_r' => 1,
+ 'vasprintf' => 1,
+ 'vdprintf' => 1,
+ 'vfprintf' => 1,
+ 'vfwprintf' => 1,
+ 'vprintf' => 1,
+ 'vsnprintf' => 1,
+ 'vsprintf' => 1,
+ 'vswprintf' => 1,
+ 'vsyslog' => 1,
+ 'vwprintf' => 1,
+ 'wcpcpy' => 1,
+ 'wcpncpy' => 1,
+ 'wcrtomb' => 1,
+ 'wcscat' => 1,
+ 'wcscpy' => 1,
+ 'wcsncat' => 1,
+ 'wcsncpy' => 1,
+ 'wcsnrtombs' => 1,
+ 'wcsrtombs' => 1,
+ 'wcstombs' => 1,
+ 'wctomb' => 1,
+ 'wmemcpy' => 1,
+ 'wmemmove' => 1,
+ 'wmempcpy' => 1,
+ 'wmemset' => 1,
+ 'wprintf' => 1,
+);
+
+# Report a good test.
+sub good {
+ my ($name, $msg_color, $msg) = @_;
+ $msg_color = colored($msg_color, 'green') if $color;
+ if (defined $msg) {
+ $msg_color .= $msg;
+ }
+ good_msg("$name: $msg_color");
+}
+
+sub good_msg($) {
+ my ($msg) = @_;
+ if ($quiet == 0) {
+ $report .= "\n$msg";
+ }
+}
+
+sub unknown {
+ my ($name, $msg) = @_;
+ $msg = colored($msg, 'yellow') if $color;
+ good_msg("$name: $msg");
+}
+
+# Report a failed test, possibly ignoring it.
+sub bad($$$$$) {
+ my ($name, $file, $long_name, $msg, $ignore) = @_;
+
+ $msg = colored($msg, 'red') if $color;
+
+ $msg = "$long_name: " . $msg;
+ if ($ignore) {
+ $msg .= " (ignored)";
+ } else {
+ $rc = 1;
+ if ($lintian) {
+ push(@tags, "$name:$file");
+ }
+ }
+ $report .= "\n$msg";
+}
+
+# Safely run list-based command line and return stdout.
+sub output(@) {
+ my (@cmd) = @_;
+ my ($pid, $stdout, $stderr);
+ if ($debug) {
+ print join(" ", @cmd), "\n";
+ }
+ $stdout = gensym;
+ $stderr = gensym;
+ $pid = open3(gensym, $stdout, $stderr, @cmd);
+
+ my $selector = IO::Select->new();
+ $selector->add($stdout);
+ $selector->add($stderr);
+
+ my $collect_out = "";
+ my $collect_err = "";
+
+ while (my @ready = $selector->can_read()) {
+ foreach my $fh (@ready) {
+ my $buf;
+ my $len = sysread($fh, $buf, 4096);
+ if ($len == 0) {
+ $selector->remove($fh);
+ next;
+ }
+
+ if ($fh == $stdout) {
+ $collect_out .= $buf;
+ } else {
+ $collect_err .= $buf;
+ }
+ }
+ }
+
+ waitpid($pid, 0);
+ my $rc = $?;
+ if ($rc != 0) {
+ print STDERR $collect_err;
+ return "";
+ }
+ return $collect_out;
+}
+
+# Find the libc used in this executable, if any.
+sub find_libc($) {
+ my ($file) = @_;
+ my $ldd = output("ldd", $file);
+ $ldd =~ /^\s*libc\.so\.\S+\s+\S+\s+(\S+)/m;
+ return $1 || "";
+}
+
+sub find_functions($$) {
+ my ($file, $undefined) = @_;
+ my (%funcs);
+
+ # Catch "NOTYPE" for object archives.
+ my $func_regex = " (I?FUNC|NOTYPE) ";
+
+ my $relocs = output("readelf", "-sW", $file);
+ for my $line (split("\n", $relocs)) {
+ next if ($line !~ /$func_regex/);
+ next if ($undefined && $line !~ /$func_regex.* UND /);
+
+ $line =~ s/ \([0-9]+\)$//;
+ $line =~ s/.* //;
+ $line =~ s/@.*//;
+ $funcs{$line} = 1;
+ }
+
+ return \%funcs;
+}
+
+$ENV{'LANG'} = "C";
+
+if ($find_libc_functions) {
+ pod2usage(1) if (!defined($ARGV[0]));
+ my $libc_path = find_libc($ARGV[0]);
+
+ my $funcs = find_functions($libc_path, 0);
+ for my $func (sort(keys(%{$funcs}))) {
+ if ($func =~ /^__(\S+)_chk$/) {
+ print " '$1' => 1,\n";
+ }
+ }
+ exit(0);
+}
+die "List of libc functions not defined!" if (scalar(keys %libc) < 1);
+
+my $name;
+foreach my $file (@ARGV) {
+ $rc = 0;
+ my $elf = 1;
+
+ $report = "$file:";
+ @tags = ();
+
+ # Get program headers.
+ my $PROG_REPORT = output("readelf", "-lW", $file);
+ if (length($PROG_REPORT) == 0) {
+ $overall = 1;
+ next;
+ }
+
+ # Get ELF headers.
+ my $DYN_REPORT = output("readelf", "-dW", $file);
+
+ # Get disassembly
+ my $DISASM
+ = output("objdump", "-d", "--no-show-raw-insn", "-M", "intel", $file);
+
+ # Get notes
+ my $NOTES = output("readelf", "-n", $file);
+
+ # Get list of all symbols needing external resolution.
+ my $functions = find_functions($file, 1);
+
+ # PIE
+ # First, verify this is an executable, not a library. This seems to be
+ # best seen by checking for the PHDR program header.
+ $name = " Position Independent Executable";
+ $PROG_REPORT =~ /^Elf file type is (\S+)/m;
+ my $elftype = $1 || "";
+ if ($elftype eq "DYN") {
+ if ($PROG_REPORT =~ /^ *\bPHDR\b/m) {
+
+ # Executable, DYN ELF type.
+ good($name, "yes");
+ } else {
+ # Shared library, DYN ELF type.
+ good($name, "no, regular shared library (ignored)");
+ }
+ } elsif ($elftype eq "EXEC") {
+
+ # Executable, EXEC ELF type.
+ bad("no-pie", $file, $name, "no, normal executable!", $skip_pie);
+ } else {
+ $elf = 0;
+
+ # Is this an ar file with objects?
+ open(AR, "<$file");
+ my $header = <AR>;
+ close(AR);
+ if ($header eq "!<arch>\n") {
+ good($name, "no, object archive (ignored)");
+ } else {
+ # ELF type is neither DYN nor EXEC.
+ bad("unknown-elf", $file, $name,
+ "not a known ELF type!? ($elftype)", 0);
+ }
+ }
+
+ # Stack-protected
+ $name = " Stack protected";
+ if (defined($functions->{'__stack_chk_fail'})
+ || (!$elf && defined($functions->{'__stack_chk_fail_local'}))) {
+ good($name, "yes");
+ } else {
+ if (%{$functions} eq 0) {
+ unknown($name, "unknown, no symbols found");
+ } else {
+ bad("no-stackprotector", $file, $name, "no, not found!",
+ $skip_stackprotector);
+ }
+ }
+
+ # Fortified Source
+ $name = " Fortify Source functions";
+ my @unprotected;
+ my @protected;
+ for my $name (keys(%libc)) {
+ if (defined($functions->{$name})) {
+ push(@unprotected, $name);
+ }
+ if (defined($functions->{"__${name}_chk"})) {
+ push(@protected, $name);
+ }
+ }
+ if ($#protected > -1) {
+ if ($#unprotected == -1) {
+
+ # Certain.
+ good($name, "yes");
+ } else {
+ # Vague, due to possible compile-time optimization,
+ # multiple linkages, etc. Assume "yes" for now.
+ good($name, "yes", " (some protected functions found)");
+ }
+ } else {
+ if ($#unprotected == -1) {
+ unknown($name, "unknown, no protectable libc functions used");
+ } else {
+ # Vague, since it's possible to have the compile-time
+ # optimizations do away with them, or be unverifiable
+ # at runtime. Assume "no" for now.
+ bad("no-fortify-functions", $file, $name,
+ "no, only unprotected functions found!",
+ $skip_fortify);
+ }
+ }
+ if ($verbose) {
+ for my $name (@unprotected) {
+ good_msg("\tunprotected: $name");
+ }
+ for my $name (@protected) {
+ good_msg("\tprotected: $name");
+ }
+ }
+
+ # Format
+ # Unfortunately, I haven't thought of a way to test for this after
+ # compilation. What it really needs is a lintian-like check that
+ # reviews the build logs and looks for the warnings, or that the
+ # argument is changed to use -Werror=format-security to stop the build.
+
+ # RELRO
+ $name = " Read-only relocations";
+ if ($PROG_REPORT =~ /^ *\bGNU_RELRO\b/m) {
+ good($name, "yes");
+ } else {
+ if ($elf) {
+ bad("no-relro", $file, $name, "no, not found!", $skip_relro);
+ } else {
+ good($name, "no", ", non-ELF (ignored)");
+ }
+ }
+
+ # BIND_NOW
+ # This marking keeps changing:
+ # 0x0000000000000018 (BIND_NOW)
+ # 0x000000006ffffffb (FLAGS) Flags: BIND_NOW
+ # 0x000000006ffffffb (FLAGS_1) Flags: NOW
+
+ $name = " Immediate binding";
+ if ( $DYN_REPORT =~ /^\s*\S+\s+\(BIND_NOW\)/m
+ || $DYN_REPORT =~ /^\s*\S+\s+\(FLAGS\).*\bBIND_NOW\b/m
+ || $DYN_REPORT =~ /^\s*\S+\s+\(FLAGS_1\).*\bNOW\b/m) {
+ good($name, "yes");
+ } else {
+ if ($elf) {
+ bad("no-bindnow", $file, $name, "no, not found!", $skip_bindnow);
+ } else {
+ good($name, "no", ", non-ELF (ignored)");
+ }
+ }
+
+ # For stack clash we need to look for a specific sequence of
+ # instructions in the objdump disassembly
+ $name = " Stack clash protection";
+ my $index = 0;
+ my $cmp_addr = 0;
+ my @patterns = (
+ qr/^\s+([0-9a-f]+):\s+cmp\s+(rsp.*|.*0x1000)/,
+ qr/^\s+[0-9a-f]+:\s+j[eb]\s+([x0-9a-f]+)/,
+ qr/^\s+[0-9a-f]+:\s+sub\s+(.*,0x1000)/,
+ qr/^\s+[0-9a-f]+:\s+or\s+(.*,0x0)/,
+ qr/^\s+([0-9a-f]+):\s+(jmp\s+([x0-9a-f]+)|cmp\s+rsp,.*)/,
+ qr/^\s+([0-9a-f]+):\s+jne\s+([x0-9a-f]+)/
+ );
+ my $found = 0;
+ foreach my $line (split /\n/, $DISASM) {
+
+ # look for each regex from patterns in succession - they all
+ # should be consecutive in the binary so we always fall back to
+ # index 0 if we fail to find the next one
+ if (my @matches = ($line =~ $patterns[$index])) {
+ if ($index == 0) {
+ $cmp_addr = hex($matches[0]);
+ } elsif ($index == 4) {
+
+ # this could be either the jmp or cmp - if is jump then
+ # this is the last instruction in the sequence otherwise
+ # cmp has a jne following for index 5
+ if ($matches[1] =~ /^jmp.*/) {
+ my $arg = hex($matches[2]);
+ if ($arg == $cmp_addr) {
+ good($name, "yes");
+ $found = 1;
+ last;
+ } else {
+ # since the expected instructions should be
+ # contiguous, always fall back to zero on failure
+ $index = 0;
+ next;
+ }
+ }
+
+ # nothing to do for the cmp case
+ } elsif ($index == 5) {
+ my $arg = hex($matches[1]);
+ if ($arg == $cmp_addr + 5) {
+ good($name, "yes");
+ $found = 1;
+ last;
+ } else {
+ # since the expected instructions should be
+ # contiguous, always fall back to zero on failure
+ $index = 0;
+ next;
+ }
+ }
+ ++$index;
+ } else {
+ $index = 0;
+ }
+ }
+ if (!$found) {
+ unknown($name,
+ "unknown, no -fstack-clash-protection instructions found");
+ }
+
+ # For cf-protection look for x86 feature: IBT, SHSTK
+ $name = " Control flow integrity";
+ if ($NOTES =~ /^\s+Properties: x86 feature: IBT, SHSTK/m) {
+ good($name, "yes");
+ } else {
+ bad("no-cfprotection", $file, $name, "no, not found!",
+ $skip_cfprotection);
+ }
+
+ if (!$lintian && (!$quiet || $rc != 0)) {
+ print $report, "\n";
+ }
+
+ if ($report_functions) {
+ for my $name (keys(%{$functions})) {
+ print $name, "\n";
+ }
+ }
+
+ if (!$lintian && $rc) {
+ $overall = $rc;
+ }
+
+ if ($lintian) {
+ for my $tag (@tags) {
+ print $tag, "\n";
+ }
+ }
+}
+
+exit($overall);
+
+__END__
+
+=head1 NAME
+
+hardening-check - check binaries for security hardening features
+
+=head1 SYNOPSIS
+
+hardening-check [options] [ELF ...]
+
+Examine a given set of ELF binaries and check for several security hardening
+features, failing if they are not all found.
+
+=head1 DESCRIPTION
+
+This utility checks a given list of ELF binaries for several security
+hardening features that can be compiled into an executable. These
+features are:
+
+=over 8
+
+=item B<Position Independent Executable>
+
+This indicates that the executable was built in such a way (PIE) that
+the "text" section of the program can be relocated in memory. To take
+full advantage of this feature, the executing kernel must support text
+Address Space Layout Randomization (ASLR).
+
+=item B<Stack Protected>
+
+This indicates that there is evidence that the ELF was compiled with the
+L<gcc(1)> option B<-fstack-protector> (e.g. uses B<__stack_chk_fail>). The
+program will be resistant to having its stack overflowed.
+
+When an executable was built without any character arrays being allocated
+on the stack, this check will lead to false alarms (since there is no
+use of B<__stack_chk_fail>), even though it was compiled with the correct
+options.
+
+=item B<Fortify Source functions>
+
+This indicates that the executable was compiled with
+B<-D_FORTIFY_SOURCE=2> and B<-O1> or higher. This causes certain unsafe
+glibc functions with their safer counterparts (e.g. B<strncpy> instead
+of B<strcpy>), or replaces calls that are verifiable at runtime with the
+runtime-check version (e.g. B<__memcpy_chk> insteade of B<memcpy>).
+
+When an executable was built such that the fortified versions of the glibc
+functions are not useful (e.g. use is verified as safe at compile time, or
+use cannot be verified at runtime), this check will lead to false alarms.
+In an effort to mitigate this, the check will pass if any fortified function
+is found, and will fail if only unfortified functions are found. Uncheckable
+conditions also pass (e.g. no functions that could be fortified are found, or
+not linked against glibc).
+
+=item B<Read-only relocations>
+
+This indicates that the executable was build with B<-Wl,-z,relro> to
+have ELF markings (RELRO) that ask the runtime linker to mark any
+regions of the relocation table as "read-only" if they were resolved
+before execution begins. This reduces the possible areas of memory in
+a program that can be used by an attacker that performs a successful
+memory corruption exploit.
+
+=item B<Immediate binding>
+
+This indicates that the executable was built with B<-Wl,-z,now> to have
+ELF markings (BIND_NOW) that ask the runtime linker to resolve all
+relocations before starting program execution. When combined with RELRO
+above, this further reduces the regions of memory available to memory
+corruption attacks.
+
+=back
+
+=head1 OPTIONS
+
+=over 8
+
+=item B<--nopie>, B<-p>
+
+Do not require that the checked binaries be built as PIE.
+
+=item B<--nostackprotector>, B<-s>
+
+Do not require that the checked binaries be built with the stack protector.
+
+=item B<--nofortify>, B<-f>
+
+Do not require that the checked binaries be built with Fortify Source.
+
+=item B<--norelro>, B<-r>
+
+Do not require that the checked binaries be built with RELRO.
+
+=item B<--nobindnow>, B<-b>
+
+Do not require that the checked binaries be built with BIND_NOW.
+
+=item B<--nocfprotection>, B<-b>
+
+Do not require that the checked binaries be built with control flow protection.
+
+=item B<--quiet>, B<-q>
+
+Only report failures.
+
+=item B<--verbose>, B<-v>
+
+Report verbosely on failures.
+
+=item B<--report-functions>, B<-R>
+
+After the report, display all external functions needed by the ELF.
+
+=item B<--find-libc-functions>, B<-F>
+
+Instead of the regular report, locate the libc for the first ELF on the
+command line and report all the known "fortified" functions exported by
+libc.
+
+=item B<--color>, B<-c>
+
+Enable colorized status output.
+
+=item B<--lintian>, B<-l>
+
+Switch reporting to lintian-check-parsable output.
+
+=item B<--debug>
+
+Report some debugging during processing.
+
+=item B<--help>, B<-h>, B<-?>
+
+Print a brief help message and exit.
+
+=item B<--man>, B<-H>
+
+Print the manual page and exit.
+
+=back
+
+=head1 RETURN VALUE
+
+When all checked binaries have all checkable hardening features detected,
+this program will finish with an exit code of 0. If any check fails, the
+exit code with be 1. Individual checks can be disabled via command line
+options.
+
+=head1 AUTHOR
+
+Kees Cook <kees@debian.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright 2009-2013 Kees Cook <kees@debian.org>.
+
+This program is free software; you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation; version 2 or later.
+
+=head1 SEE ALSO
+
+L<gcc(1)>, L<hardening-wrapper(1)>
+
+=cut