Introduction

Memory safety vulnerabilities continue to be one of the most critical security issues in systems programming. In this comprehensive guide, we’ll explore heap-based vulnerabilities, understand how they occur, and learn methods to prevent them. We’ll focus particularly on how improper string handling and memory management can lead to exploitable conditions.

Understanding the Heap

The heap is a dynamic memory region that allows programs to allocate memory at runtime. Unlike the stack, which follows a strict LIFO (Last In, First Out) order, heap memory can be allocated and freed in any order. This flexibility makes the heap crucial for dynamic data structures but also introduces complexity in memory management.

Heap Memory Layout

The heap consists of several key components:

  1. Heap Blocks: Individual chunks of allocated memory
  2. Block Headers: Metadata preceding each block containing:
    • Size of the block
    • Status flags (allocated/free)
    • Pointers to adjacent blocks
  3. Free Lists: Linked lists of available memory blocks
  4. Wilderness: The topmost chunk of the heap that can grow
flowchart TD
    subgraph "Heap Memory Layout"
        A[Heap Start] --> B[Block Header 1]
        B --> C[User Data 1]
        C --> D[Block Header 2]
        D --> E[User Data 2]
        E --> F[More Blocks...]
        F --> G[Wilderness]
    end

    subgraph "Exploitation Process"
        H[Program Start] --> I[Allocate Memory]
        I --> J[Copy User Input]
        J --> K{Buffer Overflow?}
        K -->|Yes| L[Corrupt Adjacent Memory]
        L --> M[Modify Program Flow]
        K -->|No| N[Normal Execution]
    end

    subgraph "Prevention Methods"
        O[Input Validation] --> P[Safe String Functions]
        P --> Q[Bounds Checking]
        Q --> R[Memory Tracking]
        R --> S[Proper Cleanup]
    end

Common Heap Vulnerabilities

Buffer Overflow in Heap

One of the most common heap vulnerabilities occurs when data is written beyond the bounds of an allocated buffer. Let’s examine a vulnerable program:

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

struct user_data {
    int priority;
    char* name;
};

void process_user(char* user_input1, char* user_input2) {
    struct user_data* user1 = malloc(sizeof(struct user_data));
    struct user_data* user2 = malloc(sizeof(struct user_data));
    
    if (!user1 || !user2) {
        printf("Memory allocation failed\n");
        return;
    }

    // Initialize user1
    user1->priority = 1;
    user1->name = malloc(8);  // Only allocate 8 bytes for name
    
    // Initialize user2
    user2->priority = 2;
    user2->name = malloc(8);

    // Vulnerable strcpy operations
    strcpy(user1->name, user_input1);  // No bounds checking
    strcpy(user2->name, user_input2);  // No bounds checking

    printf("User1 priority: %d, name: %s\n", user1->priority, user1->name);
    printf("User2 priority: %d, name: %s\n", user2->priority, user2->name);

    // Clean up
    free(user1->name);
    free(user2->name);
    free(user1);
    free(user2);
}

int main(int argc, char** argv) {
    if (argc != 3) {
        printf("Usage: %s <name1> <name2>\n", argv[0]);
        return 1;
    }
    process_user(argv[1], argv[2]);
    return 0;
}

To compile this code:

gcc -g -o heap_vuln heap_vuln.c

The -g flag adds debugging information that we’ll use later.

Understanding the Vulnerability

Let’s analyze why this code is vulnerable:

  1. Fixed Buffer Size: We allocate only 8 bytes for each name buffer
  2. Unsafe String Copy: strcpy is used without bounds checking
  3. Adjacent Allocations: The heap blocks are likely to be adjacent, making overflow possible

Examining the Assembly

Let’s look at the key assembly instructions generated for the vulnerable section:

gdb ./heap_vuln
(gdb) disassemble process_user

Key assembly sections to note:

# First malloc call for user1
call   malloc@plt
mov    QWORD PTR [rbp-0x18], rax

# Second malloc call for user1->name
mov    edi, 0x8
call   malloc@plt
mov    rdx, QWORD PTR [rbp-0x18]
mov    QWORD PTR [rdx+0x8], rax

# Vulnerable strcpy
mov    rax, QWORD PTR [rbp-0x18]
mov    rax, QWORD PTR [rax+0x8]
mov    rdx, QWORD PTR [rbp-0x28]
mov    rsi, rdx
mov    rdi, rax
call   strcpy@plt

