FlawOpen

flawopen.com/Teardowns/cve-2014-0160-heartbleed

● CVE-2014-0160 · CVSS 7.5 · 高危
安全研究 · FlawOpen

CVE-2014-0160:OpenSSL Heartbleed 内存泄露

CVE-2014-0160 使 TLS 或 DTLS 对端在 OpenSSL 信任记录中不存在的载荷长度时,可以读取 Heartbeat 记录之外最多 64 KiB 的数据。上游修复会先验证记录,再解析并回显载荷。[1], [2]

💡 通俗易懂的原理解析 (ELI5)

想象一名工作人员收到一张只有几个字的卡片,却相信标签上要求复制很长一段文字。他没有在卡片边缘停下,而是继续抄写共享托盘里后面的内容。Heartbleed 让 OpenSSL 读取到已接收记录之外;泄露哪些字节取决于进程内存中的实际内容。[1], [3]

核心概念与专有名词

TLS/DTLS Heartbeat(RFC 6520)
TLS 或 DTLS 的保活消息。请求携带一个由对端回显的载荷,以及协议要求的填充。[3]
payload_length
表示 Heartbeat 载荷大小的 16 位字段。扣除消息类型、长度字段和最小填充后,载荷必须能放入已接收记录。[1], [3]
memcpy()
从源指针复制指定字节数。它不知道源缓冲区的有效长度,因此调用方必须先验证边界。[1], [2]
缓冲区越界读取(CWE-126)
读取预期源缓冲区末尾之后的数据。Heartbleed 的响应可能泄露最多 64 KiB 进程内存,具体取决于内存内容。[5]

框架映射

根本原因剖析 (Root Cause)

tls1_process_heartbeat() 和 dtls1_process_heartbeat() 会读取 Heartbeat 消息的 16 位载荷长度,并在响应路径中将其传给 memcpy(),却没有确认 s->s3->rrec.length 中确实包含那么多载荷字节。对端可以声明的长度超过 TLS 或 DTLS 记录实际携带的数据。随后 OpenSSL 会回显已接收记录之后的字节,最多可能泄露 64 KiB 进程内存;内容取决于当时的内存,因此并不保证出现某个特定密钥、密码或 cookie。上游提交 96db902 先丢弃短到无法容纳类型、长度字段和最小填充的记录,再将 1 + 2 + payload + 16 与已接收记录长度比较。它还用 SSL3_RT_MAX_PLAIN_LENGTH 限制响应,并使用计算出的 write_length。TLS 和 DTLS 处理函数都采用了相应检查。[1], [2], [3], [4]

攻击执行流程分解

步骤 1

对端发送较短的 Heartbeat 记录

TLS 或 DTLS 对端发送的 Heartbeat 消息所声明的载荷长度,大于记录中实际存在的载荷字节数。[1], [3]

步骤 2

OpenSSL 信任声明的长度

修复前,处理函数读取类型和长度时没有先检查记录的最小长度,也没有将载荷与已接收记录进行核对。[1], [2]

步骤 3

响应复制到记录边界之外

memcpy() 会从记录缓冲区复制声明的字节数。响应可能包含已接收 Heartbeat 消息之后的数据,最多达到协议的 64 KiB 上限。[1], [2]

步骤 4

泄露内容取决于进程内存

对端可能收到进程内存中的字节。响应不保证包含某个特定秘密;结果取决于内存内容和端点状态。[1], [4]

步骤 5

修复后的处理函数拒绝不一致数据

修复在解析前检查头部和填充,确认载荷能放入已接收记录,并在复制前限制响应长度。[2], [1]

源代码对比:漏洞与安全实现

这些代码片段来自 OpenSSL 上游修复,并非独立可运行程序。漏洞代码在确认固定头部和最小填充存在之前就解析字段,随后按对端声明的载荷长度复制。修复先检查记录的最小长度,再将声明的载荷与已接收记录核对,并限制响应长度。OpenSSL 对 tls1_process_heartbeat() 和 dtls1_process_heartbeat() 都作了对应修改。[2], [1]

源码摘录: ssl/t1_lib.c

存在漏洞的实现
/* tls1_process_heartbeat() 修复前的代码片段。 */
hbtype = *p++;
n2s(p, payload);
pl = p;

/* 在 TLS1_HB_REQUEST 响应路径中: */
memcpy(bp, pl, payload);
r = ssl3_write_bytes(s, TLS1_RT_HEARTBEAT,
                     buffer, 3 + payload + padding);
加固后的安全修复
/* 如果记录长度不足以容纳类型、长度字段和最小填充,则拒绝该记录。 */
if (1 + 2 + 16 > s->s3->rrec.length)
    return 0;

hbtype = *p++;
n2s(p, payload);
if (1 + 2 + payload + 16 > s->s3->rrec.length)
    return 0;
pl = p;

/* 在 TLS1_HB_REQUEST 响应路径中: */
unsigned int write_length = 1 + 2 + payload + padding;
if (write_length > SSL3_RT_MAX_PLAIN_LENGTH)
    return 0;
buffer = OPENSSL_malloc(write_length);
memcpy(bp, pl, payload);
r = ssl3_write_bytes(s, TLS1_RT_HEARTBEAT, buffer, write_length);

事件演进时间线

2012-03-14

OpenSSL 1.0.1 发布,其中 TLS/DTLS Heartbeat 处理缺少记录长度检查。[3], [1]

2014-04-07

OpenSSL 发布 CVE-2014-0160 公告并推出 1.0.1g;公告感谢 Google Security 的 Neel Mehta。1.0.2 测试版的修复随后进入 beta2。[1], [2]

2014-04

运维人员修补受影响端点并评估可能的暴露。Heartbleed.com 指出,常规日志不会记录响应返回了哪些内存字节;修补后应评估更换密钥、证书、会话和凭据。[6], [1]

工程与系统安全加固清单

参考来源