afl 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/LICENSE +44 -0
- data/Makefile +21 -0
- data/README.md +108 -0
- data/Rakefile +4 -0
- data/afl-fuzz.c.patch +11 -0
- data/afl.gemspec +21 -0
- data/benchmarks/minimal/harness.rb +26 -0
- data/benchmarks/minimal/input/1 +1 -0
- data/benchmarks/minimal/raw_harness.rb +6 -0
- data/benchmarks/minimal/raw_run +7 -0
- data/benchmarks/minimal/run.rb +67 -0
- data/example/harness.rb +33 -0
- data/example/work/input/1 +1 -0
- data/ext/afl_ext/afl_ext.c +213 -0
- data/ext/afl_ext/extconf.rb +8 -0
- data/lib/afl.rb +134 -0
- data/lib/afl/.gitkeep +0 -0
- data/lib/afl/version.rb +3 -0
- data/test/afl_test.rb +156 -0
- data/test/lib/crashing_test_harness.rb +23 -0
- metadata +67 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f00f7dee91372b0f75e94feb6f5b975cec6fe451d368eb8b5541026e2fc10f0c
|
4
|
+
data.tar.gz: 9a061469bb4a76d80c978f11bb9199256c62ac84cc9dc783cac08636c8a18c30
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 88ce3e69b1f776b4c5e5057522ff1d8d49f570ca29021d46c709149e367fb0a433ee5ac4f2dfe36a0ab8f012f56c330e9a05649d6f8f6147b6a65ce824154eb9
|
7
|
+
data.tar.gz: 22e2509c7800ce93037348bf65dc97e6c270eab32abd39b9a57aa1fed289cd1e2bc647cf3dae3e80dfe9a12accffdf0c6cf12389e11ce88874e86a84a5001086
|
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
afl-ruby is licensed under the MIT license:
|
2
|
+
|
3
|
+
Copyright (c) 2018- Stripe, Inc. (https://stripe.com)
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
22
|
+
|
23
|
+
afl-ruby contains code directly dirived and based upon code from afl-python,
|
24
|
+
also released under the MIT license:
|
25
|
+
|
26
|
+
Copyright © 2013-2017 Jakub Wilk <jwilk@jwilk.net>
|
27
|
+
|
28
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
29
|
+
of this software and associated documentation files (the “Software”), to deal
|
30
|
+
in the Software without restriction, including without limitation the rights
|
31
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
32
|
+
copies of the Software, and to permit persons to whom the Software is
|
33
|
+
furnished to do so, subject to the following conditions:
|
34
|
+
|
35
|
+
The above copyright notice and this permission notice shall be included in
|
36
|
+
all copies or substantial portions of the Software.
|
37
|
+
|
38
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
39
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
40
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
41
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
42
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
43
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
44
|
+
SOFTWARE.
|
data/Makefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
VERSION=0.0.0
|
2
|
+
|
3
|
+
# Debug pretty printer
|
4
|
+
print-%: ; @echo $*=$($*)
|
5
|
+
|
6
|
+
default: build uninstall install
|
7
|
+
|
8
|
+
build:
|
9
|
+
gem build afl.gemspec
|
10
|
+
|
11
|
+
uninstall:
|
12
|
+
gem uninstall --ignore-dependencies afl
|
13
|
+
|
14
|
+
install:
|
15
|
+
gem install --verbose afl-${VERSION}.gem
|
16
|
+
|
17
|
+
test: default
|
18
|
+
ruby harness.rb
|
19
|
+
|
20
|
+
.PHONY: build install uninstall default test
|
21
|
+
.DEFAULT: default
|
data/README.md
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
# afl-ruby
|
2
|
+
|
3
|
+
AFL for Ruby! You can learn more about AFL itself [here](http://lcamtuf.coredump.cx/afl/).
|
4
|
+
|
5
|
+
## Getting Started
|
6
|
+
|
7
|
+
### 0. Clone the repo
|
8
|
+
|
9
|
+
`afl-ruby` is not yet available on Rubygems, so for now you'll have to clone and build it yourself.
|
10
|
+
|
11
|
+
git clone git@github.com:richo/afl-ruby.git
|
12
|
+
|
13
|
+
### 1. Build the extension
|
14
|
+
|
15
|
+
You will need to manually build the native extension to the Ruby interpreter in order to allow AFL to instrument your Ruby code. To do this:
|
16
|
+
|
17
|
+
cd lib/afl
|
18
|
+
ruby ../../ext/afl/extconf.rb
|
19
|
+
make
|
20
|
+
|
21
|
+
### 2. Instrument your code
|
22
|
+
|
23
|
+
To instrument your code for AFL, call `AFL.init` when you're ready to initialize the AFL forkserver,
|
24
|
+
then wrap the block of code that you want to fuzz in `AFL.with_exceptions_as_crashes { ... }`. For
|
25
|
+
example:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
def byte
|
29
|
+
$stdin.read(1)
|
30
|
+
end
|
31
|
+
|
32
|
+
def c
|
33
|
+
r if byte == 'r'
|
34
|
+
end
|
35
|
+
|
36
|
+
def r
|
37
|
+
s if byte == 's'
|
38
|
+
end
|
39
|
+
|
40
|
+
def s
|
41
|
+
h if byte == 'h'
|
42
|
+
end
|
43
|
+
|
44
|
+
def h
|
45
|
+
raise "Crashed"
|
46
|
+
end
|
47
|
+
|
48
|
+
require 'afl'
|
49
|
+
|
50
|
+
unless ENV['NO_AFL']
|
51
|
+
AFL.init
|
52
|
+
end
|
53
|
+
|
54
|
+
AFL.with_exceptions_as_crashes do
|
55
|
+
c if byte == 'c'
|
56
|
+
exit!(0)
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
### 3. Patch AFL
|
61
|
+
|
62
|
+
AFL checks if you're an instrumented binary by seeing if you have the AFL environment variable anywhere in your binary. We're using a bog stock ruby interpreter, so we can't do that. Apply `afl-fuzz.c.patch` before building AFL to remove this check. Assuming you have cloned `afl` and `afl-ruby` in the same directory (i.e. in `~/MYCODE/afl` and `~/MYCODE/afl-ruby`) you can do this by:
|
63
|
+
|
64
|
+
cd ../afl
|
65
|
+
git checkout -b apply-ruby-patch
|
66
|
+
git apply ../afl-fuzz.c.patch
|
67
|
+
git add .
|
68
|
+
git commit -m "Apply Ruby patch"
|
69
|
+
make install
|
70
|
+
# Check that this did indeed update your AFL
|
71
|
+
ls -la $(which afl-fuzz)
|
72
|
+
|
73
|
+
### 4. Run the example
|
74
|
+
|
75
|
+
You should then be able to run the sample harness in the `example/` directory:
|
76
|
+
|
77
|
+
/path/to/afl/afl-fuzz -i example/work/input -o example/work/output -- /usr/bin/ruby example/harness.rb
|
78
|
+
|
79
|
+
It should only take a few seconds to find a crash. Once a crash is found it should be written to `example/work/output/crashes/` for you to inspect.
|
80
|
+
|
81
|
+
### Troubleshooting
|
82
|
+
|
83
|
+
If AFL complains that `Program '/usr/bin/ruby' is not a 64-bit Mach-O binary` then this may be because your system Ruby has the old Mach-O magic header bytes, which AFL does not accept. You should try running `afl-fuzz` using a different Ruby interpreter. For example, you can use an rbenv Ruby like so:
|
84
|
+
|
85
|
+
# Find out which versions rbenv has available
|
86
|
+
ls ~/.rbenv/versions
|
87
|
+
# Pick an available version, then run something like this:
|
88
|
+
/path/to/afl/afl-fuzz -i work/input -o work/output -- ~/.rbenv/versions/2.4.1/bin/ruby harness.rb
|
89
|
+
|
90
|
+
# Developing
|
91
|
+
|
92
|
+
## Extensions
|
93
|
+
|
94
|
+
Be sure to build the C extension (see "Build the extension" above).
|
95
|
+
|
96
|
+
## Tests
|
97
|
+
|
98
|
+
To run the basic test suite, simply run:
|
99
|
+
|
100
|
+
rake test
|
101
|
+
|
102
|
+
Make sure you have built the extension and patched AFL first, as above.
|
103
|
+
|
104
|
+
# Credits
|
105
|
+
|
106
|
+
Substantial portions of afl-ruby are either inspired by, or transposed directly from afl-python by Jakub Wilk <jwilk@jwilk.net> licensed under MIT.
|
107
|
+
|
108
|
+
[Stripe](https://stripe.com) allowed both myself and [rob](https://github.com/robert) to spend substantial amounts of company time developing afl-ruby.
|
data/Rakefile
ADDED
data/afl-fuzz.c.patch
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
--- afl-fuzz.c.orig 2018-02-16 16:38:20.388534866 +0000
|
2
|
+
+++ afl-fuzz.c 2018-02-16 16:38:30.464519660 +0000
|
3
|
+
@@ -6917,7 +6917,7 @@
|
4
|
+
" For that, you can use the -n option - but expect much worse results.)\n",
|
5
|
+
doc_path);
|
6
|
+
|
7
|
+
- FATAL("No instrumentation detected");
|
8
|
+
+ // FATAL("No instrumentation detected");
|
9
|
+
|
10
|
+
}
|
11
|
+
|
data/afl.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.push lib unless $LOAD_PATH.include? lib
|
5
|
+
require 'afl/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = 'afl'
|
9
|
+
s.version = AFL::VERSION
|
10
|
+
s.authors = ['Richo Healey']
|
11
|
+
s.email = ['richo@psych0tik.net']
|
12
|
+
s.homepage = 'http://github.com/richo/afl-ruby'
|
13
|
+
s.summary = 'AFL support for ruby'
|
14
|
+
s.description = 'American Fuzzy Lop (AFL) support for ruby'
|
15
|
+
s.license = 'MIT'
|
16
|
+
|
17
|
+
s.files = `git ls-files -z`.split("\0")
|
18
|
+
s.test_files = Dir['test/**/*']
|
19
|
+
s.extensions = %w[ext/afl_ext/extconf.rb]
|
20
|
+
s.require_paths = ['lib']
|
21
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require_relative '../../lib/afl'
|
3
|
+
|
4
|
+
# This is a completely trivial harness that allows us to measure just the
|
5
|
+
# cost of the AFL machinery. The harness does nothing apart from send a
|
6
|
+
# message that it has completed an iteration to the Benchmarker via a
|
7
|
+
# UNIX socket.
|
8
|
+
|
9
|
+
MAX_ATTEMPTS = 10
|
10
|
+
attempts = 0
|
11
|
+
begin
|
12
|
+
socket = UNIXSocket.open('/tmp/sock')
|
13
|
+
rescue Errno::ENOENT
|
14
|
+
attempts += 1
|
15
|
+
if attempts >= MAX_ATTEMPTS
|
16
|
+
sleep(1)
|
17
|
+
retry
|
18
|
+
end
|
19
|
+
raise
|
20
|
+
end
|
21
|
+
|
22
|
+
AFL.init
|
23
|
+
AFL.with_exceptions_as_crashes do
|
24
|
+
socket.send('1', 0)
|
25
|
+
end
|
26
|
+
exit(0)
|
@@ -0,0 +1 @@
|
|
1
|
+
TEST
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'fileutils'
|
3
|
+
require_relative '../../lib/afl'
|
4
|
+
|
5
|
+
# Usage:
|
6
|
+
#
|
7
|
+
# ruby ./benchmarks/minimal/run.rb
|
8
|
+
|
9
|
+
puts("Running benchmark...")
|
10
|
+
|
11
|
+
ENV['AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES'] = '1'
|
12
|
+
|
13
|
+
input_dir = File.expand_path('input', File.dirname(__FILE__))
|
14
|
+
output_dir = File.expand_path('output', File.dirname(__FILE__))
|
15
|
+
target_path = File.expand_path('harness.rb', File.dirname(__FILE__))
|
16
|
+
|
17
|
+
cmdline_args = [
|
18
|
+
'afl-fuzz',
|
19
|
+
'-i',
|
20
|
+
input_dir,
|
21
|
+
'-o',
|
22
|
+
output_dir,
|
23
|
+
'--',
|
24
|
+
'ruby',
|
25
|
+
target_path,
|
26
|
+
]
|
27
|
+
|
28
|
+
SOCKET_PATH = '/tmp/sock'
|
29
|
+
|
30
|
+
if File.exist?(SOCKET_PATH)
|
31
|
+
FileUtils.rm(SOCKET_PATH)
|
32
|
+
end
|
33
|
+
|
34
|
+
afl_io = IO.popen(cmdline_args)
|
35
|
+
server = UNIXServer.open(SOCKET_PATH)
|
36
|
+
accepted_socket = server.accept
|
37
|
+
|
38
|
+
puts("Socket connection accepted")
|
39
|
+
|
40
|
+
begin
|
41
|
+
start_time = Time.now
|
42
|
+
last_checkpoint_time = start_time
|
43
|
+
timeout_s = 200
|
44
|
+
poll_s = 0.5
|
45
|
+
total_iterations = 0
|
46
|
+
|
47
|
+
while Time.now <= start_time + timeout_s do
|
48
|
+
break if afl_io.closed?
|
49
|
+
d = accepted_socket.recv(10000)
|
50
|
+
|
51
|
+
new_iterations = d.length
|
52
|
+
last_checkpoint_time_delta = Time.now - last_checkpoint_time
|
53
|
+
last_checkpoint_time = Time.now
|
54
|
+
current_iterations_per_s = new_iterations.to_f / last_checkpoint_time_delta
|
55
|
+
|
56
|
+
total_iterations += new_iterations
|
57
|
+
total_elapsed_time = Time.now - start_time
|
58
|
+
overall_iterations_per_s = total_iterations.to_f / total_elapsed_time
|
59
|
+
|
60
|
+
puts("ITERATIONS: #{total_iterations}\t ELAPSED TIME: #{total_elapsed_time}\t CURRENT ITERATIONS / S: #{current_iterations_per_s}\t TOTAL ITERATIONS / S: #{overall_iterations_per_s}")
|
61
|
+
sleep(poll_s)
|
62
|
+
end
|
63
|
+
end_time = Time.now
|
64
|
+
ensure
|
65
|
+
Process.kill('TERM', afl_io.pid)
|
66
|
+
end
|
67
|
+
puts("Benchmark completed")
|
data/example/harness.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
$: << "../lib"
|
2
|
+
def byte
|
3
|
+
$stdin.read(1)
|
4
|
+
end
|
5
|
+
|
6
|
+
def c
|
7
|
+
r if byte == 'r'
|
8
|
+
end
|
9
|
+
|
10
|
+
def r
|
11
|
+
s if byte == 's'
|
12
|
+
end
|
13
|
+
|
14
|
+
def s
|
15
|
+
h if byte == 'h'
|
16
|
+
end
|
17
|
+
|
18
|
+
def h
|
19
|
+
raise "Crashed"
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'afl'
|
23
|
+
|
24
|
+
AFL.with_logging_to_file('/tmp/afl-debug-test-harness') do
|
25
|
+
unless ENV['NO_AFL']
|
26
|
+
AFL.init
|
27
|
+
end
|
28
|
+
|
29
|
+
AFL.with_exceptions_as_crashes do
|
30
|
+
c if byte == 'c'
|
31
|
+
exit!(0)
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
BUTTS
|
@@ -0,0 +1,213 @@
|
|
1
|
+
#include <sys/shm.h>
|
2
|
+
#include <fcntl.h>
|
3
|
+
|
4
|
+
#include <ruby.h>
|
5
|
+
#include <ruby/st.h>
|
6
|
+
#include <errno.h>
|
7
|
+
#include <signal.h>
|
8
|
+
#include <stdio.h>
|
9
|
+
#include <unistd.h>
|
10
|
+
|
11
|
+
// These need to be in sync with afl-fuzz
|
12
|
+
static const char* SHM_ENV_VAR = "__AFL_SHM_ID";
|
13
|
+
static const int FORKSRV_FD = 198;
|
14
|
+
#define MAP_SIZE_POW2 16
|
15
|
+
static const int MAP_SIZE = 1 << MAP_SIZE_POW2;
|
16
|
+
static unsigned char *afl_area = NULL;
|
17
|
+
|
18
|
+
static VALUE AFL = Qnil;
|
19
|
+
static VALUE init_done = Qfalse;
|
20
|
+
|
21
|
+
#ifdef AFL_RUBY_EXT_DEBUG_LOG
|
22
|
+
static FILE *aflogf = NULL;
|
23
|
+
|
24
|
+
static void aflogf_init(void) {
|
25
|
+
int fd = open("/tmp/aflog",
|
26
|
+
O_WRONLY | O_CREAT | O_TRUNC,
|
27
|
+
S_IRUSR | S_IWUSR);
|
28
|
+
if (fd < 0) {
|
29
|
+
fprintf(stderr, "unable to open() /tmp/aflog\n");
|
30
|
+
_exit(1);
|
31
|
+
}
|
32
|
+
|
33
|
+
aflogf = fdopen(fd, "w");
|
34
|
+
if (!aflogf) {
|
35
|
+
fprintf(stderr, "unable to fdopen() /tmp/aflog\n");
|
36
|
+
_exit(1);
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
static void aflog_printf(const char *fmt, ...) {
|
41
|
+
va_list ap;
|
42
|
+
|
43
|
+
if (!aflogf) aflogf_init();
|
44
|
+
|
45
|
+
va_start(ap, fmt);
|
46
|
+
vfprintf(aflogf, fmt, ap);
|
47
|
+
va_end(ap);
|
48
|
+
|
49
|
+
// quiesce aflogf's writer buffer to disk immediately
|
50
|
+
fflush(aflogf);
|
51
|
+
}
|
52
|
+
|
53
|
+
#define LOG aflog_printf
|
54
|
+
#else
|
55
|
+
#define LOG(...)
|
56
|
+
#endif
|
57
|
+
|
58
|
+
/**
|
59
|
+
* Returns the location in the AFL shared memory to write the
|
60
|
+
* given Ruby trace data to.
|
61
|
+
*
|
62
|
+
* Borrowed from afl-python for consistency, then refactored
|
63
|
+
* https://github.com/jwilk/python-afl/blob/8df6bfefac5de78761254bf5d7724e0a52d254f5/afl.pyx#L74-L87
|
64
|
+
*/
|
65
|
+
#define LHASH_INIT 0x811C9DC5
|
66
|
+
#define LHASH_MAGIC_MULT 0x01000193
|
67
|
+
#define LHASH_NEXT(x) h = ((h ^ (unsigned char)(x)) * LHASH_MAGIC_MULT)
|
68
|
+
|
69
|
+
static inline unsigned int lhash(const char *key, size_t offset) {
|
70
|
+
const char *const last = &key[strlen(key) - 1];
|
71
|
+
uint32_t h = LHASH_INIT;
|
72
|
+
while (key <= last) LHASH_NEXT(*key++);
|
73
|
+
for (; offset != 0; offset >>= 8) LHASH_NEXT(offset);
|
74
|
+
return h;
|
75
|
+
}
|
76
|
+
|
77
|
+
/**
|
78
|
+
* Write Ruby trace data to AFL's shared memory.
|
79
|
+
*
|
80
|
+
* TODO: link to the AFL code that this is mimicking.
|
81
|
+
*/
|
82
|
+
static VALUE afl_trace(VALUE _self, VALUE file_name, VALUE line_no) {
|
83
|
+
static int prev_location;
|
84
|
+
int offset;
|
85
|
+
VALUE exc = rb_const_get(AFL, rb_intern("RuntimeError"));
|
86
|
+
|
87
|
+
if (init_done == Qfalse) {
|
88
|
+
rb_raise(exc, "AFL not initialized, call ::AFL.init first!");
|
89
|
+
}
|
90
|
+
|
91
|
+
char* fname = StringValueCStr(file_name);
|
92
|
+
size_t lno = FIX2INT(line_no);
|
93
|
+
unsigned int location = lhash(fname, lno) % MAP_SIZE;
|
94
|
+
LOG("[+] %s:%zu\n", fname, lno);
|
95
|
+
|
96
|
+
offset = location ^ prev_location;
|
97
|
+
prev_location = location / 2;
|
98
|
+
LOG("[!] offset 0x%x\n", offset);
|
99
|
+
afl_area[offset] += 1;
|
100
|
+
|
101
|
+
LOG("[-] done with trace");
|
102
|
+
return Qtrue;
|
103
|
+
}
|
104
|
+
|
105
|
+
/**
|
106
|
+
* Initialize the AFL forksrv by testing that we can write to it.
|
107
|
+
*/
|
108
|
+
static VALUE afl__init_forkserver(void) {
|
109
|
+
LOG("Testing writing to forksrv fd=%d\n", FORKSRV_FD);
|
110
|
+
|
111
|
+
int ret = write(FORKSRV_FD + 1, "\0\0\0\0", 4);
|
112
|
+
if (ret != 4) {
|
113
|
+
VALUE exc = rb_const_get(AFL, rb_intern("RuntimeError"));
|
114
|
+
rb_raise(exc, "Couldn't write to forksrv");
|
115
|
+
}
|
116
|
+
|
117
|
+
LOG("Successfully wrote out nulls to forksrv ret=%d\n", ret);
|
118
|
+
return Qnil;
|
119
|
+
}
|
120
|
+
|
121
|
+
static VALUE afl__forkserver_read(VALUE _self) {
|
122
|
+
unsigned int value;
|
123
|
+
int ret = read(FORKSRV_FD, &value, 4);
|
124
|
+
LOG("Read from forksrv value=%d ret=%d", value, ret);
|
125
|
+
if (ret != 4) {
|
126
|
+
LOG("Couldn't read from forksrv errno=%d", errno);
|
127
|
+
VALUE exc = rb_const_get(AFL, rb_intern("RuntimeError"));
|
128
|
+
rb_raise(exc, "Couldn't read from forksrv");
|
129
|
+
}
|
130
|
+
return INT2FIX(value);
|
131
|
+
}
|
132
|
+
|
133
|
+
/**
|
134
|
+
* Write a value (generally a child_pid) to the AFL forkserver.
|
135
|
+
*/
|
136
|
+
static VALUE afl__forkserver_write(VALUE _self, VALUE v) {
|
137
|
+
unsigned int value = FIX2INT(v);
|
138
|
+
|
139
|
+
int ret = write(FORKSRV_FD + 1, &value, 4);
|
140
|
+
LOG("Wrote to forksrv_sock value=%d ret=%d\n", value, ret);
|
141
|
+
if (ret != 4) {
|
142
|
+
VALUE exc = rb_const_get(AFL, rb_intern("RuntimeError"));
|
143
|
+
rb_raise(exc, "Couldn't write to forksrv");
|
144
|
+
}
|
145
|
+
return INT2FIX(ret);
|
146
|
+
}
|
147
|
+
|
148
|
+
/**
|
149
|
+
* Initialize AFL's shared memory segment.
|
150
|
+
*/
|
151
|
+
static VALUE afl__init_shm(void) {
|
152
|
+
LOG("Initializing SHM\n");
|
153
|
+
VALUE exc = rb_const_get(AFL, rb_intern("RuntimeError"));
|
154
|
+
|
155
|
+
if (init_done == Qtrue) {
|
156
|
+
rb_raise(exc, "AFL already initialized");
|
157
|
+
}
|
158
|
+
|
159
|
+
const char * afl_shm_id_str = getenv(SHM_ENV_VAR);
|
160
|
+
if (afl_shm_id_str == NULL) {
|
161
|
+
rb_raise(
|
162
|
+
exc,
|
163
|
+
"No AFL SHM segment specified. AFL's SHM env var is not set."
|
164
|
+
"Are we actually running inside AFL?");
|
165
|
+
}
|
166
|
+
|
167
|
+
const int afl_shm_id = atoi(afl_shm_id_str);
|
168
|
+
afl_area = shmat(afl_shm_id, NULL, 0);
|
169
|
+
if (afl_area == (void*) -1) {
|
170
|
+
rb_raise(exc, "Couldn't map shm segment");
|
171
|
+
}
|
172
|
+
LOG("afl_area at 0x%zx\n", afl_area);
|
173
|
+
|
174
|
+
init_done = Qtrue;
|
175
|
+
|
176
|
+
LOG("Done initializing SHM\n");
|
177
|
+
return Qtrue;
|
178
|
+
}
|
179
|
+
|
180
|
+
/**
|
181
|
+
* Close the AFL forksrv file descriptors.
|
182
|
+
*/
|
183
|
+
static VALUE afl__close_forksrv_fds(VALUE _self) {
|
184
|
+
close(FORKSRV_FD);
|
185
|
+
close(FORKSRV_FD + 1);
|
186
|
+
return Qnil;
|
187
|
+
}
|
188
|
+
|
189
|
+
static VALUE afl_bail_bang(VALUE _self) {
|
190
|
+
LOG("bailing\n");
|
191
|
+
#ifdef AFL_RUBY_EXT_DEBUG_LOG
|
192
|
+
if (aflogf) {
|
193
|
+
fclose(aflogf);
|
194
|
+
aflogf = NULL;
|
195
|
+
}
|
196
|
+
#endif
|
197
|
+
_exit(0);
|
198
|
+
}
|
199
|
+
|
200
|
+
void Init_afl_ext(void) {
|
201
|
+
AFL = rb_const_get(rb_cObject, rb_intern("AFL"));
|
202
|
+
LOG("...\n");
|
203
|
+
|
204
|
+
rb_define_module_function(AFL, "trace", afl_trace, 2);
|
205
|
+
rb_define_module_function(AFL, "_init_shm", afl__init_shm, 0);
|
206
|
+
rb_define_module_function(AFL, "_init_forkserver", afl__init_forkserver, 0);
|
207
|
+
rb_define_module_function(AFL, "_close_forksrv_fds", afl__close_forksrv_fds, 0);
|
208
|
+
rb_define_module_function(AFL, "_forkserver_read", afl__forkserver_read, 0);
|
209
|
+
rb_define_module_function(AFL, "_forkserver_write", afl__forkserver_write, 1);
|
210
|
+
rb_define_module_function(AFL, "bail!", afl_bail_bang, 0);
|
211
|
+
VALUE vFORKSRV_FD = INT2FIX(FORKSRV_FD);
|
212
|
+
rb_define_const(AFL, "FORKSRV_FD", vFORKSRV_FD);
|
213
|
+
}
|
data/lib/afl.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
class AFL
|
2
|
+
|
3
|
+
class RuntimeError < StandardError; end
|
4
|
+
|
5
|
+
DEFAULT_DEBUG_LOG_FILE = '/tmp/afl-debug-output'
|
6
|
+
|
7
|
+
# Initialize AFL. When using the forksrv, try to call
|
8
|
+
# this as late as possible, after you have done all your
|
9
|
+
# expensive, generic setup that will not change between
|
10
|
+
# test-cases. Your test-cases will start their runs at
|
11
|
+
# the point where you call `AFL.init`.
|
12
|
+
def self.init
|
13
|
+
self._init_shm
|
14
|
+
|
15
|
+
# Use Ruby's TracePoint to report the Ruby traces that
|
16
|
+
# have been executed to AFL.
|
17
|
+
@trace = TracePoint.new(:call, :c_call) do |tp|
|
18
|
+
AFL.trace(tp.path, tp.lineno)
|
19
|
+
end
|
20
|
+
|
21
|
+
unless ENV['AFL_NO_FORKSRV']
|
22
|
+
self._init_forkserver
|
23
|
+
self.spawn_child
|
24
|
+
self._close_forksrv_fds
|
25
|
+
end
|
26
|
+
|
27
|
+
@trace.enable
|
28
|
+
end
|
29
|
+
|
30
|
+
# Turn off reporting of trace information to AFL.
|
31
|
+
def self.deinit
|
32
|
+
@trace.disable
|
33
|
+
end
|
34
|
+
|
35
|
+
# Turn exceptions raised within the block into crashes
|
36
|
+
# that can be recorded by AFL.
|
37
|
+
def self.with_exceptions_as_crashes
|
38
|
+
begin
|
39
|
+
yield
|
40
|
+
rescue Exception
|
41
|
+
self.crash!
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# AFL does not print the output of the inferior to the
|
46
|
+
# terminal. This can make it difficult to debug errors.
|
47
|
+
# This method logs the output to tmpfiles for you to
|
48
|
+
# inspect. It should only be used for debugging.
|
49
|
+
#
|
50
|
+
# Note that this method truncates the log file at the
|
51
|
+
# beginning of each call to it in order to conserve
|
52
|
+
# disk space. A good workflow is therefore to keep:
|
53
|
+
#
|
54
|
+
# tail -f /tmp/afl-debug-output
|
55
|
+
#
|
56
|
+
# running in one window whilst running your debug
|
57
|
+
# script in another.
|
58
|
+
#
|
59
|
+
# Example usage:
|
60
|
+
#
|
61
|
+
# AFL.with_logging_to_file do
|
62
|
+
# run_your_program
|
63
|
+
# end
|
64
|
+
def self.with_logging_to_file(path=DEFAULT_DEBUG_LOG_FILE)
|
65
|
+
initial_stdout, initial_stderr = $stdout, $stderr
|
66
|
+
fh = File.open(path, 'w')
|
67
|
+
$stdout.reopen(fh)
|
68
|
+
$stderr.reopen(fh)
|
69
|
+
|
70
|
+
yield
|
71
|
+
ensure
|
72
|
+
fh.flush
|
73
|
+
fh.close
|
74
|
+
$stdout.reopen(initial_stdout)
|
75
|
+
$stderr.reopen(initial_stderr)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Manually log a debug message to a tmpfile.
|
79
|
+
def self.log(msg)
|
80
|
+
fh = File.open('/tmp/aflog-rubby', 'w')
|
81
|
+
fh.write(msg)
|
82
|
+
fh.write("\n")
|
83
|
+
fh.flush
|
84
|
+
fh.close
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.crash!
|
88
|
+
Process.kill("USR1", $$)
|
89
|
+
end
|
90
|
+
|
91
|
+
# #spawn_child is a Ruby wrapper around AFL's forksrv.
|
92
|
+
#
|
93
|
+
# When we want to run a new test-case, #spawn_child forks off a new
|
94
|
+
# thread. This thread returns to the main program, where it runs the
|
95
|
+
# test-case and then exits.
|
96
|
+
#
|
97
|
+
# Meanwhile, the forksrv thread has been waiting for its child to exit.
|
98
|
+
# Once this happens, it waits for another test-case to be ready, when
|
99
|
+
# it forks off another new thread and the cycle continues.
|
100
|
+
#
|
101
|
+
# This is very useful because it allows us to strategically choose our
|
102
|
+
# fork point in order to "cache" expensive setup of our inferior.
|
103
|
+
# Forking as late as possible means that our test-cases take less time.
|
104
|
+
def self.spawn_child
|
105
|
+
loop do
|
106
|
+
# Read and discard the previous test's status. We don't care about the
|
107
|
+
# value, but if we don't read it, the fork server eventually blocks, and
|
108
|
+
# then we block on the call to _forkserver_write below
|
109
|
+
self._forkserver_read
|
110
|
+
|
111
|
+
# Fork a child process
|
112
|
+
child_pid = fork
|
113
|
+
|
114
|
+
# If we are the child thread, return back to the main program
|
115
|
+
# and actually run a testcase.
|
116
|
+
#
|
117
|
+
# If we are the parent, we are the forkserver and we should
|
118
|
+
# continue in this loop so we can fork another child once this
|
119
|
+
# one has returned.
|
120
|
+
return if child_pid.nil?
|
121
|
+
|
122
|
+
# Write child's thread's pid to AFL's fork server
|
123
|
+
self._forkserver_write(child_pid)
|
124
|
+
# Wait for the child to return
|
125
|
+
_pid, status = Process.waitpid2(child_pid)
|
126
|
+
|
127
|
+
# Report the child's exit status to the AFL forkserver
|
128
|
+
report_status = status.termsig || status.exitstatus
|
129
|
+
self._forkserver_write(report_status)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
require 'afl_ext'
|
data/lib/afl/.gitkeep
ADDED
File without changes
|
data/lib/afl/version.rb
ADDED
data/test/afl_test.rb
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
require ::File.expand_path('../../lib/afl', __FILE__)
|
2
|
+
require 'minitest/autorun'
|
3
|
+
|
4
|
+
def read_fuzzer_stats(path)
|
5
|
+
File.open(path) do |f|
|
6
|
+
f.readlines.map do |line|
|
7
|
+
els = line.split(/\s+/)
|
8
|
+
[els[0], els[2]]
|
9
|
+
end.to_h
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe AFL do
|
14
|
+
before do
|
15
|
+
@env = {
|
16
|
+
'AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES' => '1',
|
17
|
+
}
|
18
|
+
@input_dir = File.expand_path('input', __dir__)
|
19
|
+
@output_dir = File.expand_path('output', __dir__)
|
20
|
+
@crash_dir = File.expand_path('crashes', @output_dir)
|
21
|
+
@queue_dir = File.expand_path('queue', @output_dir)
|
22
|
+
|
23
|
+
@target_path = File.expand_path('lib/crashing_test_harness.rb', __dir__)
|
24
|
+
@fuzzer_stats_path = File.expand_path('fuzzer_stats', @output_dir)
|
25
|
+
|
26
|
+
@all_dirs = [@input_dir, @output_dir, @crash_dir, @queue_dir]
|
27
|
+
@all_dirs.each do |d|
|
28
|
+
FileUtils.rm_rf(d, secure: true)
|
29
|
+
end
|
30
|
+
|
31
|
+
@all_dirs.each do |d|
|
32
|
+
unless Dir.exist?(d)
|
33
|
+
Dir.mkdir(d)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
input_file = File.expand_path('test1.txt', @input_dir)
|
38
|
+
File.open(input_file, 'w+') do |f|
|
39
|
+
f.write('0')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
after do
|
44
|
+
@all_dirs.each do |d|
|
45
|
+
FileUtils.rm_rf(d, secure: true)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'fuzzing with AFL' do
|
50
|
+
it 'can find a very basic crash and explore multiple edges' do
|
51
|
+
cmdline_args = [
|
52
|
+
# Don't worry if we are forwarding crash notifications to an external
|
53
|
+
# crash reporting utility.
|
54
|
+
'afl-fuzz',
|
55
|
+
'-i',
|
56
|
+
@input_dir,
|
57
|
+
'-o',
|
58
|
+
@output_dir,
|
59
|
+
'--',
|
60
|
+
'ruby',
|
61
|
+
@target_path,
|
62
|
+
]
|
63
|
+
afl_io = IO.popen(@env, cmdline_args)
|
64
|
+
|
65
|
+
begin
|
66
|
+
start_time = Time.now
|
67
|
+
timeout_s = 10
|
68
|
+
poll_s = 0.5
|
69
|
+
|
70
|
+
while Time.now <= start_time + timeout_s do
|
71
|
+
n_paths = Dir.glob(@queue_dir + '/id:*').length
|
72
|
+
have_paths = n_paths >= 2
|
73
|
+
have_crash = Dir.glob(@crash_dir + '/id:*').length >= 1
|
74
|
+
|
75
|
+
break if afl_io.closed?
|
76
|
+
break if have_crash && have_paths
|
77
|
+
|
78
|
+
sleep(poll_s)
|
79
|
+
end
|
80
|
+
|
81
|
+
assert(have_crash, 'Target program did not crash')
|
82
|
+
assert(have_paths, "Target program only produced #{n_paths} distinct paths")
|
83
|
+
ensure
|
84
|
+
Process.kill('TERM', afl_io.pid)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# In the past we had a bug where we didn't drain the forkserver's test output.
|
89
|
+
# This meant that we couldn't run more than 16,384 (2**14) execs before the
|
90
|
+
# fuzzer would hang and refuse to continue fuzzing. This test makes sure that
|
91
|
+
# doesn't happen again.
|
92
|
+
it 'can run more than 16,384 execs without hanging' do
|
93
|
+
cmdline_args = [
|
94
|
+
'afl-fuzz',
|
95
|
+
'-i',
|
96
|
+
@input_dir,
|
97
|
+
'-o',
|
98
|
+
@output_dir,
|
99
|
+
'--',
|
100
|
+
'ruby',
|
101
|
+
@target_path,
|
102
|
+
]
|
103
|
+
afl_io = IO.popen(@env, cmdline_args)
|
104
|
+
|
105
|
+
# Spin until the first fuzzer_stats appear. At this point we know that the
|
106
|
+
# fuzzer has started fuzzing, and we can start the clock.
|
107
|
+
begin
|
108
|
+
read_fuzzer_stats(@fuzzer_stats_path)
|
109
|
+
rescue Errno::ENOENT
|
110
|
+
sleep(1)
|
111
|
+
retry
|
112
|
+
end
|
113
|
+
|
114
|
+
begin
|
115
|
+
fuzz_start_time = Time.now
|
116
|
+
timeout_s = 60 # Note that this test does usually take the full 60s to run
|
117
|
+
poll_s = 0.5
|
118
|
+
n_execs = 0
|
119
|
+
# The real target is 16_384 (2**14), but let's add a few more to make sure
|
120
|
+
target_n_execs = 17_000
|
121
|
+
|
122
|
+
while Time.now <= fuzz_start_time + timeout_s do
|
123
|
+
stats = read_fuzzer_stats(@fuzzer_stats_path)
|
124
|
+
n_execs = stats['execs_done'].to_i
|
125
|
+
|
126
|
+
break if afl_io.closed?
|
127
|
+
# The fuzzer_stats file doesn't get updated very frequently, so this
|
128
|
+
# condition is unlikely to ever be met. However, the fuzzer does write
|
129
|
+
# fuzzer_stats when it shuts down, so the SIGTERM that we send in the
|
130
|
+
# `ensure` block should make sure that we get the data we need.
|
131
|
+
break if n_execs >= target_n_execs
|
132
|
+
|
133
|
+
sleep(poll_s)
|
134
|
+
end
|
135
|
+
ensure
|
136
|
+
Process.kill('TERM', afl_io.pid)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Make sure that afl gets a chance to write its final fuzzer_stats output
|
140
|
+
# after we SIGTERM it in the `ensure` block above.
|
141
|
+
results_read_start_time = Time.now
|
142
|
+
timeout_s = 10
|
143
|
+
poll_s = 0.5
|
144
|
+
|
145
|
+
while Time.now <= fuzz_start_time + timeout_s do
|
146
|
+
stats = read_fuzzer_stats(@fuzzer_stats_path)
|
147
|
+
n_execs = stats['execs_done'].to_i
|
148
|
+
|
149
|
+
break if n_execs >= target_n_execs
|
150
|
+
sleep(poll_s)
|
151
|
+
end
|
152
|
+
|
153
|
+
assert(n_execs >= target_n_execs, 'Target program did not complete as many execs as expected')
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
$: << "../lib"
|
2
|
+
require_relative('../../lib/afl')
|
3
|
+
|
4
|
+
def function1
|
5
|
+
return 1
|
6
|
+
end
|
7
|
+
|
8
|
+
def function2
|
9
|
+
return 2
|
10
|
+
end
|
11
|
+
|
12
|
+
AFL.init
|
13
|
+
AFL.with_exceptions_as_crashes do
|
14
|
+
input = $stdin.read(1)
|
15
|
+
if input == '7'
|
16
|
+
raise 'I hate the number 7'
|
17
|
+
elsif input.ord % 2 == 0
|
18
|
+
function1
|
19
|
+
else
|
20
|
+
function2
|
21
|
+
end
|
22
|
+
exit!(0)
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: afl
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Richo Healey
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-10-24 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: American Fuzzy Lop (AFL) support for ruby
|
14
|
+
email:
|
15
|
+
- richo@psych0tik.net
|
16
|
+
executables: []
|
17
|
+
extensions:
|
18
|
+
- ext/afl_ext/extconf.rb
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- ".gitignore"
|
22
|
+
- LICENSE
|
23
|
+
- Makefile
|
24
|
+
- README.md
|
25
|
+
- Rakefile
|
26
|
+
- afl-fuzz.c.patch
|
27
|
+
- afl.gemspec
|
28
|
+
- benchmarks/minimal/harness.rb
|
29
|
+
- benchmarks/minimal/input/1
|
30
|
+
- benchmarks/minimal/raw_harness.rb
|
31
|
+
- benchmarks/minimal/raw_run
|
32
|
+
- benchmarks/minimal/run.rb
|
33
|
+
- example/harness.rb
|
34
|
+
- example/work/input/1
|
35
|
+
- ext/afl_ext/afl_ext.c
|
36
|
+
- ext/afl_ext/extconf.rb
|
37
|
+
- lib/afl.rb
|
38
|
+
- lib/afl/.gitkeep
|
39
|
+
- lib/afl/version.rb
|
40
|
+
- test/afl_test.rb
|
41
|
+
- test/lib/crashing_test_harness.rb
|
42
|
+
homepage: http://github.com/richo/afl-ruby
|
43
|
+
licenses:
|
44
|
+
- MIT
|
45
|
+
metadata: {}
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubygems_version: 3.0.3
|
62
|
+
signing_key:
|
63
|
+
specification_version: 4
|
64
|
+
summary: AFL support for ruby
|
65
|
+
test_files:
|
66
|
+
- test/afl_test.rb
|
67
|
+
- test/lib/crashing_test_harness.rb
|