关于OpenSSL“心脏出血”漏洞的分析

0x00 背景


原作者:Sean Cassidy 原作者Twitter:@ex509 原作者博客:http://blog.existentialize.com 来源:http://blog.existentialize.com/diagnosis-of-the-openssl-heartbleed-bug.html

当我分析GnuTLS的漏洞的时候,我曾经说过,那不会是我们看到的最后一个TLS栈上的严重bug。然而我没想到这次OpenSSL的bug会如此严重。

OpenSSL“心脏出血”漏洞是一个非常严重的问题。这个漏洞使攻击者能够从内存中读取多达64 KB的数据。一些安全研究员表示:

无需任何特权信息或身份验证,我们就可以从我们自己的(测试机上)偷来X.509证书的私钥、用户名与密码、聊天工具的消息、电子邮件以及重要的商业文档和通信等数据。

这一切是如何发生的呢?让我们一起从代码中一探究竟吧。

0x01 Bug


请看ssl/dl_both.c,漏洞的补丁从这行语句开始:

#!cpp
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记录所遵循的存储格式请参见下文分析):

#!cpp
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)和一个指向记录数据的指针(data)。我们回头去看dtls1_process_heartbeat:

#!cpp
/* Read type and payload length first */
hbtype = *p++;
n2s(p, payload);
pl = p;

SSLv3记录的第一个字节标明了心跳包的类型。宏n2s从指针p指向的数组中取出前两个字节,并把它们存入变量payload中——这实际上是心跳包载荷的长度域(length)。注意程序并没有检查这条SSLv3记录的实际长度。变量pl则指向由访问者提供的心跳包数据。

这个函数的后面进行了以下工作:

#!cpp
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) 个字节。变量bp是用来访问这段内存区域的指针。

#!cpp
/* Enter response type, length and copy payload */
*bp++ = TLS1_HB_RESPONSE;
s2n(payload, bp);
memcpy(bp, pl, payload);

宏s2n与宏n2s干的事情正好相反:s2n读入一个16 bit长的值,然后将它存成双字节值,所以s2n会将与请求的心跳包载荷长度相同的长度值存入变量payload。然后程序从pl处开始复制payload个字节到新分配的bp数组中——pl指向了用户提供的心跳包数据。最后,程序将所有数据发回给用户。那么Bug在哪里呢?

0x01a 用户可以控制变量payload和pl

如果用户并没有在心跳包中提供足够多的数据,会导致什么问题?比如pl指向的数据实际上只有一个字节,那么memcpy会把这条SSLv3记录之后的数据——无论那些数据是什么——都复制出来。

很明显,SSLv3记录附近有不少东西。

说实话,我对发现了OpenSSL“心脏出血”漏洞的那些人的声明感到吃惊。当我听到他们的声明时,我认为64 KB数据根本不足以推算出像私钥一类的数据。至少在x86上,堆是向高地址增长的,所以我认为对指针pl的读取只能读到新分配的内存区域,例如指针bp指向的区域。存储私钥和其它信息的内存区域的分配早于对指针pl指向的内存区域的分配,所以攻击者是无法读到那些敏感数据的。当然,考虑到现代malloc的各种神奇实现,我的推断并不总是成立的。

当然,你也没办法读取其它进程的数据,所以“重要的商业文档”必须位于当前进程的内存区域中、小于64 KB,并且刚好位于指针pl指向的内存块附近。

研究者声称他们成功恢复了密钥,我希望能看到PoC。如果你找到了PoC,请联系我

0x01b 漏洞修补

修复代码中最重要的一部分如下:

