Skip to content

Commit

Permalink
Fuzzing improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
arcz committed Dec 29, 2022
1 parent 77829be commit 9386113
Show file tree
Hide file tree
Showing 33 changed files with 3,162 additions and 0 deletions.
2 changes: 2 additions & 0 deletions fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build
corpus
19 changes: 19 additions & 0 deletions fuzz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# OpenVPN Fuzzing

## Setup
The fuzzing setup is handled by Nix inside a `nix-shell` and works both on
Linux and macOS. Nix is the only dependency (https://nixos.org/download.html).

## Usage

```sh
$ nix-shell fuzz/shell.nix
$ autoreconf -i -v -f
$ ./configure --disable-lz4
$ cd fuzz
$ ./openvpn-fuzz.py fuzz base64
$ ./openvpn-fuzz.py fuzz parse_argv -- -fork=4 -ignore_crashes=1
$ ./openvpn-fuzz.py coverage base64 parse_argv # specified targets
$ ./openvpn-fuzz.py coverage # all targets
$ ./openvpn-fuzz.py coverage --clean # do make clean before and after, use if previously built for fuzzing
```
158 changes: 158 additions & 0 deletions fuzz/openvpn-fuzz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/usr/bin/env python

import argparse
import os
import platform
import subprocess
import sys

TARGETS = [
'base64',
'buffer',
'dhcp',
'forward',
'list',
'misc',
'mroute',
'mss',
'packet_id',
'parse_argv',
'proxy',
'route',
'verify_cert',
]

BASE_DIR = os.path.dirname(os.path.realpath(__file__))

def fuzz_target(target, args=[]):
build_targets([target])
os.makedirs(corpus_dir(target), exist_ok=True)
os.chdir(target_dir(target, "fuzzer"))
os.execv(target_bin_path(target, "fuzzer"),
[target_bin_path(target, "fuzzer"), corpus_dir(target)] + args)

def generate_coverage_report(targets=TARGETS):
"""
If OpenVPN was previously built for fuzzing run `make -C ../ clean` before and after generating coverage.
"""
wd = os.getcwd()
build_targets(targets, for_coverage=True)
profraws = []
object_args = []
for target in targets:
os.chdir(target_dir(target, "coverage"))
profraws.append(target_dir(target, "coverage", "default.profraw"))
object_args.append("-object")
object_args.append(target_bin_path(target, "coverage"))
subprocess.run([target_bin_path(target, "coverage"), corpus_dir(target), "-runs=0"])

profdata = build_dir("coverage", "combined.profdata")
subprocess.run(["llvm-profdata", "merge", "-o", profdata, "-sparse"] + profraws)
subprocess.run(["llvm-cov", "show", "--format", "html", f"-instr-profile={profdata}",
"-output-dir", build_dir("coverage", "report")] + object_args)
os.chdir(wd)

def triage_parse_argv_crashes():
"""
Filters out false positives that are caused by calling exit.
"""
import pwn
target = "parse_argv"
for filename in os.listdir(target_dir(target, "fuzzer")):
if "crash-" in filename:
print("Triaging", filename)
with open(target_dir(target, "fuzzer", filename), "rb") as f:
argv_raw = f.read()
p = pwn.process(executable="../src/openvpn/openvpn", argv=argv_raw.split(b'\x00'))
out = p.readall()
if b"SIGSEGV" in out or b"smashing" or b"AddressSanitizer" in out:
print(pwn.hexdump(argv_raw))
print(out)
exit(1)
p.close()

def build_openvpn(cflags):
"""
Build OpenVPN as usual, assumes `autoconf -f -v -f` and `./configure --disable-lz4` already run.
"""
subprocess.run(["make", "-j", "-C", "../", f"CFLAGS={cflags}"])

def build_targets(targets, for_coverage=False):
fuzzer_flags = '-g -fsanitize=address,fuzzer-no-link'
coverage_flags = '-g -fprofile-instr-generate -fcoverage-mapping'

build_subdir = 'coverage' if for_coverage else 'fuzzer'
os.makedirs(build_dir(build_subdir), exist_ok=True)

cflags = coverage_flags if for_coverage else fuzzer_flags
cflags += " -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION"
build_openvpn(cflags)

o_files = []
for file in os.listdir("../src/openvpn"):
if file.endswith(".o") and file != 'openvpn.o':
o_files.append("../src/openvpn/" + file)
subprocess.run(["ar", "r", build_dir(build_subdir, "libopenvpn.a")] + o_files)

subprocess.run(["clang++", "-c", "src/fuzz_randomizer.cpp",
"-o", build_dir(build_subdir, "fuzz_randomizer.o")] +
cflags.split(' '))

extra_libs = ["-lc++abi", "-lresolv"] if platform.system() == 'Darwin' else ['-lcap-ng']

for target in targets:
os.makedirs(target_dir(target, build_subdir), exist_ok=True)
subprocess.run(["clang", "-I../src/openvpn", "-I..", "-I../src/compat", "-I../include",
"-lssl", "-lcrypto", "-llzo2", f"src/fuzz_{target}.c",
build_dir(build_subdir, "libopenvpn.a"),
build_dir(build_subdir, "fuzz_randomizer.o"),
"-o", target_bin_path(target, build_subdir),
"-g", "-fsanitize=address,fuzzer"] +
(coverage_flags.split(' ') if for_coverage else []) +
extra_libs)

def build_dir(subdir, file=''):
"""
There are two build flavors that live in their own subdirs: coverage and fuzzer.
"""
return os.path.join(BASE_DIR, "build", subdir, file)

def target_dir(target, subdir, file=''):
return os.path.join(build_dir(subdir), f"fuzz_{target}", file)

def corpus_dir(target):
return os.path.join(BASE_DIR, "corpus", f"fuzz_{target}")

def target_bin(target):
return f"fuzz_{target}"

def target_bin_path(target, subdir):
return target_dir(target, subdir, target_bin(target))

if __name__ == "__main__":
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="subcommand")
fuzz_parser = subparsers.add_parser('fuzz')
fuzz_parser.add_argument('target', type=str)
fuzz_parser.add_argument('libfuzzer_args', type=str, nargs='*')
coverage_parser = subparsers.add_parser('coverage')
coverage_parser.add_argument('targets', type=str, nargs='*')
coverage_parser.add_argument('--clean', action=argparse.BooleanOptionalAction)

