Memory Corruption Vulnerabilities

Windows Advanced 📅 Published: 01/09/2025

A comprehensive guide to memory corruption vulnerabilities, their causes, exploitation methods, and prevention strategies.

Memory Corruption Vulnerabilities

A comprehensive guide to memory corruption vulnerabilities, their causes, exploitation methods, and prevention strategies.


Introduction to Memory Corruption

Memory corruption vulnerabilities represent one of the most critical classes of security flaws in modern software. These vulnerabilities occur when a program unintentionally modifies memory locations outside of its intended boundaries, leading to unpredictable behavior, crashes, or in the worst case, arbitrary code execution. Understanding these vulnerabilities is essential for both developers writing secure code and security researchers identifying and mitigating threats.

Types of Memory Corruption Vulnerabilities

Buffer Overflows

Buffer overflows occur when data is written beyond the boundaries of a fixed-size buffer.

Stack-Based Buffer Overflow

#include <string.h>
void vulnerable_function(char* input) {
    char buffer[64];           // Fixed-size buffer
    strcpy(buffer, input);     // No bounds checking - VULNERABLE!
    printf("Input: %s\n", buffer);
}
int main(int argc, char* argv[]) {
    if (argc > 1) {
        vulnerable_function(argv[1]);  // Can overflow buffer
    }
    return 0;
}

Heap-Based Buffer Overflow

#include <stdlib.h>
#include <string.h>
void heap_overflow_example(char* input) {
    char* buffer = malloc(64);        // Allocate 64 bytes
    strcpy(buffer, input);            // No bounds checking - VULNERABLE!
    // Adjacent heap chunks can be corrupted
    free(buffer);
}

Use-After-Free (UAF)

Occurs when a program continues to use a memory location after it has been freed.

struct User {
    char name[32];
    void (*print_info)(struct User*);
};
void print_user_info(struct User* user) {
    printf("User: %s\n", user->name);
}
void uaf_example() {
    struct User* user = malloc(sizeof(struct User));
    strcpy(user->name, "Alice");
    user->print_info = print_user_info;
    // Use the object
    user->print_info(user);
    // Free the memory
    free(user);
    // VULNERABILITY: Use after free
    user->print_info(user);  // Undefined behavior - may crash or worse
}

Double Free

Calling free() twice on the same memory location.

void double_free_example() {
    char* buffer = malloc(256);
    // Use the buffer
    strcpy(buffer, "Some data");
    // Free the memory
    free(buffer);
    // VULNERABILITY: Double free
    free(buffer);  // Corrupts heap metadata
}

Integer Overflow/Underflow

Arithmetic operations that exceed the range of the data type.

void integer_overflow_example(unsigned int count, unsigned int size) {
    // VULNERABILITY: Integer overflow
    unsigned int total_size = count * size;
    if (total_size > MAX_ALLOCATION_SIZE) {
        return;  // Size check after overflow!
    }
    char* buffer = malloc(total_size);  // May allocate small buffer
    // Fill buffer - will overflow if total_size wrapped around
    for (unsigned int i = 0; i < count; i++) {
        memset(buffer + (i * size), 0x41, size);
    }
    free(buffer);
}
// Example: count = 0x80000000, size = 4
// total_size = 0x80000000 * 4 = 0x200000000 (overflows to 0)
// malloc(0) may return small buffer, but loop writes 8GB of data

Format String Vulnerabilities

Improper use of format string functions with user-controlled input.

void format_string_vulnerability(char* user_input) {
    // VULNERABILITY: User input used directly as format string
    printf(user_input);  // Should be: printf("%s", user_input);
}
// Exploitation examples:
// Input: "%x %x %x %x"     - Reads stack memory
// Input: "%n"              - Writes to memory
// Input: "%s"              - May crash or leak memory

Heap Exploitation Techniques

Heap Layout and Metadata

Understanding heap structure is crucial for exploitation:

