TLS client fingerprinting con Bro

En esta entrada continuamos con Bro IDS, que en esta ocasión utilizaremos como herramienta para la exploración de técnicas de client fingerprinting.

Como es sabido, durante el handshake inicial del protocolo TLS (utilizado, entre otros, por HTTPS en navegadores web), se intercambia un mensaje denominado ClientHello, donde el cliente especifica las primitivas criptográficas que soporta (los llamados ciphersuites).

Por ejemplo, un Firefox 50.1.0 sobre Linux enviaría un ClientHello como el siguiente, tal como lo muestra el disector de Wireshark:


Existen páginas web que nos informan de las características de TLS de nuestro navegador web, como Qualys SSL Labs SSL Client Test.

Lo interesante es que otro tipo de cliente TLS utilizará normalmente un conjunto de ciphersuites distinto. Por ejemplo, para un wget:

$ wget -qO/dev/null https://www.google.es

tendríamos un handshake como el siguiente:


Como podemos observar, wget envía 68 ciphersuites distintos (en vez de 15 suites en el caso de Firefox), y además el orden de los mismos también es diferente.

Por lo tanto, además de otros parámetros, como la versión de TLS empleada (que también está presente en los mensajes), la cantidad de ciphersuites y el orden en que figuran en el ClientHello nos proporcionan un fingerprint (una “huella digital”) del cliente TLS (en definitiva, de la aplicación, navegador web, cliente de correo, etc.) utilizado.

No generará el mismo fingerprint un Firefox que un Chrome, y tampoco tendrá la misma huella el cliente de Tor (siempre que lo utilicemos sin pluggable transports), un script en Python que conecte a una web por HTTPS, las API de Microsoft Windows que nos permiten realizar peticiones web, o un RAT escrito en Delphi…

Esto es un hecho que podemos explotar tanto para identificar aplicaciones no autorizadas como para detectar malware que no se moleste en camuflarse como un navegador web HTTPS “de la casa” para sus actividades de comando y control o exfiltración ;-)

Enter Bro

No es muy complicado parsear los ClientHello; existen extensiones para p0f y herramientas ad hoc como FingerprinTLS. Como era de esperar, Bro IDS también es capaz de diseccionar el protocolo TLS; en el fichero /usr/local/bro/share/bro/base/protocols/ssl/consts.bro podemos encontrar las constantes empleadas por la herramienta.

Los magic numbers correspondientes a la versión de TLS y el mapping entre estos y sus nombres vendrían a ser:

[...]
        const SSLv2  = 0x0002;
        const SSLv3  = 0x0300;
        const TLSv10 = 0x0301;
        const TLSv11 = 0x0302;
        const TLSv12 = 0x0303;
        const TLSv13 = 0x0304;
[...]
        const version_strings: table[count] of string = {
                [SSLv2] = "SSLv2",
                [SSLv3] = "SSLv3",
                [TLSv10] = "TLSv10",
                [TLSv11] = "TLSv11",
                [TLSv12] = "TLSv12",
                [TLSv13] = "TLSv13",
                [DTLSv10] = "DTLSv10",
                [DTLSv12] = "DTLSv12"
        } &default=function(i: count):string
[...]

Del mismo modo, un poco más adelante en el fichero, tenemos los diferentes ciphersuites y la estructura de datos cipher_desc:

[...]
        const TLS_RSA_WITH_3DES_EDE_CBC_SHA = 0x000A;
        const TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA = 0x000B;
        const TLS_DH_DSS_WITH_DES_CBC_SHA = 0x000C;
        const TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA = 0x000D;
        const TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA = 0x000E;
