Tuesday, September 7, 2021

Checking application hardening with annocheck

Gcc and glibc have multiple mitigations that are available to prevent certain kinds of exploits. The redhat-rpm-macros package contains the flags that are passed into the build environment when a package is built. If you wanted to see this, look in /usr/lib/rpm/redhat/macros. There is a _hardened_build macro that is defined to a 1. That pulls in _hardened_cflags, which pulls in _hardening_cflags and more and more macros.

However, people write their own build system. They sometimes override environmental variables. Or maybe the spec file is written in a way that cflags cannot be injected. How can we check to see if the intended flags were applied?

One possibility is to use the checksec.sh program. If you google around, you can find it. I have put a copy into the security-assessor github tools. To use it, we can pass a file or a pid. For example:


./checksec.sh --file /usr/sbin/auditd
RELRO     STACK CANARY   FORTIFY SOURCE   PIE   FILE      PACKAGE
Full RELRO Canary found   Fortify found   PIE enabled /usr/sbin/auditd  audit-3.0.6


For common criteria, it calls out that applications should have stack smashing protection and ASLR. In the output of checksec.sh, this would be the stack canary and PIE columns. If PIE is not enabled then the application has some but not all parts randomized. Specifically the code segment doesn’t move around. However, making an application fully use ASLR causes a new layer of indirection to get added to applications. This becomes an attack point unless it’s made read only at application start-up. This is what the RELRO column is talking about. What we want is full RELRO so that we have full ASLR and complete symbol resolution so that all indirection is marked readonly. That leads to the question of how does checksec.sh determine that?

To detect whether stack smashing is enabled, we need to use readelf. What we can do is look in the symbol table. Stack smashing detection is done by placing a random number on the stack for each function call. On return, its checked to see if it's changed. If it is, then it calls the internal function __stack_check_fail(). On recent Fedora, the binutils were changed to shorten function names. To see them accurately, you need to use the ‘W’ argument. So, to check for stack smashing protection you would do this:

readelf -sW /usr/sbin/auditd | grep __stack_check_fail


To check for ASLR, we need to examine the ELF headers. One field is called Type. This is to say what type of Elf file it is. It can be an executable, dynamic, core, or object file. The dynamic type means that its a shared object or a library file. However, there is almost no difference between a shared object file and a program that is compiled with PIE. The only difference might be that the application has a main function, but so does libc. The check for PIE ASLR would look something like this:

readelf -hW /usr/sbin/auditd | grep 'Type:[[:space:]]*DYN'


But, the last item to check on is if we have full RELRO. All applications compiled on Fedora or RHEL automatically have partial RELRO. There was a patch applied to binutils that hardwires this. In order to have full RELRO, the program must be compiled with the bind_now linker flag. The check for this is located in the dynamic section of the program. A test would look like this:

readelf -dW /usr/sbin/auditd | grep  'BIND_NOW'


Simple...right? Not so fast. Some of these tests are certain to give you a correct answer. For example, there is only one program header. It can give you a reliable answer. However, what about the stack smashing protection? All we can tell is it’s enabled for at least one object file. We cannot tell if all object files were compiled with stack smashing protection. We also can’t tell if its regular, strong, or full protection. And that goes for other hardening flags such as stack clash or control flow integrity. If checksec.sh is all we have, then we are reduced to looking for the build logs and verifying that every file got every intended flag.


A Better Mousetrap

This is why we have the annobin and annocheck programs. The annobin program is a gcc plugin that annotates build information in a notes section of each object file. The annocheck program can then read these note sections and reason about the build policy being faithfully carried out. To use it, all you need to do is pass the full path to the program to it. It will check dozens of things about the application. To see these pass the --verbose flag. But what if we just wanted to recreate the 3 check that checksec.sh does? We can turn all tests off and then enable the ones we want like this:

# annocheck --verbose --skip-all --test-stack-prot --test-pie --test-bind-now /usr/sbin/auditd | grep -v info:
annocheck: Version 9.79.
Hardened: /usr/sbin/auditd: PASS: pie test
Hardened: /usr/sbin/auditd: PASS: bind-now test
Hardened: /usr/sbin/auditd: PASS: stack-prot test


Based on this, it’s possible to write a script and check all files for stack smashing protection like so:

#!/bin/sh
DIRS="/usr/lib64 /usr/lib /usr/bin /usr/sbin /usr/libexec"
FLAGS="--skip-all --ignore-gaps --test-stack-prot"
for d in $DIRS
do
        if [ ! -d $d ] ; then
                continue
        fi
        echo "Scanning files in $d..."
        for f in `/usr/bin/find $d -type f 2>/dev/null`
        do
                # Get just the elf executables
                testf=`echo $f | /usr/bin/file -n -f - 2>/dev/null | grep ELF`
                if [ x"$testf" != "x" ] ; then
                        # Get results dropping version and first 2 fields
                        res=`annocheck $FLAGS $f 2>/dev/null | grep -v '^annocheck:' | cut -d " " -f 3-`
                        if [ x"$res" != "x" ] ; then
                                echo "$f $res"
                        fi
                fi
        done
done

Saving this as check-ssp and running this on a fully patched Fedora 34 system gives the following results:

$ ./check-ssp | grep FAIL
/usr/lib64/libva.so.2.1100.0 FAIL: stack-prot test because only some functions protected
/usr/lib64/gimp/2.0/plug-ins/fourier FAIL: stack-prot test because stack protection deliberately disabled
/usr/lib64/ocaml/objinfo_helper FAIL: stack-prot test because stack protection deliberately disabled
/usr/lib64/libva-x11.so.2.1100.0 FAIL: stack-prot test because only some functions protected


There are a lot more failures in the video drivers. Hopefully there’s no bugs there.  :-)


Conclusion
The annobin / annocheck programs allow us to verify that the intended compiler mitigations are present in all ELF files in the distro. It is a better check than the old way. There are times when functions don’t have stack variables or do anything that cause stack smashing protection to be enabled. Only by looking at the annotation from the build can we tell that the flags were passed in and the compiler chose not to need the __stack_check_fail function. And without annocheck, there is otherwise no visibility into how much stack smashing protection is compiled in.

The annocheck program gives unprecedented visibility in the application hardening on your system. It can let you know if everything is good. It can also be used as a gating test when you build a package and intend to deploy it. It’s worth your time to know about. And it now has online documentation to help you fix programs.

No comments: