Simple 64-bit buffer overflow with shellcode
Introduction
Buffer overflow is a common vulnerability that has plagued software systems for years. It occurs when a program attempts to store data beyond the bounds of a buffer, causing the extra data to overwrite adjacent memory locations. This can lead to a variety of problems, including crashes, security breaches, and even the execution of malicious code. One of the most powerful ways to exploit a buffer overflow is by injecting shellcode into the overflowed buffer, which allows an attacker to take control of the program and execute arbitrary commands. In this blog post, we will explore the basics of buffer overflow attacks and demonstrate how to execute shellcode by solving Stack 5 from Pheonix.
Summary
This level from Pheonix is a simple 64-bit buffer overflow that requires us to overflow the buffer and overwrite the return pointer to return to some shellcode that we have placed on the stack.
Binary analysis
One of the first things I do when I have a binary is run file
on it
|
|
From this output we now know that we are working with a 64-bit
binary and we also know that it is dynamically linked and is not stripped of the debug symbols, which makes reverse engineering it much easier if we had to.
Source code
Since we are provided with the source code we won’t have to do any disassemling or reverse engineering to figure out how this binary works
|
|
In the comments, we are given a hint that we need to run execve("/bin/sh")
, but there is no execve()
function being ran anywhere in the source code. And if we check the security measure applied using checksec
from pwntools
:
|
|
We see that NX (No execute from stack) is disabled, as well as all the other security measures. So now it is very obvious that we are going to need to inject our own shellcode.
Starting with the main function, it simply prints the banner then calls start_level()
which defines a 128 byte buffer and then uses gets()
(the dangerous C function) to get user input and stores it in the 128 byte buffer without any checks for the length of the user supplied input whatsoever.
Running
Now that we know what it does, we can run it
|
|
As we saw in the source it just takes input and exits.
Now let’s see what happens when we give it a big input
|
|
A segmentation fault! that means accessed parts of the memory we weren’t supposed to.
Finding offset
To find the offset I will open the program in GDB and I am using GEF because it comes with useful tools to help with exploit development. GEF also comes pre-installed on the Pheonix machine.
|
|
Using the pattern create
command in GEF we can create a pattern that is unique for every 8 bytes, which will make it easy to find.
Now we can run the program again and supply this pattern and we find which of the unique 8 bytes from the pattern ended up in rip
.
|
|
The program crashes as expected and GEF has hooks set up to print the registers, stack and instructions.
Looking through the resgisters output we see:
|
|
The instruction pointer was overwritten with raaaaaaa
meaning whatever we place instead of that will be our new rip
.
Now we use pattern search
to find where that is in the pattern string
|
|
From the binary analysis we know that this binary is little endian so now we know that the offset is 136
Crafting exploit
We can now start working on the exploit
|
|
I am using the pwntools
library and I create a an ELF binary object, start the proccess which will open the program to interact with with it and I set the architecture.
Payload
Currently our exploit will take us to the address where it will overwrite rip
and then just go into the stack. So first part of our payload will be the address to write into rip
and then we are going to need a nop slide to make sure we hit our shellcode and then finally, our shellcode.
Step 1 - Find stack address
I want to make sure I have the stack address at the point where the main function would return.
We can do that by first adding a breakpoint at the return instruction of main()
in GDB.
To see the addresses of the instructions we can disassemble the function:
|
|
The address we are interested in is the last one (ret
) which is 0x4005cd
we don’t need to grab the extra 0s because pwntools knows it is a 64-bit program and will treat it accordingly.
Now we run program normally with normal input and it stops at the breakpoint.
From here we use info registers
to look at the registers
|
|
This address is the stack address. We can now update that in the exploit.
|
|
+ 40
to the address just to make sure we hit our nop slide.Step 2 - nop slide
This part is pretty simple. The opcode of a nop instruction is 0x90. We use that as a raw byte in the code as \x90
|
|
Part 3 - Shellcode
For this part we can find shellcode to execute exevce("/bin/sh")
for an amd64 linux system online. But I am going to use shellcraft
from the pwntools library to generate the shellcode.
|
|
I did not have to specify architecture becuase I set the context at the start of the script.
The output of that line will be be the raw shellcode bytes resturned by asm()
. The output of shellcraft.linux.sh()
is the assembly code for executing execve("/bin/sh")
.
Exploiting
Now that we have the payload set up, the final exploit will be:
|
|
We send the payload to the process we opened with p.sendlineafter()
to send the payload right after the banner is printed.
Then we go into interactive mode to input commands into the new /bin/sh
process.
|
|
Success!
Mitigation
This exploit would not have been possible if Address space layout randomization (ASLR) was enabled on this machine and the program was compiled with the NX
bit enabled which disables any code execution from the stack. Since this is a VM made for exploit education, those mitigations were turned off but in a real world scenario they should always be enabled.