It’s quite impressive to look back in the past to the early days of software vulnerabilities and observe the ongoing dance between new mitigation and new exploitation techniques. Powerful fuzzing tools are now commonplace and operated on a daily basis by IT corporations and security labs; either to find crashes in their software or others’ programs, seeking workable exploit out of it. New research is continuously presented to mitigate new procedures while multiple organizations develop new counter-mitigation tricks. In this post, we’ll overview the entire software exploitation process: from fuzzing with American Fuzzy Lop (AFL) to exploit development with gdb-peda and pwntools. For this purpose, we will develop a quick 64-bit program exhibiting a glaring buffer overflow vulnerability. We will then fuzz it to find that vulnerability, analyze the results and develop an exploit for it. A video is also available.

The Vuln1 Vulnerable Program

While we could use a known vulnerable program online, we decide to craft our own quick C program so we can understand all its facets. The program below uses two characters buffers of 32 characters; one to hold the username and the other one to hold a password. To manage user input, we used the well-known insecure gets() function, which fails to check buffer boundaries and leads to buffer overflows

https://gist.github.com/DeepCodeSec/522f33d8b6fcfeb0568febef32aed108#file-vuln1-c

Once executed, the program first asks for a username and a password. The inputs are stored in the login and passwd variables. Their value is then compared with the expected value using strcmp(). If the credentials entered are “root” and “1qazxsw2”, then a “Access Granted.” message is printed out to the console, otherwise “Access Denied.” is shown to the user and the program exits.

To simplify the exploitation process of this exercise, we will compile this program with absolutely no memory protection, i.e. NX will be disabled and no stack canary. NX is usually enabled to prevent code to be executed directly from the stack, which there is no need for other than exploitation purposes. As for stack canaries, they can detect stack overflows by adding extra value to the software stack. If this value is not found when the function returns, an exception is thrown to prevent further execution. Nowadays, these protection schemes are enabled in a vast majority of cases, but we adopt simplicity rather than realism for this post. Disabling these protection mechanisms can be achieved using the following GCC command:

gcc -fno-stack-protector -z execstack vuln1.c -o vuln1

The -fno-stack-protector will disable the stack canaries while the -z execstack makes both the heap and stack executable. To verify that these options have not been included, we can use a nifty tool call checksec which is included with pwntools, which will present later in this post. By executing checksec vuln1 we confirm that both the stack canaries and NX bit are disabled:

root@ReSyst:~/bin/afl1# checksec vuln1 
[*] Checking for new versions of pwntools 
To disable this functionality, set the contents of /home/infectedpackets/.pwntools-cache/update to 'never'. 
[*] A newer version of pwntools is available on pypi (3.3.3 --> 3.3.4). 
 Update with: $ pip install -U pwntools 
[!] Couldn't find relocations against PLT to get symbols 
[*] '/home/infectedpackets/bin/afl1/vuln1'
 Arch: amd64-64-little
 RELRO: Full RELRO
 Stack: No canary found
 NX: NX disabled
 PIE: PIE enabled

Checksec reports on 2 additional security mechanisms other than the stack canaries and No-eXecute bit. While these concepts are out of scope for this post, we will simply present them

With our target created, we are now ready to start fuzzing it to uncover the buffer overflow it contains.

Fuzzing with AFL

AFL is a popular open-source and free fuzzer that has been leveraged to discover vulnerabilities in a large set of applications and libraries. Before starting AFL, we need to instrumentalize our target using the afl-gcc compiler. The AFL compiler will add code around the source in order to maximize coverage. To compile the source code with AFL, use the same command used above to compile Vuln1 using afl-gcc rather than gcc or use the associated Makefile

afl-gcc -fno-stack-protector -z execstack vuln1.c -o vuln1

The resulting binary is the one that will be used with AFL, but when analyzing the crash later one, we will do it with the gcc compiled binary. Until then, let’s learn how to use AFL to assess the Vuln1 program.

A critical aspect of fuzzing is to craft meaningful test cases, e.g. inputs that will maximize code coverage by exploring all potential paths of the targeted program. The vuln1 program is simple and only has 3 paths:

  1. Username is invalid;
  2. Username is valid, but the password is invalid;
  3. Username and password are valid.

