Fuzzing/example

Revision as of 08:44, 13 January 2020 by Dbagnall (talk | contribs) (Import and wikify a samba-technical)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

This page was originally based on an email to the samba-technical list, and talks about the development of the patch at the end.


Maybe you are a Samba developer who has noticed some activity around automated fuzzing, but you haven't really got your head around how to add a fuzz target in your subdomain. This message is meant to show you how easy it is. So easy, in fact, that you might prefer to skip to the attached patch.

My process for finding this target was:

  1. git grep parse_ -- source3.
  2. look for a function that looks like it
    a. does a bit of work,
    b. probably deals with untrusted data,
    c. is not enmeshed in a whole lot of state.

So already with point 1, you know that YOU can do better.

For point 2, I saw this parse_packet() was filling out nested structures using a tree of function calls (that's 2a), has a networky sounding name (2b), and there is no context pointer involved (2c). (If there was a context pointer, that would be OK if we could create the context afresh every time, or if it wasn't really modified).

With the target selected, you make a file in lib/fuzzing with a function called LLVMFuzzerTestOneInput(). That is libfuzzer's entry point. LLVMFuzzerTestOneInput() essentially takes a datablob and returns 0 (always 0 -- you report errors with abort()). This is sort of enough to fuzz nmblib's parse_packet:

  int LLVMFuzzerTestOneInput(uint8_t *input, size_t len)
  {
          struct packet_struct *p = NULL;
          struct in_addr ip = {
                  0x0100007f /* 127.0.0.1 */
          };

          p = parse_packet((char *)input,
                           len,
                           NMB_PACKET,
                           ip,
                           1234);
          return 0;
  }

but that's a bit useless. It hopefully tells us that parse_packet() doesn't crash, but it doesn't warn us if it is returning deadly structures. So we add in a round trip using build_packet() and a test to ensure p->packet.nmb.question.question_name.name exists, which I think something somewhere assumes is true. Anybody who knew anything about nmb packets could add more checks, which they would do by straight out doing the thing that might crash, not by making careful assertions.

To compile it we add a stanza to wscript_build using the standard mix of copy, paste, trial, error, and guesswork. Then you need to compile it differently. What I use is a wrapper around libfuzzer and compilers called Honggfuzz. The steps to get it are something like:

$  git clone git at github.com:google/honggfuzz.git
$  cd honggfuzz
$  make

then back in samba, you need a fancy configure line:

$  rm -r bin
$  buildtools/bin/waf -C \
                --without-gettext \
                --enable-debug \
                --enable-developer \
                --enable-libfuzzer \
                CC=$PATH_TO_HONGGFUZZ/hfuzz_cc/hfuzz-clang \
                configure \
                LINK_CC=$PATH_TO_HONGGFUZZ/hfuzz_cc/hfuzz-clang \
                --disable-warnings-as-errors  \
                --abi-check-disable
$  make -j

Maybe not all of those options are necessary, but at some point they were each for something. I think at least you'll need --enable-libfuzzer CC=....

Then run it:

$  mkdir nmb_parse_seeds
$  $PATH_TO_HONGGFUZZ/honggfuzz  -V -T -F 50000 \
          -i nmb_parse_seeds \
          -- bin/fuzz_nmblib_parse_packet

This will create fuzz packets based on the the examples in ./nmb_parse_seeds, of which there are initially none. It will mutate and add to these (initially mutating the empty string), trying to find packets that lead to new code pathways. While it doing this it shows you how it's going on a console dashboard. If a packet causes a crash or abort (or with -T, a timeout), it saves that packet in a weirdly named file and writes a report in (by default) HONGGFUZZ_REPORT.TXT.

To replay a packet in gdb or valgrind, you just do this:

$  gdb --args ./bin/fuzz_nmblib_parse_packet $the_file_name

and the problem should become apparent.

Needless to say, I have found no crashes with this particular patch, and I suspect it is not actually a very good target. But perhaps it is an excellent target and we just happen to have very good code!

The main point is that you should consider adding targets; now that we have made fuzzing Samba easy, we want to ensure that we are the ones doing it.


The patch

From e49e685f6eea48a93d3deee83de4eba1bd014dd7 Mon Sep 17 00:00:00 2001
From: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Date: Fri, 10 Jan 2020 15:44:27 +1300
Subject: [PATCH] fuzz: add nmblib/parse_packet target

We want to ensure that parse_packet() can parse a packet without
crashing, and that that parsed packet won't cause trouble further down
the line.

Signed-off-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
---
 lib/fuzzing/fuzz_nmblib_parse_packet.c | 56 ++++++++++++++++++++++++++
 lib/fuzzing/wscript_build              |  5 +++
 2 files changed, 61 insertions(+)
 create mode 100644 lib/fuzzing/fuzz_nmblib_parse_packet.c

diff --git a/lib/fuzzing/fuzz_nmblib_parse_packet.c b/lib/fuzzing/fuzz_nmblib_parse_packet.c
new file mode 100644
index 00000000000..7b35abe9f97
--- /dev/null
+++ b/lib/fuzzing/fuzz_nmblib_parse_packet.c
@@ -0,0 +1,56 @@
+/*
+  Fuzz NMB parse_packet
+  Copyright (C) Catalyst IT 2020
+
+  This program 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 of the License, or
+  (at your option) any later version.
+
+  This program 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.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "../../source3/include/includes.h"
+#include "libsmb/libsmb.h"
+#include "libsmb/nmblib.h"
+#include "fuzzing/fuzzing.h"
+
+#define PORT 138
+#define MAX_LENGTH (1024 * 1024)
+char buf[MAX_LENGTH + 1];
+
+
+int LLVMFuzzerTestOneInput(uint8_t *input, size_t len)
+{
+	struct packet_struct *p = NULL;
+	struct in_addr ip = {
+		0x0100007f /* 127.0.0.1 */
+	};
+
+	p = parse_packet((char *)input,
+			 len,
+			 NMB_PACKET,
+			 ip,
+			 PORT);
+	/*
+	 * We expect NULL (parse failure) most of the time.
+	 *
+	 * When it is not NULL we want to ensure the parsed packet is
+	 * reasonably sound.
+	 */
+
+	if (p != NULL) {
+		struct nmb_packet *nmb = &p->packet.nmb;
+		pull_ascii_nstring(buf, MAX_LENGTH,
+				   nmb->question.question_name.name);
+		build_packet(buf, MAX_LENGTH, p);
+		free_packet(p);
+	}
+	return 0;
+}
diff --git a/lib/fuzzing/wscript_build b/lib/fuzzing/wscript_build
index 75c41ac83f4..affe7099063 100644
--- a/lib/fuzzing/wscript_build
+++ b/lib/fuzzing/wscript_build
@@ -27,6 +27,11 @@ bld.SAMBA_BINARY('fuzz_reg_parse',
                  deps='fuzzing samba3-util smbconf REGFIO afl-fuzz-main',
                  fuzzer=True)
 
+bld.SAMBA_BINARY('fuzz_nmblib_parse_packet',
+                 source='fuzz_nmblib_parse_packet.c',
+                 deps='fuzzing libsmb afl-fuzz-main',
+                 fuzzer=True)
+
 bld.SAMBA_BINARY('fuzz_regfio',
                  source='fuzz_regfio.c',
                  deps='fuzzing samba3-util smbconf REGFIO afl-fuzz-main',
-- 
2.20.1