Skip to content

Commit e105170

Browse files
committed
Fuzzing improvements
1 parent 77829be commit e105170

33 files changed

+3163
-1
lines changed

fuzz/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
build
2+
corpus

fuzz/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# OpenVPN Fuzzing
2+
3+
## Setup
4+
The fuzzing setup is handled by Nix inside a `nix-shell` and works both on
5+
Linux and macOS. Nix is the only dependency (https://nixos.org/download.html).
6+
7+
## Usage
8+
9+
```sh
10+
$ nix-shell fuzz/shell.nix
11+
$ autoreconf -i -v -f
12+
$ ./configure --disable-lz4
13+
$ cd fuzz
14+
$ ./openvpn-fuzz.py fuzz base64
15+
$ ./openvpn-fuzz.py fuzz parse_argv -- -fork=4 -ignore_crashes=1
16+
$ ./openvpn-fuzz.py coverage base64 parse_argv # specified targets
17+
$ ./openvpn-fuzz.py coverage # all targets
18+
$ ./openvpn-fuzz.py coverage --clean # do make clean before and after, use if previously built for fuzzing
19+
```

fuzz/openvpn-fuzz.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#!/usr/bin/env python
2+
3+
import argparse
4+
import os
5+
import platform
6+
import subprocess
7+
import sys
8+
9+
TARGETS = [
10+
'base64',
11+
'buffer',
12+
'dhcp',
13+
'forward',
14+
'list',
15+
'misc',
16+
'mroute',
17+
'mss',
18+
'packet_id',
19+
'parse_argv',
20+
'proxy',
21+
'route',
22+
'verify_cert',
23+
]
24+
25+
BASE_DIR = os.path.dirname(os.path.realpath(__file__))
26+
27+
def fuzz_target(target, args=[]):
28+
build_targets([target])
29+
os.makedirs(corpus_dir(target), exist_ok=True)
30+
os.chdir(target_dir(target, "fuzzer"))
31+
os.execv(target_bin_path(target, "fuzzer"),
32+
[target_bin_path(target, "fuzzer"), corpus_dir(target)] + args)
33+
34+
def generate_coverage_report(targets=TARGETS):
35+
"""
36+
If OpenVPN was previously built for fuzzing run `make -C ../ clean` before and after generating coverage.
37+
"""
38+
wd = os.getcwd()
39+
build_targets(targets, for_coverage=True)
40+
profraws = []
41+
object_args = []
42+
for target in targets:
43+
os.chdir(target_dir(target, "coverage"))
44+
profraws.append(target_dir(target, "coverage", "default.profraw"))
45+
object_args.append("-object")
46+
object_args.append(target_bin_path(target, "coverage"))
47+
subprocess.run([target_bin_path(target, "coverage"), corpus_dir(target), "-runs=0"])
48+
49+
profdata = build_dir("coverage", "combined.profdata")
50+
subprocess.run(["llvm-profdata", "merge", "-o", profdata, "-sparse"] + profraws)
51+
subprocess.run(["llvm-cov", "show", "--format", "html", f"-instr-profile={profdata}",
52+
"-output-dir", build_dir("coverage", "report")] + object_args)
53+
os.chdir(wd)
54+
55+
def triage_parse_argv_crashes():
56+
"""
57+
Filters out false positives that are caused by calling exit.
58+
"""
59+
import pwn
60+
target = "parse_argv"
61+
for filename in os.listdir(target_dir(target, "fuzzer")):
62+
if "crash-" in filename:
63+
print("Triaging", filename)
64+
with open(target_dir(target, "fuzzer", filename), "rb") as f:
65+
argv_raw = f.read()
66+
p = pwn.process(executable="../src/openvpn/openvpn", argv=argv_raw.split(b'\x00'))
67+
out = p.readall()
68+
if b"SIGSEGV" in out or b"smashing" or b"AddressSanitizer" in out:
69+
print(pwn.hexdump(argv_raw))
70+
print(out)
71+
exit(1)
72+
p.close()
73+
74+
def build_openvpn(cflags):
75+
"""
76+
Build OpenVPN as usual, assumes `autoconf -f -v -f` and `./configure --disable-lz4` already run.
77+
"""
78+
subprocess.run(["make", "-j", "-C", "../", f"CFLAGS={cflags}"])
79+
80+
def build_targets(targets, for_coverage=False):
81+
fuzzer_flags = '-g -fsanitize=address,fuzzer-no-link'
82+
coverage_flags = '-g -fprofile-instr-generate -fcoverage-mapping'
83+
84+
build_subdir = 'coverage' if for_coverage else 'fuzzer'
85+
os.makedirs(build_dir(build_subdir), exist_ok=True)
86+
87+
cflags = coverage_flags if for_coverage else fuzzer_flags
88+
cflags += " -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION"
89+
build_openvpn(cflags)
90+
91+
o_files = []
92+
for file in os.listdir("../src/openvpn"):
93+
if file.endswith(".o") and file != 'openvpn.o':
94+
o_files.append("../src/openvpn/" + file)
95+
subprocess.run(["ar", "r", build_dir(build_subdir, "libopenvpn.a")] + o_files)
96+
97+
subprocess.run(["clang++", "-c", "src/fuzz_randomizer.cpp",
98+
"-o", build_dir(build_subdir, "fuzz_randomizer.o")] +
99+
cflags.split(' '))
100+
101+
extra_libs = ["-lc++abi", "-lresolv"] if platform.system() == 'Darwin' else ['-lcap-ng']
102+
103+
for target in targets:
104+
os.makedirs(target_dir(target, build_subdir), exist_ok=True)
105+
subprocess.run(["clang", "-I../src/openvpn", "-I..", "-I../src/compat", "-I../include",
106+
"-lssl", "-lcrypto", "-llzo2", f"src/fuzz_{target}.c",
107+
build_dir(build_subdir, "libopenvpn.a"),
108+
build_dir(build_subdir, "fuzz_randomizer.o"),
109+
"-o", target_bin_path(target, build_subdir),
110+
"-g", "-fsanitize=address,fuzzer"] +
111+
(coverage_flags.split(' ') if for_coverage else []) +
112+
extra_libs)
113+
114+
def build_dir(subdir, file=''):
115+
"""
116+
There are two build flavors that live in their own subdirs: coverage and fuzzer.
117+
"""
118+
return os.path.join(BASE_DIR, "build", subdir, file)
119+
120+
def target_dir(target, subdir, file=''):
121+
return os.path.join(build_dir(subdir), f"fuzz_{target}", file)
122+
123+
def corpus_dir(target):
124+
return os.path.join(BASE_DIR, "corpus", f"fuzz_{target}")
125+
126+
def target_bin(target):
127+
return f"fuzz_{target}"
128+
129+
def target_bin_path(target, subdir):
130+
return target_dir(target, subdir, target_bin(target))
131+
132+
if __name__ == "__main__":
133+
parser = argparse.ArgumentParser()
134+
subparsers = parser.add_subparsers(dest="subcommand")
135+
fuzz_parser = subparsers.add_parser('fuzz')
136+
fuzz_parser.add_argument('target', type=str)
137+
fuzz_parser.add_argument('libfuzzer_args', type=str, nargs='*')
138+
coverage_parser = subparsers.add_parser('coverage')
139+
coverage_parser.add_argument('targets', type=str, nargs='*')
140+
coverage_parser.add_argument('--clean', action=argparse.BooleanOptionalAction)
141+
142+
args = parser.parse_args()
143+
if args.subcommand == 'fuzz':
144+
# ./openvpn-fuzz.py fuzz proxy -- -fork=4 -ignore_crashes=1
145+
fuzz_target(args.target, args.libfuzzer_args)
146+
elif args.subcommand == 'coverage':
147+
if args.clean:
148+
subprocess.run(["make", "-C", "../", "clean"])
149+
150+
if args.targets:
151+
generate_coverage_report(args.targets)
152+
else:
153+
generate_coverage_report()
154+
155+
if args.clean:
156+
subprocess.run(["make", "-C", "../", "clean"])
157+
else:
158+
parser.print_help()

fuzz/shell.nix

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
with import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/e58a7747db96c23b8a977e7c1bbfc5753b81b6fa.tar.gz") {};
2+
3+
let llvmPackages = llvmPackages_14;
4+
in llvmPackages.stdenv.mkDerivation {
5+
name = "openvpn-fuzz";
6+
buildInputs = [
7+
autoconf
8+
automake
9+
m4
10+
libtool
11+
pkg-config
12+
openssl_1_1
13+
lz4
14+
lzo
15+
pam
16+
llvmPackages.llvm
17+
python3Packages.pwntools
18+
] ++ lib.optional (!stdenv.isDarwin) libcap_ng;
19+
}

fuzz/src/fuzz_base64.c

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/* Copyright 2021 Google LLC
2+
Licensed under the Apache License, Version 2.0 (the "License");
3+
you may not use this file except in compliance with the License.
4+
You may obtain a copy of the License at
5+
http://www.apache.org/licenses/LICENSE-2.0
6+
Unless required by applicable law or agreed to in writing, software
7+
distributed under the License is distributed on an "AS IS" BASIS,
8+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
See the License for the specific language governing permissions and
10+
limitations under the License.
11+
*/
12+
#include <stdint.h>
13+
#include <stdlib.h>
14+
#include <string.h>
15+
16+
#include "base64.h"
17+
18+
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
19+
if (size > 500) {
20+
return 0;
21+
}
22+
23+
char *new_str = (char *)malloc(size + 1);
24+
if (new_str == NULL) {
25+
return 0;
26+
}
27+
memcpy(new_str, data, size);
28+
new_str[size] = '\0';
29+
30+
char *str = NULL;
31+
openvpn_base64_encode(data, size, &str);
32+
if(str != NULL) {
33+
free(str);
34+
}
35+
36+
uint16_t outsize = 10000;
37+
char *output_buf = (char *)malloc(outsize);
38+
openvpn_base64_decode(new_str, output_buf, outsize);
39+
free(output_buf);
40+
41+
free(new_str);
42+
return 0;
43+
}

0 commit comments

Comments
 (0)