Exploit Development with AFL, PEDA and PwnTools

Share

Introduction

In a previous post, we studied how to fuzz a simple homemade 64-bit program using AFL. We found that we could cause a segmentation fault in the target using some specific inputs. In this post (and in this video), we will cover the next step: confirming if the crash can lead to a vulnerability. To do so, we’ll use GDB, the GNU debugger, and PEDA to analyze the execution of the target while processing the inputs previously generated by AFL. By doing so, we will find a way to hijack the execution flow from the Vuln1 program in order to execute our own code.

Analyzing and Exploiting Vuln1

The hard part is now about to start, as we need to delve into to assembly code of the target, analyze the values of the registers and understand how they are related to the input. Fortunately, this step is greatly eased with the introduction of tools such as peda and pwntools. Let’s install these right now. To install gdb-peda, simply run this shell script provided by the developers:

Then install pwntools using pip with  sudo -H pip install pwntools . Once done, we are set to start analyzing the crash.

PEDA will automatically load whenever you start GDB. Before we start, it’s essential to understand some details about GDB, namely that it uses environment variables and hooks within the code of the debugged program. Why is this important? Exploits can heavily depend on knowing some specific memory addresses within the program itself. These locations within the code will vary depending on the defined environment variables and any additional instruction added by GDB. You must be aware that many Linux distributions are enabling Address space layout randomization (ASLR) by default. ASLR will defeat the exploit developed in this tutorial and must be disabled by editing the  /proc/sys/kernel/randomize_va_space file. This file can contain one of the following values:

  • 0 – No randomization;
  • 1 – Conservative randomization. Shared libraries, stack, mmap(), VDSO and heap are randomized; and
  • 2 – Full randomization. In addition to elements listed in the previous point, memory managed through brk() is also randomized.

For the purposes of this exercise, set the value contained in this file to  . You can see the impact of disabling ASLR has on our experiment in this companion video of this post.

That being said, let’s start gdb with the vuln1 program by typing gdb ./vuln1  at the shell. Doing so will load the vuln1 program within the debugging process, but will not execute it until you execute the  run  command, or r  for short. Once you do so, you will see that you are prompted to enter the username and password just as when you are executing the program from the shell. If you type in regular strings, the program will execute as expected and terminate. Quite uneventful. Let’s try something more interesting: execute the program with the inputs generated by AFL. Assuming you have the inputs in a file called crash1.txt, type r < crash1.txt  to start the program using the AFL inputs. This time, you’ll notice that the execution ends with a Segmentation Fault (SIGSEGV) and quite a few things are displayed in your terminal.

Figure 1: Running the vuln1 program in GDB with the fuzzed input generated by AFL.

Let’s take a closer look to the information on the screen. When the fault was caught by the debugger, it stopped the execution of vuln1. At that point, PEDA captured the current state of the application, including values of the registers, the program stack and the instruction which resulted in the fault. In this case, after reviewing the data, you should notice the value pointed by the RSP register.

Figure 2: Value of the RSP register when vuln1 is ran with AFL-generated inputs.

If we take a closer look into our input using the hexdump C crash1.txt  command. We’ll notice a familiar pattern:

Notice the various a7 61b4 6161 6161 a761 values. Well it seems that these bytes somehow ended up being pointed by the RSP register. There’s an easy way to confirm this using PEDA using its pattern generator, but first we need to know how long our input is:

The  wc command tells us that there is 114 bytes in the crash1.txt file, so let’s round this up to 120 bytes for simplicity. Going back to GDB, will use PEDA to generate a cyclic pattern of 120 characters as input using the following command:

This will create a file named pat120 with the generated cyclic pattern. If you take a quick look at it, you’ll have an idea of how it looks. For those who used Metasploit before, this functionality is similar to the pattern_create.rb script.

Now let’s run vuln1 using this newly generated input:

Once again, a segmentation fault is thrown by vuln1 and caught by GDB, however this time the values on the stack are different. The value being pointed to by RSP is also different. Looking back at our pattern, we clearly see that our input is overwriting whatever value is pointed by the RSP register.

The Cyclic Pattern is Pointed by RSP
Figure 3: The RSP register is pointing to the cyclic pattern generated at address 0x7fffffffdec8.

Why is this important? Well because the value pointed by this register will eventually be used as a return address once we hit the RET instruction at main+149. Therefore we can put a specific address in RSP and redirect execution flow to a location we control in memory and execute our own code. First thing we need to figure out is: which bytes of our input are being pointed by RSP? Again, PEDA provides an easy way to do so using the pattern_search  command. This function will provide you with information about where the pattern is detected in memory and in the registers:

In the output above, the value pointed by RSP is overwritten after reading 40 bytes of the given input. Which means that any 8-byte combination (32 bits) located at position 40 will end up being pointed to by the RSP register and eventually, loaded into RIP and used as the address of the next instruction to execute once we reach the RET instruction. Let’s test this by creating a really simple payload that will fill the RSP with “B”s. We do so by generating 40 “A”, then 8 “B” and 112 “C”s. We can use the printf command from the Bash shell, or you can also do the same in Python or Perl. We will use Bash here:

