From 1a3e1caffe707e71fd3cf99ffaa4547f7fda017a Mon Sep 17 00:00:00 2001 From: Michael Rash Date: Tue, 4 Oct 2011 23:15:04 -0400 Subject: [PATCH] Initial start on a test suite This commit begins development on a comprehensive test suite for fwknop. The initial tests are focused on compilation correctness and security options as determined by the "hardening-check" script from Kees Cook of the Debian security team. --- test/hardening-check | 269 ++++++++++++++++++++++++ test/test-fwknop.pl | 481 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 750 insertions(+) create mode 100755 test/hardening-check create mode 100755 test/test-fwknop.pl diff --git a/test/hardening-check b/test/hardening-check new file mode 100755 index 00000000..a6ad688b --- /dev/null +++ b/test/hardening-check @@ -0,0 +1,269 @@ +#!/bin/sh +# Report the hardening characterists of a set of binaries. +# Copyright (C) 2009, 2010 Kees Cook +# License: GPLv2 or newer + +skip_pie=no +skip_stackprotector=no +skip_fortify=no +skip_relro=no +skip_bindnow=no +quiet=no +while getopts psfrbq opt +do + case "$opt" in + p) skip_pie=yes ;; + s) skip_stackprotector=yes ;; + f) skip_fortify=yes ;; + r) skip_relro=yes ;; + b) skip_bindnow=yes ;; + q) quiet=yes ;; + [?]) + echo >&2 "Usage: $0 [-p] [-s] [-f] [-r] [-b] file ..." + echo >&2 " -p Do not require PIE binary" + echo >&2 " -s Do not require stack protector" + echo >&2 " -f Do not require fortify source" + echo >&2 " -r Do not require RELRO markings" + echo >&2 " -b Do not require BIND_NOW markings" + echo >&2 " -q Only report failures" + exit 1 ;; + esac +done +shift $(( $OPTIND-1 )) + +overall=0 + +rc=0 +report="" + +good () { + if [ "$quiet" != "yes" ]; then + report="$report +$1" + fi +} + +bad () { + report="$report +$1" + rc=1 +} + +for file in "$@" +do + rc=0 + report="$file:" + + PROG_REPORT=$(LANG=C readelf -lW "$file") + if [ -z "$PROG_REPORT" ]; then rc=1; continue; fi + DYN_REPORT=$(LANG=C readelf -dW "$file" 2>/dev/null) + RELOC_REPORT=$(LANG=C readelf -sW "$file" 2>/dev/null | \ + egrep ' FUNC .* UND ' | \ + sed -re 's/ \([0-9]+\)$//g; s/.* //g; s/@.*//g;') + + # PIE + # First, verify this is an executable, not a library. This seems to be + # best seen by checking for the PHDR program header. + name=" Position Independent Executable" + if echo "$PROG_REPORT" | awk '{print $1}' | grep -q '^PHDR$'; then + if echo "$PROG_REPORT" | grep -q '^Elf file type is DYN '; then + good "$name: yes" + else + msg="$name: no, normal executable!" + if [ "$skip_pie" = "yes" ]; then + good "$msg (ignored)" + else + bad "$msg" + fi + fi + else + if echo "$PROG_REPORT" | grep -q '^Elf file type is DYN '; then + good "$name: no, regular shared library (ignored)" + else + bad "$name: not a known ELF type!?" + fi + fi + + # Stack-protected + name=" Stack protected" + if echo "$RELOC_REPORT" | grep -q '^__stack_chk_fail$'; then + good "$name: yes" + else + msg="$name: no, not found!" + if [ "$skip_stackprotector" = "yes" ]; then + good "$msg (ignored)" + else + bad "$msg" + fi + fi + + # Fortified + name=" Fortify Source functions" + if echo "$RELOC_REPORT" | grep -q '^__.*_chk$'; then + good "$name: yes" + else + msg="$name: no, not found!" + if [ "$skip_fortify" = "yes" ]; then + good "$msg (ignored)" + else + bad "$msg" + fi + fi + + # Format + # unfortunately, I haven't thought of a way to test for this after + # compilation. What it really needs is a lintian-like check that + # reviews the build logs and looks for the warnings, or that the + # argument is changed to use -Werror,format-security to stop the build. + + # RELRO + name=" Read-only relocations" + if echo "$PROG_REPORT" | awk '{print $1}' | grep -q '^GNU_RELRO$'; then + good "$name: yes" + else + msg="$name: no, not found!" + if [ "$skip_relro" = "yes" ]; then + good "$msg (ignored)" + else + bad "$msg" + fi + fi + + # BIND_NOW + name=" Immediate binding" + if echo "$DYN_REPORT" | awk '{print $2}' | grep -q '^(BIND_NOW)$'; then + good "$name: yes" + else + msg="$name: no, not found!" + if [ "$skip_bindnow" = "yes" ]; then + good "$msg (ignored)" + else + bad "$msg" + fi + fi + + if [ "$quiet" != "yes" ] || [ $rc -ne 0 ]; then + echo "$report" + fi + + if [ $rc -ne 0 ]; then + overall=$rc + fi +done + +exit $overall + +:<<=cut + +=pod + +=head1 NAME + +hardening-check - check binaries for security hardening features + +=head1 SYNOPSIS + +Examine a given set of ELF binaries and check for several security hardening +features, failing if they are not all found. + +=head1 DESCRIPTION + +This utility checks a given list of ELF binaries for several security +hardening features that can be compiled into an executable. These +features are: + +=over 8 + +=item B + +This indicates that the executable was built in such a way (PIE) that +the "text" section of the program can be relocated in memory. To take +full advantage of this feature, the executing kernel must support text +Address Space Layout Randomization (ASLR). + +=item B + +This indicates that the executable was compiled with the L +option B<-fstack-protector>. The program will be resistant to have its +stack overflowed. + +=item B + +This indicates that the executable was compiled with +B<-D_FORTIFY_SOURCE=2> and B<-O2> or higher. This causes certain unsafe +glibc functions with their safer counterparts (e.g. strncpy instead +of strcpy). + +=item B + +This indicates that the executable was build with B<-Wl,-z,relro> to +have ELF markings (RELRO) that ask the runtime linker to mark any +regions of the relocation table as "read-only" if they were resolved +before execution begins. This reduces the possible areas of memory in +a program that can be used by an attacker that performs a successful +memory corruption exploit. + +=item B + +This indicates that the executable was built with B<-Wl,-z,now> to have +ELF markings (BIND_NOW) that ask the runtime linker to resolve all +relocations before starting program execution. When combined with RELRO +above, this further reduces the regions of memory available to memory +corruption attacks. + +=back + +=head1 OPTIONS + +=over 8 + +=item B<-p> + +No not require that the checked binaries be built as PIE. + +=item B<-s> + +No not require that the checked binaries be built with the stack protector. + +=item B<-f> + +No not require that the checked binaries be built with Fority Source. + +=item B<-r> + +No not require that the checked binaries be built with RELRO. + +=item B<-b> + +No not require that the checked binaries be built with BIND_NOW. + +=item B<-q> + +Only report failures. + +=back + +=head1 RETURN VALUE + +When all checked binaries have all checkable hardening features detected, +this program will finish with an exit code of 0. If any check fails, the +exit code with be 1. Individual checks can be disabled via command line +options. + +=head1 AUTHOR + +Kees Cook + +=head1 COPYRIGHT AND LICENSE + +Copyright 2009 Kees Cook . + +This program is free software; you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation; version 2 or later. + +=head1 SEE ALSO + +L, L + +=cut diff --git a/test/test-fwknop.pl b/test/test-fwknop.pl new file mode 100755 index 00000000..a4e59a1c --- /dev/null +++ b/test/test-fwknop.pl @@ -0,0 +1,481 @@ +#!/usr/bin/perl -w + +use Data::Dumper; +use Getopt::Long 'GetOptions'; +use strict; + +#==================== config ===================== +my $logfile = 'test.log'; +my $output_dir = 'output'; +my $conf_dir = 'conf'; + +my $fwknopCmd = '../client/.libs/fwknop'; +my $fwknopdCmd = '../server/.libs/fwknopd'; +#================== end config =================== + +my $passed = 0; +my $failed = 0; +my $executed = 0; +my $test_include = ''; +my @tests_to_include = (); +my $test_exclude = ''; +my @tests_to_exclude = (); +my $list_mode = 0; +my $loopback_intf = 'lo'; ### default on linux +my $prepare_results = 0; +my $current_test_file = ''; +my $help = 0; +my $YES = 1; +my $NO = 0; +my $APPEND = 1; +my $CREATE = 0; +my $PRINT_LEN = 68; +my $REQUIRED = 1; +my $OPTIONAL = 0; + +exit 1 unless GetOptions( + 'Prepare-results' => \$prepare_results, + 'fwknop-path=s' => \$fwknopCmd, + 'fwknopd-path=s' => \$fwknopdCmd, + 'loopback-intf=s' => \$loopback_intf, + 'test-include=s' => \$test_include, + 'include=s' => \$test_include, ### synonym + 'test-exclude=s' => \$test_exclude, + 'exclude=s' => \$test_exclude, ### synonym + 'List-mode' => \$list_mode, + 'help' => \$help +); + +&usage() if $help; + +### main array that defines the tests we will run +my @tests = ( + { + 'category' => 'build', + 'detail' => 'recompile and look for compilation warnings', + 'err_msg' => 'compile warnings exist', + 'function' => \&compile_warnings, + 'fatal' => $NO + }, + { + 'category' => 'build', + 'subcategory' => 'client', + 'detail' => 'new binary exists', + 'err_msg' => 'binary not found', + 'function' => \&binary_exists_fwknop_client, + 'fatal' => $YES + }, + { + 'category' => 'build', + 'subcategory' => 'server', + 'detail' => 'new binary exists', + 'err_msg' => 'binary not found', + 'function' => \&binary_exists_fwknopd_server, + 'fatal' => $YES + }, + { + 'category' => 'build', + 'subcategory' => 'client', + 'detail' => 'Position Independent Executable (PIE)', + 'err_msg' => 'non PIE binary (fwknop client)', + 'function' => \&pie_binary_fwknop_client, + 'fatal' => $NO + }, + { + 'category' => 'build', + 'subcategory' => 'server', + 'detail' => 'Position Independent Executable (PIE)', + 'err_msg' => 'non PIE binary (fwknopd server)', + 'function' => \&pie_binary_fwknopd_server,, + 'fatal' => $NO + }, + { + 'category' => 'build', + 'subcategory' => 'client', + 'detail' => 'stack protected binary', + 'err_msg' => 'non stack protected binary (fwknop client)', + 'function' => \&stack_protected_binary_fwknop_client, + 'fatal' => $NO + }, + { + 'category' => 'build', + 'subcategory' => 'server', + 'detail' => 'stack protected binary', + 'err_msg' => 'non stack protected binary (fwknopd server)', + 'function' => \&stack_protected_binary_fwknopd_server,, + 'fatal' => $NO + }, + { + 'category' => 'build', + 'subcategory' => 'client', + 'detail' => 'fortify source functions', + 'err_msg' => 'source functions not fortified (fwknop client)', + 'function' => \&fortify_source_functions_binary_fwknop_client, + 'fatal' => $NO + }, + { + 'category' => 'build', + 'subcategory' => 'server', + 'detail' => 'fortify source functions', + 'err_msg' => 'source functions not fortified (fwknopd server)', + 'function' => \&fortify_source_functions_binary_fwknopd_server,, + 'fatal' => $NO + }, + { + 'category' => 'build', + 'subcategory' => 'client', + 'detail' => 'read-only relocations', + 'err_msg' => 'no read-only relocations (fwknop client)', + 'function' => \&read_only_relocations_binary_fwknop_client, + 'fatal' => $NO + }, + { + 'category' => 'build', + 'subcategory' => 'server', + 'detail' => 'read-only relocations', + 'err_msg' => 'no read-only relocations (fwknopd server)', + 'function' => \&read_only_relocations_binary_fwknopd_server,, + 'fatal' => $NO + }, + { + 'category' => 'build', + 'subcategory' => 'client', + 'detail' => 'immediate binding', + 'err_msg' => 'no immediate binding (fwknop client)', + 'function' => \&immediate_binding_binary_fwknop_client, + 'fatal' => $NO + }, + { + 'category' => 'build', + 'subcategory' => 'server', + 'detail' => 'immediate binding', + 'err_msg' => 'no immediate binding (fwknopd server)', + 'function' => \&immediate_binding_binary_fwknopd_server,, + 'fatal' => $NO + }, + + { + 'category' => 'basic operations', + 'detail' => 'client SPA packet generation', + 'err_msg' => 'could not generate SPA packet', + 'function' => \&generate_basic_spa_packet, + 'fatal' => $YES + } +); + +my %test_keys = ( + 'category' => $REQUIRED, + 'subcategory' => $OPTIONAL, + 'detail' => $REQUIRED, + 'function' => $REQUIRED, + 'fatal' => $OPTIONAL, +); + +### make sure everything looks as expected before continuing +&init(); + +&logr("\n"); + +for my $test_hr (@tests) { + &run_test($test_hr); +} + +&logr("\n[+] passed/failed/executed: $passed/$failed/$executed tests\n\n"); + +exit 0; + +#===================== end main ======================= + +sub run_test() { + my $test_hr = shift; + + return unless &process_include_exclude($test_hr); + + $executed++; + + $current_test_file = "$output_dir/$executed.test"; + + my $msg = "[$test_hr->{'category'}]"; + $msg .= " [$test_hr->{'subcategory'}]" if $test_hr->{'subcategory'}; + $msg .= " $test_hr->{'detail'}"; + + if ($list_mode) { + print $msg, "\n"; + return; + } + + &dots_print($msg); + + if (&{$test_hr->{'function'}}) { + &logr("pass ($executed)\n"); + $passed++; + } else { + &logr("fail ($executed)\n"); + $failed++; + + if ($test_hr->{'fatal'} eq $YES) { + die "[*] required test failed, exiting."; + } + } + + return; +} + +sub process_include_exclude() { + my $test_hr = shift; + + ### inclusions/exclusions + if (@tests_to_include) { + my $found = 0; + for my $test (@tests_to_include) { + if ($test_hr->{'category'} =~ /$test)/) { + $found = 1; + last; + } + } + return 1 unless $found; + } + if (@tests_to_exclude) { + my $found = 0; + for my $test (@tests_to_exclude) { + if ($test_hr->{'category'} =~ /$test/) { + $found = 1; + last; + } + } + return 0 if $found; + } + return 1; +} + +sub generate_basic_spa_packet() { + return 1; +} + +sub compile_warnings() { + + return 0 unless &run_cmd('make -C .. clean', $CREATE); + return 0 unless &run_cmd('make -C ..', $APPEND); + + ### look for compilation warnings - something like: + + ### warning: ‘test’ is used uninitialized in this function + return 0 if &file_find_regex([qr/\swarning:\s/], $current_test_file); + + return 1; +} + +sub binary_exists_fwknop_client() { + return 0 unless -e $fwknopCmd and -x $fwknopCmd; + return 1; +} + +sub binary_exists_fwknopd_server() { + return 0 unless -e $fwknopdCmd and -x $fwknopdCmd; + return 1; +} + +### check for PIE +sub pie_binary_fwknop_client() { + return 0 unless &run_cmd("./hardening-check $fwknopCmd", $CREATE); + return 0 if &file_find_regex([qr/Position\sIndependent.*:\sno/i], + $current_test_file); + return 1; +} + +sub pie_binary_fwknopd_server() { + return 0 unless &run_cmd("./hardening-check $fwknopdCmd", $CREATE); + return 0 if &file_find_regex([qr/Position\sIndependent.*:\sno/i], + $current_test_file); + return 1; +} + +### check for stack protection +sub stack_protected_binary_fwknop_client() { + return 0 unless &run_cmd("./hardening-check $fwknopCmd", $CREATE); + return 0 if &file_find_regex([qr/Stack\sprotected.*:\sno/i], + $current_test_file); + return 1; +} + +sub stack_protected_binary_fwknopd_server() { + return 0 unless &run_cmd("./hardening-check $fwknopdCmd", $CREATE); + return 0 if &file_find_regex([qr/Stack\sprotected:\sno/i], + $current_test_file); + return 1; +} + +### check for fortified source functions +sub fortify_source_functions_binary_fwknop_client() { + return 0 unless &run_cmd("./hardening-check $fwknopCmd", $CREATE); + return 0 if &file_find_regex([qr/Fortify\sSource\sfunctions:\sno/i], + $current_test_file); + return 1; +} + +sub fortify_source_functions_binary_fwknopd_server() { + return 0 unless &run_cmd("./hardening-check $fwknopdCmd", $CREATE); + return 0 if &file_find_regex([qr/Fortify\sSource\sfunctions:\sno/i], + $current_test_file); + return 1; +} + +### check for read-only relocations +sub read_only_relocations_binary_fwknop_client() { + return 0 unless &run_cmd("./hardening-check $fwknopCmd", $CREATE); + return 0 if &file_find_regex([qr/Read.only\srelocations:\sno/i], + $current_test_file); + return 1; +} + +sub read_only_relocations_binary_fwknopd_server() { + return 0 unless &run_cmd("./hardening-check $fwknopdCmd", $CREATE); + return 0 if &file_find_regex([qr/Read.only\srelocations:\sno/i], + $current_test_file); + return 1; +} + +### check for immediate binding +sub immediate_binding_binary_fwknop_client() { + return 0 unless &run_cmd("./hardening-check $fwknopCmd", $CREATE); + return 0 if &file_find_regex([qr/Immediate\sbinding:\sno/i], + $current_test_file); + return 1; +} + +sub immediate_binding_binary_fwknopd_server() { + return 0 unless &run_cmd("./hardening-check $fwknopdCmd", $CREATE); + return 0 if &file_find_regex([qr/Immediate\sbinding:\sno/i], + $current_test_file); + return 1; +} + +sub run_cmd() { + my ($cmd, $file_mode) = @_; + + if ($file_mode == $APPEND) { + open F, ">> $current_test_file" + or die "[*] Could not open $current_test_file: $!"; + print F "CMD: $cmd\n"; + close F; + } else { + open F, "> $current_test_file" + or die "[*] Could not open $current_test_file: $!"; + print F "CMD: $cmd\n"; + close F; + } + my $rv = ((system "$cmd >> $current_test_file 2>&1") >> 8); + if ($rv == 0) { + return 1; + } + return 0; +} + +sub pass() { + return; +} + +sub dots_print() { + my $msg = shift; + &logr($msg); + my $dots = ''; + for (my $i=length($msg); $i < $PRINT_LEN; $i++) { + $dots .= '.'; + } + &logr($dots); + return; +} + +sub init() { + + ### validate test hashes + my $hash_num = 0; + for my $test_hr (@tests) { + for my $key (keys %test_keys) { + if ($test_keys{$key} == $REQUIRED) { + die "[*] Missing '$key' element in hash: $hash_num" + unless defined $test_hr->{$key}; + } else { + $test_hr->{$key} = '' unless defined $test_hr->{$key}; + } + } + $hash_num++; + } + + $|++; ### turn off buffering + + $< == 0 && $> == 0 or + die "[*] $0: You must be root (or equivalent ", + "UID 0 account) to effectively test fwknop"; + + die "[*] $conf_dir directory does not exist." unless -d $conf_dir; + unless (-d $output_dir) { + mkdir $output_dir or die "[*] Could not mkdir $output_dir: $!"; + } + + for my $file (glob("$output_dir/*.test")) { + unlink $file or die "[*] Could not unlink($file)"; + } + + if (-e $logfile) { + unlink $logfile or die $!; + } + + unless ((&find_command('cc') or &find_command('gcc')) and &find_command('make')) { + ### disable compilation checks + push @tests_to_exclude, 'compile'; + } + + if ($test_include) { + @tests_to_include = split /\s*,\s*/, $test_include; + } + if ($test_exclude) { + @tests_to_exclude = split /\s*,\s*/, $test_exclude; + } + + return; +} + +sub file_find_regex() { + my ($re_ar, $file) = @_; + + my $found = 0; + + open F, "< $file" or die "[*] Could not open $file: $!"; + LINE: while () { + my $line = $_; + for my $re (@$re_ar) { + if ($line =~ $re) { + $found = 1; + last LINE; + } + } + } + close F; + + return $found; +} + +sub find_command() { + my $cmd = shift; + + my $found = 0; + open C, "which $cmd |" or die "[*] Could not execute: which $cmd: $!"; + while () { + if (m|/.*$cmd$|) { + $found = 1; + last; + } + } + close C; + return $found; +} + +sub logr() { + my $msg = shift; + print STDOUT $msg; + open F, ">> $logfile" or die $!; + print F $msg; + close F; + return; +}