@Lenciel

Openssl Heartbleed Bug

连某宝都中招的Heartbleed bug究竟是个什么东西?简单地说就是攻击者可以读最多 64KB 内存的内容。

读了这 64KB 能干嘛?用报这个 bug 的人的话来说:

Without using any privileged information or credentials we were able steal from ourselves the secret keys used for our X.509 certificates, user names and passwords, instant messages, emails and business critical documents and communication.

那么读取 64KB 内存和获取这么多关键信息究竟有什么关系呢?

The bug

先来看看patch里面的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 */

可以看到,heartbeat 里有一个 SSLv3 record 的指针,这个record的代码如下:

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;

可以看到,每个record有它的typelengthdata,规规矩矩。

回到dtls1_process_heartbeat

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

可以看到SSLv3 record的第一个 byte 就是放这个heartbeattype。 宏n2s 则是从p里面取两个 byte 放到 payload 里面,被用来作为 payload 的长度。 注意这里并没有检查SSLv3 record 实际的长度。

接下来在这个函数里面干了下面这些事情:

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);

s2nn2s做的操作恢复出来:先拿 16 个 bit 的值放到 2 个 byte 里面,也就是原来请求的 payload 的长度。然后把pl里面放的 payload(请求者提交的 data)拷贝到新分配的bp里面。

看起来是很平常的操作,只不过没有认真的检查用户输入而已,但问题也就在这里了。

Where is the bug

如果用户并没有正在提交声称的那么多个 bytes 的 payload,那么 memcpy 就会读到同一个 process 里面 SSLv3 record 附近的内存内容。

这附近有哪些内容呢?

首先要明白在 linux 上,内存的动态分配主要是通过sbrk 或者是 mmap。如果内存是通过 sbrk 分配的,它会使用heap-grows-up规则,泄露出来的东西不会那么多(但是如果是同时并发请求还是有东西会漏)。

在这里,pl因为 malloc 里面的 mmap_threshhold 多半是 sbrk 分配的,但是,那些关键的用户数据,则多半是通过 mmap 分配内存。于是这些数据就会被攻击者用pl拿到。如果再考虑并发请求,就…

The fix

所以,整个 patch 里面最主要的 fix 就是:

  • 检查是否有长度为 0 的虚假 heartbeat
  • 检查 record 的真实长度

代码如下:

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

So?

这个 bug 大概算是影响这么剧烈的 bug 里面最好明白的一个,所以居然我也看明白了。感受:

  • 为了可扩展性引入了复杂度,经常都会带来恶梦
  • 用户的输入,无论如何都不能相信,一定要 check
  • C 语言的确是大牛小牛都会踩到坑啊

欢迎留言