Breaking Ruby’s Unmarshal with AFL-Fuzz

I found an integer overflow in the rb_str_modify_expand()function (fromstring.c)of the Ruby MRI interpreter by fuzzing the Marshal.load function with the killer AFL (American Fuzzy Lop) fuzzer. Please see the full bug report here.

Ruby serializes (and unserializes) objects using a process called ‘marshalling’. Marshalling converts an object to a binary string and unmarshalling converts it back. Marshalling allows an object to be saved or sent over a network.

It is an important security practice (CWE-502) to never unmarshal (or deserialize) untrusted objects as the data cannot be trusted to be well formed. Inspired by a critical serialization vulnerability in Java, I decided to fuzz Ruby’s unmarshalling mechanism with the AFL fuzzer in hopes of shaking out a bug or two.

After compiling ruby with the AFL instrumenting compiler, I wrote a simple ‘driver’ script in ruby to unmarshal the input crafted by the fuzzer:

File.open(ARGV[0]) do |f|
@gc = Marshal.load(f)
end

The next step was to create a seed file. AFL works best with a small, valid seed file so I simply ran Marshal.dump() with some trivial input and used the result as the initial seed file. I created a directory called in and moved the seed file into it.

I started the fuzz run with this command line:

afl-fuzz -i in -o out ruby load.rb @@

The @@ is a placeholder to indicate to AFL where to put the seed file (or mutations of it) in the command line. Let the fuzz run commence!

Since I had not yet written the cloud based clustering technology that would become Fuzz Stati0n’s product, the fuzz run took several days to finish. But AFL did find a crash. When the crashing file was unmarshalled, Ruby outputted:

# ruby load.rb marshal-overflow 
load.rb:3: [BUG] probable buffer overflow: 12 for 11
ruby 2.3.1p112 (2016-04-26 revision 54768) [i686-linux]
<snip - backtrace>
[NOTE]
You may have encountered a bug in the Ruby interpreter or extension libraries.
Bug reports are welcome.

Further investigation with Valgrind revealed a one byte invalid read in the heap due to an integer overflow — a classic off-by-one error found by AFL’s ‘interesting integers’ mutation algorithm. A string length in the marshalled data of 0x7fffffff triggers the overflow on 32 -bit Ubuntu 14.04. Here is the crafted input file that crashed ruby:

# xxd marshal-overflow
0000000: 0408 3afc ffff ff7f 3030 3030 3030 3030 ..:.....00000000
0000010: 3030 3030

I reported the bug to the maintainers and they fixed it very quickly — here is the diff:

Could this bug be exploitable as a security vulnerability? The function rb_str_modify_expand is used throughout ruby but one byte bad reads are usually pretty hard to exploit directly. However, it could play a critical role in a bug-chain style exploit — perhaps as an information leak used to defeat ASLR.

For information on Fuzz Stati0n’s scalable, cloud based continuous fuzz testing solution, please see our website.