args = parser.parse_args()
if args.subcommand == 'fuzz':
# ./openvpn-fuzz.py fuzz proxy -- -fork=4 -ignore_crashes=1
fuzz_target(args.target, args.libfuzzer_args)
elif args.subcommand == 'coverage':
if args.clean:
subprocess.run(["make", "-C", "../", "clean"])

if args.targets:
generate_coverage_report(args.targets)
else:
generate_coverage_report()

if args.clean:
subprocess.run(["make", "-C", "../", "clean"])
else:
parser.print_help()
19 changes: 19 additions & 0 deletions fuzz/shell.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
with import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/e58a7747db96c23b8a977e7c1bbfc5753b81b6fa.tar.gz") {};

let llvmPackages = llvmPackages_14;
in llvmPackages.stdenv.mkDerivation {
name = "openvpn-fuzz";
buildInputs = [
autoconf
automake
m4
libtool
pkg-config
openssl_1_1
lz4
lzo
pam
llvmPackages.llvm
python3Packages.pwntools
] ++ lib.optional (!stdenv.isDarwin) libcap_ng;
}
43 changes: 43 additions & 0 deletions fuzz/src/fuzz_base64.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* Copyright 2021 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#include "base64.h"

int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
if (size > 500) {
return 0;
}

char *new_str = (char *)malloc(size + 1);
if (new_str == NULL) {
return 0;
}
memcpy(new_str, data, size);
new_str[size] = '\0';

char *str = NULL;
openvpn_base64_encode(data, size, &str);
if(str != NULL) {
free(str);
}

uint16_t outsize = 10000;
char *output_buf = (char *)malloc(outsize);
openvpn_base64_decode(new_str, output_buf, outsize);
free(output_buf);

free(new_str);
return 0;
}
Loading

0 comments on commit 9386113

Please sign in to comment.