]> ruderich.org/simon Gitweb - blhc/blhc.git/blob - bin/blhc
Support versioned libraries (e.g. test.so.1.0.0).
[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\b|gcc\b|g\+\+|c\+\+)/;
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     # Is this a compiler or linker command?
269     my $compiler = 1;
270     my $linker   = 0;
271
272     # Linker commands.
273     if ($line =~ /\s-l[A-Za-z0-9.-]+(\s|\\|$)/
274             or $line =~ m{\s-o ([A-Za-z0-9_/.-]+/)?[A-Za-z0-9_-]+(\.so([0-9.])*|\.la)?(\s|\\|\$)}
275             or $line =~ /^libtool: link: /
276             or $line =~ m{\s*/bin/bash .+?libtool\s+(.+?\s+)?--mode=(re)?link}) {
277         $compiler = 0;
278         $linker   = 1;
279     }
280
281     # If there are source files then it's compiling/linking in one step and we
282     # must check both.
283     if ($line =~ /\.(c|cc|cpp)\b/) {
284         $compiler = 1;
285     }
286
287     # Check hardening flags.
288     my @missing;
289     if ($compiler and not all_flags_used($line, \@missing, @cflags)
290             # Libraries linked with -fPIC don't have to (and can't) be linked
291             # with -fPIE as well. It's no error if only PIE flags are missing.
292             and not pic_pie_conflict($line, $pie, \@missing, @cflags_pie)) {
293         error_flags('CFLAGS missing', \@missing, \%flag_renames, $line);
294         $exit |= 1 << 2;
295     }
296     if ($compiler and not all_flags_used($line, \@missing, @cppflags)) {
297         error_flags('CPPFLAGS missing', \@missing, \%flag_renames, $line);
298         $exit |= 1 << 2;
299     }
300     if ($linker and not all_flags_used($line, \@missing, @ldflags)
301             # Same here, -fPIC conflicts with -fPIE.
302             and not pic_pie_conflict($line, $pie, \@missing, @ldflags_pie)) {
303         error_flags('LDFLAGS missing', \@missing, \%flag_renames, $line);
304         $exit |= 1 << 2;
305     }
306 }
307
308 exit $exit;
309
310
311 __END__
312
313 =head1 NAME
314
315 blhc - build log hardening check, checks build logs for missing hardening flags
316
317 =head1 SYNOPSIS
318
319 B<blhc> [-h -? --help]
320
321 B<blhc> [--pie] [--bindnow] [--all]
322
323     --help                  available options
324     --version               version number and license
325     --pie                   force +pie check
326     --bindnow               force +bindbow check
327     --all                   force +all (+pie, +bindnow) check
328
329 =head1 DESCRIPTION
330
331 blhc is a small tool which checks build logs for missing hardening flags and
332 other important warnings. It's licensed under the GPL 3 or later.
333
334 =head1 OPTIONS
335
336 =over 8
337
338 =item B<-h -? --help>
339
340 Print available options.
341
342 =item B<--version>
343
344 Print version number and license.
345
346 =item B<--pie>
347
348 Force check for all +pie hardening flags. By default it's auto detected.
349
350 =item B<--bindnow>
351
352 Force check for all +bindnow hardening flags. By default it's auto detected.
353
354 =item B<--all>
355
356 Force check for all +all (+pie, +bindnow) hardening flags. By default it's
357 auto detected.
358
359 =back
360
361 Auto detection only works if at least one command uses the required hardening
362 flag (e.g. -fPIE). Then it's required for all other commands as well.
363
364 =head1 EXIT STATUS
365
366 The exit status is a "bit mask", each listed status is ORed when the error
367 condition occurs to get the result.
368
369 =over 8
370
371 =item B<0>
372
373 Success.
374
375 =item B<1>
376
377 No compiler commands were found.
378
379 =item B<2>
380
381 Invalid arguments/options given to blhc.
382
383 =item B<4>
384
385 Missing hardening flags.
386
387 =back
388
389 =head1 AUTHOR
390
391 Simon Ruderich, E<lt>simon@ruderich.orgE<gt>
392
393 =head1 COPYRIGHT AND LICENSE
394
395 Copyright (C) 2012 by Simon Ruderich
396
397 This program is free software: you can redistribute it and/or modify
398 it under the terms of the GNU General Public License as published by
399 the Free Software Foundation, either version 3 of the License, or
400 (at your option) any later version.
401
402 This program is distributed in the hope that it will be useful,
403 but WITHOUT ANY WARRANTY; without even the implied warranty of
404 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
405 GNU General Public License for more details.
406
407 You should have received a copy of the GNU General Public License
408 along with this program.  If not, see <http://www.gnu.org/licenses/>.
409
410 =cut