X-Git-Url: https://ruderich.org/simon/gitweb/?a=blobdiff_plain;f=bin%2Fblhc;h=62ddb86a6076f986eb6fdeffec5b84e71b6f8731;hb=17f9869280cd3b89f8a6a1dd172d03a0871bf786;hp=880d24df4d2599f6cae394595bb1eafd6fce4665;hpb=50b731db501c79ad33cb40845c16638703a4ea15;p=blhc%2Fblhc.git diff --git a/bin/blhc b/bin/blhc index 880d24d..62ddb86 100755 --- a/bin/blhc +++ b/bin/blhc @@ -23,21 +23,213 @@ use warnings; use Getopt::Long (); use Term::ANSIColor (); +use Text::ParseWords (); our $VERSION = '0.01'; +# CONSTANTS/VARIABLES + +# Regex to catch compiler commands. +my $cc_regex = qr/ + (? 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 @def_cflags = ( + '-g', + '-O(?:2|3)', +); +my @def_cflags_format = ( + '-Wformat', + '-Wformat-security', + '-Werror=format-security', +); +my @def_cflags_fortify = ( + # fortify needs at least -O1, but -O2 is recommended anyway +); +my @def_cflags_stack = ( + '-fstack-protector', + '--param=ssp-buffer-size=4', +); +my @def_cflags_pie = ( + '-fPIE', +); +my @def_cxxflags = ( + @def_cflags, +); +# @def_cxxflags_* is the same as @def_cflags_*. +my @def_cppflags = (); +my @def_cppflags_fortify = ( + '-D_FORTIFY_SOURCE=2', +); +my @def_ldflags = (); +my @def_ldflags_relro = ( + '-Wl,(-z,)?relro', +); +my @def_ldflags_bindnow = ( + '-Wl,(-z,)?now', +); +my @def_ldflags_pie = ( + '-fPIE', + '-pie', +); +my @def_ldflags_pic = ( + '-fPIC', + '-fpic', +); +# Renaming rules for the output so the regex parts are not visible. Also +# stores string values of flag regexps above, see compile_flag_regexp(). +my %flag_renames = ( + '-O(?:2|3)' => '-O2', + '-Wl,(-z,)?relro' => '-Wl,-z,relro', + '-Wl,(-z,)?now' => '-Wl,-z,now', +); + +# Statistics of missing flags and non-verbose build commands. Used for +# $option_buildd. +my %statistics = ( + preprocess => 0, + preprocess_missing => 0, + compile => 0, + compile_missing => 0, + compile_cpp => 0, + compile_cpp_missing => 0, + link => 0, + link_missing => 0, + commands => 0, + commands_nonverbose => 0, +); + +# Use colored (ANSI) output? +my $option_color; + + # FUNCTIONS sub error_flags { my ($message, $missing_flags_ref, $flag_renames_ref, $line) = @_; - # Rename flags if requested. + # Get string value of qr//-escaped regexps and if requested rename them. my @missing_flags = map { - (exists $flag_renames_ref->{$_}) - ? $flag_renames_ref->{$_} - : $_ - } @{$missing_flags_ref}; + $flag_renames_ref->{$_} + } @{$missing_flags_ref}; my $flags = join ' ', @missing_flags; printf "%s (%s)%s %s", @@ -52,11 +244,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; @@ -67,7 +264,7 @@ sub any_flags_used { my ($line, @flags) = @_; foreach my $flag (@flags) { - return 1 if $line =~ /\s$flag(?:\s|\\|$)/; + return 1 if $line =~ /$flag/; } return 0; @@ -77,14 +274,12 @@ sub all_flags_used { my @missing_flags = (); foreach my $flag (@flags) { - if ($line !~ /\s$flag(?:\s|\\|$)/) { + if (not $line =~ /$flag/) { push @missing_flags, $flag; } } - if (scalar @missing_flags == 0) { - return 1; - } + return 1 if scalar @missing_flags == 0; @{$missing_flags_ref} = @missing_flags; return 0; @@ -95,7 +290,7 @@ sub pic_pie_conflict { my ($line, $pie, $missing_flags_ref, @flags_pie) = @_; return 0 if not $pie; - return 0 if not any_flags_used($line, ('-fPIC', '-fpic')); + return 0 if not any_flags_used($line, @def_ldflags_pic); my %flags = map { $_ => 1 } @flags_pie; @@ -113,10 +308,10 @@ sub pic_pie_conflict { } sub is_non_verbose_build { - my ($line, $next_line, $cc_regex, $skip_ref) = @_; + my ($line, $next_line, $skip_ref) = @_; if (not ($line =~ /^checking if you want to see long compiling messages\.\.\. no/ - or $line =~ /^\s*(?:CC|CCLD)\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 (.+?)$/)) { @@ -137,10 +332,10 @@ sub is_non_verbose_build { if (defined $file) { # Get filename, we can't use the complete path as only parts of it are # used in the real compiler command. - $file =~ m{/([a-zA-Z0-9._-]+)$}; + $file =~ m{/([^/\s]+)$}; $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; @@ -151,67 +346,60 @@ sub is_non_verbose_build { return 1; } +sub compile_flag_regexp { + my ($flag_renames_ref, @flags) = @_; -# CONSTANTS/VARIABLES + my @result = (); + foreach my $flag (@flags) { + # Store flag name in replacement string for correct flags in messages + # with qr//ed flag regexps. + $flag_renames_ref->{qr/\s$flag(?:\s|\\)/} + = (exists $flag_renames_ref->{$flag}) + ? $flag_renames_ref->{$flag} + : $flag; + + # Compile flag regexp for faster execution. + push @result, qr/\s$flag(?:\s|\\)/; + } + return @result; +} -# Regex to catch compiler commands. -my $cc_regex = qr/(?:x86_64-linux-gnu-)?(?:(? '-Wl,-z,relro', - '-Wl,(-z,)?now' => '-Wl,-z,now', -); + my $found = 0; + foreach my $extension (@extensions) { + if (exists $extensions_ref->{$extension}) { + $found = 1; + last; + } + } + return $found; +} # MAIN -# Additional hardening options. -my $pie = 0; -my $bindnow = 0; - # Parse command line arguments. -my $option_all = 0; my $option_help = 0; my $option_version = 0; +my $option_pie = 0; +my $option_bindnow = 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, - 'pie' => \$pie, - 'bindnow' => \$bindnow, + # Hardening options. + 'pie' => \$option_pie, + 'bindnow' => \$option_bindnow, 'all' => \$option_all, + # Misc. + 'color' => \$option_color, + 'arch=s' => \$option_arch, + 'buildd' => \$option_buildd, )) { require Pod::Usage; Pod::Usage::pod2usage(2); @@ -240,151 +428,417 @@ along with this program. If not, see . } if ($option_all) { - $pie = 1; - $bindnow = 1; + $option_pie = 1; + $option_bindnow = 1; } +# Precompile all flag regexps. any_flags_used(), all_flags_used() get a lot +# faster with this. +@def_cflags = compile_flag_regexp(\%flag_renames, @def_cflags); +@def_cflags_format = compile_flag_regexp(\%flag_renames, @def_cflags_format); +@def_cflags_fortify = compile_flag_regexp(\%flag_renames, @def_cflags_fortify); +@def_cflags_stack = compile_flag_regexp(\%flag_renames, @def_cflags_stack); +@def_cflags_pie = compile_flag_regexp(\%flag_renames, @def_cflags_pie); +@def_cxxflags = compile_flag_regexp(\%flag_renames, @def_cxxflags); +@def_cppflags = compile_flag_regexp(\%flag_renames, @def_cppflags); +@def_cppflags_fortify = compile_flag_regexp(\%flag_renames, @def_cppflags_fortify); +@def_ldflags = compile_flag_regexp(\%flag_renames, @def_ldflags); +@def_ldflags_relro = compile_flag_regexp(\%flag_renames, @def_ldflags_relro); +@def_ldflags_bindnow = compile_flag_regexp(\%flag_renames, @def_ldflags_bindnow); +@def_ldflags_pie = compile_flag_regexp(\%flag_renames, @def_ldflags_pie); +@def_ldflags_pic = compile_flag_regexp(\%flag_renames, @def_ldflags_pic); + # Final exit code. my $exit = 0; -# Input lines, contain only the lines with compiler commands. -my @input = (); +FILE: foreach my $file (@ARGV) { + open my $fh, '<', $file or die "$!: $file"; + + # Hardening options. Not all architectures support all hardening options. + my $harden_format = 1; + my $harden_fortify = 1; + my $harden_stack = 1; + my $harden_relro = 1; + my $harden_bindnow = $option_bindnow; # defaults to 0 + my $harden_pie = $option_pie; # defaults to 0 + + while (my $line = <$fh>) { + # dpkg-buildflags only provides hardening flags since 1.16.1, don't + # check for hardening flags in buildd mode if an older dpkg-dev is + # used. Default flags (-g -O2) are still checked. + # + # Packages which were built before 1.16.1 but used their own hardening + # flags are not checked. + if ($option_buildd and $line =~ /^Toolchain package versions: /) { + require Dpkg::Version; + if ($line !~ /dpkg-dev_(\S+)/ + or Dpkg::Version::version_compare($1, '1.16.1') < 0) { + $harden_format = 0; + $harden_fortify = 0; + $harden_stack = 0; + $harden_relro = 0; + $harden_bindnow = 0; + $harden_pie = 0; + } + } + + # If hardening wrapper is used (wraps calls to gcc and adds hardening + # flags automatically) we can't perform any checks, abort. + if ($line =~ /^Build-Depends: .*\bhardening-wrapper\b/) { + if (not $option_buildd) { + error_hardening_wrapper(); + } else { + print "I-hardening-wrapper-used\n"; + } + $exit |= 1 << 4; + next FILE; + } + + # We skip over unimportant lines at the beginning of the log to + # prevent false positives. + last if $line =~ /^dpkg-buildpackage:/; + } + + # Input lines, contain only the lines with compiler commands. + my @input = (); + + my $continuation = 0; + my $complete_line = undef; + while (my $line = <$fh>) { + # 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 + and $line =~ /^dpkg-buildpackage: host architecture (.+)$/) { + $option_arch = $1; + } -my $start = 0; -my $continuation = 0; -while (my $line = <>) { - # We skip over unimportant lines at the beginning to prevent false - # positives. - $start = 1 if $line =~ /^dpkg-buildpackage:/; - next if not $start; + # Ignore compiler warnings for now. + 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; + } - # Ignore compiler warnings for now. - next if $line =~ /$warning_regex/; + # 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. parse_line() is slow, only use it when necessary. + my @line = (not $line =~ /;/) + ? ($line) + : map { + # Ensure newline at the line end - necessary for + # correct parsing later. + $_ =~ s/\s+$//; + $_ .= "\n"; + } Text::ParseWords::parse_line(';', 1, $line); + foreach $line (@line) { + if ($continuation) { + $continuation = 0; + + # Join lines, but leave the "\" in place so it's clear where + # the original line break was. + chomp $complete_line; + $complete_line .= ' ' . $line; + } + # Line continuation, line ends with "\". + if ($line =~ /\\\s*$/) { + $continuation = 1; + # Start line continuation. + if (not defined $complete_line) { + $complete_line = $line; + } + next; + } + + if (not $continuation) { + # Use the complete line if a line continuation occurred. + if (defined $complete_line) { + $line = $complete_line; + $complete_line = undef; + } + + # Ignore lines with no compiler commands. + 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|(?:C|c)onfigure:) /; + next if $line =~ /^\s*(?:Host\s+)?(?:C\s+)? + (?:C|c)ompiler[\s.]*:?\s+ + $cc_regex_full + (?:\s-std=[a-z0-9:+]+)?\s*$ + /xo + or $line =~ /^\s*(?:- )?(?:HOST_)?(?:CC|CXX)\s*=\s*$cc_regex_full\s*$/o + or $line =~ /^\s*-- Check for working (?:C|CXX) compiler: / + or $line =~ /^\s*(?:echo )?Using [A-Z_]+\s*=\s*/; + # `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, @def_cflags_pie, @def_ldflags_pie); + $harden_bindnow = 1 if any_flags_used($line, @def_ldflags_bindnow); + + push @input, $line; + } + } + } + + close $fh; + + if (scalar @input == 0) { + print "No compiler commands!\n"; + $exit |= 1; + next FILE; + } + + if ($option_buildd) { + $statistics{commands} += scalar @input; + } - # Check if this line indicates a non verbose build. - my $non_verbose = is_non_verbose_build($line); + # Option or auto detected. + if ($option_arch) { + # The following was partially copied from dpkg-dev 1.16.1.2 + # (/usr/share/perl5/Dpkg/Vendor/Debian.pm, add_hardening_flags()), + # copyright Raphaël Hertzog , Kees Cook + # , Canonical, Ltd. licensed under GPL version 2 or + # later. Keep it in sync. - # One line may contain multiple commands (";"). Treat each one as single - # line. - my @line = split /(? [-h -? --help] - -B [--pie] [--bindnow] [--all] +B [options] .. + --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 - --pie force +pie check - --bindnow force +bindbow check - --all force +all (+pie, +bindnow) check =head1 DESCRIPTION @@ -415,38 +870,63 @@ other important warnings. It's licensed under the GPL 3 or later. =over 8 -=item B<-h -? --help> +=item B<--all> -Print available options. +Force check for all +all (+pie, +bindnow) hardening flags. By default it's +auto detected. -=item B<--version> +=item B<--arch> -Print version number and license. +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 +changes are in effect: + +=over 2 + +=item + +Don't check hardening flags in old log files (if dpkg-dev << 1.16.1 is +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<--bindnow> +=item B<-h -? --help> -Force check for all +bindnow hardening flags. By default it's auto detected. +Print available options. -=item B<--all> +=item B<--version> -Force check for all +all (+pie, +bindnow) hardening flags. By default it's -auto detected. +Print version number and license. =back -Auto detection only works if at least one command uses the required hardening -flag (e.g. -fPIE). Then it's required for all other commands as well. +Auto detection for B<--pie> and B<--bindnow> only works if at least one +command uses the required hardening flag (e.g. -fPIE). Then it's required for +all other commands as well. =head1 EXIT STATUS 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> @@ -468,6 +948,10 @@ Non verbose build. Missing hardening flags. +=item B<16> + +Hardening wrapper detected, no tests performed. + =back =head1 AUTHOR