X-Git-Url: https://ruderich.org/simon/gitweb/?a=blobdiff_plain;f=bin%2Fblhc;h=5d408c31e0c6a70aaa5360c9c9ae667785abd001;hb=efdac689c531b78c27e7ec2c9e03af621c104e40;hp=5f37c79023690487f34844969c538c39ef51560c;hpb=ac520d864fb61ea87c938921440d151d88214719;p=blhc%2Fblhc.git diff --git a/bin/blhc b/bin/blhc index 5f37c79..5d408c3 100755 --- a/bin/blhc +++ b/bin/blhc @@ -23,6 +23,7 @@ use warnings; use Getopt::Long (); use Term::ANSIColor (); +use Text::ParseWords (); our $VERSION = '0.01'; @@ -30,12 +31,119 @@ our $VERSION = '0.01'; # CONSTANTS/VARIABLES # Regex to catch compiler commands. -my $cc_regex = qr/(?:[a-z0-9_]+-(?:linux|kfreebsd)-gnu(?:eabi|eabihf)?-)? - (?:(? 1 } ( + @source_no_preprocess, +); +my %extensions_preprocess = map { $_ => 1 } ( + @header_preprocess, + @source_preprocess, +); +my %extensions_compile_link = map { $_ => 1 } ( + @source_preprocess, + @source_no_preprocess, +); +my %extensions_compile = map { $_ => 1 } ( + @source_preprocess_compile, + @source_no_preprocess_compile, +); +my %extensions_no_compile = map { $_ => 1 } ( + @source_preprocess_no_compile, + @source_no_preprocess_no_compile, +); +my %extensions_compile_cpp = map { $_ => 1 } ( + @source_preprocess_compile_cpp, + @source_no_preprocess_compile_cpp, +); +my %extension = map { $_ => 1 } ( + @source_no_preprocess, + @source_no_preprocess_compile, + @source_no_preprocess_compile_cpp, + @source_no_preprocess_no_compile, + @header_preprocess, + @source_preprocess, + @source_preprocess_compile, + @source_preprocess_compile_cpp, + @source_preprocess_no_compile, +); + +# Regexp to match file extensions. +my $file_extension_regex = qr/ + \s + \S+ # Filename without extension. + \. + ([^\\.\s]+) # File extension. + (?=\s|\\) # At end of word. Can't use \b because some files have non + # word characters at the end and because \b matches double + # extensions (like .cpp.o). Works always as all lines are + # terminated with "\n". + /x; + # Expected (hardening) flags. All flags are used as regexps. my @cflags = ( '-g', @@ -56,6 +164,11 @@ my @cflags_stack = ( my @cflags_pie = ( '-fPIE', ); +my @cxxflags = ( + '-g', + '-O(?:2|3)', +); +# @cxxflags_* is the same as @cflags_*. my @cppflags = (); my @cppflags_fortify = ( '-D_FORTIFY_SOURCE=2', @@ -78,6 +191,9 @@ my %flag_renames = ( '-Wl,(-z,)?now' => '-Wl,-z,now', ); +# Use colored (ANSI) output? +my $option_color; + # FUNCTIONS @@ -104,11 +220,16 @@ sub error_non_verbose_build { error_color(':', 'yellow'), $line; } +sub error_hardening_wrapper { + printf "%s%s %s\n", + error_color('HARDENING WRAPPER', 'red'), + error_color(':', 'yellow'), + 'no checks possible, aborting'; +} sub error_color { my ($message, $color) = @_; - # Use colors when writing to a terminal. - if (-t STDOUT) { + if ($option_color) { return Term::ANSIColor::colored($message, $color); } else { return $message; @@ -134,9 +255,7 @@ sub all_flags_used { } } - if (scalar @missing_flags == 0) { - return 1; - } + return 1 if scalar @missing_flags == 0; @{$missing_flags_ref} = @missing_flags; return 0; @@ -168,7 +287,7 @@ sub is_non_verbose_build { my ($line, $next_line, $skip_ref) = @_; if (not ($line =~ /^checking if you want to see long compiling messages\.\.\. no/ - or $line =~ /^\s*\[?(?:CC|CCLD|CXX|CXXLD|LD)\]?\s+(.+?)$/ + or $line =~ /^\s*\[?(?:CC|CCLD|CXX|CXXLD|LD|LINK)\]?\s+(.+?)$/ or $line =~ /^\s*(?:C|c)ompiling\s+(.+?)(?:\.\.\.)?$/ or $line =~ /^\s*(?:B|b)uilding (?:program|shared library)\s+(.+?)$/ or $line =~ /^\s*\[[\d ]+%\] Building (?:C|CXX) object (.+?)$/)) { @@ -192,7 +311,7 @@ sub is_non_verbose_build { $file =~ m{/([a-zA-Z0-9._-]+)$}; $file = $1; - if ($next_line =~ /\Q$file\E/ and $next_line =~ /$cc_regex/) { + if ($next_line =~ /\Q$file\E/ and $next_line =~ /$cc_regex/o) { # We still have to skip the current line as it doesn't contain any # compiler commands. ${$skip_ref} = 1; @@ -203,6 +322,19 @@ sub is_non_verbose_build { return 1; } +sub extension_found { + my ($extensions_ref, @extensions) = @_; + + my $found = 0; + foreach my $extension (@extensions) { + if (exists $extensions_ref->{$extension}) { + $found = 1; + last; + } + } + return $found; +} + # MAIN @@ -220,6 +352,7 @@ my $option_version = 0; my $option_all = 0; my $option_arch = undef; my $option_buildd = 0; + $option_color = 0; if (not Getopt::Long::GetOptions( 'help|h|?' => \$option_help, 'version' => \$option_version, @@ -228,7 +361,8 @@ if (not Getopt::Long::GetOptions( 'bindnow' => \$harden_bindnow, 'all' => \$option_all, # Misc. - 'arch' => \$option_arch, + 'color' => \$option_color, + 'arch=s' => \$option_arch, 'buildd' => \$option_buildd, )) { require Pod::Usage; @@ -292,10 +426,22 @@ while (my $line = <>) { } } - # We skip over unimportant lines at the beginning to prevent false - # positives. + # If hardening wrapper is used (wraps calls to gcc and adds hardening + # flags automatically) we can't perform any checks, abort. + if (not $start and $line =~ /^Build-Depends: .*\bhardening-wrapper\b/) { + error_hardening_wrapper(); + $exit |= 1 << 4; + exit $exit; + } + + # We skip over unimportant lines at the beginning of the log to prevent + # false positives. $start = 1 if $line =~ /^dpkg-buildpackage:/; next if not $start; + # And stop at the end of the build log. Package details (reported by the + # buildd logs) are not important for us. This also prevents false + # positives. + last if $line =~ /^Build finished at \d{8}-\d{4}$/; # Detect architecture automatically unless overridden. if (not $option_arch @@ -304,24 +450,28 @@ while (my $line = <>) { } # Ignore compiler warnings for now. - next if $line =~ /$warning_regex/; - - # Remove all ANSI color sequences which are sometimes used in non-verbose - # builds. - $line = Term::ANSIColor::colorstrip($line); - # Also strip '\0xf' (delete previous character), used by Elink's build - # system. - $line =~ s/\x0f//g; - # And "ESC(B" which seems to be used on armhf and hurd (not sure what it - # does). - $line =~ s/\033\(B//g; + next if $line =~ /$warning_regex/o; + + if ($line =~ /\033/) { # esc + # Remove all ANSI color sequences which are sometimes used in + # non-verbose builds. + $line = Term::ANSIColor::colorstrip($line); + # Also strip '\0xf' (delete previous character), used by Elinks' build + # system. + $line =~ s/\x0f//g; + # And "ESC(B" which seems to be used on armhf and hurd (not sure what + # it does). + $line =~ s/\033\(B//g; + } # Check if this line indicates a non verbose build. my $non_verbose = is_non_verbose_build($line); # One line may contain multiple commands (";"). Treat each one as single - # line. - my @line = split /(?) { } # Ignore lines with no compiler commands. - next if $line !~ /\b$cc_regex(?:\s|\\)/ and not $non_verbose; + next if $line !~ /\b$cc_regex(?:\s|\\)/o and not $non_verbose; # Ignore false positives. # # `./configure` output. - next if not $non_verbose and $line =~ /^checking /; - next if $line =~ /^\s*(?:C\s+)? - (?:C|c)ompiler[\s.]*:\s+ + next if not $non_verbose + and $line =~ /^(?:checking|(?:C|c)onfigure:) /; + next if $line =~ /^\s*(?:Host\s+)?(?:C\s+)? + (?:C|c)ompiler[\s.]*:?\s+ $cc_regex (?:\s-std=[a-z0-9:+]+)?\s*$ - /x - or $line =~ /^\s*(?:- )?(?:CC|CXX)\s*=\s*$cc_regex\s*$/ + /xo + or $line =~ /^\s*(?:- )?(?:HOST_)?(?:CC|CXX)\s*=\s*$cc_regex\s*$/o or $line =~ /^\s*-- Check for working (?:C|CXX) compiler: / or $line =~ /^\s*(?:echo )?Using [A-Z_]+\s*=\s*/; - # Debian buildd output. - next if $line =~ /^\s*Depends: .*?$cc_regex.*?$/ - and $line !~ /\s-./; # option, prevent false negatives + # `make` output. + next if $line =~ /^Making [a-z]+ in \S+/; # e.g. "[...] in c++" + # Check if additional hardening options were used. Used to ensure + # they are used for the complete build. + $harden_pie = 1 if any_flags_used($line, @cflags_pie, @ldflags_pie); + $harden_bindnow = 1 if any_flags_used($line, @ldflags_bindnow); push @input, $line; } @@ -404,27 +558,24 @@ if ($option_arch) { } } -# Check if additional hardening options were used. Used to ensure they are -# used for the complete build. -foreach my $line (@input) { - $harden_pie = 1 if any_flags_used($line, @cflags_pie, @ldflags_pie); - $harden_bindnow = 1 if any_flags_used($line, @ldflags_bindnow); -} - # Check the specified hardening options, same order as dpkg-buildflags. if ($harden_pie) { @cflags = (@cflags, @cflags_pie); + @cxxflags = (@cxxflags, @cflags_pie); @ldflags = (@ldflags, @ldflags_pie); } if ($harden_stack) { @cflags = (@cflags, @cflags_stack); + @cxxflags = (@cxxflags, @cflags_stack); } if ($harden_fortify) { @cflags = (@cflags, @cflags_fortify); + @cxxflags = (@cxxflags, @cflags_fortify); @cppflags = (@cppflags, @cppflags_fortify); } if ($harden_format) { @cflags = (@cflags, @cflags_format); + @cxxflags = (@cxxflags, @cflags_format); } if ($harden_relro) { @ldflags = (@ldflags, @ldflags_relro); @@ -445,50 +596,96 @@ for (my $i = 0; $i < scalar @input; $i++) { # Even if it's a verbose build, we might have to skip this line. next if $skip; + # Remove everything until and including the compiler command. Makes checks + # easier and faster. + $line =~ s/^.*?$cc_regex//o; + + # Skip unnecessary tests when only preprocessing. + my $flag_preprocess = 0; + + my $preprocess = 0; + my $compile = 0; + my $link = 0; + + # Preprocess, compile, assemble. + if ($line =~ /\s(-E|-S|-c)\b/) { + $preprocess = 1; + $flag_preprocess = 1 if $1 eq '-E'; + $compile = 1 if $1 eq '-S' or $1 eq '-c'; + # Otherwise assume we are linking. + } else { + $link = 1; + } + + # Get all file extensions on this line. + my @extensions = $line =~ /$file_extension_regex/go; + # Ignore all unknown extensions to speedup the search below. + @extensions = grep { exists $extension{$_} } @extensions; - # Is this a compiler or linker command? - my $compiler = 1; - my $linker = 0; - - # Linker commands. - if ($line =~ m{\s-o # -o - [\s\\]*\s+ # possible line continuation - (?:[/.A-Za-z0-9~_-]+/)? # path to file - [A-Za-z0-9~_-]+ # binary name (no dots!) - (?:[0-9.]*\.so[0-9.]*[a-z]? # library (including version) - |\.la - |\.cgi)? # CGI binary - (?:\s|\\|$) # end of file name - }x - or $line =~ /^libtool: link: / - or $line =~ m{\s*/bin/bash .+?libtool\s+(.+?\s+)?--mode=(re)?link}) { - $compiler = 0; - $linker = 1; + # These file types don't require preprocessing. + if (extension_found(\%extensions_no_preprocess, @extensions)) { + $preprocess = 0; + } + # These file types require preprocessing. + if (extension_found(\%extensions_preprocess, @extensions)) { + $preprocess = 1; } # If there are source files then it's compiling/linking in one step and we - # must check both. - if ($line =~ /\.(?:c|cc|cpp)\b/) { - $compiler = 1; + # must check both. We only check for source files here, because header + # files cause too many false positives. + if (not $flag_preprocess + and extension_found(\%extensions_compile_link, @extensions)) { + # Assembly files don't need CFLAGS. + if (not extension_found(\%extensions_compile, @extensions) + and extension_found(\%extensions_no_compile, @extensions)) { + $compile = 0; + # But the rest does. + } else { + $compile = 1; + } + } + + # Assume CXXFLAGS are required when a C++ file is specified in the + # compiler line. + my $compile_cpp = 0; + if ($compile + and extension_found(\%extensions_compile_cpp, @extensions)) { + $compile = 0; + $compile_cpp = 1; } # Check hardening flags. my @missing; - if ($compiler and not all_flags_used($line, \@missing, @cflags) + if ($compile and not all_flags_used($line, \@missing, @cflags) + # Libraries linked with -fPIC don't have to (and can't) be linked + # with -fPIE as well. It's no error if only PIE flags are missing. + and not pic_pie_conflict($line, $harden_pie, \@missing, @cflags_pie) + # Assume dpkg-buildflags returns the correct flags. + and not $line =~ /`dpkg-buildflags --get CFLAGS`/) { + error_flags('CFLAGS missing', \@missing, \%flag_renames, $input[$i]); + $exit |= 1 << 3; + } elsif ($compile_cpp and not all_flags_used($line, \@missing, @cflags) # Libraries linked with -fPIC don't have to (and can't) be linked # with -fPIE as well. It's no error if only PIE flags are missing. - and not pic_pie_conflict($line, $harden_pie, \@missing, @cflags_pie)) { - error_flags('CFLAGS missing', \@missing, \%flag_renames, $line); + and not pic_pie_conflict($line, $harden_pie, \@missing, @cflags_pie) + # Assume dpkg-buildflags returns the correct flags. + and not $line =~ /`dpkg-buildflags --get CXXFLAGS`/) { + error_flags('CXXFLAGS missing', \@missing, \%flag_renames, $input[$i]); $exit |= 1 << 3; } - if ($compiler and not all_flags_used($line, \@missing, @cppflags)) { - error_flags('CPPFLAGS missing', \@missing, \%flag_renames, $line); + if ($preprocess and not all_flags_used($line, \@missing, @cppflags) + # Assume dpkg-buildflags returns the correct flags. + and not $line =~ /`dpkg-buildflags --get CPPFLAGS`/) { + error_flags('CPPFLAGS missing', \@missing, \%flag_renames, $input[$i]); $exit |= 1 << 3; } - if ($linker and not all_flags_used($line, \@missing, @ldflags) + if ($link and not all_flags_used($line, \@missing, @ldflags) # Same here, -fPIC conflicts with -fPIE. - and not pic_pie_conflict($line, $harden_pie, \@missing, @ldflags_pie)) { - error_flags('LDFLAGS missing', \@missing, \%flag_renames, $line); + and not pic_pie_conflict($line, $harden_pie, \@missing, @ldflags_pie) + # Assume dpkg-buildflags returns the correct flags. + and not $line =~ /`dpkg-buildflags --get LDFLAGS`/) { + error_flags('LDFLAGS missing', \@missing, \%flag_renames, $input[$i]); $exit |= 1 << 3; } } @@ -504,17 +701,16 @@ blhc - build log hardening check, checks build logs for missing hardening flags =head1 SYNOPSIS -B [-h -? --help] - -B [--pie] [--bindnow] [--all] +B [options] - --help available options - --version version number and license - --pie force +pie check - --bindnow force +bindbow check --all force +all (+pie, +bindnow) check --arch set architecture (autodetected) + --bindnow force +bindbow check --buildd parser mode for buildds + --color use colored output + --pie force +pie check + --help available options + --version version number and license =head1 DESCRIPTION @@ -525,22 +721,6 @@ other important warnings. It's licensed under the GPL 3 or later. =over 8 -=item B<-h -? --help> - -Print available options. - -=item B<--version> - -Print version number and license. - -=item B<--pie> - -Force check for all +pie hardening flags. By default it's auto detected. - -=item B<--bindnow> - -Force check for all +bindnow hardening flags. By default it's auto detected. - =item B<--all> Force check for all +all (+pie, +bindnow) hardening flags. By default it's @@ -552,6 +732,10 @@ Set the specific architecture (e.g. amd64, armel, etc.), automatically disables hardening flags not available on this architecture. Is detected automatically if dpkg-buildpackage is used. +=item B<--bindnow> + +Force check for all +bindnow hardening flags. By default it's auto detected. + =item B<--buildd> Special mode for buildds when automatically parsing log files. The following @@ -566,6 +750,22 @@ detected). =back +=item B<--color> + +Use colored (ANSI) output for warning messages. + +=item B<--pie> + +Force check for all +pie hardening flags. By default it's auto detected. + +=item B<-h -? --help> + +Print available options. + +=item B<--version> + +Print version number and license. + =back Auto detection for B<--pie> and B<--bindnow> only works if at least one @@ -577,7 +777,7 @@ all other commands as well. The exit status is a "bit mask", each listed status is ORed when the error condition occurs to get the result. -=over 8 +=over 4 =item B<0> @@ -599,6 +799,10 @@ Non verbose build. Missing hardening flags. +=item B<16> + +Hardening wrapper detected, no tests performed. + =back =head1 AUTHOR