Fuzzing¶
Introduction¶
In this tutorial you will be guided through building a simple fuzzing harness and fuzzing it with AFL++. It assumes you are already familiar with building a SmallWorld harness and with AFL++. For this tutorial we will be using a very simple binary that reads four bytes (which we’re going to pretend is a size) and exits if they are 11 or less, then reads the next four bytes and compares them one-by-one to the values 98, 97, 100, and 33 exiting if they are not equal. If it makes it through all of the compares it crashes by writing to an unmapped address.
BITS 64;
vuln:
cmp DWORD [rdi], 11
jbe .L5
cmp BYTE [rdi+4], 98
je .L7
.L3:
xor eax, eax
jmp .FAKERET
.L7:
cmp BYTE [rdi+5], 97
jne .L3
cmp BYTE [rdi+6], 100
jne .L3
cmp BYTE [rdi+7], 33
jne .L3
movsx rax, BYTE [rdi+8]
mov QWORD [ds:305419896], rax
jmp .L3
.L5:
mov eax, -1
.FAKERET:
nop
The source for this example can be found in the tests/fuzz directory of the
repository in the file fuzz.amd64.s. This file can be assembled with nasm
with the command nasm fuzz.amd64.bin -o fuzz.amd64.bin.
Building Our Harness¶
Now we need to build a SmallWorld harness for this code. First, we begin be initializing the machine, platform, cpu, and code. This is done in the same way as any other SmallWorld harness.
machine = smallworld.state.Machine()
platform = smallworld.platforms.Platform(
smallworld.platforms.Architecture.X86_64, smallworld.platforms.Byteorder.LITTLE
)
cpu = smallworld.state.cpus.CPU.for_platform(platform)
code = smallworld.state.memory.code.Executable.from_filepath(
(pathlib.Path(__file__).parent / "fuzz.amd64.bin").as_posix(), address=0x1000
)
heap = smallworld.state.memory.heap.BumpAllocator(0x2000, 0x4000)
Next, we need somewhere for our input to live. We’re going to use a heap to make things easy. After we create the heap, we’re going to put some example input on it.
heap = smallworld.state.memory.heap.BumpAllocator(0x2000, 0x4000)
user_input = str.encode("goodgoodgood", "utf-8")
size_addr = heap.allocate_integer(
len(user_input), 4, "user input size", smallworld.platforms.Byteorder.LITTLE
)
Then we need to set our registers and add everthing to our machine. We set rip to 0x1000 (where we mapped the code) and rdi to point to our input.
input_addr = heap.allocate_bytes(user_input, "user input")
cpu.rip.set_content(0x1000)
cpu.rdi.set_content(size_addr)
machine.add(heap)
machine.add(cpu)
Now for the part where most of the work happens. AFL++ is going to generate an input and hand it to our harness. However, that input is just a series of bytes. We need to apply that input to our machine. In order to do that we define a callback function. The parameters to this callback are specified in the AFL++ documentation for Unicorn mode. Briefly, the uc is a Unicorn emulator object which you can use to write the input to memory or registers and input is the input generated by AFL++ as a Python bytes. If the input given by AFL++ won’t work for your case then you can return False to skip it.
For our example we will check to make sure the input isn’t bigger than our heap and then just write it to the address of our input.
def input_callback(uc, input, persistent_round, data):
if len(input) > 0x1000:
return False
All that’s left is to start our emulation. SmallWorld only supports fuzzing with our UnicornEmulator. The only other difference that we end with machine.fuzz() and pass in out input callback.
emulator = smallworld.emulators.UnicornEmulator(platform)
emulator.add_exit_point(cpu.rip.get() + 55)
machine.fuzz(emulator, input_callback)
And that’s it. If we put it all together, we have:
#!/usr/bin/env python3
import logging
import pathlib
import smallworld
smallworld.logging.setup_logging(level=logging.INFO)
machine = smallworld.state.Machine()
platform = smallworld.platforms.Platform(
smallworld.platforms.Architecture.X86_64, smallworld.platforms.Byteorder.LITTLE
)
cpu = smallworld.state.cpus.CPU.for_platform(platform)
code = smallworld.state.memory.code.Executable.from_filepath(
(pathlib.Path(__file__).parent / "fuzz.amd64.bin").as_posix(), address=0x1000
)
heap = smallworld.state.memory.heap.BumpAllocator(0x2000, 0x4000)
user_input = str.encode("goodgoodgood", "utf-8")
size_addr = heap.allocate_integer(
len(user_input), 4, "user input size", smallworld.platforms.Byteorder.LITTLE
)
input_addr = heap.allocate_bytes(user_input, "user input")
cpu.rip.set_content(0x1000)
cpu.rdi.set_content(size_addr)
machine.add(heap)
machine.add(cpu)
machine.add(code)
def input_callback(uc, input, persistent_round, data):
if len(input) > 0x1000:
return False
uc.mem_write(size_addr, input)
emulator = smallworld.emulators.UnicornEmulator(platform)
emulator.add_exit_point(cpu.rip.get() + 55)
machine.fuzz(emulator, input_callback)
Running With AFL++¶
To run our harness with AFL++ using a command such as the following:
afl-fuzz -t 10000 -U -m none -i inputs -o outputs -- python3 our_fuzz_harness.py @@
and you should see a TUI that looks like this:
american fuzzy lop ++4.35c {default} (python3) [explore]
┌─ process timing ────────────────────────────────────┬─ overall results ────┐
│ run time : 1 days, 20 hrs, 1 min, 46 sec │ cycles done : 1205 │
│ last new find : 1 days, 17 hrs, 14 min, 29 sec │ corpus count : 5 │
│last saved crash : 0 days, 12 hrs, 54 min, 20 sec │saved crashes : 1 │
│ last saved hang : none seen yet │ saved hangs : 0 │
├─ cycle progress ─────────────────────┬─ map coverage┴──────────────────────┤
│ now processing : 4.1787 (80.0%) │ map density : 0.01% / 0.02% │
│ runs timed out : 0 (0.00%) │ count coverage : 1.00 bits/tuple │
├─ stage progress ─────────────────────┼─ findings in depth ─────────────────┤
│ now trying : havoc │ favored items : 5 (100.00%) │
│ stage execs : 33/120 (27.50%) │ new edges on : 5 (100.00%) │
│ total execs : 339k │ total crashes : 4 (1 saved) │
│ exec speed : 2.15/sec (zzzz...) │ total tmouts : 0 (0 saved) │
├─ fuzzing strategy yields ────────────┴─────────────┬─ item geometry ───────┤
│ bit flips : 0/0, 0/0, 0/0 │ levels : 5 │
│ byte flips : 0/0, 0/0, 0/0 │ pending : 0 │
│ arithmetics : 0/0, 0/0, 0/0 │ pend fav : 0 │
│ known ints : 0/0, 0/0, 0/0 │ own finds : 4 │
│ dictionary : 0/0, 0/0, 0/0, 0/0 │ imported : 0 │
│havoc/splice : 5/339k, 0/0 │ stability : 100.00% │
│py/custom/rq : unused, unused, unused, unused ├───────────────────────┘
│ trim/eff : 75.00%/18, n/a │ [cpu000: 50%]
└─ strategy: exploit ────────── state: finished... ──┘
See the AFL++ documentation for a more complete listing to all of the arguments and options.
Breakdown of an Input¶
Now, lets look at an input the crashed our program. Using xxd we have the following:
00000000: ff7f 0000 6261 6421 ....bad!
As you can see, the first four bytes ff7f 0000 are an integer greater than 11. Then we have the bytes 0x62 (98), 0x61 (97), 0x64 (100), and 0x21 (33) which spells bad! in ascii. Note that this input matches what was required as discussed in the introduction of this tutorial.
Next Steps¶
If you are interested in a more in depth tutorial on using SmallWorld for vulnerability research (including fuzzing), then checkout our in depth tutorial on using SmallWorld to analyize an RTOS found in the repo under use_cases/rtos_demo.