近日闹的沸沸扬扬的Heartbleed漏洞,仿佛一下子再次将人们拉回了对网络安全的关注和担忧。

这个问题就是由于服务器端没有对用户发过来的心跳包数据进行边界检查,服务端根据用户心跳包指定的数据长度来返回同样长度的数据。如果用户指定长度为100字节,而实际心跳数据的长度只有1字节,服务端还是会memcpy长度100字节的数据,这样就会把服务端内存中的数据返回给用户,可能会泄漏一些敏感信息。

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记录中数据的指针p。结构体SSL3_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;

每条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指向了用户提供的心跳包数据。最后,程序将所有数据发回给用户。