afl 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|