openssl heartbeat问题

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已被窃取

发表评论

您的电子邮箱地址不会被公开。