OpenSSL Heartbleed 漏洞分析

发布时间 2023-04-12 11:11:35作者: StreamAzure

OpenSSL Heartbleed 漏洞分析

一、漏洞简介

Heartbleed是在互联网上广泛应用的OpenSSL开源库的一个严重漏洞,它允许在正常情况下窃取本应受SSL协议加密保护的信息。这个漏洞在OpenSSL的心跳机制实现代码中被引入。代码中某些存在缺陷的函数未对将要发送的消息进行边界检查,攻击者可以通过构造特定请求数据包,使得服务器回复包含内存数据的消息,从而窃取至多64KB的内存数据。这些数据中可能包含敏感信息,如密钥、密码等。

Heartbleed是OpenSSL在心跳机制的代码实现中产生的漏洞,并非SSL协议中的设计缺陷。

二、基础知识

2.1 SSL协议

SSL(Security Sockets Layer, 安全套接字层)协议是广泛应用于互联网的安全协议,为客户端与服务器之间的通信提供安全服务,包括机密性、认证性、数据完整性等。

SSL的安全服务基于它在通信过程中所使用的若干密码学算法,这些算法实现所需的某些初始参数/密钥/密码的集合称为SSL安全参数,需要通信双方在建立SSL连接前协商完成。

为使通信双方进行算法协商、获取安全参数、进行身份认证,SSL中设计了握手协议。在握手协议中,客户端与服务器之间需要进行两轮通信:第一轮通信完成加密方式的协商;第二轮通信基于已确定的加密方式,进一步协商后续数据传输所使用的对称加密密钥。两轮通信完毕后,SSL连接建立完成。在这一过程中,还需要通信双方相互通告协商好的安全参数。密钥交换协议独立于握手协议之外,作为一种消息通知机制,用于完成安全参数的相互通告。此外,警告协议用于提供报错机制,在SSL连接建立过程中出现问题时通知另一方,并且提供安全断链机制,允许通信双方以可认证的方式关闭连接终止会话。以上三种协议构成了SSL协议中的协商层

安全参数协商完毕后,通信双方开始数据传输。记录协议为SSL协议的数据承载协议,协商层的协议以及应用数据都要被记录协议封装后传输。记录协议单独构成了SSL协议中的记录层

2.2 SSL心跳机制

2.2.1 心跳机制简介

在长连接下,通信双方可能在很长一段时间内都没有数据往来。理论上,这个连接是一直保持且畅通的,但在实际中,中间节点可能已经出现了某些故障而通信双方均不知晓。更致命的是,有的节点(防火墙)会自动把一定时间之内没有数据交互的连接给断掉。在此情况下,心跳机制可用于确定连接是否能够正常通信、避免长时间无数据往来而导致的连接自动断开(保活)。

心跳机制的具体实现是:通信一方每隔一段时间(如几分钟)向另一方发送心跳请求消息,另一方收到后回复心跳响应消息,如此周期性地重复以上过程。若某一方在一定时间内未收到对方响应,则视为连接已断开。

2.2.2 SSL协议中的心跳机制

SSL协议工作在TCP协议之上,而TCP是面向连接的协议,这要求通信双方的SSL协议也应保持持续连接的状态;另一方面,SSL协议的安全服务是以内存、CPU、网络带宽的额外开销为代价的。这就意味着,当服务器资源有限,且连接的客户端数量较大时,服务器需要及时断开已完成通信的连接以减轻负载压力,此时便需要应用心跳机制。

RFC6520文件对SSL协议中的心跳机制作出了详细的规定,其中心跳数据包的结构如下图所示:

img

心跳数据包的具体代码结构如下所示:

struct {
      HeartbeatMessageType type;    // 1 bytes,包括request 和 response两种类型
      uint16 payload_length;    	// 2 bytes,载荷长度
      opaque payload[HeartbeatMessage.payload_length];    // payload_length bytes,载荷内容
      opaque padding[padding_length];    // 填充字节,至少为16 bytes
} HeartbeatMessage;

2.3 OpenSSL

OpenSSL是一套开源的SSL协议的实现,以C语言开发,支持Linux、Windows、BSD、Mac、VMS等平台,因此OpenSSL具有良好的跨平台性与适用性。OpenSSL是在全球互联网应用最广泛的安全认证和传输服务之一,几乎被所有的主流网站所应用,包括Google、Facebook、Yahoo等。

三、漏洞分析

3.1 漏洞复现

通过Vulhub提供的漏洞靶场镜像进行复现。攻击代码的关键在于心跳请求数据包的构造,具体如下:

hb = h2bin(''' 18 03 02 00 03 01 40 00 ''')

与心跳数据包结构对应,0x18为数据类型,0x0302为TLS版本,0x0003为数据包长度,以上构成SSL记录头部。接下来为心跳消息部分,0x01为心跳数据包类型(请求),0x4000为心跳消息载荷长度。

攻击结果如下图所示。图中通信双方成功建立了SSL连接,攻击者发送了构造的心跳请求数据包,并收到了包含服务器内存数据的响应数据包:

image

3.2 漏洞原理

存在漏洞的代码文件为OpenSSL源代码(此处为1.0.1版本)中的ssl/d1_both.cssl/t1_lib.c,分别包含函数dlts1_process_heartbeattlsl_process_heartbeat,为DTLS(数据报安全传输协议)和TLS(传输层安全协议)处理心跳请求包的函数。

下面以dlts1_process_heartbeat函数为例进行分析:

int
dtls1_process_heartbeat(SSL *s)
{
	unsigned char *p = &s->s3->rrec.data[0], *pl;
	....
}

