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