[...]
        const cipher_desc: table[count] of string = {
                [SSLv20_CK_RC4_128_EXPORT40_WITH_MD5] =
                        "SSLv20_CK_RC4_128_EXPORT40_WITH_MD5",
                [SSLv20_CK_RC4_128_WITH_MD5] = "SSLv20_CK_RC4_128_WITH_MD5",
                [SSLv20_CK_RC2_128_CBC_WITH_MD5] = "SSLv20_CK_RC2_128_CBC_WITH_MD5",
                [SSLv20_CK_RC2_128_CBC_EXPORT40_WITH_MD5] =
                        "SSLv20_CK_RC2_128_CBC_EXPORT40_WITH_MD5",
[...]

Como ya comentamos en el post anterior, cuando ocurre un hecho relevante, Bro “dispara” un evento, que en el caso de diseccionar un mensaje ClientHello es (fichero /usr/local/bro/share/bro/base/bif/plugins/Bro_SSL.events.bif.bro):

global ssl_client_hello: event(
  c: connection,
  version: count,
  possible_ts: time,
  client_random: string,
  session_id: string,
  ciphers: index_vec );

Ya disponemos por lo tanto de todos los ingredientes para ejecutar el fingerprinting. El script sería algo como:

event ssl_client_hello (c: connection,
    version: count,
    possible_ts: time,
    client_random: string,
    session_id: string,
    ciphers: index_vec)
{
    local ciphers_str: vector of string;

    for (i in ciphers) {
        ciphers_str[i] = SSL::cipher_desc[ciphers[i]];
    }

    print fmt("%s %s", SSL::version_strings[version],
        join_string_vec(ciphers_str, ","));
}

El vector ciphers presente en el prototipo del evento almacena los índices con los que es posible acceder a los diferentes ciphersuites contenidos en el mensaje, que mapeamos a sus correspondientes cadenas de texto utilizando la tabla cipher_desc. Para mostrarlos separados por una coma, utilizamos la función join_string_vec, similar a la función join de Perl, Python, y otros lenguajes. Si guardamos el script en el fichero log_cipher_suites.bro, podemos realizar una prueba rápida pasándolo sobre un fichero tls.pcap previamente capturado:

$ /usr/local/bro/bin/bro -C -r tls.pcap log_cipher_suites.bro
TLSv12 TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_RSA_WITH_3DES_EDE_CBC_SHA
TLSv12 TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_RSA_WITH_3DES_EDE_CBC_SHA

Y ahí podemos ver los 15 ciphersuites enviados por Firefox, que utiliza TLSv1.2.

Pasándolo a producción: logging framework

El print funciona, y se almacenará en stdout.log cuando Bro se ejecute como demonio, pero… ¿y si queremos enviar la salida a otro fichero, o a un Elastic Search? Para eso Bro proporciona el logging framework, cuyo uso es muy sencillo (una vez se le ha cogido el truco, claro ;-)

Para transformar el anterior script “de pruebas” en la versión final, en primer lugar crearemos un módulo llamado TlsFingerprint:

module TlsFingerprint;

A continuación, hemos de definir dos variables que el módulo exportará al resto de Bro. Por convención, se suelen llamar LOG e Info:

export {
    redef enum Log::ID += { LOG };

    type Info: record {
        ts: time &log;
        uid: string &log;
        id: conn_id &log;
        tls_version: string &log;
        ciphers: vector of string &log;
    };
}

Como vemos, Info guarda la entrada en sí que se registrará en el fichero correspondiente: cada registro del log contendrá el timestamp (en el formato UN*X epoch habitual), un par de identificadores, la versión de TLS y los diferentes ciphersuites. Como vemos, el registro se define con tipos de datos de Bro, como vector of string, y el framework ya se encargará de almacenarlo en un fichero de texto, un Elastic Search, o lo que tenga configurado, de forma automática por nosotros.

Para especificar el nombre del fichero de log, hookeamos la inicialización de Bro (evento bro_init()) y llamamos a la función create_stream del módulo Log (donde reside el código del logging framework):

event bro_init() &priority=5
{
    Log::create_stream(TlsFingerprint::LOG, [$columns=Info, $path="tls_finger"]);
}

El fichero será por lo tanto tls_finger.log, dentro de los directorios de registro de Bro, y será rotado de forma automática.

Por último, el nuevo código para el evento ssl_client_hello() queda de la siguiente forma:

event ssl_client_hello (c: connection,
    version: count,
    possible_ts: time,
    client_random: string,
    session_id: string,
    ciphers: index_vec)
{
    local ciphers_str: vector of string;
    local rec: TlsFingerprint::Info;

    for (i in ciphers) {
        ciphers_str[i] = SSL::cipher_desc[ciphers[i]];
    }

    rec$ts = network_time();
    rec$uid = c$uid;
    rec$id = c$id;
    rec$tls_version = SSL::version_strings[version];
    rec$ciphers = ciphers_str;

    Log::write(TlsFingerprint::LOG, rec);
}

Como vemos, definimos la variable rec con el tipo del registro que declaramos antes (TlsFingerprint::Info), lo rellenamos con los valores correspondientes, y por último invocamos la función Log::write(). Bro ya sabe cómo representar datos de tipo connection o vector of string en el fichero de registro o el backend que corresponda, separando los campos (dirección IP origen, puerto origen, dirección IP destino, puerto destino, etc.) según corresponda.

El fichero tls_finger.bro final sería:

module TlsFingerprint;

export {
    redef enum Log::ID += { LOG };

    type Info: record {
        ts: time &log;
        uid: string &log;
        id: conn_id &log;
        tls_version: string &log;
        ciphers: vector of string &log;
    };
}

event bro_init() &priority=5
{
    Log::create_stream(TlsFingerprint::LOG, [$columns=Info, $path="tls_finger"]);
}

event ssl_client_hello (c: connection,
    version: count,
    possible_ts: time,
    client_random: string,
    session_id: string,
    ciphers: index_vec)
{
    local ciphers_str: vector of string;
    local rec: TlsFingerprint::Info;

    for (i in ciphers) {
        ciphers_str[i] = SSL::cipher_desc[ciphers[i]];
    }

    rec$ts = network_time();
    rec$uid = c$uid;
    rec$id = c$id;
    rec$tls_version = SSL::version_strings[version];
    #rec$ciphers = join_string_vec(ciphers_str, ",");
    rec$ciphers = ciphers_str;

    Log::write(TlsFingerprint::LOG, rec);
}

Si lo “dejamos caer” en el directorio /usr/local/share/bro/site, podemos cargarlo desde local.bro añadiendo la línea:

@load site/tls_finger

Tras hacer el correspondiente broctl deploy, el fichero de registro se irá poblando con entradas como la siguiente:

#fields ts      uid     id.orig_h       id.orig_p       id.resp_h       id.resp_p       tls_version     ciphers
#types  time    string  addr    port    addr    port    string  vector[string]
1485109320.798053  CxDC4MuavmTCRktJ3  192.168.1.1  52659  192.168.1.2  443  TLSv10  TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,TLS_DHE_RSA_WITH_AES_256_CBC_SHA,TLS_DHE_RSA_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_AES_256_CBC_SHA,TLS_RSA_WITH_AES_128_CBC_SHA,TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,TLS_DHE_DSS_WITH_AES_256_CBC_SHA,TLS_DHE_DSS_WITH_AES_128_CBC_SHA,TLS_RSA_WITH_3DES_EDE_CBC_SHA,TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA,TLS_RSA_WITH_RC4_128_SHA,TLS_RSA_WITH_RC4_128_MD5

Conforme vayamos teniendo información, es cuestión de ir analizando en busca de anomalías ;-) Por ejemplo, para ordenar los conjuntos de ciphersuites de los menos utilizados a los más utilizados:

$ cat tls_finger.log | awk -F"\t" '{ print $8 }' | sort | uniq -c | sort -n 

Por supuesto, un malware bien hecho tratará de imitar el fingerprint TLS del navegador normalmente utilizado en el equipo contaminado, pero a lo mejor pescamos algo interesante ;-)

¡Hasta el próximo post! @pmarinram

Ver también en: