From 0ddbda1f829a2d1b27d7e6519900201111702823 Mon Sep 17 00:00:00 2001 From: Ivan Nardi <12729895+IvanNardi@users.noreply.github.com> Date: Mon, 16 Sep 2024 18:38:26 +0200 Subject: [PATCH] Add an heuristic to detect encrypted/obfuscated OpenVPN flows (#2547) Based on the paper: "OpenVPN is Open to VPN Fingerprinting" See: https://www.usenix.org/conference/usenixsecurity22/presentation/xue-diwen Basic idea: * the distribution of the first byte of the messages (i.e. the distribution of the op-codes) is quite unique * this fingerprint might be still detectable even if the OpenVPN packets are somehow fully encrypted/obfuscated The heuristic is disabled by default. --- doc/configuration_parameters.md | 2 + fuzz/fuzz_config.cpp | 8 + fuzz/fuzz_ndpi_reader.c | 2 + src/include/ndpi_private.h | 4 + src/include/ndpi_typedefs.h | 19 +- src/lib/ndpi_main.c | 35 +++ src/lib/ndpi_utils.c | 7 + src/lib/protocols/openvpn.c | 282 ++++++++++++++++-- .../default/pcap/openvpn_obfuscated.pcapng | Bin 0 -> 59460 bytes .../result/openvpn_obfuscated.pcapng.out | 36 +++ .../cfgs/openvpn_heuristic_enabled/config.txt | 1 + .../pcap/openvpn_obfuscated.pcapng | 1 + .../result/openvpn_obfuscated.pcapng.out | 31 ++ 13 files changed, 396 insertions(+), 32 deletions(-) create mode 100644 tests/cfgs/default/pcap/openvpn_obfuscated.pcapng create mode 100644 tests/cfgs/default/result/openvpn_obfuscated.pcapng.out create mode 100644 tests/cfgs/openvpn_heuristic_enabled/config.txt create mode 120000 tests/cfgs/openvpn_heuristic_enabled/pcap/openvpn_obfuscated.pcapng create mode 100644 tests/cfgs/openvpn_heuristic_enabled/result/openvpn_obfuscated.pcapng.out diff --git a/doc/configuration_parameters.md b/doc/configuration_parameters.md index 8d826c61858..75cc9beb145 100644 --- a/doc/configuration_parameters.md +++ b/doc/configuration_parameters.md @@ -49,6 +49,8 @@ TODO | "ookla" | "dpi.aggressiveness", | 0x01 | 0x00 | 0x01 | Detection aggressiveness for Ookla. The value is a bitmask. Values: 0x0 = disabled; 0x01 = enable heuristic for detection over TLS (via Ookla LRU cache) | | "zoom" | "max_packets_extra_dissection" | 4 | 0 | 255 | After a flow has been classified has Zoom, nDPI might analyse more packets to look for a sub-classification or for metadata. This parameter set the upper limit on the number of these packets | | "rtp" | "search_for_stun" | disable | NULL | NULL | After a flow has been classified as RTP or RTCP, nDPI might analyse more packets to look for STUN/DTLS packets, i.e. to try to tell if this flow is a "pure" RTP/RTCP flow or if the RTP/RTCP packets are multiplexed with STUN/DTLS. Useful for proper (sub)classification when the beginning of the flows are not captured or if there are lost packets in the the captured traffic. If enabled, nDPI requires more packets to process for each RTP/RTCP flow. | +| "openvpn" | "dpi.heuristics", | 0x00 | 0 | 0x01 | Enable/disable some heuristics to better detect OpenVPN. The value is a bitmask. Values: 0x0 = disabled; 0x01 = enable heuristic based on op-code frequency. If enabled, some false positives are expected. See: https://www.usenix.org/conference/usenixsecurity22/presentation/xue-diwen | +| "openvpn" | "dpi.heuristics.num_messages", | 10 | 0 | 255 | If at least one OpenVPN heuristics is enabled (see `openvpn,"dpi.heuristics"`, this parameter set the maximum number of OpenVPN messages required for each flow. Note that an OpenVPN message may be splitted into multiple (TCP/UDP) packets and that a (TCP/UDP) packet may contains multiple OpenVPN messages. Higher the value, lower the false positive rate but more packets are required by nDPI for processing. | | "openvpn" | "subclassification_by_ip" | enable | NULL | NULL | Enable/disable sub-classification of OpenVPN flows using server IP. Useful to detect the specific VPN application/app. At the moment, this knob allows to identify: Mullvad, NordVPN, ProtonVPN. | | "wireguard" | "subclassification_by_ip" | enable | NULL | NULL | Enable/disable sub-classification of Wireguard flows using server IP. Useful to detect the specific VPN application/app. At the moment, this knob allows to identify: Mullvad, NordVPN, ProtonVPN. | | $PROTO_NAME | "log" | disable | NULL | NULL | Enable/disable logging/debug for specific protocol. Use "any" as protocol name if you want to easily enable/disable logging/debug for all protocols | diff --git a/fuzz/fuzz_config.cpp b/fuzz/fuzz_config.cpp index 4fdfea776f2..4a5715551d4 100644 --- a/fuzz/fuzz_config.cpp +++ b/fuzz/fuzz_config.cpp @@ -239,6 +239,14 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { snprintf(cfg_value, sizeof(cfg_value), "%d", value); ndpi_set_config(ndpi_info_mod, "rtp", "search_for_stun", cfg_value); } + if(fuzzed_data.ConsumeBool()) { + value = fuzzed_data.ConsumeIntegralInRange(0, 0x01 + 1); + snprintf(cfg_value, sizeof(cfg_value), "%d", value); + ndpi_set_config(ndpi_info_mod, "openvpn", "dpi.heuristics", cfg_value); + value = fuzzed_data.ConsumeIntegralInRange(0, 255 + 1); + snprintf(cfg_value, sizeof(cfg_value), "%d", value); + ndpi_set_config(ndpi_info_mod, "openvpn", "dpi.heuristics.num_messages", cfg_value); + } if(fuzzed_data.ConsumeBool()) { value = fuzzed_data.ConsumeIntegralInRange(0, 0x01 + 1); snprintf(cfg_value, sizeof(cfg_value), "%d", value); diff --git a/fuzz/fuzz_ndpi_reader.c b/fuzz/fuzz_ndpi_reader.c index 5f27ed3bfe6..ae8cac68c98 100644 --- a/fuzz/fuzz_ndpi_reader.c +++ b/fuzz/fuzz_ndpi_reader.c @@ -89,6 +89,8 @@ int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { ndpi_set_config(workflow->ndpi_struct, "stun", "max_packets_extra_dissection", "255"); ndpi_set_config(workflow->ndpi_struct, "zoom", "max_packets_extra_dissection", "255"); ndpi_set_config(workflow->ndpi_struct, "rtp", "search_for_stun", "1"); + ndpi_set_config(workflow->ndpi_struct, "openvpn", "dpi.heuristics", "0x01"); + ndpi_set_config(workflow->ndpi_struct, "openvpn", "dpi.heuristics.num_messages", "255"); ndpi_finalize_initialization(workflow->ndpi_struct); diff --git a/src/include/ndpi_private.h b/src/include/ndpi_private.h index b9f197ad612..809d6c7b39d 100644 --- a/src/include/ndpi_private.h +++ b/src/include/ndpi_private.h @@ -269,6 +269,8 @@ struct ndpi_detection_module_config_struct { int rtp_search_for_stun; + int openvpn_heuristics; + int openvpn_heuristics_num_msgs; int openvpn_subclassification_by_ip; int wireguard_subclassification_by_ip; @@ -609,6 +611,8 @@ u_int ndpi_search_tcp_or_udp_raw(struct ndpi_detection_module_struct *ndpi_struc char* ndpi_intoav4(unsigned int addr, char* buf, u_int16_t bufLen); +int is_flow_addr_informative(const struct ndpi_flow_struct *flow); + u_int16_t icmp4_checksum(u_int8_t const * const buf, size_t len); ndpi_risk_enum ndpi_network_risk_ptree_match(struct ndpi_detection_module_struct *ndpi_str, diff --git a/src/include/ndpi_typedefs.h b/src/include/ndpi_typedefs.h index 9086e456ee5..263d81dc57c 100644 --- a/src/include/ndpi_typedefs.h +++ b/src/include/ndpi_typedefs.h @@ -169,7 +169,8 @@ typedef enum { NDPI_MALWARE_HOST_CONTACTED, /* Flow client contacted a malware host */ NDPI_BINARY_DATA_TRANSFER, /* Attempt to transfer something in binary format */ NDPI_PROBING_ATTEMPT, /* Probing attempt (e.g. TCP connection with no data exchanged or unidirection traffic for bidirectional flows such as SSH) */ - + NDPI_OBFUSCATED_TRAFFIC, + /* Leave this as last member */ NDPI_MAX_RISK /* must be <= 63 due to (**) */ } ndpi_risk_enum; @@ -791,6 +792,10 @@ struct ndpi_lru_cache { /* Ookla */ #define NDPI_AGGRESSIVENESS_OOKLA_TLS 0x01 /* Enable detection over TLS (using ookla cache) */ +/* OpenVPN */ +#define NDPI_HEURISTICS_OPENVPN_OPCODE 0x01 /* Enable heuristic based on opcode frequency */ + + /* ************************************************** */ struct ndpi_flow_tcp_struct { @@ -1520,6 +1525,14 @@ struct ndpi_flow_struct { /* NDPI_PROTOCOL_OPENVPN */ u_int8_t ovpn_session_id[2][8]; + u_int8_t ovpn_alg_standard_state : 2; + u_int8_t ovpn_alg_heur_opcode_state : 2; + u_int8_t ovpn_heur_opcode__codes_num : 4; + u_int8_t ovpn_heur_opcode__num_msgs; +#define OPENVPN_HEUR_MAX_NUM_OPCODES 4 + u_int8_t ovpn_heur_opcode__codes[OPENVPN_HEUR_MAX_NUM_OPCODES]; + u_int8_t ovpn_heur_opcode__resets[2]; + u_int16_t ovpn_heur_opcode__missing_bytes[2]; /* NDPI_PROTOCOL_TINC */ u_int8_t tinc_state; @@ -1549,8 +1562,8 @@ struct ndpi_flow_struct { _Static_assert(sizeof(((struct ndpi_flow_struct *)0)->protos) <= 264, "Size of the struct member protocols increased to more than 264 bytes, " "please check if this change is necessary."); -_Static_assert(sizeof(struct ndpi_flow_struct) <= 1136, - "Size of the flow struct increased to more than 1136 bytes, " +_Static_assert(sizeof(struct ndpi_flow_struct) <= 1152, + "Size of the flow struct increased to more than 1152 bytes, " "please check if this change is necessary."); #endif #endif diff --git a/src/lib/ndpi_main.c b/src/lib/ndpi_main.c index a61a8e8045d..8080755153d 100644 --- a/src/lib/ndpi_main.c +++ b/src/lib/ndpi_main.c @@ -198,6 +198,7 @@ static ndpi_risk_info ndpi_known_risks[] = { { NDPI_MALWARE_HOST_CONTACTED, NDPI_RISK_SEVERE, CLIENT_HIGH_RISK_PERCENTAGE, NDPI_CLIENT_ACCOUNTABLE }, { NDPI_BINARY_DATA_TRANSFER, NDPI_RISK_MEDIUM, CLIENT_FAIR_RISK_PERCENTAGE, NDPI_CLIENT_ACCOUNTABLE }, { NDPI_PROBING_ATTEMPT, NDPI_RISK_MEDIUM, CLIENT_FAIR_RISK_PERCENTAGE, NDPI_CLIENT_ACCOUNTABLE }, + { NDPI_OBFUSCATED_TRAFFIC, NDPI_RISK_HIGH, CLIENT_HIGH_RISK_PERCENTAGE, NDPI_BOTH_ACCOUNTABLE }, /* Leave this as last member */ { NDPI_MAX_RISK, NDPI_RISK_LOW, CLIENT_FAIR_RISK_PERCENTAGE, NDPI_NO_ACCOUNTABILITY } @@ -438,6 +439,38 @@ void ndpi_set_proto_category(struct ndpi_detection_module_struct *ndpi_str, u_in /* ********************************************************************************** */ +int is_flow_addr_informative(const struct ndpi_flow_struct *flow) +{ + /* The ideas is to tell if the address itself carries some useful information or not. + Examples: + a flow to a Facebook address is quite likely related to some Facebook apps + a flow to an AWS address might be potentially anything + */ + + switch(flow->guessed_protocol_id_by_ip) { + case NDPI_PROTOCOL_UNKNOWN: + /* This is basically the list of cloud providers supported by nDPI */ + case NDPI_PROTOCOL_TENCENT: + case NDPI_PROTOCOL_EDGECAST: + case NDPI_PROTOCOL_ALIBABA: + case NDPI_PROTOCOL_YANDEX_CLOUD: + case NDPI_PROTOCOL_AMAZON_AWS: + case NDPI_PROTOCOL_MICROSOFT_AZURE: + case NDPI_PROTOCOL_CACHEFLY: + case NDPI_PROTOCOL_CLOUDFLARE: + case NDPI_PROTOCOL_GOOGLE_CLOUD: + return 0; + /* This is basically the list of VPNs (with **entry** addresses) supported by nDPI */ + case NDPI_PROTOCOL_NORDVPN: + case NDPI_PROTOCOL_PROTONVPN: + return 0; + default: + return 1; + } +} + +/* ********************************************************************************** */ + /* There are some (master) protocols that are informative, meaning that it shows what is the subprotocol about, but also that the subprotocol isn't a real protocol. @@ -11439,6 +11472,8 @@ static const struct cfg_param { { "rtp", "search_for_stun", "disable", NULL, NULL, CFG_PARAM_ENABLE_DISABLE, __OFF(rtp_search_for_stun), NULL }, + { "openvpn", "dpi.heuristics", "0x00", "0", "0x01", CFG_PARAM_INT, __OFF(openvpn_heuristics), NULL }, + { "openvpn", "dpi.heuristics.num_messages", "10", "0", "255", CFG_PARAM_INT, __OFF(openvpn_heuristics_num_msgs), NULL }, { "openvpn", "subclassification_by_ip", "enable", NULL, NULL, CFG_PARAM_ENABLE_DISABLE, __OFF(openvpn_subclassification_by_ip), NULL }, { "wireguard", "subclassification_by_ip", "enable", NULL, NULL, CFG_PARAM_ENABLE_DISABLE, __OFF(wireguard_subclassification_by_ip), NULL }, diff --git a/src/lib/ndpi_utils.c b/src/lib/ndpi_utils.c index 012c49baa16..27dd8c7db17 100644 --- a/src/lib/ndpi_utils.c +++ b/src/lib/ndpi_utils.c @@ -2097,6 +2097,9 @@ const char* ndpi_risk2str(ndpi_risk_enum risk) { case NDPI_PROBING_ATTEMPT: return("Probing Attempt"); + case NDPI_OBFUSCATED_TRAFFIC: + return("Obfuscated Traffic"); + default: ndpi_snprintf(buf, sizeof(buf), "%d", (int)risk); return(buf); @@ -2221,6 +2224,8 @@ const char* ndpi_risk2code(ndpi_risk_enum risk) { return STRINGIFY(NDPI_BINARY_DATA_TRANSFER); case NDPI_PROBING_ATTEMPT: return STRINGIFY(NDPI_PROBING_ATTEMPT); + case NDPI_OBFUSCATED_TRAFFIC: + return STRINGIFY(NDPI_OBFUSCATED_TRAFFIC); default: return("Unknown risk"); @@ -2342,6 +2347,8 @@ ndpi_risk_enum ndpi_code2risk(const char* risk) { return(NDPI_BINARY_DATA_TRANSFER); else if(strcmp(STRINGIFY(NDPI_PROBING_ATTEMPT), risk) == 0) return(NDPI_PROBING_ATTEMPT); + else if(strcmp(STRINGIFY(NDPI_OBFUSCATED_TRAFFIC), risk) == 0) + return(NDPI_OBFUSCATED_TRAFFIC); else return(NDPI_MAX_RISK); } diff --git a/src/lib/protocols/openvpn.c b/src/lib/protocols/openvpn.c index a56af25be5f..1c63f5ecdc1 100644 --- a/src/lib/protocols/openvpn.c +++ b/src/lib/protocols/openvpn.c @@ -61,13 +61,14 @@ static void ndpi_int_openvpn_add_connection(struct ndpi_detection_module_struct * const ndpi_struct, - struct ndpi_flow_struct * const flow) + struct ndpi_flow_struct * const flow, + ndpi_confidence_t confidence) { if(ndpi_struct->cfg.openvpn_subclassification_by_ip && ndpi_struct->proto_defaults[flow->guessed_protocol_id_by_ip].protoCategory == NDPI_PROTOCOL_CATEGORY_VPN) { - ndpi_set_detected_protocol(ndpi_struct, flow, flow->guessed_protocol_id_by_ip, NDPI_PROTOCOL_OPENVPN, NDPI_CONFIDENCE_DPI); + ndpi_set_detected_protocol(ndpi_struct, flow, flow->guessed_protocol_id_by_ip, NDPI_PROTOCOL_OPENVPN, confidence); } else { - ndpi_set_detected_protocol(ndpi_struct, flow, NDPI_PROTOCOL_OPENVPN, NDPI_PROTOCOL_UNKNOWN, NDPI_CONFIDENCE_DPI); + ndpi_set_detected_protocol(ndpi_struct, flow, NDPI_PROTOCOL_OPENVPN, NDPI_PROTOCOL_UNKNOWN, confidence); } } @@ -132,8 +133,8 @@ static int8_t detect_hmac_size(const u_int8_t *payload, int payload_len) { return(-1); } -static void ndpi_search_openvpn(struct ndpi_detection_module_struct* ndpi_struct, - struct ndpi_flow_struct* flow) { +static int search_standard(struct ndpi_detection_module_struct* ndpi_struct, + struct ndpi_flow_struct* flow) { struct ndpi_packet_struct* packet = &ndpi_struct->packet; const u_int8_t * ovpn_payload = packet->payload; const u_int8_t * session_remote; @@ -151,24 +152,21 @@ static void ndpi_search_openvpn(struct ndpi_detection_module_struct* ndpi_struct */ if(ovpn_payload_len < 14 + 2 * (packet->tcp != NULL)) { - NDPI_EXCLUDE_PROTO(ndpi_struct, flow); - return; + return 1; /* Exclude */ } - + /* Skip openvpn TCP transport packet size */ if(packet->tcp != NULL) ovpn_payload += 2, ovpn_payload_len -= 2; opcode = ovpn_payload[0] & P_OPCODE_MASK; if(!is_opcode_valid(opcode)) { - NDPI_EXCLUDE_PROTO(ndpi_struct, flow); - return; + return 1; /* Exclude */ } /* Maybe a strong assumption... */ if((ovpn_payload[0] & ~P_OPCODE_MASK) != 0) { NDPI_LOG_DBG2(ndpi_struct, "Invalid key id\n"); - NDPI_EXCLUDE_PROTO(ndpi_struct, flow); - return; + return 1; /* Exclude */ } if(flow->packet_direction_counter[dir] == 1 && !(opcode == P_CONTROL_HARD_RESET_CLIENT_V1 || @@ -177,15 +175,23 @@ static void ndpi_search_openvpn(struct ndpi_detection_module_struct* ndpi_struct opcode == P_CONTROL_HARD_RESET_SERVER_V2 || opcode == P_CONTROL_HARD_RESET_CLIENT_V3)) { NDPI_LOG_DBG2(ndpi_struct, "Invalid first packet\n"); - NDPI_EXCLUDE_PROTO(ndpi_struct, flow); - return; + return 1; /* Exclude */ + } + /* Resets are small packets */ + if(packet->payload_packet_len >= 1200 && + (opcode == P_CONTROL_HARD_RESET_CLIENT_V1 || + opcode == P_CONTROL_HARD_RESET_CLIENT_V2 || + opcode == P_CONTROL_HARD_RESET_SERVER_V1 || + opcode == P_CONTROL_HARD_RESET_SERVER_V2 || + opcode == P_CONTROL_HARD_RESET_CLIENT_V3)) { + NDPI_LOG_DBG2(ndpi_struct, "Invalid len first pkt (QUIC collision)\n"); + return 1; /* Exclude */ } if(flow->packet_direction_counter[dir] == 1 && packet->tcp && ntohs(*(u_int16_t *)(packet->payload)) != ovpn_payload_len) { NDPI_LOG_DBG2(ndpi_struct, "Invalid tcp len on reset\n"); - NDPI_EXCLUDE_PROTO(ndpi_struct, flow); - return; + return 1; /* Exclude */ } NDPI_LOG_DBG2(ndpi_struct, "[packets %d/%d][opcode: %u][len: %u]\n", @@ -196,22 +202,19 @@ static void ndpi_search_openvpn(struct ndpi_detection_module_struct* ndpi_struct if(flow->packet_direction_counter[dir] > 1) { if(memcmp(flow->ovpn_session_id[dir], ovpn_payload + 1, 8) != 0) { NDPI_LOG_DBG2(ndpi_struct, "Invalid session id on two consecutive pkts in the same dir\n"); - NDPI_EXCLUDE_PROTO(ndpi_struct, flow); - return; + return 1; /* Exclude */ } if(flow->packet_direction_counter[dir] >= 2 && flow->packet_direction_counter[!dir] >= 2) { /* (2) */ NDPI_LOG_INFO(ndpi_struct,"found openvpn (session ids match on both direction)\n"); - ndpi_int_openvpn_add_connection(ndpi_struct, flow); - return; - } + return 2; /* Found */ + } if(flow->packet_direction_counter[dir] >= 4 && flow->packet_direction_counter[!dir] == 0) { /* (3) */ NDPI_LOG_INFO(ndpi_struct,"found openvpn (asymmetric)\n"); - ndpi_int_openvpn_add_connection(ndpi_struct, flow); - return; + return 2; /* Found */ } } else { memcpy(flow->ovpn_session_id[dir], ovpn_payload + 1, 8); @@ -241,9 +244,8 @@ static void ndpi_search_openvpn(struct ndpi_detection_module_struct* ndpi_struct if(memcmp(flow->ovpn_session_id[!dir], session_remote, 8) == 0) { NDPI_LOG_INFO(ndpi_struct,"found openvpn\n"); - ndpi_int_openvpn_add_connection(ndpi_struct, flow); - return; - } else { + return 2; /* Found */ + } else { NDPI_LOG_DBG2(ndpi_struct, "key mismatch 0x%lx\n", ndpi_ntohll(*(u_int64_t *)session_remote)); } } @@ -254,11 +256,233 @@ static void ndpi_search_openvpn(struct ndpi_detection_module_struct* ndpi_struct } } - if(failed) + if(failed || flow->packet_counter > 5) + return 1; /* Exclude */ + return 0; /* Continue */ +} + +/* Heuristic to detect encrypted/obfusctaed OpenVPN flows, based on + https://www.usenix.org/conference/usenixsecurity22/presentation/xue-diwen. + Main differences between the paper and our implementation: + * only op-code fingerprint + + Core idea: even if the OpenVPN packets are somehow encrypted to avoid trivial + detection, the distibution of the first byte of the messages (i.e. the + distribution of the op-codes) might still be unique +*/ + +static int search_heur_opcode_common(struct ndpi_detection_module_struct* ndpi_struct, + struct ndpi_flow_struct* flow, + u_int8_t first_byte) { + u_int8_t opcode, found = 0, i; + int dir = ndpi_struct->packet.packet_direction; + + opcode = first_byte & P_OPCODE_MASK; + + /* Handshake: + * 2 different resets + * up to 3 different opcodes (ack, control, wkc) + * 1 data (v1 or v2) + So, other than the resets: + * at least 2 different opcodes (ack, control) + * no more than 4 (i.e. OPENVPN_HEUR_MAX_NUM_OPCODES) different opcodes + */ + + NDPI_LOG_DBG2(ndpi_struct, "Heur-opcode: [packets %d/%d msgs %d, dir %d][first byte 0x%x][opcode: 0x%x]\n", + flow->packet_direction_counter[0], + flow->packet_direction_counter[1], + flow->ovpn_heur_opcode__num_msgs, + dir, first_byte, opcode); + + flow->ovpn_heur_opcode__num_msgs++; + + if(flow->packet_direction_counter[dir] == 1) { + flow->ovpn_heur_opcode__resets[dir] = opcode; + if(flow->packet_direction_counter[!dir] > 0 && + opcode == flow->ovpn_heur_opcode__resets[!dir]) { + NDPI_LOG_DBG2(ndpi_struct, "Heur-opcode: same resets\n"); + return 1; /* Exclude */ + } + return 0; /* Continue */ + } + + if(opcode == flow->ovpn_heur_opcode__resets[dir]) { + if(flow->ovpn_heur_opcode__codes_num > 0) { + NDPI_LOG_DBG2(ndpi_struct, "Heur-opcode: resets after other opcodes\n"); + return 1; /* Exclude */ + } + return 0; /* Continue */ + } + if(flow->packet_direction_counter[!dir] > 0 && + opcode == flow->ovpn_heur_opcode__resets[!dir]) { + NDPI_LOG_DBG2(ndpi_struct, "Heur-opcode: same resets\n"); + return 1; /* Exclude */ + } + + if(flow->packet_direction_counter[!dir] == 0) { + NDPI_LOG_DBG2(ndpi_struct, "Heur-opcode: opcode different than reset but not reset in the other direction\n"); + return 1; /* Exclude */ + } + + if(flow->ovpn_heur_opcode__codes_num == OPENVPN_HEUR_MAX_NUM_OPCODES && + opcode != flow->ovpn_heur_opcode__codes[OPENVPN_HEUR_MAX_NUM_OPCODES - 1]) { + NDPI_LOG_DBG2(ndpi_struct, "Heur-opcode: once data we can't have other opcode\n"); + /* TODO: this check assumes that the "data" opcode is the 4th one (after the resets). + * But we usually have only ack + control + data... */ + return 1; /* Exclude */ + } + + for(i = 0; i < flow->ovpn_heur_opcode__codes_num; i++) { + if(flow->ovpn_heur_opcode__codes[i] == opcode) + found = 1; + } + if(found == 0) { + if(flow->ovpn_heur_opcode__codes_num == OPENVPN_HEUR_MAX_NUM_OPCODES) { + NDPI_LOG_DBG2(ndpi_struct, "Heur-opcode: too many opcodes. Early exclude\n"); + return 1; /* Exclude */ + } + flow->ovpn_heur_opcode__codes[flow->ovpn_heur_opcode__codes_num++] = opcode; + } + + NDPI_LOG_DBG2(ndpi_struct, "Heur-opcode: Resets 0x%x,0x%x Num %d\n", + flow->ovpn_heur_opcode__resets[0], + flow->ovpn_heur_opcode__resets[1], + flow->ovpn_heur_opcode__codes_num); + + if(flow->ovpn_heur_opcode__num_msgs < ndpi_struct->cfg.openvpn_heuristics_num_msgs) + return 0; /* Continue */ + + /* Done. Check what we have found...*/ + + if(flow->packet_direction_counter[0] == 0 || + flow->packet_direction_counter[1] == 0) { + NDPI_LOG_DBG2(ndpi_struct, "Heur-opcode: excluded because asymmetric traffic\n"); + return 1; /* Exclude */ + } + + if(flow->ovpn_heur_opcode__codes_num >= 2) { + NDPI_LOG_INFO(ndpi_struct,"found openvpn (Heur-opcode)\n"); + return 2; /* Found */ + } + NDPI_LOG_DBG2(ndpi_struct, "Heur-opcode: excluded\n"); + return 1; /* Exclude */ +} + +static int search_heur_opcode(struct ndpi_detection_module_struct* ndpi_struct, + struct ndpi_flow_struct* flow) { + struct ndpi_packet_struct* packet = &ndpi_struct->packet; + const u_int8_t *ovpn_payload = packet->payload; + u_int16_t ovpn_payload_len = packet->payload_packet_len; + int dir = packet->packet_direction; + u_int16_t pdu_len; + int rc, iter, offset; + + /* To reduce false positives number, trigger the heuristic only for flows to + suspicious/unknown addresses */ + if(is_flow_addr_informative(flow)) { + NDPI_LOG_DBG2(ndpi_struct, "Heur-opcode: flow to informative address. Exclude\n"); + return 1; /* Exclude */ + } + + if(packet->tcp != NULL) { + /* Two bytes field with pdu length */ + + NDPI_LOG_DBG2(ndpi_struct, "Heur-opcode: TCP length %d (remaining %d)\n", + ovpn_payload_len, + flow->ovpn_heur_opcode__missing_bytes[dir]); + + /* We might need to "reassemble" the OpenVPN messages. + Luckily, we are not interested in the message itself, but only in the first byte + (after the length field), so as state we only need to know the "missing bytes" + of the latest pdu (from the previous TCP packets) */ + if(flow->ovpn_heur_opcode__missing_bytes[dir] > 0) { + NDPI_LOG_DBG2(ndpi_struct, "Heur-opcode: TCP, remaining bytes to ignore %d length %d\n", + flow->ovpn_heur_opcode__missing_bytes[dir], ovpn_payload_len); + if(flow->ovpn_heur_opcode__missing_bytes[dir] >= ovpn_payload_len) { + flow->ovpn_heur_opcode__missing_bytes[dir] -= ovpn_payload_len; + return 0; /* Continue */ + } else { + offset = flow->ovpn_heur_opcode__missing_bytes[dir]; + flow->ovpn_heur_opcode__missing_bytes[dir] = 0; + } + } else { + offset = 0; + } + + iter = 0; + rc = 1; /* Exclude */ + while(offset + 2 + 1 /* The first byte is the opcode */ <= ovpn_payload_len) { + pdu_len = ntohs((*(u_int16_t *)(ovpn_payload + offset))); + NDPI_LOG_DBG2(ndpi_struct, "Heur-opcode: TCP, iter %d offset %d pdu_length %d\n", + iter, offset, pdu_len); + if(pdu_len < 14) + return 1; /* Exclude */ + rc = search_heur_opcode_common(ndpi_struct, flow, *(ovpn_payload + offset + 2)); + NDPI_LOG_DBG2(ndpi_struct, "Heur-opcode: TCP, rc %d\n", rc); + if(rc > 0) /* Exclude || Found --> stop */ + return rc; + + if(offset + 2 + pdu_len <= ovpn_payload_len) { + offset += 2 + pdu_len; + } else { + flow->ovpn_heur_opcode__missing_bytes[dir] = pdu_len - (ovpn_payload_len - (offset + 2)); + NDPI_LOG_DBG2(ndpi_struct, "Heur-opcode: TCP, missing %d bytes\n", + flow->ovpn_heur_opcode__missing_bytes[dir]); + return 0; /* Continue */ + } + iter++; + } + return rc; + } else { + if(ovpn_payload_len < 14) + return 1; /* Exclude */ + return search_heur_opcode_common(ndpi_struct, flow, ovpn_payload[0]); + } +} + + +static void ndpi_search_openvpn(struct ndpi_detection_module_struct* ndpi_struct, + struct ndpi_flow_struct* flow) { + struct ndpi_packet_struct* packet = &ndpi_struct->packet; + + NDPI_LOG_DBG(ndpi_struct, "Search opnvpn\n"); + + if(packet->payload_packet_len > 10 && + ntohl(*(u_int32_t *)&packet->payload[4 + 2 * (packet->tcp != NULL)]) == 0x2112A442) { + NDPI_LOG_DBG2(ndpi_struct, "Avoid collision with STUN\n"); NDPI_EXCLUDE_PROTO(ndpi_struct, flow); + return; + } + + NDPI_LOG_DBG2(ndpi_struct, "States (before): %d %d\n", + flow->ovpn_alg_standard_state, + flow->ovpn_alg_heur_opcode_state); + + if(flow->ovpn_alg_standard_state == 0) { + flow->ovpn_alg_standard_state = search_standard(ndpi_struct, flow); + } + if(ndpi_struct->cfg.openvpn_heuristics & NDPI_HEURISTICS_OPENVPN_OPCODE) { + if(flow->ovpn_alg_heur_opcode_state == 0) { + flow->ovpn_alg_heur_opcode_state = search_heur_opcode(ndpi_struct, flow); + } + } else { + flow->ovpn_alg_heur_opcode_state = 1; + } + + NDPI_LOG_DBG2(ndpi_struct, "States (after): %d %d\n", + flow->ovpn_alg_standard_state, + flow->ovpn_alg_heur_opcode_state); + + if(flow->ovpn_alg_standard_state == 2) { + ndpi_int_openvpn_add_connection(ndpi_struct, flow, NDPI_CONFIDENCE_DPI); + } else if (flow->ovpn_alg_heur_opcode_state == 2) { + ndpi_int_openvpn_add_connection(ndpi_struct, flow, NDPI_CONFIDENCE_DPI_AGGRESSIVE); + ndpi_set_risk(flow, NDPI_OBFUSCATED_TRAFFIC, "Obfuscated OpenVPN"); + } else if(flow->ovpn_alg_standard_state == 1 && + flow->ovpn_alg_heur_opcode_state == 1) { + NDPI_EXCLUDE_PROTO(ndpi_struct, flow); + } - if(flow->packet_counter > 5) - NDPI_EXCLUDE_PROTO(ndpi_struct, flow); } void init_openvpn_dissector(struct ndpi_detection_module_struct *ndpi_struct, diff --git a/tests/cfgs/default/pcap/openvpn_obfuscated.pcapng b/tests/cfgs/default/pcap/openvpn_obfuscated.pcapng new file mode 100644 index 0000000000000000000000000000000000000000..439c209103e35fc5b25c4ccd5603a6ce35af0bca GIT binary patch literal 59460 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

Jz#Z(NI&w|V>cXWr})fxMLi7=bIc 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 Ar2qf` literal 0 HcmV?d00001 diff --git a/tests/cfgs/default/result/openvpn_obfuscated.pcapng.out b/tests/cfgs/default/result/openvpn_obfuscated.pcapng.out new file mode 100644 index 00000000000..f93cacad45b --- /dev/null +++ b/tests/cfgs/default/result/openvpn_obfuscated.pcapng.out @@ -0,0 +1,36 @@ +Guessed flow protos: 3 + +DPI Packets (TCP): 38 (19.00 pkts/flow) +DPI Packets (UDP): 9 (9.00 pkts/flow) +Confidence Match by port : 2 (flows) +Confidence Match by IP : 1 (flows) +Num dissector calls: 708 (236.00 diss/flow) +LRU cache ookla: 0/0/0 (insert/search/found) +LRU cache bittorrent: 0/9/0 (insert/search/found) +LRU cache stun: 0/0/0 (insert/search/found) +LRU cache tls_cert: 0/0/0 (insert/search/found) +LRU cache mining: 0/3/0 (insert/search/found) +LRU cache msteams: 0/0/0 (insert/search/found) +LRU cache fpc_dns: 0/3/0 (insert/search/found) +Automa host: 0/0 (search/found) +Automa domain: 0/0 (search/found) +Automa tls cert: 0/0 (search/found) +Automa risk mask: 0/0 (search/found) +Automa common alpns: 0/0 (search/found) +Patricia risk mask: 4/0 (search/found) +Patricia risk mask IPv6: 0/0 (search/found) +Patricia risk: 0/0 (search/found) +Patricia risk IPv6: 0/0 (search/found) +Patricia protocols: 4/2 (search/found) +Patricia protocols IPv6: 0/0 (search/found) + +SMTPS 60 17222 1 +TLS 87 25469 1 +NordVPN 30 10598 1 + +Safe 147 42691 2 +Acceptable 30 10598 1 + + 1 TCP 107.161.86.131:443 <-> 192.168.12.156:48072 [proto: 91/TLS][IP: 0/Unknown][Encrypted][Confidence: Match by port][FPC: 0/Unknown, Confidence: Unknown][DPI packets: 15][cat: Web/5][40 pkts/9272 bytes <-> 47 pkts/16197 bytes][Goodput ratio: 70/81][3.15 sec][bytes ratio: -0.272 (Download)][IAT c2s/s2c min/avg/max/stddev: 0/0 57/52 212/303 66/79][Pkt Len c2s/s2c min/avg/max/stddev: 66/66 232/345 1514/1090 370/406][Plen Bins: 35,3,3,15,1,1,0,0,1,3,5,1,0,1,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,18,0,0,0,0,0,0,0,0,0,1,0,0,3,0,0] + 2 TCP 192.168.12.156:37976 <-> 185.128.25.99:465 [proto: 29/SMTPS][IP: 426/NordVPN][Encrypted][Confidence: Match by port][FPC: 426/NordVPN, Confidence: IP address][DPI packets: 23][cat: Email/3][29 pkts/7410 bytes <-> 31 pkts/9812 bytes][Goodput ratio: 74/79][1.73 sec][bytes ratio: -0.139 (Mixed)][IAT c2s/s2c min/avg/max/stddev: 0/0 66/26 1019/153 204/31][Pkt Len c2s/s2c min/avg/max/stddev: 66/66 256/317 1090/1514 256/424][Risk: ** Fully Encrypted Flow **][Risk Score: 50][PLAIN TEXT (HrFTzP)][Plen Bins: 0,0,14,30,14,2,0,2,5,0,5,5,2,0,0,2,0,0,0,0,0,2,0,2,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0] + 3 UDP 192.168.12.156:47128 <-> 149.102.238.108:1214 [proto: 426/NordVPN][IP: 426/NordVPN][Encrypted][Confidence: Match by IP][FPC: 426/NordVPN, Confidence: IP address][DPI packets: 9][cat: VPN/2][19 pkts/3629 bytes <-> 11 pkts/6969 bytes][Goodput ratio: 78/93][1.26 sec][bytes ratio: -0.315 (Download)][IAT c2s/s2c min/avg/max/stddev: 0/0 78/132 1156/1023 278/337][Pkt Len c2s/s2c min/avg/max/stddev: 115/136 191/634 782/1158 153/438][Risk: ** Susp Entropy **][Risk Score: 10][Risk Info: Entropy: 6.051 (Executable?)][PLAIN TEXT (SFhAFI)][Plen Bins: 0,0,23,41,3,0,0,0,3,0,3,6,0,0,0,0,0,0,0,0,0,0,0,3,0,3,0,0,0,0,0,0,0,0,13,0,0,0,0,0,0,0,0,0,0,0,0,0] diff --git a/tests/cfgs/openvpn_heuristic_enabled/config.txt b/tests/cfgs/openvpn_heuristic_enabled/config.txt new file mode 100644 index 00000000000..81203be0f86 --- /dev/null +++ b/tests/cfgs/openvpn_heuristic_enabled/config.txt @@ -0,0 +1 @@ +--cfg=openvpn,dpi.heuristics,0x01 --cfg=packets_limit_per_flow,64 diff --git a/tests/cfgs/openvpn_heuristic_enabled/pcap/openvpn_obfuscated.pcapng b/tests/cfgs/openvpn_heuristic_enabled/pcap/openvpn_obfuscated.pcapng new file mode 120000 index 00000000000..4e91a46c179 --- /dev/null +++ b/tests/cfgs/openvpn_heuristic_enabled/pcap/openvpn_obfuscated.pcapng @@ -0,0 +1 @@ +../../default/pcap/openvpn_obfuscated.pcapng \ No newline at end of file diff --git a/tests/cfgs/openvpn_heuristic_enabled/result/openvpn_obfuscated.pcapng.out b/tests/cfgs/openvpn_heuristic_enabled/result/openvpn_obfuscated.pcapng.out new file mode 100644 index 00000000000..808f5fc4437 --- /dev/null +++ b/tests/cfgs/openvpn_heuristic_enabled/result/openvpn_obfuscated.pcapng.out @@ -0,0 +1,31 @@ +DPI Packets (TCP): 59 (29.50 pkts/flow) +DPI Packets (UDP): 10 (10.00 pkts/flow) +Confidence DPI (aggressive) : 3 (flows) +Num dissector calls: 748 (249.33 diss/flow) +LRU cache ookla: 0/0/0 (insert/search/found) +LRU cache bittorrent: 0/9/0 (insert/search/found) +LRU cache stun: 0/0/0 (insert/search/found) +LRU cache tls_cert: 0/0/0 (insert/search/found) +LRU cache mining: 0/0/0 (insert/search/found) +LRU cache msteams: 0/0/0 (insert/search/found) +LRU cache fpc_dns: 0/3/0 (insert/search/found) +Automa host: 0/0 (search/found) +Automa domain: 0/0 (search/found) +Automa tls cert: 0/0 (search/found) +Automa risk mask: 0/0 (search/found) +Automa common alpns: 0/0 (search/found) +Patricia risk mask: 2/0 (search/found) +Patricia risk mask IPv6: 0/0 (search/found) +Patricia risk: 0/0 (search/found) +Patricia risk IPv6: 0/0 (search/found) +Patricia protocols: 4/2 (search/found) +Patricia protocols IPv6: 0/0 (search/found) + +OpenVPN 87 25469 1 +NordVPN 90 27820 2 + +Acceptable 177 53289 3 + + 1 TCP 107.161.86.131:443 <-> 192.168.12.156:48072 [proto: 159/OpenVPN][IP: 0/Unknown][Encrypted][Confidence: DPI (aggressive)][FPC: 0/Unknown, Confidence: Unknown][DPI packets: 40][cat: VPN/2][40 pkts/9272 bytes <-> 47 pkts/16197 bytes][Goodput ratio: 70/81][3.15 sec][bytes ratio: -0.272 (Download)][IAT c2s/s2c min/avg/max/stddev: 0/0 57/52 212/303 66/79][Pkt Len c2s/s2c min/avg/max/stddev: 66/66 232/345 1514/1090 370/406][Risk: ** Known Proto on Non Std Port **** Obfuscated Traffic **][Risk Score: 150][Risk Info: Obfuscated OpenVPN / Expected on port 1194][PLAIN TEXT (MhLYoT)][Plen Bins: 35,3,3,15,1,1,0,0,1,3,5,1,0,1,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,18,0,0,0,0,0,0,0,0,0,1,0,0,3,0,0] + 2 TCP 192.168.12.156:37976 <-> 185.128.25.99:465 [proto: 159.426/OpenVPN.NordVPN][IP: 426/NordVPN][Encrypted][Confidence: DPI (aggressive)][FPC: 426/NordVPN, Confidence: IP address][DPI packets: 19][cat: VPN/2][29 pkts/7410 bytes <-> 31 pkts/9812 bytes][Goodput ratio: 74/79][1.73 sec][bytes ratio: -0.139 (Mixed)][IAT c2s/s2c min/avg/max/stddev: 0/0 66/26 1019/153 204/31][Pkt Len c2s/s2c min/avg/max/stddev: 66/66 256/317 1090/1514 256/424][Risk: ** Known Proto on Non Std Port **** Obfuscated Traffic **][Risk Score: 150][Risk Info: Obfuscated OpenVPN / Expected on port 1194][PLAIN TEXT (HrFTzP)][Plen Bins: 0,0,14,30,14,2,0,2,5,0,5,5,2,0,0,2,0,0,0,0,0,2,0,2,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0] + 3 UDP 192.168.12.156:47128 <-> 149.102.238.108:1214 [proto: 159.426/OpenVPN.NordVPN][IP: 426/NordVPN][Encrypted][Confidence: DPI (aggressive)][FPC: 426/NordVPN, Confidence: IP address][DPI packets: 10][cat: VPN/2][19 pkts/3629 bytes <-> 11 pkts/6969 bytes][Goodput ratio: 78/93][1.26 sec][bytes ratio: -0.315 (Download)][IAT c2s/s2c min/avg/max/stddev: 0/0 78/132 1156/1023 278/337][Pkt Len c2s/s2c min/avg/max/stddev: 115/136 191/634 782/1158 153/438][Risk: ** Known Proto on Non Std Port **** Susp Entropy **** Obfuscated Traffic **][Risk Score: 160][Risk Info: Entropy: 6.051 (Executable?) / Obfuscated OpenVPN][PLAIN TEXT (SFhAFI)][Plen Bins: 0,0,23,41,3,0,0,0,3,0,3,6,0,0,0,0,0,0,0,0,0,0,0,3,0,3,0,0,0,0,0,0,0,0,13,0,0,0,0,0,0,0,0,0,0,0,0,0]