// Simplified heap chunk structure (glibc malloc)
struct malloc_chunk {
    size_t prev_size;    // Size of previous chunk (if free)
    size_t size;         // Size of chunk, with flags in lower 3 bits
    // For free chunks only:
    struct malloc_chunk* fd;  // Forward pointer to next free chunk
    struct malloc_chunk* bk;  // Backward pointer to previous free chunk
    // User data follows...
};
// Chunk flags (stored in lower 3 bits of size)
#define PREV_INUSE  0x1  // Previous chunk is in use
#define IS_MMAPPED  0x2  // Chunk was obtained with mmap()
#define NON_MAIN_ARENA 0x4  // Chunk belongs to non-main arena

Heap Overflow Exploitation

Unlink Attack

// Classic unlink exploitation (now mitigated in modern allocators)
void unlink_attack_example() {
    // Allocate three chunks
    char* chunk1 = malloc(64);  // Victim chunk
    char* chunk2 = malloc(64);  // Overflow target
    char* chunk3 = malloc(64);  // Prevent top chunk consolidation
    // Craft fake chunk in chunk1
    size_t* fake_chunk = (size_t*)chunk1;
    fake_chunk[0] = 0;              // prev_size
    fake_chunk[1] = 0x40;           // size (with PREV_INUSE bit clear)
    fake_chunk[2] = (size_t)(&fake_chunk[2] - 3); // fd (points to fd - 12)
    fake_chunk[3] = (size_t)(&fake_chunk[3] - 2); // bk (points to bk - 8)
    // Overflow chunk2 to corrupt chunk3's metadata
    char* overflow_data = chunk2;
    size_t* chunk3_header = (size_t*)(chunk2 + 64);
    chunk3_header[0] = 0x40;        // fake prev_size
    chunk3_header[1] &= ~PREV_INUSE; // Clear PREV_INUSE bit
    // Free chunk3 - triggers unlink on fake chunk
    free(chunk3);  // Results in arbitrary write primitive
}

Fastbin Attack

// Fastbin double-free attack
void fastbin_attack() {
    char* chunk1 = malloc(32);  // Fastbin size
    char* chunk2 = malloc(32);
    // Free chunks to populate fastbin
    free(chunk1);
    free(chunk2);
    free(chunk1);  // Double free - creates cycle in fastbin list
    // Allocate and corrupt fastbin list
    char* new_chunk1 = malloc(32);  // Returns chunk1
    // Write fake pointer to arbitrary location
    size_t target_address = 0x601060;  // Target location
    *(size_t*)new_chunk1 = target_address;
    char* new_chunk2 = malloc(32);  // Returns chunk2
    char* new_chunk3 = malloc(32);  // Returns chunk1 again
    char* arbitrary_chunk = malloc(32);  // Returns target_address!
    // Now we can write to arbitrary memory location
}

Use-After-Free Exploitation

// UAF exploitation example
struct VulnerableObject {
    void (*callback)(void);
    char data[64];
};
void legitimate_callback() {
    printf("Legitimate function called\n");
}
void evil_callback() {
    printf("Evil function called - code execution!\n");
    system("/bin/sh");
}
void uaf_exploitation() {
    // Allocate object
    struct VulnerableObject* obj = malloc(sizeof(struct VulnerableObject));
    obj->callback = legitimate_callback;
    strcpy(obj->data, "Some data");
    // Free the object
    free(obj);
    // Allocate new data that overlaps freed memory
    char* controlled_data = malloc(sizeof(struct VulnerableObject));
    // Fill with attacker-controlled data
    size_t* fake_obj = (size_t*)controlled_data;
    fake_obj[0] = (size_t)evil_callback;  // Overwrite function pointer
    // Use after free - calls evil_callback instead of legitimate_callback
    obj->callback();  // Code execution achieved!
}

Advanced Exploitation Techniques

Return-Oriented Programming (ROP)

Technique to bypass non-executable memory protections by chaining existing code snippets.

