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.
@@ -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