Exploitation Analysis

When we provide input longer than 8 bytes for the first name, the overflow can corrupt:

  1. The heap metadata following the first name buffer
  2. The second user struct’s data
  3. Subsequent heap allocations

Secure Implementation

Here’s a secure version of the same functionality:

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

#define MAX_NAME_LEN 128

struct user_data_safe {
    int priority;
    char* name;
    size_t name_len;
};

void process_user_safe(const char* user_input1, const char* user_input2) {
    struct user_data_safe* user1 = malloc(sizeof(struct user_data_safe));
    struct user_data_safe* user2 = malloc(sizeof(struct user_data_safe));
    
    if (!user1 || !user2) {
        printf("Memory allocation failed\n");
        goto cleanup;
    }

    // Calculate required buffer sizes
    size_t len1 = strnlen(user_input1, MAX_NAME_LEN);
    size_t len2 = strnlen(user_input2, MAX_NAME_LEN);

    // Initialize user1
    user1->priority = 1;
    user1->name = malloc(len1 + 1);
    user1->name_len = len1;
    
    // Initialize user2
    user2->priority = 2;
    user2->name = malloc(len2 + 1);
    user2->name_len = len2;

    if (!user1->name || !user2->name) {
        printf("Memory allocation failed\n");
        goto cleanup;
    }

    // Safe string operations
    strncpy(user1->name, user_input1, len1);
    user1->name[len1] = '\0';
    
    strncpy(user2->name, user_input2, len2);
    user2->name[len2] = '\0';

    printf("User1 priority: %d, name: %s\n", user1->priority, user1->name);
    printf("User2 priority: %d, name: %s\n", user2->priority, user2->name);

cleanup:
    if (user1) {
        free(user1->name);
        free(user1);
    }
    if (user2) {
        free(user2->name);
        free(user2);
    }
}

int main(int argc, char** argv) {
    if (argc != 3) {
        printf("Usage: %s <name1> <name2>\n", argv[0]);
        return 1;
    }
    process_user_safe(argv[1], argv[2]);
    return 0;
}

Let’s Try To Do Heap Exploitation and Assembly Analysis

Part 1: Exploiting heap_vuln

Step-by-Step Exploitation Process

  1. Compile the vulnerable program
    gcc -o heap_vuln heap_vuln.c -fno-stack-protector -z execstack
    
  2. Understanding the Memory Layout
    • Two user_data structures are allocated
    • Each structure has:
      • 4 bytes for priority (int)
      • 8 bytes for name pointer (char*)
    • Each name buffer is allocated 8 bytes
  3. Crafting the Exploit ```bash

    Create a pattern to overflow first buffer

    python3 -c ‘print(“A”*8 + “BBBBCCCC” + “DDDDEEEE”)’ > input1.txt

    Create second input to control execution

    python3 -c ‘print(“\x41\x41\x41\x41”)’ > input2.txt

Run the program

./heap_vuln $(cat input1.txt) $(cat input2.txt)


4. **Exploitation Analysis**
- First 8 bytes ("AAAAAAAA") fill the intended buffer
- Next 8 bytes ("BBBBCCCC") overflow and corrupt heap metadata
- Final 8 bytes ("DDDDEEEE") overwrite user2's priority and name pointer

5. **Using GDB to Verify**
```bash
gdb ./heap_vuln
(gdb) break process_user
(gdb) run $(cat input1.txt) $(cat input2.txt)
(gdb) x/32x $rsp-64

Part 2: Assembly Code Analysis

Vulnerable Version Key Points

; Vulnerable allocation (only 8 bytes)
mov edi, 8
call malloc

; Unsafe strcpy
call strcpy        ; No bounds checking

Key vulnerabilities in assembly:

  1. Fixed Size Allocation
    mov edi, 8        ; Always allocates 8 bytes
    call malloc
    
  2. No Length Checking
    • No instructions for checking input length
    • Direct use of strcpy without bounds verification
  3. Simple Memory Layout
    mov edi, 16       ; struct size
    call malloc       ; Allocate struct
    mov QWORD PTR [rbp-8], rax
    

Safe Version Key Points

; Safe string length check
mov esi, 128      ; Max length parameter
call strnlen      ; Get actual length