首先需要了解指针p指向了什么,因此从SSL开始查看定义:

// 在`crypto/ossl_typ.h`文件中可知`SSL`其实是`ssl_st`:
typedef struct ssl_st SSL;

// 在`ssl/ssl.h`文件中找到`ssl_st`的定义,并找到`s3`属性:
struct ssl_st
{	
    ...
	struct ssl2_state_st *s2; /* SSLv2 variables */
	struct ssl3_state_st *s3; /* SSLv3 variables */
	struct dtls1_state_st *d1; /* DTLSv1 variables */
    ...
}

//在`ssl/ssl3.h`文件中找到`ssl3_state_st`的定义,并找到rrec属性:
typedef struct ssl3_state_st
{
    ...
    SSL3_RECORD rrec;	/* each decoded record goes in here */
	SSL3_RECORD wrec;	/* goes out from here */
    ...
}

//最后在同一文件内找到`SSL3_RECORD`的定义,可以看到data为一个字节数组:
typedef struct ssl3_record_st
{
    /*r */	int type;               /* type of record */
    /*rw*/	unsigned int length;    /* How many bytes available */
    /*r */	unsigned int off;       /* read/write offset into 'buf' */
    /*rw*/	unsigned char *data;    /* pointer to the record data */
    /*rw*/	unsigned char *input;   /* where the decode bytes are */
    /*r */	unsigned char *comp;    /* only used with decompression - malloc()ed */
    /*r */  unsigned long epoch;    /* epoch number, needed by DTLS1 */
    /*r */  unsigned char seq_num[8]; /* sequence number, needed by DTLS1 */
} SSL3_RECORD;

于是可知缺陷函数中的指针p在初始化时指向的是心跳消息(整条消息,包含载荷)的首个字节,即消息类型。将消息类型的值赋给变量hbtype后,指针p移动至下一个字节。

而后取出两个字节的内容赋值给变量payload,指针p向后移动两个字节,指向了rrec.data[3],即心跳消息载荷的起始字节。

int
dtls1_process_heartbeat(SSL *s)
{
	unsigned char *p = &s->s3->rrec.data[0], *pl;
	unsigned short hbtype; // 1字节长的心跳消息类型数据
	unsigned int payload;  // 2字节长的心跳载荷长度数据
	unsigned int padding = 16; /* Use minimum padding */

	/* Read type and payload length first */
	hbtype = *p++; 
    // 取出首个字节内容(心跳消息类型)赋值给hbtype,p后移一字节
	n2s(p, payload); 
	// n2s是一段宏定义的操作
    // 即 payload = (((unsigned int)(p[0]) << 8) | ((unsigned int)(p[1]))), p+=2
    // 即 取两个字节内容(心跳包载荷长度)赋给变量payload,然后指针p向后移动两字节
	pl = p;
    // pl 为心跳消息载荷的起始位置
	....
}

若接收到的心跳消息为请求类型,该函数开始构造响应消息。首先,它以请求数据包中的载荷长度数据payload为主要参数,分配响应数据包所需的内存空间;然后,依次向响应数据包填充数据包类型、载荷长度、载荷等数据。

向响应数据包填充载荷时,实际为复制请求数据包中的载荷数据。具体为:从请求数据包的载荷数据起始处(也即指针pl)开始,复制连续payload个字节到响应数据包中。此操作默认了一个前提:请求数据包中的载荷长度值是真实的载荷长度,即请求数据包中确实存在payload个字节的载荷可供复制。

当请求数据包中的载荷长度值大于其真实载荷长度时,请求数据包载荷范围外的其他内存数据也被涵盖在了复制范围内。内存数据被错误地复制到响应数据包中,并发回给对方,最终导致内存数据泄露。

int
dtls1_process_heartbeat(SSL *s)
{
	...
	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; 
        // bp的第1个字节,填充数据包类型(为响应)
		s2n(payload, bp);		  
        // 与n2s操作相反,将payload转换成字节数据,填充bp的第2、3个字节
		memcpy(bp, pl, payload);
        // 向bp继续填充载荷数据,即从请求数据包的载荷数据起始处开始,复制连续payload个字节到bp
        ...
    }
	...	
}

由于payload被定义为16位无符号整数,代码最多可复制的内存大小为64KB,单次畸形请求心跳数据包攻击可窃取最多64KB的内存数据。

3.3 漏洞修复

由上述分析可知,要修复该漏洞,只需确认心跳请求数据包的实际长度,过滤载荷长度值大于实际长度的请求数据包即可。OpenSSL对该漏洞的修复方式为:1. 检查心跳请求数据包的长度是否大于最小长度;2. 检查载荷长度值加上头尾后的长度是否大于心跳消息实际长度。

int dtls1_process_heartbeat(SSL *s)
{
	...
    /* 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;
    ...
}

四、总结

Heartbleed漏洞产生的最直接原因是开发者未能充分考虑到内存操作可能存在的安全问题,编写了不够严格的代码。在使用C/C++等语言进行开发时,开发者需要慎重对待常用的mellocmemcpy等涉及内存数据的操作。

此外,代码审查的疏漏也是漏洞产生的原因之一。包含漏洞的更新代码在提交至OpenSSL后经过了核心开发者的审查,但审查人未能发现代码中的错误,导致代码被合并到OpenSSL源码中并开始广泛使用。这体现了人工审查及自动化测试的重要性,然而OpenSSL代码结构的复杂性加大了审查与测试难度。当代码过于繁复以至于影响正确性检查时,适当的重构也是有必要的。