Restart vuln1 by providing input2 as input and take a look at the value of the RIP register:

Failed Hijack of the RSP register
Figure 4: The RIP register didn’t take the address stored in RSP.

Something didn’t go the way we expected. We should have RSP pointing to 0x4242424242424242 (‘BBBBBBBB’), but that’s not the case and the reason for this is that we’re trying to fill RIP with an invalid 64-bit address, which causes a segmentation fault. Despite being able to address 64 bits, current processors actually only support 48 bits, so the upper two bytes should always be null, i.e. 00 00. Knowing this, we’ll modify slightly our payload to include 6x “B” rather than 8.

Note that we are in little endian and therefore, we will write the 2 null bytes AFTER the 6 “B”s. And if we try again, we should be more successful:

We still get a segmentation fault, but this time, it’s because we are trying to reach an invalid address. We now proven that we can take control of the RIP register and redirect execution flow. We also know that we can put our own code within the input given to the vuln1 application, and it will be stored on the stack, which we made executable for the purpose of this exercise. The next step is to actually develop our exploit.

Crafting the Shellcode

Now that we have demonstrated the vulnerability, we need to decide what kind of payload we want. We’ll also determine where our shellcode is stored and then test our payload. This is the trickiest part of software exploitation as many variables comes into play. The addresses obtained in this post will differ from system system and yours will likely be different as well. 

To generate our payload, we will use pwntools, a Python module extremely efficient  at prototyping workable exploits. You will often find it used by participants of CTF. First install the Pwntools module using  pip install pwn . Afterwards, open a Python interpreter such as ipython and import the Pwn-tools module with from pwn import * .

We’ll use the shellcraft sub-module, which offers a wide selection of payloads for multiple architectures. To output the shellcode, simply select the appropriate function from the shellcraft module. for the target architecture and operating system, i.e. ‘amd64’ and ‘linux’. You can obtain more information by using help(shellcraft) in a Python interpreter. You can also combine multiple payloads. Prior to using any of the shellcode, make sure to specify the architect by setting the context object adequately:

For the purpose of this post, we’ll use a lame shellcode that will simply output “Hello!” and exit cleanly. In future posts, we’ll have more useful payloads. We’ll structure our payload as follow:

Basic Exploit Structure for Vuln1
Figure 5: Basic payload structure for the Vuln1 exploit.

The first 40 bytes are simply used to fill the initial buffers and variables to reach the value pointed by RSP. While we used ‘NOP‘ instructions, any value could be used. Bytes 40 to 48 will store the address to our shellcode, which follows it immediately. Our shellcode will occupy the last 112 bytes if it fits that space:

Since our payload is 44 bytes – less than 112 bytes – it will easily fit in the space we have after the address, i.e in the “C” segment of the payload. Had it been greater than 112 bytes, we would have to split it. But let’s stick to the basics for now.

Jump Around

Next, we have to determine the address of our shellcode, i.e. where is our shellcode is located on the stack? Looking back at figure 4,  our place holder of the address (i.e. “BBBBBB”) is at 0x7fffffffdec8. The length of the address is 8 bytes and thus our shellcode will start at 0x7fffffffdec8+8:

Detailed structure of the Vuln1 payload.
Figure 6: Detailed structure of the Vuln1 payload.

Notice that we added a NOP sled after the address. Although not strictly needed, doing so will gives us some leeway for error, especially when testing our payload outside GDB as you will see later. Now let’s craft a quick python function to create our payload and store it into a file. This will automate testing and make things much faster.

Let’s explain some of the code above:

    • start_addr :  contains the address to jump to;
    • nop = asm('nop', arch="amd64") returns the opcode for the “NOP” instruction for amd64 architectures.
    • nop1 = nop*40 : Creates 40 NOPs instructions at the beginning of our payload. 
    • This line, struct.pack("<Q", addr)  will store the address of our shellcode on our payload in little-endian format. The Q is used to specify to the pack function that we are storing the address as a  unsigned long long . See the related Python docs for more information about using  struct

Start GDB again, make sure you have a breakpoint at the RET instruction, so we can follow the execution of our shell code. If you look back at previous runs, you’ll notice that our RET instruction is located at main+149.  To put a breakpoint to this address, simply type b* main+149  in GDB. This will cause the execution to pause at this address. Once paused, type  nexti and you’ll notice that we are jumping into our short NOP sled.

If you’d like to see the assembly translation of opcodes at a specific address, use the pdisass . For example, use  pdisass 0x7fffffffded0 and you should see the instructions listed below.

You can move forward faster thru each of the instructions by using  nexti <count> or just type continue or c to continue execution.  If everything goes well, the program will output “Hello!” and exit without any error:

Working Exploit in Vuln1 (GDB)
Figure 6: Our exploit correctly prints “Hello!” and exits without error.