; Dynamic allocation with added space
add rax, 1        ; Add null terminator space
mov rdi, rax      ; Use calculated size
call malloc

Security improvements visible in assembly:

  1. Length Calculation
    mov esi, 128          ; Maximum allowed length
    mov rdi, rax          ; Input string
    call strnlen          ; Get safe length
    mov QWORD PTR [rbp-24], rax   ; Store length
    
  2. Dynamic Allocation
    mov rax, QWORD PTR [rbp-24]   ; Get stored length
    add rax, 1                    ; Add null terminator
    mov rdi, rax                  ; Pass as malloc size
    call malloc
    
  3. Safe Copy
    mov rdx, QWORD PTR [rbp-24]   ; Pass length
    call strncpy                  ; Bounded copy
    

Part 3: Key Differences Analysis

Memory Management

  1. Vulnerable Version
    • Fixed 8-byte allocations
    • No length tracking
    • Simple struct layout
  2. Safe Version
    • Dynamic sizing based on input
    • Length tracking in struct
    • Additional safety fields

String Operations

  1. Vulnerable Version
    call strcpy       ; Unbounded copy
    
  2. Safe Version
    mov rdx, QWORD PTR [rbp-24]   ; Length parameter
    call strncpy                  ; Bounded copy
    

Error Handling

  1. Vulnerable Version
    • Basic NULL checks
    • No cleanup on partial failure
  2. Safe Version
    test rax, rax     ; Check allocation
    je .L5            ; Jump to cleanup
    
    • Comprehensive error handling
    • Proper cleanup sequence
    • Null termination verification

Stack Frame

  1. Vulnerable Version
    sub rsp, 32       ; Smaller stack frame
    
  2. Safe Version
    sub rsp, 48       ; Larger stack frame for safety variables
    

Practical Mitigation Techniques

  1. Input Validation
    size_t len = strnlen(input, MAX_LEN);
    if (len >= MAX_LEN) {
     return ERROR_TOO_LONG;
    }
    
  2. Safe Allocation
    size_t safe_size = len + 1;
    if (safe_size < len) {  // Check for overflow
     return ERROR_OVERFLOW;
    }
    char *buf = malloc(safe_size);
    
  3. Proper Cleanup
    if (buf != NULL) {
     free(buf);
     buf = NULL;
    }
    

Security Improvements

  1. Length Tracking: Added name_len to track buffer sizes
  2. Bounds Checking: Using strncpy instead of strcpy
  3. Maximum Limits: Defined MAX_NAME_LEN to prevent excessive allocations
  4. Null Termination: Explicit null termination after string operations
  5. Cleanup Handling: Added proper cleanup using goto for error cases

Memory Safety Best Practices

  1. Input Validation
    • Always validate input lengths
    • Define reasonable maximum sizes
    • Check return values of memory operations
  2. Safe String Functions
    • Use strncpy, strncat instead of strcpy, strcat
    • Consider using safer alternatives like strlcpy where available
    • Always ensure proper null termination
  3. Memory Management
    • Track allocation sizes
    • Use consistent allocation/deallocation patterns
    • Implement proper cleanup in error cases
  4. Defensive Programming
    • Assume inputs could be malicious
    • Check all return values
    • Use safe defaults

Advanced Topics

Heap Exploitation Techniques

  1. Use-After-Free
    • Accessing freed memory
    • Can lead to arbitrary code execution
    • Prevention: Set pointers to NULL after free
  2. Double Free
    • Freeing the same memory twice
    • Can corrupt heap management structures
    • Prevention: Track allocation status
  3. Heap Spraying
    • Filling heap with malicious code
    • Used to increase exploitation reliability
    • Prevention: Memory randomization

Memory Debugging Tools

  1. Valgrind
    • Memory leak detection
    • Use-after-free detection
    • Buffer overflow detection
  2. AddressSanitizer
    • Runtime memory error detector
    • Minimal performance impact
    • Integrated with modern compilers

Further Reading

  1. Common Weakness Enumeration (CWE) Database
    • CWE-122: Heap-based Buffer Overflow
    • CWE-415: Double Free
    • CWE-416: Use After Free
  2. OWASP Secure Coding Practices
  3. CERT C Coding Standard

Conclusion

Understanding heap vulnerabilities and implementing proper memory safety measures is crucial for developing secure software. By following best practices and using appropriate tools, many common memory-related vulnerabilities can be prevented.