afl 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,12 @@
1
+ benchmarks/minimal/output/*
2
+
3
+ example/work/output/*
4
+
5
+ lib/afl/afl.bundle
6
+ lib/afl/afl.o
7
+ lib/afl/Makefile
8
+
9
+ test/output/*
10
+ test/input/*
11
+
12
+ *.gem
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.
@@ -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
@@ -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.
@@ -0,0 +1,4 @@
1
+ task :default => :test
2
+ task :test do
3
+ Dir.glob('./test/*_test.rb').each { |file| require file}
4
+ end
@@ -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
+
@@ -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,6 @@
1
+ require 'socket'
2
+ require_relative '../../lib/afl'
3
+
4
+ AFL.init
5
+ AFL.with_exceptions_as_crashes {}
6
+ exit(0)
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+
3
+ export AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1
4
+
5
+ afl-fuzz -i ./benchmarks/minimal/input -o ./benchmarks/minimal/output \
6
+ -- ~/.rbenv/versions/2.4.1/bin/ruby \
7
+ ./benchmarks/minimal/raw_harness.rb
@@ -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")
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ require 'mkmf'
2
+
3
+ if enable_config('debug')
4
+ debug = '-DAFL_RUBY_EXT_DEBUG_LOG'
5
+ $defs.push(debug) unless $defs.include? debug
6
+ end
7
+
8
+ create_makefile('afl_ext')
@@ -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'
File without changes
@@ -0,0 +1,3 @@
1
+ module AFL
2
+ VERSION = '0.0.3'
3
+ end
@@ -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