Web-server – SSL cipher suite usage statistics based on captured data

tlsweb-server

Is there a reasonable method of building SSL cipher usage statistics based on captured packets?

Let's say my web server supports a set of ciphers and I would like to find out how many clients negotiate each cipher suite.

Best Answer

Yes. If you've got captured packets, simply extract the negotiated cipher from the Server Hello handshake packet:

cipher_suite

  The single cipher suite selected by the server from the list in
  ClientHello.cipher_suites.  For resumed sessions, this field is
  the value from the state of the session being resumed.

The packet itself can be easily identified, and the selected cipher is in a set location within it, easy enough to parse. So capture the first few packets of all your SSL connections, extract the cipher chosen, and you've got what you're looking for.

The idea was interesting enough that I decided to try it. With a little hacking and slashing I was able to adapt a Python script to do it:

$ ./parser.py random2.pcap | sort -u
TLS 1.0 0x00,0x14
TLS 1.2 0x00,0x2f
TLS 1.2 0x00,0x30
$

With that info, you can correlate the cipher suite IDs against the TLS Cipher Suite Registry from IANA:

$ ./parser.py random2.pcap  | sort -u | awk '{print $3}' | grep -if - ~/Downloads/tls-parameters-4.csv 
"0x00,0x14",TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA,Y,[RFC4346]
"0x00,0x2F",TLS_RSA_WITH_AES_128_CBC_SHA,Y,[RFC5246]
"0x00,0x30",TLS_DH_DSS_WITH_AES_128_CBC_SHA,Y,[RFC5246]
$ 

Here's the code. It's a real strip-down of TLS Client Hello Tools so if you want to play with it, consider going back to there for a less drastic version (and bear in mind the original focuses on the Client Hello whereas we care about the Server Hello).

#!/usr/bin/env python
# Hack-and-slash derived from https://github.com/pquerna/tls-client-hello-stats

import os, sys, dpkt
TLS_HANDSHAKE = 22

def pcap_reader(fp):
    return dpkt.pcap.Reader(fp)

def grab_negotiated_ciphers(cap):
    for ts, buf in cap:
        eth = dpkt.ethernet.Ethernet(buf)
        if not isinstance(eth.data, dpkt.ip.IP):
            continue
        ip = eth.data
        if not isinstance(ip.data, dpkt.tcp.TCP):
            continue

        tcp = ip.data
        if (tcp.dport != 443 and tcp.sport != 443) or (len(tcp.data) <= 0) or (ord(tcp.data[0]) != TLS_HANDSHAKE):
            continue

        records = []
        try:
            records, bytes_used = dpkt.ssl.TLSMultiFactory(tcp.data)
        except dpkt.ssl.SSL3Exception, e:
            continue
        except dpkt.dpkt.NeedData, e:
            continue

        if len(records) <= 0:
            continue

        for record in records:
            # TLS handshake only
            if (record.type == 22 and len(record.data) != 0 and ord(record.data[0]) == 2):
                try:
                    handshake = dpkt.ssl.TLSHandshake(record.data)
                except dpkt.dpkt.NeedData, e:
                    continue
                if isinstance(handshake.data, dpkt.ssl.TLSServerHello):
                    ch = handshake.data
                    print '%s\t0x%0.2x,0x%0.2x' %(dpkt.ssl.ssl3_versions_str[ch.version], (ch.cipher_suite&0xff00)>>8, ch.cipher_suite&0xff)
                else:
                    continue

def main(argv):
    if len(argv) != 2:
        print "Tool to grab and print TLS Server Hello cipher_suite"
        print ""
        print "Usage: parser.py <pcap file>"
        print ""
        sys.exit(1)

    with open(argv[1], 'rb') as fp:
        capture = pcap_reader(fp)
        stats = grab_negotiated_ciphers(capture)

if __name__ == "__main__":
    main(sys.argv)