// Example ROP chain construction
struct rop_chain {
    void* gadget1;     // pop rdi; ret
    void* arg1;        // "/bin/sh"
    void* gadget2;     // pop rsi; ret  
    void* arg2;        // NULL
    void* gadget3;     // pop rdx; ret
    void* arg3;        // NULL
    void* gadget4;     // pop rax; ret
    void* syscall_num; // 59 (sys_execve)
    void* syscall_gadget; // syscall; ret
};
void build_rop_chain(struct rop_chain* chain, void* binsh_addr) {
    // Find gadgets in binary/libraries
    chain->gadget1 = find_gadget("pop rdi; ret");
    chain->arg1 = binsh_addr;
    chain->gadget2 = find_gadget("pop rsi; ret");
    chain->arg2 = NULL;
    chain->gadget3 = find_gadget("pop rdx; ret");
    chain->arg3 = NULL;
    chain->gadget4 = find_gadget("pop rax; ret");
    chain->syscall_num = (void*)59;  // execve system call
    chain->syscall_gadget = find_gadget("syscall; ret");
}

Jump-Oriented Programming (JOP)

Similar to ROP but uses jump instructions instead of return instructions.

// JOP gadget examples
// Gadget 1: Load immediate and jump
// mov eax, 0x41414141
// jmp [ebx + 0x4]
// Gadget 2: Arithmetic operation and jump  
// add ecx, edx
// jmp [esi + 0x8]
// Gadget 3: System call and jump
// int 0x80
// jmp [edi + 0xc]
// JOP dispatcher (controls execution flow)
void jop_dispatcher() {
    asm volatile (
        "mov ebx, %0\n"     // Load gadget table address
        "jmp [ebx]\n"       // Jump to first gadget
        :
        : "m" (gadget_table)
        : "ebx"
    );
}

Heap Feng Shui

Technique to control heap layout for reliable exploitation.

void heap_feng_shui() {
    // Phase 1: Create holes in heap
    char* chunks[100];
    // Allocate many chunks
    for (int i = 0; i < 100; i++) {
        chunks[i] = malloc(64);
    }
    // Free every other chunk to create pattern
    for (int i = 0; i < 100; i += 2) {
        free(chunks[i]);
        chunks[i] = NULL;
    }
    // Phase 2: Fill holes with controlled data
    for (int i = 0; i < 50; i++) {
        char* controlled = malloc(64);
        // Fill with attacker-controlled data
        memset(controlled, 0x41, 64);
    }
    // Phase 3: Trigger vulnerability
    // Now heap layout is predictable for exploitation
    trigger_heap_overflow();
}

Detection Techniques

Static Analysis

Pattern Recognition

// Dangerous function patterns to detect
const char* dangerous_functions[] = {
    "strcpy",    // No bounds checking
    "strcat",    // No bounds checking  
    "sprintf",   // No bounds checking
    "gets",      // Never safe
    "scanf",     // Format string issues
    "alloca",    // Stack allocation issues
    NULL
};
bool scan_for_dangerous_functions(const char* source_code) {
    for (int i = 0; dangerous_functions[i]; i++) {
        if (strstr(source_code, dangerous_functions[i])) {
            printf("Warning: Found dangerous function: %s\n", 
                   dangerous_functions[i]);
            return true;
        }
    }
    return false;
}

Control Flow Analysis

// Simplified control flow graph analysis
struct CFGNode {
    void* address;
    struct CFGNode* successors[4];
    int successor_count;
    bool is_call;
    bool is_return;
    bool is_indirect;
};
bool analyze_control_flow(struct CFGNode* cfg) {
    // Check for suspicious patterns
    for (struct CFGNode* node = cfg; node; node = node->successors[0]) {
        // Detect indirect calls with user-controlled data
        if (node->is_indirect && is_user_controlled(node->address)) {
            printf("Potential vulnerability: Indirect call with user data\n");
            return true;
        }
        // Detect return to user-controlled addresses
        if (node->is_return && is_stack_corrupted(node)) {
            printf("Potential vulnerability: Return address corruption\n");
            return true;
        }
    }
    return false;
}

Dynamic Analysis

AddressSanitizer (ASan)

# Compile with AddressSanitizer
gcc -fsanitize=address -g -O1 vulnerable.c -o vulnerable
# Run the program - ASan will detect memory errors
./vulnerable
# Example ASan output:
# ==12345==ERROR: AddressSanitizer: heap-buffer-overflow
# WRITE of size 1 at 0x60300000eff0 thread T0
# #0 0x4007a8 in vulnerable_function vulnerable.c:10
# #1 0x4007f2 in main vulnerable.c:15

