]> ruderich.org/simon Gitweb - blhc/blhc.git/blob - bin/blhc
Accept libraries which end with a character (e.g. .so.0d).
[blhc/blhc.git] / bin / blhc
1 #!/usr/bin/perl
2
3 # Build log hardening check, checks build logs for missing hardening flags.
4
5 # Copyright (C) 2012  Simon Ruderich
6 #
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20
21 use strict;
22 use warnings;
23
24 use Getopt::Long ();
25 use Term::ANSIColor ();
26
27 our $VERSION = '0.01';
28
29
30 # FUNCTIONS
31
32 sub error_flags {
33     my ($message, $missing_flags_ref, $flag_renames_ref, $line) = @_;
34
35     # Rename flags if requested.
36     my @missing_flags = map {
37         (exists $flag_renames_ref->{$_})
38             ? $flag_renames_ref->{$_}
39             : $_
40     } @{$missing_flags_ref};
41
42     my $flags = join ' ', @missing_flags;
43     printf "%s (%s)%s %s",
44            error_color($message, 'red'), $flags, error_color(':', 'yellow'),
45            $line;
46 }
47 sub error_color {
48     my ($message, $color) = @_;
49
50     # Use colors when writing to a terminal.
51     if (-t STDOUT) {
52         return Term::ANSIColor::colored($message, $color);
53     } else {
54         return $message;
55     }
56 }
57
58 sub any_flags_used {
59     my ($line, @flags) = @_;
60
61     foreach my $flag (@flags) {
62         return 1 if $line =~ /\s$flag(\s|\\|$)/;
63     }
64
65     return 0;
66 }
67 sub all_flags_used {
68     my ($line, $missing_flags_ref, @flags) = @_;
69
70     my @missing_flags = ();
71     foreach my $flag (@flags) {
72         if ($line !~ /\s$flag(\s|\\|$)/) {
73             push @missing_flags, $flag;
74         }
75     }
76
77     if (scalar @missing_flags == 0) {
78         return 1;
79     }
80
81     @{$missing_flags_ref} = @missing_flags;
82     return 0;
83 }
84
85 # Modifies $missing_flags_ref array.
86 sub pic_pie_conflict {
87     my ($line, $pie, $missing_flags_ref, @flags_pie) = @_;
88
89     return 0 if not $pie;
90     return 0 if not any_flags_used($line, ('-fPIC'));
91
92     my %flags = map { $_ => 1 } @flags_pie;
93
94     # Remove all PIE flags from @missing_flags as they are not required with
95     # -fPIC.
96     my @result = grep {
97         not exists $flags{$_}
98     } @{$missing_flags_ref};
99     @{$missing_flags_ref} = @result;
100
101     # We got a conflict when no flags are left, thus only PIE flags were
102     # missing. If other flags were missing abort because the conflict is not
103     # the problem.
104     return scalar @result == 0;
105 }
106
107
108 # CONSTANTS/VARIABLES
109
110 # Regex to catch (GCC) compiler warnings.
111 my $warning_regex = qr/^(.+?):([0-9]+):[0-9]+: warning: (.+?) \[(.+?)\]$/;
112
113 # Expected hardening flags. All flags are used as regexps.
114 my @cflags = (
115     '-g',
116     '-O2',
117     '-fstack-protector',
118     '--param=ssp-buffer-size=4',
119     '-Wformat',
120     '-Wformat-security',
121     '-Werror=format-security',
122 );
123 my @cflags_pie = (
124     '-fPIE',
125 );
126 my @cppflags = (
127     '-D_FORTIFY_SOURCE=2',
128 );
129 my @ldflags = (
130     '-Wl,(-z,)?relro',
131 );
132 my @ldflags_pie = (
133     '-fPIE',
134     '-pie',
135 );
136 my @ldflags_bindnow = (
137     '-Wl,(-z,)?now',
138 );
139 # All (hardening) flags.
140 my @flags = (@cflags, @cflags_pie,
141              @cppflags,
142              @ldflags, @ldflags_pie, @ldflags_bindnow);
143 # Renaming rules for the output so the regex parts are not visible.
144 my %flag_renames = (
145     '-Wl,(-z,)?relro' => '-Wl,-z,relro',
146     '-Wl,(-z,)?now'   => '-Wl,-z,now',
147 );
148
149
150 # MAIN
151
152 # Additional hardening options.
153 my $pie     = 0;
154 my $bindnow = 0;
155
156 # Parse command line arguments.
157 my $option_all     = 0;
158 my $option_help    = 0;
159 my $option_version = 0;
160 if (not Getopt::Long::GetOptions(
161             'help|h|?' => \$option_help,
162             'version'  => \$option_version,
163             'pie'      => \$pie,
164             'bindnow'  => \$bindnow,
165             'all'      => \$option_all,
166         )) {
167     require Pod::Usage;
168     Pod::Usage::pod2usage(2);
169 }
170 if ($option_help) {
171     require Pod::Usage;
172     Pod::Usage::pod2usage(1);
173 }
174 if ($option_version) {
175     print "blhc $VERSION  Copyright (C) 2012  Simon Ruderich
176
177 This program is free software: you can redistribute it and/or modify
178 it under the terms of the GNU General Public License as published by
179 the Free Software Foundation, either version 3 of the License, or
180 (at your option) any later version.
181
182 This program is distributed in the hope that it will be useful,
183 but WITHOUT ANY WARRANTY; without even the implied warranty of
184 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
185 GNU General Public License for more details.
186
187 You should have received a copy of the GNU General Public License
188 along with this program.  If not, see <http://www.gnu.org/licenses/>.
189 ";
190     exit 0;
191 }
192
193 if ($option_all) {
194     $pie     = 1;
195     $bindnow = 1;
196 }
197
198 # Final exit code.
199 my $exit = 0;
200
201 # Input lines, contain only the lines with compiler commands.
202 my @input = ();
203
204 my $continuation = 0;
205 while (my $line = <>) {
206     # Ignore compiler warnings for now.
207     next if $line =~ /$warning_regex/;
208
209     # One line may contain multiple commands (";"). Treat each one as single
210     # line.
211     my @line = split /(?<!\\);/, $line;
212     foreach $line (@line) {
213         # Add newline, drop all other whitespace at the end of a line.
214         $line =~ s/\s+$//;
215         $line .= "\n";
216
217         if ($continuation) {
218             $continuation = 0;
219
220             # Join lines, but leave the "\" in place so it's clear where the
221             # original line break was.
222             chomp $input[-1];
223             $input[-1] .= ' ' . $line;
224
225         } else {
226             # Ignore lines with no compiler commands.
227             next if $line !~ /\b(cc|gcc|g\+\+|c\+\+)(\s|\\)/;
228
229             # Ignore false positives.
230             #
231             # `./configure` output.
232             if ($line =~ /^checking /) {
233                 next;
234             }
235
236             push @input, $line;
237         }
238
239         # Line continuation, line ends with "\".
240         if ($line =~ /\\\s*$/) {
241             $continuation = 1;
242         }
243     }
244 }
245
246 if (scalar @input == 0) {
247     print "No compiler commands!\n";
248     $exit |= 1;
249     exit $exit;
250 }
251
252 # Check if additional hardening options were used. Used to ensure they are
253 # used for the complete build.
254 foreach my $line (@input) {
255     $pie     = 1 if any_flags_used($line, @cflags_pie, @ldflags_pie);
256     $bindnow = 1 if any_flags_used($line, @ldflags_bindnow);
257 }
258
259 if ($pie) {
260     @cflags  = (@cflags,  @cflags_pie);
261     @ldflags = (@ldflags, @ldflags_pie);
262 }
263 if ($bindnow) {
264     @ldflags = (@ldflags, @ldflags_bindnow);
265 }
266
267 foreach my $line (@input) {
268     # Ignore false positives.
269     #
270     # ./configure summary.
271     next if $line =~ /^Compiler:\s+(cc|gcc|g\+\+|c\+\+)$/;
272
273     # Is this a compiler or linker command?
274     my $compiler = 1;
275     my $linker   = 0;
276
277     # Linker commands.
278     if ($line =~ m{\s-o                  # -o
279                    [\s\\]*\s+            # possible line continuation
280                    ([A-Za-z0-9_/.-]+/)?  # path to file
281                    [A-Za-z0-9_-]+        # binary name (no dots!)
282                    (\.so[0-9.]*[a-z]?|\.la)? # library (including version)
283                    (\s|\\|\$)            # end of file name
284                   }x
285             or $line =~ /^libtool: link: /
286             or $line =~ m{\s*/bin/bash .+?libtool\s+(.+?\s+)?--mode=(re)?link}) {
287         $compiler = 0;
288         $linker   = 1;
289     }
290
291     # If there are source files then it's compiling/linking in one step and we
292     # must check both.
293     if ($line =~ /\.(c|cc|cpp)\b/) {
294         $compiler = 1;
295     }
296
297     # Check hardening flags.
298     my @missing;
299     if ($compiler and not all_flags_used($line, \@missing, @cflags)
300             # Libraries linked with -fPIC don't have to (and can't) be linked
301             # with -fPIE as well. It's no error if only PIE flags are missing.
302             and not pic_pie_conflict($line, $pie, \@missing, @cflags_pie)) {
303         error_flags('CFLAGS missing', \@missing, \%flag_renames, $line);
304         $exit |= 1 << 2;
305     }
306     if ($compiler and not all_flags_used($line, \@missing, @cppflags)) {
307         error_flags('CPPFLAGS missing', \@missing, \%flag_renames, $line);
308         $exit |= 1 << 2;
309     }
310     if ($linker and not all_flags_used($line, \@missing, @ldflags)
311             # Same here, -fPIC conflicts with -fPIE.
312             and not pic_pie_conflict($line, $pie, \@missing, @ldflags_pie)) {
313         error_flags('LDFLAGS missing', \@missing, \%flag_renames, $line);
314         $exit |= 1 << 2;
315     }
316 }
317
318 exit $exit;
319
320
321 __END__
322
323 =head1 NAME
324
325 blhc - build log hardening check, checks build logs for missing hardening flags
326
327 =head1 SYNOPSIS
328
329 B<blhc> [-h -? --help]
330
331 B<blhc> [--pie] [--bindnow] [--all]
332
333     --help                  available options
334     --version               version number and license
335     --pie                   force +pie check
336     --bindnow               force +bindbow check
337     --all                   force +all (+pie, +bindnow) check
338
339 =head1 DESCRIPTION
340
341 blhc is a small tool which checks build logs for missing hardening flags and
342 other important warnings. It's licensed under the GPL 3 or later.
343
344 =head1 OPTIONS
345
346 =over 8
347
348 =item B<-h -? --help>
349
350 Print available options.
351
352 =item B<--version>
353
354 Print version number and license.
355
356 =item B<--pie>
357
358 Force check for all +pie hardening flags. By default it's auto detected.
359
360 =item B<--bindnow>
361
362 Force check for all +bindnow hardening flags. By default it's auto detected.
363
364 =item B<--all>
365
366 Force check for all +all (+pie, +bindnow) hardening flags. By default it's
367 auto detected.
368
369 =back
370
371 Auto detection only works if at least one command uses the required hardening
372 flag (e.g. -fPIE). Then it's required for all other commands as well.
373
374 =head1 EXIT STATUS
375
376 The exit status is a "bit mask", each listed status is ORed when the error
377 condition occurs to get the result.
378
379 =over 8
380
381 =item B<0>
382
383 Success.
384
385 =item B<1>
386
387 No compiler commands were found.
388
389 =item B<2>
390
391 Invalid arguments/options given to blhc.
392
393 =item B<4>
394
395 Missing hardening flags.
396
397 =back
398
399 =head1 AUTHOR
400
401 Simon Ruderich, E<lt>simon@ruderich.orgE<gt>
402
403 =head1 COPYRIGHT AND LICENSE
404
405 Copyright (C) 2012 by Simon Ruderich
406
407 This program is free software: you can redistribute it and/or modify
408 it under the terms of the GNU General Public License as published by
409 the Free Software Foundation, either version 3 of the License, or
410 (at your option) any later version.
411
412 This program is distributed in the hope that it will be useful,
413 but WITHOUT ANY WARRANTY; without even the implied warranty of
414 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
415 GNU General Public License for more details.
416
417 You should have received a copy of the GNU General Public License
418 along with this program.  If not, see <http://www.gnu.org/licenses/>.
419
420 =cut