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.
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
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.
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:
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.
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:
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.
Two rows of this section are particularly interesting in this example:
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:
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.
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.