annobin/annobin-3.2/scripts/hardened.sh
2018-01-26 12:36:02 +00:00

815 lines
18 KiB
Bash
Executable File

#!/bin/bash
# Script to check for hardening options in annotated binaries
#
# Created by Nick Clifton.
# Copyright (c) 2017-2018 Red Hat.
#
# This 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; either version 3, or (at your
# option) any later version.
#
# It is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# Usage:
# hardened.sh [switches] file(s)
#
# This script does not handle directories. This is deliberate.
# It is intended that if recursion is needed then it will be
# invoked from find, like this:
#
# find . -type f -exec hardened.sh {} \;
# To Do:
# * Allow arguments to command line options to be separated from the
# the option name by a space. Eg: --readelf foobar
version=3.0
help ()
{
# The following exec goop is so that we don't have to manually
# redirect every message to stderr in this function.
exec 4>&1 # save stdout fd to fd #4
exec 1>&2 # redirect stdout to stderr
cat <<__EOM__
This is a shell script to check that the given file(s) have been
built with the recommended hardening options. These options are:
-O2 (or higher)
-fstack-protector-strong
-D_FORTIFY_SOURCE=2
-Wl,-z,now
-Wl,-z,relro
Plus for shared objects/libraries:
-fPIC
Plus for executables (although on RHEL6 these should be omitted due to a kernel bug):
-fPIE
-Wl,-pie
Usage: $prog {files|options}
{options} are:
-h --help Display this information and exit.
-v --version Report the version number of this script and exit.
-s --silent Produce no output, just an exit status.
-V --verbose Report on progress.
-u --vulnerable Only report files known to be vulnerable. [default]
-n --not-hardened Report any file that is not proven to be hardened.
-a --all Report the hardening status of all files.
[The last one of these on the command line is used].
-f=auto --file-type=auto Automatically distinguish libraries from executables. [default]
-f=lib --file-type=lib Assume all files are shared libraries.
-f=exec --file-type=exec Assume all files are executables.
-f=obj --filetype=obj Assume all files are object files/archives.
[The last one of these on the command line is used].
-k=opt --skip=opt Skip check of optimization level
-k=stack --skip=stack Skip check of stack-protector status
-k=fort --skip=fort Skip check of fortify source status
-k=now --skip=now Skip check of BIND_NOW status
-k=relro --skip=relro Skip check of RELRO status
-k=pic --skip=pic Skip check for PIC/PIE compilation. (Good for RHEL-6 binaries)
-k=operator --skip=operator Skip check for operator[] range testing.
-k=clash --skip=clash Skip check for stack clash protection.
[These options stack]
-i --ignore-unknown Silently skip any file that is not an ELF binary.
-r=<PATH> --readelf=<PATH> Path to version of readelf to use.
-t=<PATH> --tmpfile=<PATH> Temporary file to use.
-- Stop accumulating options. Any text that follows this
option is assumed to be a file name, even if it starts
with a dash.
__EOM__
exec 1>&4 # Copy stdout fd back from temporary save fd, #4
}
main ()
{
init
parse_args ${1+"$@"}
scan_files
if [ $failed -ne 0 ];
then
exit 1
else
exit 0
fi
}
report ()
{
if [ $report -ne 0 ]
then
echo $prog":" ${1+"$@"}
fi
}
ICE ()
{
report "Internal error: " ${1+"$@"}
exit 1
}
verbose ()
{
if [ $verb -ne 0 ]
then
echo $prog":" ${1+"$@"}
fi
}
maybe ()
{
if [ $report -gt 1 ]
then
echo $prog": $file: MAYBE:" ${1+"$@"}
fi
vulnerable=1
}
fail ()
{
if [ $report -gt 0 ]
then
echo $prog": $file: FAIL:" ${1+"$@"}
fi
vulnerable=1
}
pass ()
{
if [ $report -gt 2 ]
then
echo $prog": $file: PASS:" ${1+"$@"}
fi
}
# Initialise global variables.
init ()
{
files[0]="";
# num_files is the number of files to be listed minus one.
# This is because we are indexing the files[] array from zero.
num_files=0;
failed=0
report=1 # Quad-state, 0=> report nothing, 1=> report known vulnerable, 2=> report not proven hardened, 3=> report all
verb=0
filetype=auto
skip_opt=0
skip_stack=0
skip_fortify=0
skip_bind_now=0
skip_relro=0
skip_pic=0
skip_operator=0
skip_clash=0
ignore_unknown=0
scanner=readelf
tmpfile=/dev/shm/hardened.delme
}
# Parse our command line
parse_args ()
{
prog=`basename $0`;
# Locate any additional command line switches
# Likewise accumulate non-switches to the files list.
while [ $# -gt 0 ]
do
optname="`echo $1 | sed 's,=.*,,'`"
optarg="`echo $1 | sed 's,^[^=]*=,,'`"
case "$optname" in
-v | --version)
report "version: $version"
exit 0
;;
-h | --help)
help
exit 0
;;
-s | --silent)
report=0;
verb=0;
;;
-V | --verbose)
verb=1;
;;
-u | --vulnerable)
report=1;
;;
-n | --not-hardened)
report=2;
;;
-a | --all)
report=3;
;;
-f | --file-type)
case "$optarg" in
auto)
filetype=auto
;;
exec)
filetype=exec
;;
lib)
filetype=library
;;
obj)
filetype=object
;;
*)
report "unknown file type: $optarg"
;;
esac
;;
-k | --skip)
case "$optarg" in
opt)
skip_opt=1
;;
stack)
skip_stack=1
;;
fort)
skip_fortify=1
;;
now)
skip_bind_now=1
;;
relro)
skip_relro=1
;;
pic)
skip_pic=1;
;;
operator)
skip_operator=1;
;;
clash)
skip_clash=1;
;;
*)
report "unknown option skip: $optarg"
;;
esac
;;
-r | --readelf)
scanner="$optarg"
;;
-t | --tmpfile)
tmpfile="$optarg"
;;
-i | --ignore-unknown)
ignore_unknown=1;
;;
--)
break;
;;
--*)
report "unrecognised option: $1"
help
exit 1
;;
*)
files[$num_files]="$1";
let "num_files++"
;;
esac
shift
done
# Accumulate any remaining arguments without processing them.
while [ $# -gt 0 ]
do
files[$num_files]="$1";
let "num_files++";
shift
done
if [ $num_files -gt 0 ];
then
# Remember that we are counting from zero not one.
let "num_files--"
else
report "must specify at least one file to scan"
exit 1
fi
}
scan_files ()
{
local i
i=0;
while [ $i -le $num_files ]
do
scan_file i
let "i++"
done
}
scan_file ()
{
local file
# Paranoia checks - the user should never encounter these.
if test "x$1" = "x" ;
then
ICE "scan_file called without an argument"
fi
if test "x$2" != "x" ;
then
ICE "scan_file called with too many arguments"
fi
# Use quotes when accessing files in order to preserve
# any spaces that might be in the directory name.
file="${files[$1]}";
# Catch names that start with a dash - they might confuse readelf
if test "x${file:0:1}" = "x-" ;
then
file="./$file"
fi
if ! [ -a "$file" ]
then
if [ $ignore_unknown -eq 0 ];
then
report "$file: file not found"
failed=1
fi
return
fi
if ! [ -f "$file" ]
then
if [ $ignore_unknown -eq 0 ];
then
report "$file: not an ordinary file"
failed=1
fi
return
fi
if ! [ -r "$file" ]
then
if [ $ignore_unknown -eq 0 ];
then
report "$file: not readable"
failed=1
fi
return
fi
file $file | grep --silent -e ELF
if [ $? != 0 ];
then
if [ $ignore_unknown -eq 0 ];
then
report "$file: not an ELF format file"
failed=1
fi
return
fi
$scanner --wide --notes --debug-dump=info --dynamic --segments $file > $tmpfile 2>&1
if [ $? != 0 ];
then
report "scanner '$scanner' failed - see $tmpfile"
failed=1
# Leave the tmpfile intact so that it can be examined by the user.
return
fi
grep -q -e "Unknown note" $tmpfile
if [ $? == 0 ];
then
# The FORTIFY checks need fully parsed notes.
# The other checks can use other sources of information.
if [ $skip_fortify -eq 0 ];
then
report "scanner '$scanner' did not recognise the build attribute notes - see $tmpfile"
failed=1
# Leave the tmpfile intact so that it can be examined by the user.
return
fi
fi
grep -q -e "Gap in build notes" $tmpfile
if [ $? == 0 ];
then
maybe "there are gaps in the build notes"
fi
local -a hard
local vulnerable=0
if [ $skip_opt -eq 0 ];
then
check_optimization_level
fi
if [ $skip_stack -eq 0 ];
then
check_for_stack_protector
fi
if [ $skip_fortify -eq 0 ];
then
check_for_fortify
fi
# Do not check the bind_now or relro status of unlinked files.
if [[ $filetype == exec || $filetype == lib || ( $filetype == auto && $file != *.o && $file != x*.a ) ]] ;
then
if [ $skip_bind_now -eq 0 ];
then
check_for_bind_now
fi
if [ $skip_relro -eq 0 ];
then
check_for_relro
fi
fi
if [ $skip_pic -eq 0 ];
then
check_for_pie_or_pic
fi
if [ $skip_operator -eq 0 ];
then
check_operator_range
fi
if [ $skip_clash -eq 0 ];
then
check_stack_clash
fi
# If we found a vulnerable file then consider the check to have failed.
if [ $vulnerable -gt 0 ];
then
failed=1
fi
rm -f $tmpfile
}
check_for_fortify ()
{
# Turn:
# *FORTIFY:2 0x00000000 NT_GNU_BUILD_ATTRIBUTE_OPEN
# or:
# GA*FORTIFY:2 0x00000000 NT_GNU_BUILD_ATTRIBUTE_OPEN
# into:
# 2
eval 'hard=($(grep -e FORTIFY $tmpfile | grep OPEN | cut -f 2 -d ":" | cut -b 3-5 | sed -e "s/ff/-1/" | sort -u))'
verbose "FORTIFY Info: ${hard[*]}"
if [ ${#hard[*]} -lt 1 ];
then
# Or an old version of readelf is being used which does not recognise the fortify note...
maybe "does not record _FORTIFY_SOURCE level"
else
# Check the value(s) to make sure that they are all >= 2.
local i
i=0;
while [ $i -lt ${#hard[*]} ]
do
if [ ${hard[i]} -lt 0 ];
then
maybe "sources compiled with --save-temps do not record _FORTIFY_SOURCE level"
else
if [[ ${hard[i]} -lt 2 ]];
then
fail "insufficient value for -D_FORTIFY_SOURCE=${hard[i]}"
else
pass "-D_FORTIFY_SOURCE=${hard[i]}"
fi
fi
let "i++"
done
fi
}
check_for_stack_protector ()
{
# Turn:
# *<stack prot>strong 0x00000000 NT_GNU_BUILD_ATTRIBUTE_OPEN
# or:
# GA*<stack prot>strong 0x00000000 NT_GNU_BUILD_ATTRIBUTE_OPEN
# into:
# strong
eval 'hard=($(grep -e "stack prot" $tmpfile | grep OPEN | cut -f 2 -d ">" | cut -f 1 -d " " | sort -u))'
verbose "Stack Protection Info: ${hard[*]}"
if [ ${#hard[*]} -lt 1 ];
then
# Stack protector note not recorded. Try examining the debug
# information in case -grecord-gcc-switches has been used.
# Turn:
# <c> DW_AT_producer : (indirect string, offset: 0x0): GNU C11 6.3.1 20161221 (Red Hat 6.3.1-1) -fstack-proector-strong
# into:
# strong
eval hard=($(gawk -e 'BEGIN { FPAT = "-f[no-]*stack-protector[^ ]*" } /f/ { print substr ($1,19) ; }' $tmpfile | sort | uniq))
verbose "DW_AT_producer stack records: ${hard[*]}"
fi
if [ ${#hard[*]} -lt 1 ];
then
maybe "does not record -fstack_protector setting"
else
if [ ${#hard[*]} -gt 1 ];
then
fail "multiple, different, settings of -fstack-protector used"
else
if test "x${hard[0]}" = "xstrong" ;
then
pass "compiled with -fstack-protector-strong"
else
fail "compiled with -fstack-protector-${hard[0]}"
fi
fi
fi
# Also check to see if any individual functions have been compiled explicitly without stack protection.
# Turn:
# *<stack prot>strong 0x00000000 NT_GNU_BUILD_ATTRIBUTE_FUNC
# GA*<stack prot>strong 0x00000000 NT_GNU_BUILD_ATTRIBUTE_FUNC
# into:
# strong
eval 'hard=($(grep -e "stack prot" $tmpfile | grep -e NT_GNU_BUILD_ATTRIBUTE_FUNC -e func | cut -f 2 -d ">" | cut -f 1 -d " " | sort -u))'
verbose "Stack Prot Info: ${hard[*]}"
if [ ${#hard[*]} -gt 0 ];
then
if [ ${#hard[*]} -gt 1 ];
then
fail "contains functions compiled without -fstack-protector=strong"
else
if test "x${hard[0]}" != "xstrong" ;
then
fail "contains functions compiled with -fstack-protector-${hard[0]}"
fi
fi
fi
}
check_for_pie_or_pic ()
{
# Turn:
# *<PIC>PIE 0x00000000 NT_GNU_BUILD_ATTRIBUTE_OPEN
# GA*<PIC>PIE 0x00000000 NT_GNU_BUILD_ATTRIBUTE_OPEN
# into:
# PIE
eval 'hard=($(grep -e "<PIC>" $tmpfile | grep OPEN | cut -f 2 -d ">" | cut -f 1 -d " " | sort -u))'
verbose "PIC Info: ${hard[*]}"
if [ ${#hard[*]} -lt 1 ];
then
# <PIC> note not recorded. Try examining the debug information
# in case -grecord-gcc-switches has been used.
# Turn:
# <c> DW_AT_producer : (indirect string, offset: 0x0): GNU C11 6.3.1 20161221 (Red Hat 6.3.1-1) -g -O2 -fPIC
# into:
# PIC
eval hard=($(gawk -e 'BEGIN { FPAT = "-f[pP][iI][cCeE]" } /f/ { print substr ($1,3) ; }' $tmpfile | sort -u))
verbose "DW_AT_producer records: ${hard[*]}"
fi
if [ ${#hard[*]} -lt 1 ];
then
maybe "does not record -fpic/-fpie setting"
else
if [ ${#hard[*]} -gt 1 ];
then
fail "multiple, different, settings of -fpic/-fpie used"
else
if [[ $filetype = lib || ( $filetype = auto && $file == *.so ) ]] ;
then
if [[ "x${hard[0]}" -eq "xPIC" || "x${hard[0]}" -eq "xpic" ]] ;
then
pass "compiled with -f${hard[0]}"
else
fail "compiled with -f${hard[0]}"
fi
else
if [[ "x${hard[0]}" -eq "xPIE" || "x${hard[0]}" -eq "xpie" ]] ;
then
pass "compiled with -f${hard[0]}"
else
fail "compiled with -f${hard[0]}"
fi
fi
fi
fi
# FIXME: Do we need to check for individual functions compiled without PIE support ?
}
check_optimization_level ()
{
# The bits in the GOW value encode the following information:
#
# bits 0 - 2 : debug type (from enum debug_info_type)
# bit 3 : with GNU extensions
# bits 4 - 5 : debug level (from enum debug_info_levels)
# bits 6 - 8 : DWARF version level
# bits 9 - 10 : optimization level
# bit 11 : -Os
# bit 12 : -Ofast
# bit 13 : -Og
# bit 14 : -Wall
#
# For now all that we care about is the optimization level (bits 9,10)
# so turn:
# *GOW:0x052b 0x00000000 NT_GNU_BUILD_ATTRIBUTE_OPEN
# GA*GOW:0x052b 0x00000000 NT_GNU_BUILD_ATTRIBUTE_OPEN
# into:
# 0x052b
eval 'hard=($(grep -e "GOW:" $tmpfile | grep OPEN | cut -f 2 -d ":" | cut -f 1 -d " " | sort -u))'
verbose "Optimization Info: ${hard[*]}"
if [ ${#hard[*]} -lt 1 ];
then
# GOW note not recorded. Try examining the debug information
# in case -grecord-gcc-switches has been used.
# Turn:
# <c> DW_AT_producer : (indirect string, offset: 0x0): GNU C11 6.3.1 20161221 (Red Hat 6.3.1-1) -g -O2 -fPIC
# into:
# 2
eval hard=($(gawk -e 'BEGIN { FPAT = "-O[0123]" } /O[0123]/ { print substr ($1,3,1) ; }' $tmpfile | sort -u))
verbose "DW_AT_producer records: ${hard[*]}"
if [ ${#hard[*]} -lt 1 ];
then
maybe "does not record -O setting"
else
local i
i=0;
while [ $i -lt ${#hard[*]} ]
do
if [ ${hard[i]} -lt 2 ];
then
fail "optimization level of -O${hard[i]} used"
break
else
pass "optimization level of -O${hard[i]} used"
fi
let "i++"
done
fi
else
local i
i=0;
while [ $i -lt ${#hard[*]} ]
do
declare -i opt=$(((${hard[i]} & 0x600) >> 9))
if [ $opt -lt 2 ];
then
fail "optimization level of -O$opt used"
break
else
pass "optimization level of -O$opt used"
fi
let "i++"
done
fi
}
check_for_bind_now ()
{
# Look for the DT_BIND_NOW dynamic tag
eval hard='($(grep -e BIND_NOW $tmpfile))'
verbose "BIND_NOW tags: ${hard[*]}"
if [ ${#hard[*]} -lt 1 ];
then
fail "-Wl,-z,now not used"
else
pass "-Wl,-z,now used"
fi
}
check_for_relro ()
{
# Look for the DT_BIND_NOW dynamic tag
eval hard='($(grep -e GNU_RELRO $tmpfile))'
verbose "GNU_RELRO tags: ${hard[*]}"
if [ ${#hard[*]} -lt 1 ];
then
fail "-Wl,-z,relro not used"
else
pass "-Wl,-z,relro used"
fi
}
check_operator_range ()
{
# Turn:
# GA!GLIBCXX_ASSERTIONS:false 0x00000000 OPEN Applies to region from 0 to 0x3a
# into:
# false
eval 'hard=($(grep -e "ASSERTIONS" $tmpfile | cut -f 2 -d ":" | cut -f 1 -d " " | sort -u))'
verbose "Operator Range Info: ${hard[*]}"
if [ ${#hard[*]} -lt 1 ];
then
maybe "does not record operator range test setting"
else
if [ ${#hard[*]} -gt 1 ];
then
fail "some parts built without operator range checking"
else
if [ "x${hard[0]}" == "xtrue" ];
then
pass "compiled with operator range checking enabled"
else
fail "compiled with operator range checking disabled"
fi
fi
fi
# FIXME: Do we need to check for individual functions compiled without range checking ?
}
check_stack_clash ()
{
# Turn:
# GA+stack_clash:true 0x00000000 OPEN Applies to region from 0 to 0x3a
# into:
# true
eval 'hard=($(grep -e "stack_clash" $tmpfile | cut -f 2 -d ":" | cut -f 1 -d " " | sort -u))'
verbose "Stack Clash Info: ${hard[*]}"
if [ ${#hard[*]} -lt 1 ];
then
maybe "does not record stack clash protection setting"
else
if [ ${#hard[*]} -gt 1 ];
then
fail "some parts built without stack clash protection enabled"
else
if [ "x${hard[0]}" == "xtrue" ];
then
pass "compiled with stack clash protection enabled"
else
fail "compiled with stack clash protection disabled"
fi
fi
fi
# FIXME: Do we need to check for individual functions compiled without protection ?
}
# Invoke main
main ${1+"$@"}