In order to reach these 3 paths, we will design our test cases appropriately by creating 3 files. The first file will have 2 lines, none of them containing the appropriate credentials, the second file will have the right username, but an invalid password and the third file will have both correct credentials. AFL will read the contents of each file and feed each line to the stdin of Vuln1. Create a directory called test cases and in it, create 3 files representing these cases. The name of the files does not matter.

test1.txttest2.txttest3.txt
arootroot
aa1qazxsw2
Test cases to be used with AFL.

After creating these 3 files, create another directory called results, which will contain the results of the fuzzing run. At this point, you’re ready to start AFL using afl-fuzz, the actual fuzzing program. You can do so with the following command:

afl-fuzz -i ./testcases/ -o ./results/ ./vuln1

Where -t ./testcases specifies the directory containing the test cases, -o ./results specifies the output directory and ./vuln1 is that target program. If you run AFL for the first time, you’ll likely be greeted with the following warning:

AFL Warns that the core_pattern file must be changed.
AFL Warns that the core_pattern file must be changed.

Just follow the instruction given and you’ll get rid of this message. Simply a shell as root using sudo bash and type the suggested command, i.e.

echo core > /proc/sys/kernel/core_pattern

Retry to start AFL using the same command and you should have no issue this time. A screen will appear and present you with quite a few statistics. This AFL Readme file explains all of these fields very well, and should definitively be read and well understood. For now, let’s focus on the “Overall Results” section.

Results of Fuzzing the Vuln1 Program
Results of Fuzzing the Vuln1 Program

Two rows of this section are particularly interesting in this example:

  • Total paths; and
  • Unique crashes.

Notice that after a few seconds, the total paths field is 3, which is what we expected based on the code of vuln1. As such, once we reached 3 paths, we can stop AFL by pressing Ctrl-C, as it will not find anything new. In the real world, we have no idea how many paths may be possible. As such AFL provides color codes to help you assess if it’s time to stop. Another field that can help is the last path found. When no new paths have been found after a while, AFL may have covered most of the code it can find and is unlikely to find anything new. Finally, the most interesting field is the unique crashes, which indicates that some of the inputs, stored in the results directory, have successfully crashed the program and should be investigated. We have 2 files in the results/crashes directory:

 

id:000000,sig:11,src:000002,op:havoc,rep:128
id:000001,sig:11,src:000000,op:havoc,rep:128

Each file contains the input that crashed the program so you can reproduce the event and investigate to see if the crash is exploitable.

00000000  74 6f 6f 74 00 00 80 00  f8 1a 7f aa aa aa 88 88  |toot............|
00000010  88 88 88 88 88 88 88 88  88 88 88 88 88 88 88 88  |................|
00000020  0a 46 46 46 46 46 46 46  46 46 46 46 46 46 46 46  |.FFFFFFFFFFFFFFF|
00000030  46 46 46 46 46 46 46 46  46 46 46 46 46 aa af 21  |FFFFFFFFFFFFF..!|
00000040  aa 6f 6f ff aa aa aa aa  7f aa aa aa aa aa ae aa  |.oo.............|
00000050  ab aa aa aa aa f3 00 ff  0f 22 60 21 0a ba 02 0a  |........."`!....|
00000060  3e 18 16 f9 65 21 0a ba  02 13                    
|>...e!....|

We can confirm the crash and observe a segmentation fault by piping the contents of the crash to our vuln1 program:

root@ReSyst:~/bin/afl1/results/crashes# cat id\:000000\,sig\:11\,src\:000002\,op\:havoc\,rep\:128 | ../../vuln1 
Login: 
Password: 
Access Denied. 
Segmentation fault

The next step is to analyze the crash data and determine if it can be converted into an exploitable vulnerability. Spoiler alert: it can.

Conclusion

This short post is a simple introduction to AFL, a powerful fuzzer that can be leveraged on source code and binaries to find potential vulnerabilities. This step is usually the first step in exploit development. In the next post, we’ll use PEDA to analyze the results found here and determine it exploitability.

References

See Also

  • Related YouTube Video: Software Exploitation Research: Fuzzing with AFL

Further Reading