Valgrind Memcheck

# Run with Valgrind
valgrind --tool=memcheck --leak-check=full --track-origins=yes ./vulnerable
# Example output:
# ==12345== Invalid write of size 1
# ==12345==    at 0x4007A8: vulnerable_function (vulnerable.c:10)
# ==12345==    by 0x4007F2: main (vulnerable.c:15)
# ==12345==  Address 0x4a9d040 is 0 bytes after a block of size 64 alloc'd

Runtime Protection Mechanisms

Stack Canaries

// Compiler-generated stack canary check
void protected_function(char* input) {
    // Compiler inserts canary
    unsigned long canary = __stack_chk_guard;
    char buffer[256];
    // Function body
    strcpy(buffer, input);
    // Compiler inserts canary check before return
    if (__stack_chk_guard != canary) {
        __stack_chk_fail();  // Abort on corruption
    }
}

Heap Canaries

// Custom heap canary implementation
#define HEAP_CANARY 0xDEADBEEF
void* safe_malloc(size_t size) {
    // Allocate extra space for canary
    size_t total_size = size + sizeof(uint32_t) * 2;
    char* ptr = malloc(total_size);
    if (!ptr) return NULL;
    // Place canaries before and after user data
    *(uint32_t*)ptr = HEAP_CANARY;
    *(uint32_t*)(ptr + sizeof(uint32_t) + size) = HEAP_CANARY;
    return ptr + sizeof(uint32_t);
}
void safe_free(void* ptr) {
    if (!ptr) return;
    char* real_ptr = (char*)ptr - sizeof(uint32_t);
    size_t size = malloc_usable_size(real_ptr) - sizeof(uint32_t) * 2;
    // Check canaries
    if (*(uint32_t*)real_ptr != HEAP_CANARY ||
        *(uint32_t*)(real_ptr + sizeof(uint32_t) + size) != HEAP_CANARY) {
        fprintf(stderr, "Heap corruption detected!\n");
        abort();
    }
    free(real_ptr);
}

Prevention and Mitigation

Secure Coding Practices

Safe String Handling

// Instead of strcpy
void safe_string_copy(char* dest, size_t dest_size, const char* src) {
    if (!dest || !src || dest_size == 0) return;
    size_t src_len = strlen(src);
    size_t copy_len = (src_len < dest_size - 1) ? src_len : dest_size - 1;
    memcpy(dest, src, copy_len);
    dest[copy_len] = '\0';
}
// Or use safe alternatives
#include <bsd/string.h>
strlcpy(dest, src, dest_size);  // BSD systems
strncpy_s(dest, dest_size, src, _TRUNCATE);  // Windows

Memory Management

// Safe memory allocation wrapper
void* safe_malloc(size_t size) {
    if (size == 0 || size > MAX_ALLOCATION_SIZE) {
        return NULL;
    }
    void* ptr = malloc(size);
    if (ptr) {
        memset(ptr, 0, size);  // Initialize to zero
    }
    return ptr;
}
// Safe free wrapper
void safe_free(void** ptr) {
    if (ptr && *ptr) {
        // Clear memory before freeing (for sensitive data)
        size_t size = malloc_usable_size(*ptr);
        memset(*ptr, 0, size);
        free(*ptr);
        *ptr = NULL;  // Prevent use-after-free
    }
}
// Usage
char* buffer = safe_malloc(256);
// ... use buffer ...
safe_free((void**)&buffer);  // buffer is now NULL

Integer Overflow Prevention

// Safe multiplication check
bool safe_multiply(size_t a, size_t b, size_t* result) {
    if (a == 0 || b == 0) {
        *result = 0;
        return true;
    }
    // Check for overflow
    if (a > SIZE_MAX / b) {
        return false;  // Overflow would occur
    }
    *result = a * b;
    return true;
}
// Safe allocation with overflow check
void* safe_calloc(size_t count, size_t size) {
    size_t total_size;
    if (!safe_multiply(count, size, &total_size)) {
        errno = ENOMEM;
        return NULL;
    }
    return calloc(count, size);
}

Compiler Security Features

GCC Security Flags

