The Annobin plugin for GCC stores extra information inside binary files as they are compiled. Examining this information used to be performed by a set of shell scripts, but that has now changed and a new program—annocheck—has been written to do the job. The advantage of the program is that it is faster and more flexible than the scripts, and it does not rely upon other utilities to actually peer inside the binaries.
This article is about the annocheck program: how to use it, how it works, and how to extend it. The program's main purpose is to examine how a binary was built and to check that it has all of the appropriate security hardening features enabled. But that is not its only use. It also has several other modes that perform different kinds of examination of binary files.
Another feature of annocheck is that it was designed to be easily extensible. It provides a framework for dissecting binary files and a set of utilities to help with this examination. It also knows how to handle archives, RPMs, and directories, presenting the contents of these to each tool as a series of ordinary files. Thus, tools need only worry about the specific tasks they want to carry out.
Using annocheck
Annocheck is supplied as part of the Annobin RPM shipped with Fedora. It can also be built directly from the source code obtained from the git repository. The simplest way of running it is to just invoke it with the name of a file to inspect. (Note: The file has to be an ELF format file. Annocheck does not handle other binary file types.)
annocheck <file>
This command will run the default tool—the security hardening checker—on <file>
. The usual command-line options of --help
, --version
, and --verbose
are supported to provide more information. Annocheck also supports directories, archives, and RPMs, so all of the following will also work:
annocheck <directory>
annocheck lib<foo>.a
annocheck <rpm>
The first version will recursively run annocheck on all the ELF files inside <directory>
. (Other types of file will be ignored.) The second version will run annocheck on all the ELF files inside lib<foo>.a
. If this is a thin archive, the links inside the archive will be followed to find the real files.
The third version will run annocheck on all the ELF files inside <rpm>
. This version also supports a command-line option to provide the name of a debug info RPM associated with the binary RPM:
annocheck <rpm> --debug-rpm <debuginfo rpm>
Providing the debug info RPM is not essential, but it can be very helpful as it often contains information that makes the tool's work easier.
Annocheck contains multiple tools for examining binary files, although only the security hardening checker is enabled by default. The other tools can be enabled via specific command-line options. Currently, the extra tools that are supported by annocheck are as described below:
annocheck --enable-built-by
This tool reports the name of the compiler that was used to build the binary file. It looks in various different places in the binary, and if the --all
command-line option is present, it will report all the strings that it finds, instead of just the first one.
annocheck --enable-notes
This tool displays the notes that are stored inside a binary file by the Annobin GCC plugin. It is similar to the readelf
program's --notes
option, except that it sorts the notes by address range and then displays them sequentially.
annocheck --section-size=<name>
This tool displays the size of the named sections. It is similar to the readelf
program's --sections
option, except that the output is restricted to specific sections, and it produces a cumulative result at the end. Thus, for example, it could be used to compute the total size of all of the .text
sections in all the binary files inside a particular directory.
Each tool also has its own set of command-line options to modify its behavior. Use the --help
command-line option to see a full list of these. Multiple tools can be enabled at the same time, and the security hardening checker can be disabled, if desired, via the following option:
annocheck --disable-hardened
The security hardening checker
This is the largest, and arguably most important, tool in the annocheck framework. It runs a series of checks on each binary file to see if it has been built with the appropriate security hardening options. It obtains a lot of the information about these options via the notes generated by the Annobin plugin.
The security checker runs several tests to make sure that a program was linked correctly:
- The program must not have a stack in an executable region of memory.
- No segment in the program should have all three of the read, write, and execute permission bits set.
- There should be no relocations against executable code.
- The program must not have any relocations that are held in writable memory.
- The runpath information used to locate shared libraries at runtime must include only directory paths that start with
/usr
. - Dynamic executables must have a dynamic segment.
- The
-pie
and-z now
linker options must have been enabled.
The security checker also runs tests on the Annobin data to make sure the program was compiled correctly:
- The program must have Annobin notes, and there should be no gaps in the notes.
- The program must have been compiled with the following options
enabled:-D_FORTIFY_SOURCE=2
-fpic
or-fPIC
or-fpie
or-fPIE
-fstack-protector-strong
or-fstack-protector-all
-O2
(or-O3
or-Og
)
- Programs which use exception handling must have been compiled with
-fexceptions
enabled and with-D_GLIBCXX_ASSERTIONS
specified. - If supported by the compiler, and appropriate for the specific target architecture, the following options must also have been enabled:
-fcf-protection=full
-fstack-clash-protection
-mcet
-mstackrealign
Extending annocheck
Each tool in annocheck is provided by an independent source file (or set of source files) that is compiled and linked into the annocheck binary. Tools can be omitted by simply not including them on the final link command line. The annocheck framework does not have any built-in knowledge of any of the tools, and instead it interacts with them via a global structure that each tool creates for itself. The
structure looks like this:
struct { const char * name; bool (* process_arg) void (* usage) void (* version) void (* start_scan) void (* end_scan) bool (* start_file) bool (* interesting_sec) bool (* check_sec) bool (* interesting_seg) bool (* check_seg) bool (* end_file) }
This structure is defined in the annocheck.h
header file. The full details of the structure have been omitted here in order to keep things simple. The fields are mostly callback functions, except for the first—name
—which is the name of the tool. This name is used when reporting tool-specific information to the user.
The callback functions are the main part of the structure. The functions can be NULL if the tool does not need that particular feature. The process_arg
, usage
, and version
functions are part of the housekeeping facilities and are there to process command-line arguments, display help information, and display version information, respectively.
The start_scan
function is called when annocheck starts up (although after the command-line arguments have been processed). Then once all the input files have been processed the end_scan
function is called. A quirk of the RPM format, however, means that it is necessary for annocheck to recursively invoke itself inside an RPM. This is allowed for in the start_scan
and end_scan
functions, which have a depth parameter and also the name of a file that can be used to pass information between invocation levels.
The start_file
function is called after an individual binary file has been located, but before any processing has started. It gives the tool a chance to initialize any per-file data, and to decide if it is interested in processing the contents of the file. If the tool is not interested in the file, annocheck will not call any of the other per-file functions on it.
Once processing of a file has begun, the interesting_sec
callback is called for every section in the file. This allows the tool to decide if it wants to examine the section in more detail and, if so, the check_sec
callback will be called. The checking is done in this way so that if no tool is interested in a particular section, its contents are never loaded into memory, speeding up execution.
Once the sections have been processed—if there are any—the segments are scanned in a similar fashion using the interesting_seg
and check_seg
callbacks.
Once a file has been fully scanned, the end_file
callback is called in order to allow the tool to perform any final processing and display its results.
In addition to the callback structure, the annocheck.h
header also provides prototypes for all the utility functions exported by annocheck itself. This includes functions for exploring debug information and ELF notes, locating symbols, and printing out messages.
The header also defines a function to register a tool with the annocheck framework. This function is special in that it is expected to be called from inside a constructor, rather than from normal code. This is how tools tell annocheck about their existence, and it also allows annocheck to be built without any specific knowledge of any of the tools. The constructor function should look something like this:
static bool disabled = false; static __attribute__((constructor)) void tool_register_checker (void) { if (! annocheck_add_checker (& tool_structure, major_version)) disabled = true; }
In this function, tool_structure
is the callback structure described above. The major_version
variable is provided by annocheck, and it allows a consistency check to be made to ensure that both annocheck and the tool are using the same version of callback structure. If the annocheck_add_checker
function returns false, the tool should not execute any further, because something has gone wrong with the registration process.
Future work
Development of annocheck is an ongoing process with new features being added all the time. Currently, the following enhancements are planned:
- Add support for binary files inside tar files and, especially, inside compressed tar files. Possibly also add support for zip files and other, similar archive systems.
- Improve the argument handling. Currently, each tool defines its own command-line options, and these can conflict with other tools. Instead, the annocheck framework ought to enforce a policy of supplying only specific options to specific tools.
- Improve the notes tool to display a matrix of notes and the memory regions to which they apply. Also, add support for displaying section and function names that apply to specific regions.