Dnsmasq fuzzing with American Fuzzy Lop (afl)
Posted by Klaus Eisentraut in fuzzing
Back in December I was did some fuzzing of dnsmasq. I'm using it quite often for test setups and for a "self-built" Pi-Hole on my local network which blocks ads and tracking, too.
Before I started my own fuzzing attempts, I read this article about dnsmasq fuzzing.
The guy there fuzzed the network parsing code of dnsmasq
, but in order to do that, he had to adapt the dnsmasq source code.
I wanted to start easy and decided to start with something less promising: fuzzing of the configuration file parsing code.
The downside is that this approach can only find bugs which are not actual security vulnerabilities.
The reason is simple:
The configuration file is only parsed once at the start of the dnsmasq
process and manipulating the configuration file is in all (common) scenarios out-of-control for an attacker.
But hey, it doesn't take long to set up this fuzzing, so let's do it:
- Download the latest git version:
git clone git://thekelleys.org.uk/dnsmasq.git
- Then, compile it with afl-gcc:
CC=/home/klaus/dev/afl-2.52b/afl-gcc make
- Get magic strings from the example configuration file in order to build a dictionary for afl-fuzz:
grep -E '^#[A-Za-z0-9]+' /etc/dnsmasq.conf | cut -b 2- | sort -u | while read -r i; do echo $RANDOM='"'${i//\"/\\\"}'"'; done > dict.txt
. This command gets all lines with look like a dnsmasq option and we give those lines to afl-fuzz. This increases the speed of finding bugs a lot, because even ifafl-fuzz
will detect, if its mutation engine made a new, valid command line option, this will take a very long time. An example line in the dictionary looks like this:17384="txt-record=example.com,\"v=spf1 a -all\""
. - Prepare the directories for the input and output test cases:
mkdir in/ out/
- Give it an empty configuration file for starters:
echo a > in/a
. - Start the fuzzing:
/home/klaus/dev/afl-2.52b/afl-fuzz -i in/ -o out/ -x dict.txt src/dnsmasq -C @@ --test
I did not expect any findings for this very simple approach, because I assumed someone else would have done it before. It was already late in the evening by now, so I left it running on my laptop and went to bed and then to work. 20 hours later I returned and, and was surprised that it found 12 unique crashes!
Three complete cycles were already done, so I did not expect any more crashes by now.
I stopped it with Ctrl+C and started analyzing the crashes.
It turned out that all of the twelve crashes had one of two distinct root causes.
One error occured while parsing the dhcp-match
commandline option and the other while parsing the MAC address in the ``dhcp-mac``` option.
Crash #1 (dhcp-match)
The crashing config file which was found by afl-fuzz
looked like this:
$ hexdump -C ./out/crashes/id\:000000*
00000000 64 68 63 70 2d 6d 61 74 63 68 3d 75 ff 14 3c 13 |dhcp-match=u..<.|
00000010 2c 31 32 30 2c 2c 72 65 63 6f 72 64 |,120,,record|
This crash was very easy to minimize by hand and the root cause was a NULL pointer dereference.
(gdb) run --dhcp-match=a,120,
Starting program: dnsmasq --dhcp-match=a,120,
Program received signal SIGSEGV, Segmentation fault.
0x000055555556aaf8 in parse_dhcp_opt (errstr=0x5555555c06b0 "",
arg=0x5555555c02a6 "", flags=128) at option.c:1473
1473 m[0] = 0;
(gdb) p m
$1 = (unsigned char *) 0x0
This crash has been reported here and was fixed in commit 9e732445cfde9f6fc9574b2f36da15f294ac3177.
Crash #2 (dhcp-mac)
The crashing config file which was found by afl-fuzz
looked like this:
$ hexdump -vC ./out/crashes/id\:000009*
00000000 64 68 63 70 2d 6d 61 63 3d 6f 6e 2c 1a 1a 1a 1a |dhcp-mac=on,....|
00000010 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a |................|
00000020 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a |................|
00000030 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a |................|
00000040 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a |................|
00000050 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a |................|
00000060 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a |................|
00000070 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a 1a |................|
00000080 1a |.|
This crash was very easy to reproduce and minimize by hand, too:
$ dnsmasq --dhcp-mac=,AAAAA...AAAAA
malloc(): invalid next size (unsorted)
Terminated (core dumped)
The error message malloc(): invalid next size (unsorted)
indicates that internal glibc metadata on the heap was overwritten by some faulty code but the program did not crash immediately. Instead it crashed at some later point in time when another malloc
or free
operation was attempted.
In order to find the root cause for this, I built dnsmasq again with Address Sanitizer (-fsanitize=address
) and then ran it again:
$ ./dnsmasq-asan --dhcp-mac=,AAAAA...AAAAA
=================================================================
==6423==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x606000000118 at pc 0x561346291e2d bp 0x7ffc01e73a80 sp 0x7ffc01e73a70
WRITE of size 1 at 0x606000000118 thread T0
#0 0x561346291e2c in parse_hex /tmp/dnsmasq/src/util.c:573
#1 0x5613462bd6b9 in one_opt /tmp/dnsmasq/src/option.c:3690
#2 0x5613462f2879 in read_opts /tmp/dnsmasq/src/option.c:5045
#3 0x56134624198e in main /tmp/dnsmasq/src/dnsmasq.c:95
#4 0x7fbfa20ae152 in __libc_start_main (/usr/lib/libc.so.6+0x27152)
#5 0x56134624dfbd in _start (/home/klaus/dnsmasq-fuzzing/src/dnsmasq-asan+0x2ffbd)
0x606000000118 is located 0 bytes to the right of 56-byte region [0x6060000000e0,0x606000000118)
allocated by thread T0 here:
#0 0x7fbfa235ecd8 in __interceptor_calloc /build/gcc/src/gcc/libsanitizer/asan/asan_malloc_linux.cc:153
#1 0x56134628e5a8 in safe_malloc /tmp/dnsmasq/src/util.c:278
SUMMARY: AddressSanitizer: heap-buffer-overflow /tmp/dnsmasq/src/util.c:573 in parse_hex
The bug is therefore in the parse_hex
function and was reported here and fixed in commit 7d04e17444793a840f98a0283968b96502b112dc.
Conclusion (tl;dr)
I found two bugs in configuration file parsing of dnsmasq
with almost zero effort.
Both are fixed and are rather an annoyance and certainly not security relevant.
Nevertheless, I think the bug per time invested-ratio for the two bugs was pretty good, because the actual fuzzing was set up in less than 20min. Writing this blog post took probably more time than actually finding, analyzing and reporting the two bugs :)