Kea 2.7.5
|
Fuzzing is a software-testing technique whereby a program is presented with a variety of generated data as input and is monitored for abnormal conditions such as crashes or hangs.
There are two ways to fuzz Kea.
Option 1. With the libfuzzer harness function LLVMFuzzerTestOneInput.
Option 2. With the AFL (American Fuzzy Lop) compiler.
This mode of fuzzing works with virtually any compiler.
There are four types of fuzzers implemented with this mode:
There are two binaries under test:
kea-dhcp4
kea-dhcp6
Combining the binaries and the fuzzer types results in eight fuzzing binaries:
fuzz/fuzz_config_kea_dhcp4
fuzz/fuzz_config_kea_dhcp6
fuzz/fuzz_http_endpoint_kea_dhcp4
fuzz/fuzz_http_endpoint_kea_dhcp6
fuzz/fuzz_packets_kea_dhcp4
fuzz/fuzz_packets_kea_dhcp6
fuzz/fuzz_unix_socket_kea_dhcp4
fuzz/fuzz_unix_socket_kea_dhcp6
Use the "--enable-fuzzing" during the configure step. Then just compile as usual.
You can check that config.report
shows these lines:
Compiling with AFL is permitted, but is not required.
Each of these binaries has two ways to run. It tries to find a directory called input/<name>
relative to the binary where <name>
is the name of the binary.
doc/examples/kea[46]
symlinked.After compiling, all the fuzzers can be run with make check
in the fuzz
directory. The reasoning behind this is that while writing code, developers can quickly check if anything is broken. Obviously, this is not real fuzzing as long since the input from the fuzz/input
directory is the same, but it rather tests if the fuzzers were broken during development.
make check
runs these fuzzers with sudo
. It may interrupt the process asking for a password on systems that don't have passwordless root set up.
The following functions are required to be implemented in each new fuzzer:
int LLVMFuzzerInitialize();
- Does initialization that is required by the fuzzing. Is only run once. Is run automatically. It does not need to be run explicitly by the fuzzing engine.int LLVMFuzzerTearDown();
- Cleans up the setup like removing leftover files. Is automatically run at the beginning and the end of the fuzzing. It does not need to be run explicitly by the fuzzing engine.int LLVMFuzzerTestOneInput(uint8_t const* data, size_t size);
- Implements the actual fuzzing. Takes the parameter input and achieves the object of the fuzzing with it. It needs to start with static bool initialized(DoInitialization());
to do the initialization only once. This function is the only one that needs to be run explicitly by the fuzzing engine.The following functions are common to all fuzzers:
int main(int, char* argv[])
- Implements the input searching mentioned above.bool DoInitialization();
- Sets up logging to prevent spurious logging and calls int LLVMFuzzerInitialize();
.void writeToFile(std::string const& file, std::string const& content);
- A helpful function to write to file used in some fuzzers.Exceptions make it difficult to maintain a fuzzer. We have to triage some of the exceptions. For example, JSONError is thrown when an invalid JSON is provided as input in config fuzzing. That leads to a core dump and can be interpreted as a crash by the fuzzing engine, which is not really what we're interested in because this exception is caught in the kea-dhcp[46] binaries. The old way of fuzzing may have been better from this point of view, because there was the guarantee that the right exceptions were caught and nothing more and we didn't have to pay attention to what exceptions needed to be ignored and which weren't.
In this, Kea is built using an AFL-supplied program that not only compiles the software but also instruments it. When run, AFL generates test cases and monitors the execution of Kea as it processes them. AFL will adjust the input based on these measurements, seeking to discover and test new execution paths.
In this mode, AFL will start an instance of Kea and send it a packet of data. Kea reads this packet and processes it in the normal way. AFL monitors code paths taken by Kea and, based on this, will vary the data sent in subsequent packets.
Kea has a configuration file check mode whereby it will read a configuration file, report whether the file is valid, then immediately exit. Operation of the configuration parsing code can be tested with AFL by fuzzing the configuration file: AFL generates example configuration files based on a dictionary of valid keywords and runs Kea in configuration file check mode on them. As with network packet fuzzing, the behaviour of Kea is monitored and the content of subsequent files adjusted accordingly.
Whatever tests are done, Kea needs to be built with fuzzing in mind. The steps for this are:
AFL can be used to check the parsing of the configuration files. In this type of fuzzing, AFL generates configuration files which it passes to Kea to check. Steps for this fuzzing are:
The AFL fuzzer delivers packets to Kea's stdin. Although the part of Kea concerning the reception of packets could have been modified to accept input from stdin and have Kea pick them up in the normal way, a less-intrusive method was adopted.
The packet loop in the main server code for kea-dhcp4 and kea-dhcp6 is essentially:
When –enable-fuzzing is specified, this is conceptually modified to:
Implementation is via an object of class "PacketFuzzer". When created, it identifies an interface, address and port on which Kea is listening and creates the appropriate address structures for these. The port is passed as an argument to the constructor because at the point at which the object is constructed, that information is readily available. The interface and address are picked up from the environment variables mentioned above. Consideration was given to extracting the interface and address information from the configuration file, but it was decided not to do this:
Every time through the loop, the object reads the data from stdin and writes it to the identified address/port. Control then returns to the main Kea code, which finds data available on the address/port on which it is listening and handles the data in the normal way.
In practice, the "while" line is actually:
__AFL_LOOP is a token recognized and expanded by the AFL compiler (so no need to "#include" a file defining it) that implements the logic for the fuzzing. Each time through the loop (apart from the first), it raises a SIGSTOP signal telling AFL that the packet has been processed and instructing it to provide more data. The "count" value is the number of times through the loop before the loop terminates and the process is allowed to exit normally. When this happens, AFL will start the process anew. The purpose of periodically shutting down the process is to avoid issues raised by the fuzzing being confused with any issues associated with the process running for a long time (e.g. memory leaks).
No changes were required to Kea source code to fuzz configuration files. In fact, other than compiling with afl-clang++ and installing the resultant executable, no other steps are required. In particular, there is no need to use the "--enable-fuzzing" switch in the configuration command line (although doing so will not cause any problems).
The early versions of the fuzzing code used a separate thread to receive the packets from AFL and to write them to the socket on which Kea is listening. The lack of synchronization proved a problem, with Kea hanging in some instances. Although some experiments with thread synchronization were successful, in the end the far simpler single-threaded implementation described above was adopted for the single-threaded Kea 1.6. Should Kea be modified to become multi-threaded, the fuzzing code will need to be changed back to reading the AFL input in the background.
If unit tests are built when –enable-fuzzing is specified and with the AFL compiler, note that tests which check or use the DHCP servers (i.e. the unit tests in src/bin/dhcp4, src/bin/dhcp6 and src/bin/kea-admin) will fail. With no AFL-related environment variables defined, a C++ exception will be thrown with the description "no fuzzing interface has been set". However, if the KEA_AFL_INTERFACE
and KEA_AFL_ADDRESS
variables are set to valid values, the tests will hang.
Both these results are expected and should cause no concern. The exception is thrown by the fuzzing object constructor when it attempts to create the address structures for routing packets between AFL and Kea but discovers it does not have the necessary information. The hang is due to the fact that the AFL processing loop does a synchronous read from stdin, something not expected by the test. (Should random input be supplied on stdin, e.g. from the keyboard, the test will most likely fail as the input is unlikely to be that expected by the test.)