We now have a working exploit in GDB…admittedly not a terribly useful one. However if you try to launch this shellcode directly from the command-line, you’ll notice that you will get a Segmentation Fault warning:

As mentioned at the beginning, This is due to GDB adding environment variables on the stack when loading the Vuln1 program. You compare the environment variables between your shell and GDB by typing  env at the shell and using   show env at the GDB prompt. You’ll notice that GDB adds the “LINES” and “COLUMNS” variables. Also notice that different systems will have different variables defined, adding to the instability of our exploit. To prevent environment variables from interfering with our exploit, we will unset all of them in GDB by using the unset env command. When running our target from the shell, we will prefix it with  env - to unset all variables in the shell. For example:

To make our exploit work in the shell, we will need to adjust our jumping address. If we rerun our target with a new cyclic pattern without the environment variables, we obtain a different address:

In our first attempt with the environment variables, RSP contained the address 0x7fffffffdec8, this time RSP has 0x7fffffffed18 as value. Adjust this value in the script provided above and try again.

Despite our best efforts, we still can’t exploit from the shell, but we are getting closer. At this point, there is no trick to getting it right but only trial and error. GDB may be adding extra instructions in the code for debugging purposes and still making our jump invalid. There are 2 variables you can adjust to guess to fix the issue:

  1. Gradually increase the jump address; and
  2. Increase the size of the NOP sled preceding the shellcode.

In my case, the exploit finally worked by increasing the jump address by 140 bytes and increasing the NOP sled to 80 bytes. Below is the update Python script used. Note that these values will be different based on your system:

Conclusion

Crafting exploit is a mixture of technology and art. While there is a process and a methodology to go about it, there’s always something different that will require additional research, experimenting and tweaking. Even then, your exploit is not guaranteed 100%, given the multiple variables that can affect offsets or memory locations. Practice and experience will take care of honing these skills. This post is a very timid first step and has many failings: we removed common memory protections, we have include quite a useless exploit and we need to remove environment variables for it to work outside of GDB. We’ll gradually improve the realism of this exercise. However we now understand the overall exploit development process better, along with the various difficulties involved.

YouTube Video

References

Zalewski, M. American Fuzzy Lop. Retrieved June 24, 2017, from http://lcamtuf.coredump.cx/afl/

Salzman, P. %. (n.d.). Using GNU’s GDB Debugger. Retrieved June 24, 2017, from http://www.dirac.org/linux/gdb/

PEDA – Python Exploit Development Assistance for GDB. Retrieved June 24, 2017, from https://github.com/longld/peda

http://a-za-z0-9.net/csaw-2013-quals-expl400-miteegashun/

64-bit Linux stack smashing tutorial: Part 1. Retrieved June 24, 2017, from https://blog.techorganic.com/2015/04/10/64-bit-linux-stack-smashing-tutorial-part-1/

Mr.Un1k0d3r. 64 Bits Linux Stack Based Buffer Overflow. Retrieved June 24, 2017, from https://www.exploit-db.com/docs/33698.pdf

Bravon, A. “How Effective is ASLR on Linux Systems?” Security et alii. February 03, 2013. Accessed July 12, 2017. https://securityetalii.es/2013/02/03/how-effective-is-aslr-on-linux-systems/.

Retrieved June 24, 2017, from http://www.mathyvanhoef.com/2012/11/common-pitfalls-when-writing-exploits.html

Additional Reading

Klein, Tobias. A bug hunter’s diary: a guided tour through the wilds of software security. No Starch Press, 2011. http://amzn.to/2h7imkp

Koziol, Jack, David Litchfield, Dave Aitel, Chris Anley, Sinan Eren, Neel Mehta, and Riley Hassell. “The Shellcoder’s Handbook.” Edycja polska. Helion, Gliwice (2004). http://amzn.to/2h7iArF

Dowd, Mark, John McDonald, and Justin Schuh. The art of software security assessment: Identifying and preventing software vulnerabilities. Pearson Education, 2006. http://amzn.to/2vNZBG2

Software Exploit Development – Fuzzing with AFL

Share

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 common place and operated on a daily basis by IT corporations and security labs; either to find crashes in their software or others’ program, 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 development 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.

Once executed, the program first asks for a username and a password. The inputs are stored in the login and passwd variables. Their value are 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 an 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 mechanism can be achieved using the following GCC command:

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 will present later in this post. By executing  checksec vuln1 we confirm that both the stack canaries and NX bit are disabled:

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

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 password in 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 testcases and in it, create 3 files representing these cases. The name of the files does not matter.

test1.txt test2.txt test3.txt
a
a
root
a
root
1qazxsw2

After creating these 3 files, create another directory called results, which will contains 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:

Where -t ./testcases specifies the directory containing the testcases, -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:

Core_pattern file warning when running AFL
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.

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.

Fuzzing Vuln1 with AFL - Results
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 cover 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.

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

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