openssl 官方网站4月7日的公布:有研究人员发现OpenSSL 1.0.1和1.0.2-beta版本中存在安全漏洞(编号为CVE-2014-0160),可能暴露密钥和私密通信,应该尽快修补,方法是:
升级到最新版本OpenSSL 1.0.1g
无法立即升级的用户可以以-DOPENSSL_NO_HEARTBEATS开关重新编译OpenSSL
1.0.2-beta版本的漏洞将在beta2版本修复
更老版本的OpenSSL(1.0.0和0.9.8等)反而不受影响
site:https://www.openssl.org/news/secadv_20140407.txt
这个漏洞是由安全公司Codenomicon的研究人员和Google安全小组的Neel Mehta相互独立地发现的。漏洞出在OpenSSL对TLS的心跳扩展(RFC6520)的实现代码中,由于漏了一处边界检查,而可能在每次心跳中暴露客户端与服务器通信中的64K内存,这并不是设计错误。
Hacker News网友drv在阅读了漏洞代码后指出,这是一个低级错误。他解释说:
TLS心跳由一个请求包组成,其中包括有效载荷(payload),通信的另一方将读取这个包并发送一个响应,其中包含同样的载荷。在处理心跳请求的代码中,载荷大小是从攻击者可能控制的包中读取的:
openssl的漏洞的修复地址
http://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=96db9023b881d7cd9f379b0c154650d6c108e9a3
可以看到漏洞从ssl/d1_both.c:开始修复
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 */
这是一条指向SSLv3记录中的数据的指针,结构体SSL3_RECORD的定义如下:
结构体SSL3_RECORD不是SSLv3记录的实际存储格式。一条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;
每条SSLv3的记录由type(类型),length和pointer to the record data(指向记录数据的指针)*data.
/* Read type and payload length first */ hbtype = *p++; n2s(p, payload); pl = p;
SSLv3记录的第一个字节表明了心跳包的类型,n2s从指针p指向的数组中提取前2个字节,并把它保存在payload变量中,实际上是心跳包载荷的长度length.这里没有检查SSLv3的实际长度。变量pl则是指向访问者实际提供的心跳包数据。
接着:
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;
这段程序分配一段有访问者指定大小的内存区域,这段内存区域最大为65535+1+2+16个字节((2^16)-1,65535)
变量bp则指向了这段内存区域。
然后响应包是这样构造的:
/* Enter response type, length and copy payload */ *bp++ = TLS1_HB_RESPONSE; s2n(payload, bp); memcpy(bp, pl, payload);
s2n与n2s的功能相反,s2n读入一个16bit的值,然后将它存成双字节值,s2n会将与请求的心跳包载荷长度相同的值存入变量payload。然后程序从pl处开始复制payload个字节到新分配bp数组中,pl指向了用户提供的心跳包数据,然后,晨曦将所以的数据发回给用户。
这样一来,用户可以控制变量payload,以达到控制pl。
如果用户并没有在心跳包中提供足够多的数据,会导致什么问题呢。如果pl指向的数据实际长度只有一个字节,那么memcpy会把这条SSLv3记录之后的数据,无论这些数据是什么,都会被复制出来。
很明显,SSLv3记录附近有不少东西的。
当然,你也没办法读取其它进程的数据,所以“重要的商业文档”必须位于当前进程的内存区域中、小于64KB,并且刚好位于指针pl指向的内存块附近
修复代码中最重要的一部分如下:
/* 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;
这段代码干了两件事情:首先第一行语句抛弃了长度为0的心跳包,然后第二步检查确保了心跳包足够长。就这么简单。
顺便了解一下
我们可以通过在和ssl建立hello之后,发送一个短字节的心跳。我们建立一个hello的通信
客户问候消息(client hello)的结构如下:
struct { ProtocolVersion client_version; Random random; SessionID session_id; CipherSuite cipher_suites<2..2^16 -1>; CompressionMethod compression_methods<1..2^8 -1>; } ClientHello;
client_version 客户端希望在此次对话中使用的SSL协议的版本。这应该是被客户
端所支持的最新的版本(最高值)。对于本文所描述的SSL协议,版
本号应该是3.0。(关于背景兼容信息请见附录E)。
random 一个客户端生成的随机结构。
session_id 客户端在此次连接中想使用的对话标识符(ID)。如果没有可用的
session—ID或者客户端想要生成新的加密参数,这个值应该为空。
cipher_suites 这是一个由客户端支持的,由客户端按其自身的偏爱而选定的加密套
接字的列表(列表的第一项是其最喜爱的),如果session_id 域非
空(暗示重新开始一已有的对话),则此向量必须至少包含来自已有
对话的cipher_suite。加密套接字的值的定义见附录A.6。
compression_methods 这是一个由客户端支持的压缩算法的列表,他根据客户端自身的偏
爱而选定的(列表的第一项是其最喜爱的),如果session_id 域非
空(暗示重新开始一已有的对话),则此向量必须至少包含一个来自
已有对话的compression_methods的参数。所有实现均必须支持
CompressionMethod.null。
继发送client hello消息之后,客户端等候一个服务器问候消息(server hello message)。除了hello消息外,由服务器返回的任何其他握手消息,均被视为致命错误(fatal error)。
发送的hello消息如下:
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
16 :代表records contains some handshake message data(建立握手信息) SSLv3
03 02 :TLS 1.0通常被标示为SSL 3.1,TLS 1.1为SSL 3.2,TLS 1.2为SSL 3.3。所以03 01表示SSL使用的版本:SSL 3.1AKA TLS 1.0,03 02对应SSL3.2
00 dc:2 bytes长度,代表消息长度
01:代表请求client请求
00 00 d8:代表clienthello的消息长度
可以用以下这个公式表示:
0x16 0x03 X Y Z 0x01 A B C
X #可能是0,1,2,3
Y Z #是消息报文长度
A B C #是客户端hello的消息长度。这个hello message开始于一个4个字节的报头,但未包含在这个长度里,应该是独自的记录。
所以你可以得到 A = 0; 256*X+Y = 256*B +C +4 也就是X*2^8 +y
上面的长度则是00*256+dc =220; 256*0+d8 + 4 =216+4 =220;后面的信息则是random+session_id+cipher_suites+compression_methods
+-----+-------------+----------+----------------+----------------+ |版本 |随机数 |会话ID |加密套件列表 |压缩方法列表 | +-----+-------------+----------+----------------+----------------+ |主|从 |时间|随机字节 |长度(1)|ID|长度(2)|套件列表 |长度(2) |方法列表| +-----+-------------+----------+----------------+----------------+
16 03 02 00 dc #TLS 头部信息
01 00 00 d8 #握手信息
03 02 #[客户端hello区域]:主:03 从:02
53 43 5b 90 #[客户端hello区域] 时间4字节,2014年4月8日 上午10:14:40
00 #[客户端hello区域] 会话ID长度 0;
00 66 #[客户端hello区域] 加密套件长度102字节
01 #[客户端hello区域] 压缩支持长度1,length (1)
00 #[客户端hello区域]压缩支持,不压缩,no compression (0)
00 49#[客户端hello区域]压缩方法列表长度,73字节
同样的心跳信息如下:
18 03 02 00 03
01 40 00
18 #心跳类型
03 02 #TLS版本号
00 03 #心跳报文长度
01 #客户端请求
40 00 #代表payload 长度,2^14 =16384;
drv评述说:
很难相信OpenSSL的代码居然没有对字节流的处理做抽象,如果包用(指针,长度)对来表示,用简单的封装函数复制,就能避免这个漏洞。用C语言的时候,写这种问题代码太容易了,但API设计仔细一点,就会大大增加犯错的难度。
受影响的版本主要有:
OpenSSL 1.0.1f (受影响)
OpenSSL 1.0.2-beta (受影响)
OpenSSL 1.0.1g (不受影响)
OpenSSL 1.0.0 branch (不受影响)
OpenSSL 0.9.8 branch (不受影响)
建议进行版本更新,更新后重启服务,通过lsof -n | grep ssl | grep DEL列出需要重新启动服务,然后将列出的服务做重启。
建议重新生成ssl key,避免原先的key已被窃取