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

Fish 2014-04-08 14:17:00

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,漏洞的补丁从这行语句开始:

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

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

SSLv3记录的第一个字节标明了心跳包的类型。宏n2s从指针p指向的数组中取出前两个字节,并把它们存入变量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) 个字节。变量bp是用来访问这段内存区域的指针。

/* 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 漏洞修补

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

/* 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/

评论

H

hellok 2014-04-08 15:08:03

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

I

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

膜拜fish大神。

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

坐等各大厂商躺枪……

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

mark

路人甲 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.

F

Fish 2014-04-08 16:00:49

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

P

Pooke 2014-04-08 16:48:58

坐等

F

Focusstart 2014-04-08 16:49:04

mark!

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

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

路人甲 2014-04-08 17:46:07

伙呆了

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

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

S

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

坐等被刷屏..

H

hellok 2014-04-08 20:25:51

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

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

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

路人甲 2014-04-09 10:12:29

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

路人甲 2014-04-09 11:08:00

谁把这个bug引进来的?

路人甲 2014-04-09 11:08:43

但是95年没有这个bug

L

livers 2014-04-09 12:10:05

何止是厂商啊!!!!!

路人甲 2014-04-09 14:36:09

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

路人甲 2014-04-09 15:15:21

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

路人甲 2014-04-09 15:45:17

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

路人甲 2014-04-09 17:40:34

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

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

尼玛 凶残

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

一大波漏洞来袭。

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

围观!

路人甲 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.

W

warrioj4 2014-04-10 12:20:32

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

W

warrioj4 2014-04-10 12:21:18

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

路人甲 2014-04-10 12:30:36

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

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

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

W

warrioj4 2014-04-10 12:49:33

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

W

warrioj4 2014-04-10 12:51:00

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

W

warrioj4 2014-04-10 12:54:07

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

W

warrioj4 2014-04-10 12:54:57

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

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

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

路人甲 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] ?

C

Comer 2014-04-10 17:27:39

赞龙哥。

F

Fish 2014-04-11 02:23:29

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

路人甲 2014-04-11 09:48:59

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

F

Fish 2014-04-11 13:59:14

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

路人甲 2014-04-13 16:53:55

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

路人甲 2014-04-15 17:01:41

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

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

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

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

mark

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

利用脚本怎么没放上来

B

Budi 2016-03-14 07:49:57

源代码分析,不错

F

Fish

...

twitter weibo github wechat

随机分类

浏览器安全 文章:36 篇
Windows安全 文章:88 篇
Python安全 文章:13 篇
IoT安全 文章:29 篇
Ruby安全 文章:2 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Article_kelp

因为这里的静态目录访功能应该理解为绑定在static路径下的内置路由,你需要用s

N

Nas

师傅您好!_static_url_path那 flag在当前目录下 通过原型链污

Z

zhangy

你好,为什么我也是用windows2016和win10,但是流量是smb3,加密

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

目录