#!cpp
/* 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的心跳包,然后第二步检查确保了心跳包足够长。就这么简单。

0x02 前车之鉴


我们能从这个漏洞中学到什么呢?

我是C的粉丝。这是我最早接触的编程语言,也是我在工作中使用的第一门得心应手的语言。但是和之前相比,现在我更清楚地看到了C语言的局限性。

GnuTLS漏洞和这个漏洞出发,我认为我们应当做到下面三条:

花钱请人对像OpenSSL这样的关键安全基础设施进行安全审计;
为这些库写大量的单元测试和综合测试;
开始在更安全的语言中编写替代品。

考虑到使用C语言进行安全编程的困难性,我不认为还有什么其他的解决方案。我会试着做这些,你呢?

作者简介:Sean是一位关于如何把事儿干好的软件工程师。现在他在Squadron工作。Squadron是一个专为SaaS应用程序准备的配置与发布管理工具。

测试版本的结果以及检测工具:

OpenSSL 1.0.1 through 1.0.1f (inclusive) are vulnerable
OpenSSL 1.0.1g is NOT vulnerable
OpenSSL 1.0.0 branch is NOT vulnerable
OpenSSL 0.9.8 branch is NOT vulnerable

http://filippo.io/Heartbleed/

©乌云知识库版权所有 未经许可 禁止转载


30
Budi 2016-03-14 07:49:57

源代码分析,不错

30
黑吃黑 2015-02-06 10:15:25

利用脚本怎么没放上来

30
小贱人 2014-05-27 15:17:04

mark

30
小森森 2014-04-20 10:08:46

@Fish 揪个错~~前边的文件名写错啦,应该是ssl/d1_both.c而不是ssl/dl_both.c,d后边的是数字1~

30
zaobao 2014-04-15 17:01:41

程序员自己疏忽了,不能赖c吧

30
11 2014-04-13 16:53:55

http://my.oschina.net/gschen/blog/221796

30
Fish 2014-04-11 13:59:14

再次看的时候发现我写的“数据包格式”因为有尖括号而被过滤掉了(囧)。
补充下,heartbeat 数据包的格式应该是这样的:
type, 1 byte
length, 2 bytes
data, #length# bytes

30
dood 2014-04-11 09:48:59

多谢指明,因为 struct ssl3_record_st 的前三个域 type, length 和 off无关,所有数据都是用户发来的data[] 里面取出type,length,由此造成了这次风险。

30
Fish 2014-04-11 02:23:29

根据我的理解,指针 data 指向的 buffer 存储的是客户端发来的 heartbeat 包,格式应该是 ,它和 struct ssl3_record_st 的前三个域 type, length 和 off 无关。
最后应该有 pl == &s->s3->rrec.data[3]; /* pl 是个指针 */。
我没有调试过这段代码。所以如果我理解的不对,请告诉我。

30
Comer 2014-04-10 17:27:39

赞龙哥。

30
dood 2014-04-10 15:55:23

求指点,
unsigned char *p = &s->s3->rrec.data[0];
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;
};
///此时p 指向的data数组, ssl3_record_st结构体的成员type,length没有用到啊
/* Read type and payload length first */
hbtype = *p++; /// 怎么得到type,p 指向的是data
n2s(p, payload); ///同上
pl = p; /// 此时p1 == rrec.data[3] ?

30
瞌睡龙 2014-04-10 14:15:42

话不能这么说,总有人考虑不到的地方,之前估计谁也不会想到OpenSSL会有这么一个大坑,并且,你刚给的解决方案并不能防御的了,我到内存当中就dump出密码的hash,再用这个hash去请求就是了,一样认证成功,好点的方案是加一个once token带入hash中再认证,但是同样阻挡不了session劫持。再安全的方式就是绑定ip之类的,但是作为一个大量用户业务来讲,基本不可能这么做。

30
warrioj4 2014-04-10 12:54:57

google一下,你知道的就太多了

30
warrioj4 2014-04-10 12:54:07

传输的时候肯定是密文,到内存里面计算匹配也是密文,关键是这些密文是不是通用的不可逆算法,例如MD5 如果没加盐的MD5是很容易被查表攻破的

30
warrioj4 2014-04-10 12:51:00

为了可能的安全问题,客户端跟服务端应该要形成一种安全的共识,不断的加盐 (任你跑个天昏地暗 都跑不出明文),当然 针对客户端的攻击那就没办法了

