Understanding Heap Exploitation An memory Safety
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:
- Heap Blocks: Individual chunks of allocated memory
- Block Headers: Metadata preceding each block containing:
- Size of the block
- Status flags (allocated/free)
- Pointers to adjacent blocks
- Free Lists: Linked lists of available memory blocks
- 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:
- Fixed Buffer Size: We allocate only 8 bytes for each name buffer
- Unsafe String Copy:
strcpy
is used without bounds checking - 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:
- The heap metadata following the first name buffer
- The second user struct’s data
- 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
- Compile the vulnerable program
gcc -o heap_vuln heap_vuln.c -fno-stack-protector -z execstack
- 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
- 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:
- Fixed Size Allocation
mov edi, 8 ; Always allocates 8 bytes call malloc
- No Length Checking
- No instructions for checking input length
- Direct use of strcpy without bounds verification
- 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:
- 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
- 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
- Safe Copy
mov rdx, QWORD PTR [rbp-24] ; Pass length call strncpy ; Bounded copy
Part 3: Key Differences Analysis
Memory Management
- Vulnerable Version
- Fixed 8-byte allocations
- No length tracking
- Simple struct layout
- Safe Version
- Dynamic sizing based on input
- Length tracking in struct
- Additional safety fields
String Operations
- Vulnerable Version
call strcpy ; Unbounded copy
- Safe Version
mov rdx, QWORD PTR [rbp-24] ; Length parameter call strncpy ; Bounded copy
Error Handling
- Vulnerable Version
- Basic NULL checks
- No cleanup on partial failure
- Safe Version
test rax, rax ; Check allocation je .L5 ; Jump to cleanup
- Comprehensive error handling
- Proper cleanup sequence
- Null termination verification
Stack Frame
- Vulnerable Version
sub rsp, 32 ; Smaller stack frame
- Safe Version
sub rsp, 48 ; Larger stack frame for safety variables
Practical Mitigation Techniques
- Input Validation
size_t len = strnlen(input, MAX_LEN); if (len >= MAX_LEN) { return ERROR_TOO_LONG; }
- Safe Allocation
size_t safe_size = len + 1; if (safe_size < len) { // Check for overflow return ERROR_OVERFLOW; } char *buf = malloc(safe_size);
- Proper Cleanup
if (buf != NULL) { free(buf); buf = NULL; }
Security Improvements
- Length Tracking: Added
name_len
to track buffer sizes - Bounds Checking: Using
strncpy
instead ofstrcpy
- Maximum Limits: Defined
MAX_NAME_LEN
to prevent excessive allocations - Null Termination: Explicit null termination after string operations
- Cleanup Handling: Added proper cleanup using goto for error cases
Memory Safety Best Practices
- Input Validation
- Always validate input lengths
- Define reasonable maximum sizes
- Check return values of memory operations
- Safe String Functions
- Use
strncpy
,strncat
instead ofstrcpy
,strcat
- Consider using safer alternatives like
strlcpy
where available - Always ensure proper null termination
- Use
- Memory Management
- Track allocation sizes
- Use consistent allocation/deallocation patterns
- Implement proper cleanup in error cases
- Defensive Programming
- Assume inputs could be malicious
- Check all return values
- Use safe defaults
Advanced Topics
Heap Exploitation Techniques
- Use-After-Free
- Accessing freed memory
- Can lead to arbitrary code execution
- Prevention: Set pointers to NULL after free
- Double Free
- Freeing the same memory twice
- Can corrupt heap management structures
- Prevention: Track allocation status
- Heap Spraying
- Filling heap with malicious code
- Used to increase exploitation reliability
- Prevention: Memory randomization
Memory Debugging Tools
- Valgrind
- Memory leak detection
- Use-after-free detection
- Buffer overflow detection
- AddressSanitizer
- Runtime memory error detector
- Minimal performance impact
- Integrated with modern compilers
Further Reading
- Common Weakness Enumeration (CWE) Database
- CWE-122: Heap-based Buffer Overflow
- CWE-415: Double Free
- CWE-416: Use After Free
- OWASP Secure Coding Practices
- 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.