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 00000000000..439c209103e Binary files /dev/null and b/tests/cfgs/default/pcap/openvpn_obfuscated.pcapng differ 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]