# Comprehensive security compilation
gcc -Wall -Wextra -Werror \
    -fstack-protector-strong \    # Stack canaries
    -D_FORTIFY_SOURCE=2 \        # Runtime bounds checking
    -fPIE -pie \                 # Position independent executable  
    -Wl,-z,relro \              # Read-only relocations
    -Wl,-z,now \                # Immediate binding
    -Wl,-z,noexecstack \        # Non-executable stack
    -fno-common \               # Prevent global variable overlap
    -Wformat-security \         # Format string warnings
    -Wcast-align \              # Alignment warnings
    program.c -o program

Control Flow Integrity (CFI)

# Clang CFI compilation
clang -fsanitize=cfi \
      -fsanitize-cfi-cross-dso \
      -flto \
      -fvisibility=hidden \
      program.c -o program
# CFI protections:
# - cfi-icall: Indirect call protection
# - cfi-vcall: Virtual call protection  
# - cfi-nvcall: Non-virtual call protection
# - cfi-derived-cast: Derived class cast protection
# - cfi-unrelated-cast: Unrelated cast protection

Memory-Safe Languages

Rust Memory Safety

// Rust prevents memory corruption at compile time
fn safe_string_operations() {
    let mut buffer = String::new();
    let input = "Hello, World!";
    // Safe string operations
    buffer.push_str(input);
    // Bounds checking
    let slice = &input[0..5];  // "Hello"
    // Memory is automatically managed
    // No manual free() needed
}
// Ownership prevents use-after-free
fn ownership_example() {
    let data = vec![1, 2, 3, 4, 5];
    let reference = &data[0];
    drop(data);  // Frees memory
    // println!("{}", reference);  // Compile error - use after free prevented
}

C++ Smart Pointers

#include <memory>
#include <vector>
class SafeMemoryExample {
public:
    // RAII and smart pointers prevent memory leaks
    void smart_pointer_example() {
        // Automatic memory management
        auto buffer = std::make_unique<char[]>(256);
        // Shared ownership
        auto shared_data = std::make_shared<std::vector<int>>(1000);
        // Memory automatically freed when objects go out of scope
    }
    // Weak pointers prevent circular references
    void weak_pointer_example() {
        auto shared_ptr = std::make_shared<int>(42);
        std::weak_ptr<int> weak_ptr = shared_ptr;
        // Check if object still exists
        if (auto locked = weak_ptr.lock()) {
            // Safe to use
            std::cout << *locked << std::endl;
        }
    }
};

Testing and Fuzzing

Automated Fuzzing

AFL++ Setup

# Install AFL++
git clone https://github.com/AFLplusplus/AFLplusplus
cd AFLplusplus && make && sudo make install
# Instrument target program
afl-gcc -g -O1 -fsanitize=address vulnerable.c -o vulnerable-fuzz
# Create test cases
mkdir testcases
echo "AAAA" > testcases/input1.txt
echo "BBBBBBBB" > testcases/input2.txt
# Start fuzzing
afl-fuzz -i testcases -o findings -m none ./vulnerable-fuzz @@

LibFuzzer Integration

// LibFuzzer target function
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
    if (size < 4) return 0;
    // Create null-terminated string
    char* input = malloc(size + 1);
    memcpy(input, data, size);
    input[size] = '\0';
    // Test vulnerable function
    vulnerable_function(input);
    free(input);
    return 0;
}
# Compile with LibFuzzer
clang -fsanitize=fuzzer,address vulnerable.c libfuzzer_target.c -o fuzzer
# Run fuzzer
./fuzzer -max_total_time=3600  # Fuzz for 1 hour

Property-Based Testing

// Hypothesis property-based testing (Python)
from hypothesis import given, strategies as st
import ctypes
# Load vulnerable C library
lib = ctypes.CDLL('./vulnerable.so')
@given(st.binary(min_size=0, max_size=1000))
def test_buffer_overflow(input_data):
    """Test that function doesn't crash with arbitrary input"""
    try:
        # Convert to C string
        c_input = ctypes.c_char_p(input_data)
        # Call vulnerable function
        result = lib.vulnerable_function(c_input)
        # Check for crashes or unexpected behavior
        assert result >= 0  # Function should return success
    except SystemError:
        # Catch segmentation faults
        assert False, f"Crash detected with input: {input_data}"

