Hi all, On Thu, 9 Feb 2017 07:37:51 +0100 Willy Tarreau <w...@1wt.eu> wrote:
> Hi Olivier, > > On Sat, Feb 04, 2017 at 11:52:30AM +0100, Olivier Doucet wrote: > > Hello, > > > > I'm trying to capture the cipher suites sent by browser when negociating > > the encryption level with HAProxy. > > Digging into the haproxy doc, I can already find the TLS version and cipher > > used (variables %sslc and %sslv), but not the complete list of ciphers sent > > by the browser. > > > > Why such information ? This could be used as a method of fingerprintin ! > > For example, finding malware that emulates a browser. Such malwares could > > be spotted by comparing the user-agent field (on http level) with the > > cipher suites used (and how the are ordered) and see if they match. An > > example of implementation could be found here : > > https://www.securityartwork.es/2017/02/02/tls-client-fingerprinting-with-bro/ > > That's an interesting idea! I'm not sure how accurate it can be since > users can change their ciphers in their browser's config, and even the > list of negociated TLS versions (I do it personally). Yes, it is interesting ! > > Is this even possible with HAProxy ? > > I'm not sure. I don't even know if openssl exposes this. However if you > want to do this on the TCP connection only (without deciphering), you > could possibly extend the SSL client hello parser to emit the list of > such ciphers as a string. The patch implementing this idea is in attachment. It returns the client-hello cioher list as binary, hexadecimal string, xxh64 and with the decoded ciphers. BR, Thierry > Regards, > Willy >
>From 044fb7e7797cea553cf9379bfe0592de4604167d Mon Sep 17 00:00:00 2001 From: Thierry FOURNIER <thierry.fourn...@ozon.io> Date: Sat, 25 Feb 2017 12:45:22 +0100 Subject: [PATCH 2/2] MEDIUM: ssl: add new sample-fetch which captures the cipherlist X-Bogosity: Ham, tests=bogofilter, spamicity=0.000000, version=1.2.4 This new sample-fetches captures the cipher list offer by the client SSL connection during the client-hello phase. This is useful for fingerprint the SSL connection. --- doc/configuration.txt | 32 ++++++ src/ssl_sock.c | 287 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 319 insertions(+) diff --git a/doc/configuration.txt b/doc/configuration.txt index c2ede71..a8ac9ab 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -618,6 +618,7 @@ The following keywords are supported in the "global" section : - tune.ssl.maxrecord - tune.ssl.default-dh-param - tune.ssl.ssl-ctx-cache-size + - tune.ssl.capture-cipherlist-size - tune.vars.global-max-size - tune.vars.proc-max-size - tune.vars.reqres-max-size @@ -1502,6 +1503,11 @@ tune.ssl.ssl-ctx-cache-size <number> dynamically is expensive, they are cached. The default cache size is set to 1000 entries. +tune.ssl.capture-cipherlist-size <number> + Sets the maximum size of the buffer used for capturing client-hello cipher + list. If the value is 0 (default value) the capture is disabled, otherwise + a buffer is allocated for each SSL/TLS connection. + tune.vars.global-max-size <size> tune.vars.proc-max-size <size> tune.vars.reqres-max-size <size> @@ -13868,6 +13874,32 @@ ssl_fc_cipher : string Returns the name of the used cipher when the incoming connection was made over an SSL/TLS transport layer. +ssl_fc_cipherlist_bin : binary + Returns the binary form of the client hello cipher list. The maximum returned + value length is according with the value of + "tune.ssl.capture-cipherlist-size". Note that this sample-fetch is available + only with OpenSSL > 0.9.7 + +ssl_fc_cipherlist_hex : string + Returns the binary form of the client hello cipher list encoded as + hexadecimal. The maximum returned value length is according with the value of + "tune.ssl.capture-cipherlist-size". Note that this sample-fetch is available + only with OpenSSL > 0.9.7 + +ssl_fc_cipherlist_str : string + Returns the decoded text form of the client hello cipher list. The maximum + number of ciphers returned is according with the value of + "tune.ssl.capture-cipherlist-size". Note that this sample-fetch is only + avaible with OpenSSL > 1.0.2 compiled with the option enable-ssl-trace. + If the function is not enabled, this sample-fetch returns the hash + like "ssl_fc_cipherlist_xxh". + +ssl_fc_cipherlist_xxh : integer + Returns a xxh64 of the cipher list. This hash can be return only is the value + "tune.ssl.capture-cipherlist-size" is set greater than 0, however the hash + take in account all the data of the cipher list. Note that this sample-fetch is + avalaible only with OpenSSL > 0.9.7 + ssl_fc_has_crt : boolean Returns true if a client certificate is present in an incoming connection over SSL/TLS transport layer. Useful if 'verify' statement is set to 'optional'. diff --git a/src/ssl_sock.c b/src/ssl_sock.c index 2d24130..55bbcaf 100644 --- a/src/ssl_sock.c +++ b/src/ssl_sock.c @@ -146,6 +146,7 @@ static struct { unsigned int max_record; /* SSL max record size */ unsigned int default_dh_param; /* SSL maximum DH parameter size */ int ctx_cache; /* max number of entries in the ssl_ctx cache. */ + int capture_cipherlist; /* Size of the cipherlist buffer. */ } global_ssl = { #ifdef LISTEN_DEFAULT_CIPHERS .listen_default_ciphers = LISTEN_DEFAULT_CIPHERS, @@ -161,8 +162,31 @@ static struct { #endif .default_dh_param = SSL_DEFAULT_DH_PARAM, .ctx_cache = DEFAULT_SSL_CTX_CACHE, + .capture_cipherlist = 0, }; +/* This memory pool is used for capturing clienthello parameters. + * The message callback is only available after openssl 0.9.7, + * so the memory pool is useless before this version. + */ +struct ssl_capture { + struct connection *conn; + unsigned long long int xxh64; + unsigned char ciphersuite_len; + char ciphersuite[0]; +}; +struct pool_head *pool2_ssl_capture = NULL; + +#if OPENSSL_VERSION_NUMBER >= 0x00907000L +/* This fu**ing funtion is announced in some OpenSSL manual pages, + * but doesn't exists in the OpenSSL library ! + * eg. https://www.openssl.org/docs/man1.0.1/ssl/SSL_get_msg_callback_arg.html + */ +static void *SSL_get_msg_callback_arg(SSL *ssl) +{ + return ssl->msg_callback_arg; +} +#endif #if (defined SSL_CTRL_SET_TLSEXT_TICKET_KEY_CB && TLS_TICKETS_NO > 0) struct list tlskeys_reference = LIST_HEAD_INIT(tlskeys_reference); @@ -1134,9 +1158,111 @@ int ssl_sock_bind_verifycbk(int ok, X509_STORE_CTX *x_store) return 0; } +static inline +void ssl_sock_parse_clienthello(int write_p, int version, int content_type, + const void *buf, size_t len, + struct ssl_capture *capture) +{ + unsigned char *msg; + unsigned char *end; + unsigned int rec_len; + + /* This function is called for "from client" and "to server" + * connections. The combination of write_p == 0 and content_type == 22 + * is only avalaible during "from client" connection. + */ + + /* "write_p" is set to 0 is the bytes are received messages, + * otherwise it is set to 1. + */ + if (write_p != 0) + return; + + /* content_type contains the type of message received or sent + * according with the SSL/TLS protocol spec. This message is + * encoded with one byte. The value 256 (two bytes) is used + * for designing the SSL/TLS record layer. According with the + * rfc6101, the expected message (other than 256) are: + * - change_cipher_spec(20) + * - alert(21) + * - handshake(22) + * - application_data(23) + * - (255) + * We are interessed by the handshake and specially the client + * hello. + */ + if (content_type != 22) + return; + + /* The message length is at least 4 bytes, containing the + * message type and the message length. + */ + if (len < 4) + return; + + /* First byte of the handshake message id the type of + * message. The konwn types are: + * - hello_request(0) + * - client_hello(1) + * - server_hello(2) + * - certificate(11) + * - server_key_exchange (12) + * - certificate_request(13) + * - server_hello_done(14) + * We are interested by the client hello. + */ + msg = (unsigned char *)buf; + if (msg[0] != 1) + return; + + /* Next three bytes are the length of the message. The total length + * must be this decoded length + 4. If the length given as argument + * is not the same, we abort the protocol dissector. + */ + rec_len = (msg[1] << 3) + (msg[2] << 2) + msg[3]; + if (len < rec_len + 4) + return; + msg += 4; + end = msg + rec_len; + if (end <= msg) + return; + + /* Expect 2 bytes for protocol version (1 byte for major and 1 byte + * for minor, the random, composed by 4 bytes for the unix time and + * 28 bytes for unix payload, and them 1 byte for the session id. So + * we jump 1 + 1 + 4 + 28 + 1 bytes. + */ + msg += 1 + 1 + 4 + 28 + 1; + if (msg >= end) + return; + + /* Next two bytes are the ciphersuite length. */ + if (msg + 2 > end) + return; + rec_len = (msg[0] << 2) + msg[1]; + msg += 2; + if (msg + rec_len > end || msg + rec_len < msg) + return; + + /* Compute the xxh64 of the ciphersuite. */ + capture->xxh64 = XXH64(msg, rec_len, 0); + + /* Capture the ciphersuite. */ + capture->ciphersuite_len = rec_len; + if (capture->ciphersuite_len > global_ssl.capture_cipherlist) + capture->ciphersuite_len = global_ssl.capture_cipherlist; + memcpy(capture->ciphersuite, msg, capture->ciphersuite_len); +} + /* Callback is called for ssl protocol analyse */ void ssl_sock_msgcbk(int write_p, int version, int content_type, const void *buf, size_t len, SSL *ssl, void *arg) { + /* If the SSL connection doesn't had sufficient memory while + * the structure was initialized, arg is NULL. + */ + if (global_ssl.capture_cipherlist && arg) + ssl_sock_parse_clienthello(write_p, version, content_type, buf, len, arg); + #ifdef TLS1_RT_HEARTBEAT /* test heartbeat received (write_p is set to 0 for a received record) */ @@ -3606,6 +3732,8 @@ ssl_sock_free_ca(struct bind_conf *bind_conf) */ static int ssl_sock_init(struct connection *conn) { + struct ssl_capture *capture; + /* already initialized */ if (conn->xprt_ctx) return 0; @@ -3713,6 +3841,20 @@ static int ssl_sock_init(struct connection *conn) return -1; } +#if OPENSSL_VERSION_NUMBER >= 0x00907000L + /* Set capture struct as opaque argument for the msg callback. */ + if (global_ssl.capture_cipherlist > 0) { + capture = pool_alloc_dirty(pool2_ssl_capture); + if (capture) { + capture->conn = conn; + capture->ciphersuite_len = 0; + SSL_set_msg_callback_arg(conn->xprt_ctx, capture); + } + } else { + SSL_set_msg_callback_arg(conn->xprt_ctx, NULL); + } +#endif + SSL_set_accept_state(conn->xprt_ctx); /* leave init state and start handshake */ @@ -4160,8 +4302,13 @@ static int ssl_sock_from_buf(struct connection *conn, struct buffer *buf, int fl } static void ssl_sock_close(struct connection *conn) { + struct ssl_capture *capture; if (conn->xprt_ctx) { +#if OPENSSL_VERSION_NUMBER >= 0x00907000L + capture = SSL_get_msg_callback_arg(conn->xprt_ctx); + pool_free2(pool2_ssl_capture, capture); +#endif SSL_free(conn->xprt_ctx); conn->xprt_ctx = NULL; sslconns--; @@ -5273,6 +5420,111 @@ smp_fetch_ssl_fc_sni(const struct arg *args, struct sample *smp, const char *kw, } static int +smp_fetch_ssl_fc_cl_bin(const struct arg *args, struct sample *smp, const char *kw, void *private) +{ +#if OPENSSL_VERSION_NUMBER >= 0x00907000L + struct connection *conn; + struct ssl_capture *capture; + + conn = objt_conn(smp->sess->origin); + if (!conn || !conn->xprt_ctx || conn->xprt != &ssl_sock) + return 0; + + capture = SSL_get_msg_callback_arg(conn->xprt_ctx); + if (!capture) + return 0; + + smp->flags = SMP_F_CONST; + smp->data.type = SMP_T_BIN; + smp->data.u.str.str = capture->ciphersuite; + smp->data.u.str.len = capture->ciphersuite_len; + return 1; + +#else + return 0; +#endif +} + +static int +smp_fetch_ssl_fc_cl_hex(const struct arg *args, struct sample *smp, const char *kw, void *private) +{ + struct chunk *data; + + if (!smp_fetch_ssl_fc_cl_bin(args, smp, kw, private)) + return 0; + + data = get_trash_chunk(); + dump_binary(data, smp->data.u.str.str, smp->data.u.str.len); + smp->data.type = SMP_T_BIN; + smp->data.u.str = *data; + return 1; +} + +static int +smp_fetch_ssl_fc_cl_xxh64(const struct arg *args, struct sample *smp, const char *kw, void *private) +{ +#if OPENSSL_VERSION_NUMBER >= 0x00907000L + struct connection *conn; + struct ssl_capture *capture; + + conn = objt_conn(smp->sess->origin); + if (!conn || !conn->xprt_ctx || conn->xprt != &ssl_sock) + return 0; + + capture = SSL_get_msg_callback_arg(conn->xprt_ctx); + if (!capture) + return 0; + + smp->data.type = SMP_T_SINT; + smp->data.u.sint = capture->xxh64; + return 1; + +#else + return 0; +#endif +} + +static int +smp_fetch_ssl_fc_cl_str(const struct arg *args, struct sample *smp, const char *kw, void *private) +{ +#if (OPENSSL_VERSION_NUMBER >= 0x1000200fL) && !OPENSSL_NO_SSL_TRACE + struct chunk *data; + SSL_CIPHER cipher; + int i; + const char *str; + unsigned char *bin; + + if (!smp_fetch_ssl_fc_cl_bin(args, smp, kw, private)) + return 0; + + /* The cipher algorith must not be SSL_SSLV2, because this + * SSL version seems to not have the same cipher encoding, + * and it is not supported by OpenSSL. Unfortunately, the + * #define SSL_SSLV2, SSL_SSLV3 and others are not available + * with standard defines. We just set the variable to 0, + * ensure that the match with SSL_SSLV2 fails. + */ + cipher.algorithm_ssl = 0; + + data = get_trash_chunk(); + for (i = 0; i + 1 < smp->data.u.str.len; i += 2) { + bin = (unsigned char *)smp->data.u.str.str + i; + cipher.id = (unsigned int)(bin[0] << 8) | bin[1]; + str = SSL_CIPHER_standard_name(&cipher); + if (!str || strcmp(str, "UNKNOWN") == 0) + chunk_appendf(data, "%sUNKNOWN(%04x)", i == 0 ? "" : ",", (unsigned int)cipher.id); + else + chunk_appendf(data, "%s%s", i == 0 ? "" : ",", str); + } + smp->data.type = SMP_T_STR; + smp->data.u.str = *data; + return 1; +#else + return smp_fetch_ssl_fc_cl_xxh64(args, smp, kw, private); +#endif +} + +static int smp_fetch_ssl_fc_unique_id(const struct arg *args, struct sample *smp, const char *kw, void *private) { #if OPENSSL_VERSION_NUMBER > 0x0090800fL @@ -6416,6 +6668,8 @@ static int ssl_parse_global_int(char **args, int section_type, struct proxy *cur target = &global_ssl.ctx_cache; else if (strcmp(args[0], "maxsslconn") == 0) target = &global.maxsslconn; + else if (strcmp(args[0], "tune.ssl.capture-cipherlist-size") == 0) + target = &global_ssl.capture_cipherlist; else { memprintf(err, "'%s' keyword not unhandled (please report this bug).", args[0]); return -1; @@ -6437,6 +6691,34 @@ static int ssl_parse_global_int(char **args, int section_type, struct proxy *cur return 0; } +static int ssl_parse_global_capture_cipherlist(char **args, int section_type, struct proxy *curpx, + struct proxy *defpx, const char *file, int line, + char **err) +{ +#if OPENSSL_VERSION_NUMBER >= 0x00907000L + int ret; + + ret = ssl_parse_global_int(args, section_type, curpx, defpx, file, line, err); + if (ret != 0) + return ret; + + if (pool2_ssl_capture) { + memprintf(err, "'%s' is already configured.", args[0]); + return -1; + } + + pool2_ssl_capture = create_pool("ssl-capture", sizeof(struct ssl_capture) + global_ssl.capture_cipherlist, MEM_F_SHARED); + if (!pool2_ssl_capture) { + memprintf(err, "Out of memory error."); + return -1; + } + return 0; +#else + memprintf(err, "'%s' requires OpenSSL 0.9.7 or above.", args[0]); + return -1; +#endif +} + /* parse "ssl.force-private-cache". * Returns <0 on alert, >0 on warning, 0 on success. */ @@ -6836,6 +7118,10 @@ static struct sample_fetch_kw_list sample_fetch_keywords = {ILH, { { "ssl_fc_use_keysize", smp_fetch_ssl_fc_use_keysize, 0, NULL, SMP_T_SINT, SMP_USE_L5CLI }, { "ssl_fc_session_id", smp_fetch_ssl_fc_session_id, 0, NULL, SMP_T_BIN, SMP_USE_L5CLI }, { "ssl_fc_sni", smp_fetch_ssl_fc_sni, 0, NULL, SMP_T_STR, SMP_USE_L5CLI }, + { "ssl_fc_cipherlist_bin", smp_fetch_ssl_fc_cl_bin, 0, NULL, SMP_T_STR, SMP_USE_L5CLI }, + { "ssl_fc_cipherlist_hex", smp_fetch_ssl_fc_cl_hex, 0, NULL, SMP_T_BIN, SMP_USE_L5CLI }, + { "ssl_fc_cipherlist_str", smp_fetch_ssl_fc_cl_str, 0, NULL, SMP_T_STR, SMP_USE_L5CLI }, + { "ssl_fc_cipherlist_xxh", smp_fetch_ssl_fc_cl_xxh64, 0, NULL, SMP_T_SINT, SMP_USE_L5CLI }, { NULL, NULL, 0, 0, 0 }, }}; @@ -6956,6 +7242,7 @@ static struct cfg_kw_list cfg_kws = {ILH, { { CFG_GLOBAL, "tune.ssl.lifetime", ssl_parse_global_lifetime }, { CFG_GLOBAL, "tune.ssl.maxrecord", ssl_parse_global_int }, { CFG_GLOBAL, "tune.ssl.ssl-ctx-cache-size", ssl_parse_global_int }, + { CFG_GLOBAL, "tune.ssl.capture-cipherlist-size", ssl_parse_global_capture_cipherlist }, { CFG_GLOBAL, "ssl-default-bind-ciphers", ssl_parse_global_ciphers }, { CFG_GLOBAL, "ssl-default-server-ciphers", ssl_parse_global_ciphers }, { 0, NULL, NULL }, -- 1.7.10.4