30
warrioj4 2014-04-10 12:49:33

那我只能说这些网站程序设计者脑残不? 如果是我,有控件的话,肯定会先给客户端加点盐,然后生成一个临时表,让客户端返回 MD5(MD5(密码)+盐) 这样服务端只要计算好是否跟数据库匹配与否就好了,这样内存里面一大堆密文 还加了盐 你觉得那些SB黑客 会为了你账户的里面的100块 去跑明文么?

30
瞌睡龙 2014-04-10 12:38:56

事实胜于雄辩,yahoo与淘宝几乎每个dump下来的16kb都有账户的明文账户密码,yahoo是18小时后修复的,你觉得被全球黑客dump下了多少呢?

30
morris2600 2014-04-10 12:30:36

传输过程不是密文吗? 收到之后先解成明文放到内存?

30
warrioj4 2014-04-10 12:21:18

64KB 能干嘛 也许后面覆盖的内存区域 什么都没有 也许就是一堆无效的数据 或者是一些密文之类

30
warrioj4 2014-04-10 12:20:32

我就呵呵了 用户提交的内容 如果有安全控件的 一般在客户端就已经加密了

30
broxian 2014-04-10 10:18:31

There is no total of 64 kilobytes limitation to the attack, that limit applies only to a single heartbeat. Attacker can either keep reconnecting or during an active TLS connection keep requesting arbitrary number of 64 kilobyte chunks of memory content until enough secrets are revealed.

30
小石头 2014-04-10 09:42:16

围观!

30
野驴~ 2014-04-09 21:02:42

一大波漏洞来袭。

30
耐小心 2014-04-09 19:02:12

尼玛 凶残

30
sameul 2014-04-09 17:40:34

用户提交的都是明文的, 数据库里存储的才是密文

30
cisse 2014-04-09 15:45:17

多谢分享 多谢指教 大神果然厉害

30
chenlog 2014-04-09 15:15:21

请问密码在内存中不是密文存储的吗?

30
wwt 2014-04-09 14:36:09

这个应该是拷贝进程所分配的内存,如果没登陆,应该不会泄密吧。

30
livers 2014-04-09 12:10:05

何止是厂商啊!!!!!

30
archa 2014-04-09 11:08:43

但是95年没有这个bug

30
archa 2014-04-09 11:08:00

谁把这个bug引进来的?

30
拆哪小白菜 2014-04-09 10:12:29

这个绝对是一个后门!95年OpenSSL就有了,呵呵呵呵

30
银冥币 2014-04-08 22:35:09

求个poc和exp,用来写批量利用工具

30
hellok 2014-04-08 20:25:51

http://phrack.org/papers/fall_of_groups.html

30
Seven.Sea 2014-04-08 20:13:32

坐等被刷屏..

30
高斯 2014-04-08 19:31:54

我tm没法用电脑,泪奔。。。。。。。

30
Soga 2014-04-08 17:46:07

伙呆了

30
大亮 2014-04-08 17:12:44

⊙︿⊙╭( ̄m ̄*)╮(+﹏+)~狂晕,没看懂啊,看来还是回去好好学习吧

30
Focusstart 2014-04-08 16:49:04

mark!

30
Pooke 2014-04-08 16:48:58

坐等

30
Fish 2014-04-08 16:00:49

是的。反复攻击,直到得到敏感数据即可。

30
refer 2014-04-08 15:57:33

There is no total of 64 kilobytes limitation to the attack, that limit applies only to a single heartbeat. Attacker can either keep reconnecting or during an active TLS connection keep requesting arbitrary number of 64 kilobyte chunks of memory content until enough secrets are revealed.

30
肉肉 2014-04-08 15:38:57

mark

30
瞌睡龙 2014-04-08 15:34:36

坐等各大厂商躺枪……

30
insight-labs 2014-04-08 15:22:09

膜拜fish大神。

30
hellok 2014-04-08 15:08:03

如果你找到了PoC,请联系我

感谢知乎授权页面模版