Real-World Case Studies

Heartbleed (CVE-2014-0160)

Buffer over-read vulnerability in OpenSSL:

// Simplified Heartbleed vulnerability
int heartbeat_vulnerable(char* payload, int payload_length, int claimed_length) {
    char response[MAX_RESPONSE_SIZE];
    // VULNERABILITY: No validation that claimed_length matches actual length
    if (claimed_length > MAX_RESPONSE_SIZE) {
        return -1;  // Too large
    }
    // Copy claimed_length bytes, even if payload is shorter
    memcpy(response, payload, claimed_length);  // Buffer over-read!
    // Send response back to client
    send_response(response, claimed_length);
    return 0;
}
// Attack: payload_length = 1, claimed_length = 64000
// Reads 64KB of server memory beyond the 1-byte payload

Stagefright (CVE-2015-1538)

Integer overflow leading to heap corruption in Android:

// Simplified Stagefright vulnerability
status_t parseChunk(uint32_t chunk_size, uint32_t chunk_type) {
    // VULNERABILITY: Integer overflow
    uint32_t total_size = chunk_size + sizeof(chunk_header);
    if (total_size < chunk_size) {
        // Overflow check - but comes too late for allocation
        return ERROR_MALFORMED;
    }
    // Allocation with overflowed size
    uint8_t* buffer = malloc(total_size);  // May allocate small buffer
    // Copy chunk_size bytes into potentially small buffer
    memcpy(buffer + sizeof(chunk_header), chunk_data, chunk_size);  // Heap overflow!
    free(buffer);
    return OK;
}

Future Directions

Hardware-Assisted Security

Intel Memory Protection Extensions (MPX)

// MPX bounds checking (deprecated but educational)
void mpx_protected_function(char* buffer, size_t size) {
    // Compiler inserts bounds information
    __builtin_ia32_bndstx(buffer, buffer + size);
    // Bounds checked memory access
    for (size_t i = 0; i < size; i++) {
        buffer[i] = 'A';  // Hardware checks bounds automatically
    }
}

ARM Memory Tagging Extension (MTE)

// MTE provides tag-based memory safety
void* mte_malloc(size_t size) {
    void* ptr = malloc(size);
    // Generate random tag (4 bits)
    uint8_t tag = random() & 0xF;
    // Tag the memory and pointer
    ptr = __builtin_arm_ldg(ptr, tag);  // Set pointer tag
    __builtin_arm_stg(ptr, size);       // Set memory tags
    return ptr;
}
void mte_access(void* ptr, size_t index) {
    char* buffer = (char*)ptr;
    // Hardware automatically checks tag match
    buffer[index] = 'A';  // Faults if tags don't match
}

Memory-Safe System Programming

Emerging languages for system programming with memory safety:

  • Rust: Zero-cost abstractions with compile-time memory safety
  • Zig: Low-level control with optional safety checks
  • Swift: Memory management with ARC and safety features
  • Go: Garbage collection with bounds checking

Conclusion

Memory corruption vulnerabilities remain one of the most significant threats in software security. Despite decades of research and mitigation development, they continue to appear in modern software due to the fundamental challenges of manual memory management in languages like C and C++.

Key takeaways from this comprehensive analysis:

  • Understanding is crucial: Developers must understand memory management fundamentals
  • Defense in depth: Multiple protection layers provide better security
  • Tool integration: Automated testing and analysis tools are essential
  • Secure by default: Use safe alternatives and compiler protections
  • Language evolution: Memory-safe languages offer the best long-term solution

The future of secure software development lies in combining traditional mitigation techniques with modern memory-safe languages and hardware-assisted security features. Organizations should adopt comprehensive strategies that include secure coding practices, automated testing, runtime protections, and gradual migration to memory-safe technologies where possible.

As attackers develop new exploitation techniques, defenders must remain vigilant and continue evolving their approaches to memory safety. The ultimate goal is to make memory corruption vulnerabilities a thing of the past through better tools, practices, and technologies.