diff options
author | Luca Deri <deri@ntop.org> | 2022-12-29 19:38:25 +0100 |
---|---|---|
committer | Luca Deri <deri@ntop.org> | 2022-12-29 19:38:25 +0100 |
commit | 8f91b8ba72e61eb15a3fdfcddd6339b2fae341be (patch) | |
tree | bb20eebc202f69a6fd95146191e718e8249c168f | |
parent | 560280e6f082d22e6a9de8e537b7876bacf8d072 (diff) |
Implemented EDNS(0) support in DNS dissector
Improved DNS dissection
-rw-r--r-- | src/include/ndpi_typedefs.h | 2 | ||||
-rw-r--r-- | src/lib/ndpi_main.c | 4 | ||||
-rw-r--r-- | src/lib/ndpi_utils.c | 8 | ||||
-rw-r--r-- | src/lib/protocols/dns.c | 229 | ||||
-rw-r--r-- | tests/result/fuzz-2006-06-26-2594.pcap.out | 2 | ||||
-rw-r--r-- | tests/result/malformed_dns.pcap.out | 2 |
6 files changed, 191 insertions, 56 deletions
diff --git a/src/include/ndpi_typedefs.h b/src/include/ndpi_typedefs.h index d9de63d5e..16612ce87 100644 --- a/src/include/ndpi_typedefs.h +++ b/src/include/ndpi_typedefs.h @@ -1381,7 +1381,7 @@ struct ndpi_flow_struct { /* the only fields useful for nDPI and ntopng */ struct { u_int8_t num_queries, num_answers, reply_code, is_query; - u_int16_t query_type, query_class, rsp_type; + u_int16_t query_type, query_class, rsp_type, edns0_udp_payload_size; ndpi_ip_addr_t rsp_addr; /* The first address in a DNS response packet (A and AAAA) */ char ptr_domain_name[64 /* large enough but smaller than { } tls */]; } dns; diff --git a/src/lib/ndpi_main.c b/src/lib/ndpi_main.c index 95d880dcb..739010025 100644 --- a/src/lib/ndpi_main.c +++ b/src/lib/ndpi_main.c @@ -6447,8 +6447,10 @@ ndpi_protocol ndpi_detection_process_packet(struct ndpi_detection_module_struct struct ndpi_packet_struct *packet; NDPI_SELECTION_BITMASK_PROTOCOL_SIZE ndpi_selection_packet; u_int32_t num_calls = 0; - ndpi_protocol ret = { 0 }; + ndpi_protocol ret; + memset(&ret, 0, sizeof(ret)); + if(!flow || !ndpi_str) return(ret); diff --git a/src/lib/ndpi_utils.c b/src/lib/ndpi_utils.c index 990aef9bd..12313a0f7 100644 --- a/src/lib/ndpi_utils.c +++ b/src/lib/ndpi_utils.c @@ -2355,8 +2355,9 @@ static u_int8_t ndpi_check_hostname_risk_exception(struct ndpi_detection_module_ if(automa->ac_automa) { AC_TEXT_t ac_input_text; - AC_REP_t match = {0}; - + AC_REP_t match; + + memset(&match, 0, sizeof(match)); ac_input_text.astring = hostname, ac_input_text.length = strlen(hostname); ac_input_text.option = 0; @@ -2666,8 +2667,9 @@ u_int8_t is_a_common_alpn(struct ndpi_detection_module_struct *ndpi_str, if(automa->ac_automa) { AC_TEXT_t ac_input_text; - AC_REP_t match = {0}; + AC_REP_t match; + memset(&match, 0, sizeof(match)); ac_input_text.astring = (char*)alpn_to_check, ac_input_text.length = alpn_to_check_len; ac_input_text.option = 0; diff --git a/src/lib/protocols/dns.c b/src/lib/protocols/dns.c index 298f0967f..7df825f8b 100644 --- a/src/lib/protocols/dns.c +++ b/src/lib/protocols/dns.c @@ -29,7 +29,7 @@ #define FLAGS_MASK 0x8000 -// #define DNS_DEBUG 1 +/* #define DNS_DEBUG 1 */ #define DNS_PORT 53 #define LLMNR_PORT 5355 @@ -202,7 +202,7 @@ static char* dns_error_code2string(u_int16_t error_code, char *buf, u_int buf_le case 7: return((char*)"XRRSET"); case 8: return((char*)"NOTAUTH"); case 9: return((char*)"NOTZONE"); - + default: snprintf(buf, buf_len, "%u", error_code); return(buf); @@ -217,7 +217,7 @@ static u_int8_t ndpi_grab_dns_name(struct ndpi_packet_struct *packet, u_int *_hostname_len) { u_int8_t hostname_is_valid = 1; u_int j = 0; - + max_len--; while((j < max_len) @@ -255,12 +255,12 @@ static u_int8_t ndpi_grab_dns_name(struct ndpi_packet_struct *packet, } _hostname[j] = '\0', *_hostname_len = j; - + return(hostname_is_valid); } /* *********************************************** */ - + static int search_valid_dns(struct ndpi_detection_module_struct *ndpi_struct, struct ndpi_flow_struct *flow, struct ndpi_dns_packet_header *dns_header, @@ -331,7 +331,7 @@ static int search_valid_dns(struct ndpi_detection_module_struct *ndpi_struct, x++; } } - + flow->protos.dns.reply_code = dns_header->flags & 0x0F; if(flow->protos.dns.reply_code != 0) { @@ -342,35 +342,54 @@ static int search_valid_dns(struct ndpi_detection_module_struct *ndpi_struct, ndpi_set_risk(ndpi_struct, flow, NDPI_ERROR_CODE_DETECTED, str); } else { if(ndpi_isset_risk(ndpi_struct, flow, NDPI_SUSPICIOUS_DGA_DOMAIN)) { - ndpi_set_risk(ndpi_struct, flow, NDPI_RISKY_DOMAIN, "DGA Name Query with no Error Code"); + ndpi_set_risk(ndpi_struct, flow, NDPI_RISKY_DOMAIN, "DGA Name Query with no Error Code"); } } - + if((dns_header->num_queries > 0) && (dns_header->num_queries <= NDPI_MAX_DNS_REQUESTS) /* Don't assume that num_queries must be zero */ && ((((dns_header->num_answers > 0) && (dns_header->num_answers <= NDPI_MAX_DNS_REQUESTS)) || ((dns_header->authority_rrs > 0) && (dns_header->authority_rrs <= NDPI_MAX_DNS_REQUESTS)) || ((dns_header->additional_rrs > 0) && (dns_header->additional_rrs <= NDPI_MAX_DNS_REQUESTS)))) ) { /* This is a good reply: we dissect it both for request and response */ + + if(dns_header->num_queries > 0) { + u_int16_t rsp_type; + u_int16_t num; - /* Leave the statement below commented necessary in case of call to ndpi_get_partial_detection() */ - x++; + for(num = 0; num < dns_header->num_queries; num++) { + u_int16_t data_len; - if(x < packet->payload_packet_len && packet->payload[x] != '\0') { - while((x < packet->payload_packet_len) - && (packet->payload[x] != '\0')) { - x++; - } + if((x+6) >= packet->payload_packet_len) { + break; + } - x++; - } + if((data_len = getNameLength(x, packet->payload, + packet->payload_packet_len)) == 0) { + break; + } else + x += data_len; + + if((x+8) >= packet->payload_packet_len) { + break; + } + + rsp_type = get16(&x, packet->payload); + +#ifdef DNS_DEBUG + printf("[DNS] [response (query)] response_type=%d\n", rsp_type); +#endif - x += 4; + /* here x points to the response "class" field */ + x += 2; /* Skip class */ + } + } if(dns_header->num_answers > 0) { u_int16_t rsp_type; u_int32_t rsp_ttl; u_int16_t num; + u_int8_t found = 0; for(num = 0; num < dns_header->num_answers; num++) { u_int16_t data_len; @@ -393,61 +412,171 @@ static int search_valid_dns(struct ndpi_detection_module_struct *ndpi_struct, rsp_ttl = ntohl(*((u_int32_t*)&packet->payload[x+2])); if(rsp_ttl == 0) - ndpi_set_risk(ndpi_struct, flow, NDPI_DNS_SUSPICIOUS_TRAFFIC, "DNS Record with zero TTL"); + ndpi_set_risk(ndpi_struct, flow, NDPI_DNS_SUSPICIOUS_TRAFFIC, "DNS Record with zero TTL"); #ifdef DNS_DEBUG printf("[DNS] TTL = %u\n", rsp_ttl); printf("[DNS] [response] response_type=%d\n", rsp_type); #endif - ndpi_check_dns_type(ndpi_struct, flow, rsp_type); - - flow->protos.dns.rsp_type = rsp_type; - - /* here x points to the response "class" field */ + if(found == 0) { + ndpi_check_dns_type(ndpi_struct, flow, rsp_type); + flow->protos.dns.rsp_type = rsp_type; + } + + /* x points to the response "class" field */ if((x+12) <= packet->payload_packet_len) { x += 6; data_len = get16(&x, packet->payload); if((x + data_len) <= packet->payload_packet_len) { - // printf("[rsp_type: %u][data_len: %u]\n", rsp_type, data_len); +#ifdef DNS_DEBUG + printf("[DNS] [rsp_type: %u][data_len: %u]\n", rsp_type, data_len); +#endif if(rsp_type == 0x05 /* CNAME */) { - x += data_len; - continue; /* Skip CNAME */ - } - - if(rsp_type == 0x0C /* PTR */) { + ; + } else if(rsp_type == 0x0C /* PTR */) { u_int16_t ptr_len = (packet->payload[x-2] << 8) + packet->payload[x-1]; if((x + ptr_len) <= packet->payload_packet_len) { - u_int len; - - ndpi_grab_dns_name(packet, &x, - flow->protos.dns.ptr_domain_name, + if(found == 0) { + u_int len; + + ndpi_grab_dns_name(packet, &x, + flow->protos.dns.ptr_domain_name, sizeof(flow->protos.dns.ptr_domain_name), &len); + found = 1; + } } } else if((((rsp_type == 0x1) && (data_len == 4)) /* A */ || ((rsp_type == 0x1c) && (data_len == 16)) /* AAAA */ )) { - memcpy(&flow->protos.dns.rsp_addr, packet->payload + x, data_len); + if(found == 0) { + memcpy(&flow->protos.dns.rsp_addr, packet->payload + x, data_len); + found = 1; + } } + + x += data_len; } } - break; + if(found && (dns_header->additional_rrs == 0)) { + /* + In case we have RR we need to iterate + all the answers and not just consider the + first one as we need to properly move 'x' + to the right offset + */ + break; + } } } - if((flow->detected_protocol_stack[0] == NDPI_PROTOCOL_DNS) - || (flow->detected_protocol_stack[1] == NDPI_PROTOCOL_DNS)) { - /* Request already set the protocol */ - // flow->extra_packets_func = NULL; /* Removed so the caller can keep dissecting DNS flows */ - } else { - /* We missed the request */ - u_int16_t s_port = packet->udp ? ntohs(packet->udp->source) : ntohs(packet->tcp->source); + if(dns_header->additional_rrs > 0) { + /* + Dissect the rest of the packet only if there are + additional_rrs as we need to check fo EDNS(0) + + In this case we need to go through the whole packet + as we need to update the 'x' offset + */ + if(dns_header->authority_rrs > 0) { + u_int16_t rsp_type; + u_int32_t rsp_ttl; + u_int16_t num; + + for(num = 0; num < dns_header->authority_rrs; num++) { + u_int16_t data_len; + + if((x+6) >= packet->payload_packet_len) { + break; + } + + if((data_len = getNameLength(x, packet->payload, + packet->payload_packet_len)) == 0) { + break; + } else + x += data_len; + + if((x+8) >= packet->payload_packet_len) { + break; + } + + rsp_type = get16(&x, packet->payload); + rsp_ttl = ntohl(*((u_int32_t*)&packet->payload[x+2])); + +#ifdef DNS_DEBUG + printf("[DNS] [RRS response] response_type=%d\n", rsp_type); +#endif + + /* here x points to the response "class" field */ + if((x+12) <= packet->payload_packet_len) { + x += 6; + data_len = get16(&x, packet->payload); + + if((x + data_len) <= packet->payload_packet_len) + x += data_len; + } + } + } + + if(dns_header->additional_rrs > 0) { + u_int16_t rsp_type; + u_int16_t num; + + for(num = 0; num < dns_header->additional_rrs; num++) { + u_int16_t data_len; + + if((x+6) > packet->payload_packet_len) { + break; + } + + if((data_len = getNameLength(x, packet->payload, packet->payload_packet_len)) == 0) { + break; + } else + x += data_len; + + if((x+10) > packet->payload_packet_len) { + break; + } + + rsp_type = get16(&x, packet->payload); + +#ifdef DNS_DEBUG + printf("[DNS] [RR response] response_type=%d\n", rsp_type); +#endif + + if(rsp_type == 41 /* OPT */) { + /* https://en.wikipedia.org/wiki/Extension_Mechanisms_for_DNS */ + flow->protos.dns.edns0_udp_payload_size = ntohs(*((u_int16_t*)&packet->payload[x])); /* EDNS(0) */ - ndpi_set_detected_protocol(ndpi_struct, flow, checkPort(s_port), NDPI_PROTOCOL_UNKNOWN, NDPI_CONFIDENCE_DPI); +#ifdef DNS_DEBUG + printf("[DNS] [response] edns0_udp_payload_size: %u\n", flow->protos.dns.edns0_udp_payload_size); +#endif + x += 6; + } else { + x += 6; + } + + if((data_len = getNameLength(x, packet->payload, packet->payload_packet_len)) == 0) { + break; + } else + x += data_len; + } + } + + if((flow->detected_protocol_stack[0] == NDPI_PROTOCOL_DNS) + || (flow->detected_protocol_stack[1] == NDPI_PROTOCOL_DNS)) { + /* Request already set the protocol */ + // flow->extra_packets_func = NULL; /* Removed so the caller can keep dissecting DNS flows */ + } else { + /* We missed the request */ + u_int16_t s_port = packet->udp ? ntohs(packet->udp->source) : ntohs(packet->tcp->source); + + ndpi_set_detected_protocol(ndpi_struct, flow, checkPort(s_port), NDPI_PROTOCOL_UNKNOWN, NDPI_CONFIDENCE_DPI); + } } } } @@ -547,7 +676,7 @@ static void ndpi_search_dns(struct ndpi_detection_module_struct *ndpi_struct, st for(idx=0; idx<name_len; idx++) printf("%c", packet->payload[i+1+idx]); - + printf("]\n"); } } @@ -581,7 +710,7 @@ static void ndpi_search_dns(struct ndpi_detection_module_struct *ndpi_struct, st ndpi_hostname_sni_set(flow, (const u_int8_t *)_hostname, len); if (hostname_is_valid == 0) - ndpi_set_risk(ndpi_struct, flow, NDPI_INVALID_CHARACTERS, NULL); + ndpi_set_risk(ndpi_struct, flow, NDPI_INVALID_CHARACTERS, NULL); if(len > 0) { ndpi_protocol_match_result ret_match; @@ -652,13 +781,15 @@ static void ndpi_search_dns(struct ndpi_detection_module_struct *ndpi_struct, st || (flow->detected_protocol_stack[1] == NDPI_PROTOCOL_DNS)) { /* TODO: add support to RFC6891 to avoid some false positives */ if((packet->udp != NULL) - && (packet->payload_packet_len > PKT_LEN_ALERT)) { + && (packet->payload_packet_len > PKT_LEN_ALERT) + && (packet->payload_packet_len > flow->protos.dns.edns0_udp_payload_size) + ) { char str[48]; snprintf(str, sizeof(str), "%u Bytes DNS Packet", packet->payload_packet_len); ndpi_set_risk(ndpi_struct, flow, NDPI_DNS_LARGE_PACKET, str); } - + if(packet->iph != NULL) { /* IPv4 */ u_int8_t flags = ((u_int8_t*)packet->iph)[6]; @@ -674,7 +805,7 @@ static void ndpi_search_dns(struct ndpi_detection_module_struct *ndpi_struct, st if(ip6_hdr->ip6_un1_nxt == 0x2C /* Next Header: Fragment Header for IPv6 (44) */) { ndpi_set_risk(ndpi_struct, flow, NDPI_DNS_FRAGMENTED, NULL); - } + } } } } diff --git a/tests/result/fuzz-2006-06-26-2594.pcap.out b/tests/result/fuzz-2006-06-26-2594.pcap.out index 3b1a8963c..4a167174b 100644 --- a/tests/result/fuzz-2006-06-26-2594.pcap.out +++ b/tests/result/fuzz-2006-06-26-2594.pcap.out @@ -96,7 +96,7 @@ SIP 85 39540 15 60 UDP 192.168.1.2:2810 -> 192.168.1.1:53 [proto: 5/DNS][IP: 0/Unknown][ClearText][Confidence: DPI][cat: Network/14][3 pkts/258 bytes -> 0 pkts/0 bytes][Goodput ratio: 51/0][4.01 sec][Hostname/SNI: _sip._udp.sip.nybercity.dk][::][Risk: ** Unidirectional Traffic **][Risk Score: 10][Risk Info: No server to client traffic][PLAIN TEXT (Mybercity)][Plen Bins: 0,100,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 61 UDP 192.168.1.2:2814 -> 192.168.1.1:53 [proto: 5/DNS][IP: 0/Unknown][ClearText][Confidence: DPI][cat: Network/14][3 pkts/258 bytes -> 0 pkts/0 bytes][Goodput ratio: 51/0][9.01 sec][Hostname/SNI: _sib._udp.sip.cybercity.dk][::][Risk: ** Malformed Packet **** Unidirectional Traffic **][Risk Score: 20][Risk Info: No server to client traffic / Invalid DNS Query Lenght][PLAIN TEXT (cybercity)][Plen Bins: 0,100,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 62 UDP 192.168.1.2:138 -> 192.168.1.251:138 [proto: 10.16/NetBIOS.SMBv1][IP: 0/Unknown][ClearText][Confidence: DPI][cat: System/18][1 pkts/243 bytes -> 0 pkts/0 bytes][Goodput ratio: 82/0][< 1 sec][Risk: ** Unsafe Protocol **** Unidirectional Traffic **][Risk Score: 20][Risk Info: No server to client traffic][Plen Bins: 0,0,0,0,0,0,100,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - 63 UDP 192.168.1.2:2719 <-> 192.168.1.1:53 [proto: 5/DNS][IP: 0/Unknown][ClearText][Confidence: DPI][cat: Network/14][1 pkts/75 bytes <-> 1 pkts/168 bytes][Goodput ratio: 43/75][1.01 sec][147.234.1.253][Risk: ** Malformed Packet **][Risk Score: 10][Risk Info: Invalid DNS Query Lenght][PLAIN TEXT (ecitele)][Plen Bins: 0,50,0,50,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + 63 UDP 192.168.1.2:2719 <-> 192.168.1.1:53 [proto: 5/DNS][IP: 0/Unknown][ClearText][Confidence: DPI][cat: Network/14][1 pkts/75 bytes <-> 1 pkts/168 bytes][Goodput ratio: 43/75][1.01 sec][::][Risk: ** Malformed Packet **][Risk Score: 10][Risk Info: Invalid DNS Query Lenght][PLAIN TEXT (ecitele)][Plen Bins: 0,50,0,50,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 64 UDP 192.168.1.41:138 -> 192.168.1.255:394 [proto: 10/NetBIOS][IP: 0/Unknown][ClearText][Confidence: Match by port][cat: System/18][1 pkts/243 bytes -> 0 pkts/0 bytes][Goodput ratio: 82/0][< 1 sec][PLAIN TEXT (MEBECDBDBDBCACACACACACACACACACA)][Plen Bins: 0,0,0,0,0,0,100,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 65 UDP 81.168.1.2:30000 -> 212.242.33.36:40392 [proto: 87/RTP][IP: 0/Unknown][ClearText][Confidence: DPI][cat: Media/1][1 pkts/214 bytes -> 0 pkts/0 bytes][Goodput ratio: 80/0][< 1 sec][RTP Stream Type: audio][Plen Bins: 0,0,0,0,0,100,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] 66 UDP 192.168.1.2:30000 -> 37.115.0.36:40392 [proto: 87/RTP][IP: 0/Unknown][ClearText][Confidence: DPI][cat: Media/1][1 pkts/214 bytes -> 0 pkts/0 bytes][Goodput ratio: 80/0][< 1 sec][RTP Stream Type: audio][PLAIN TEXT (njlndlj)][Plen Bins: 0,0,0,0,0,100,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] diff --git a/tests/result/malformed_dns.pcap.out b/tests/result/malformed_dns.pcap.out index fa0af82b1..db360b157 100644 --- a/tests/result/malformed_dns.pcap.out +++ b/tests/result/malformed_dns.pcap.out @@ -22,4 +22,4 @@ Patricia protocols: 2/0 (search/found) DNS 6 5860 1 - 1 UDP 127.0.0.1:50435 <-> 127.0.0.1:53 [proto: 5/DNS][IP: 0/Unknown][ClearText][Confidence: DPI][cat: Network/14][2 pkts/140 bytes <-> 4 pkts/5720 bytes][Goodput ratio: 40/97][5.03 sec][Hostname/SNI: www.xt.com][0.0.0.0][bytes ratio: -0.952 (Download)][IAT c2s/s2c min/avg/max/stddev: 4999/13 4999/1670 4999/4983 0/2343][Pkt Len c2s/s2c min/avg/max/stddev: 70/1430 70/1430 70/1430 0/0][Risk: ** Malformed Packet **** Large DNS Packet (512+ bytes) **][Risk Score: 60][Risk Info: Invalid DNS Query Lenght / 1388 Bytes DNS Packet][PLAIN TEXT (AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA)][Plen Bins: 33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,66,0,0,0,0] + 1 UDP 127.0.0.1:50435 <-> 127.0.0.1:53 [proto: 5/DNS][IP: 0/Unknown][ClearText][Confidence: DPI][cat: Network/14][2 pkts/140 bytes <-> 4 pkts/5720 bytes][Goodput ratio: 40/97][5.03 sec][Hostname/SNI: www.xt.com][66.66.66.66][bytes ratio: -0.952 (Download)][IAT c2s/s2c min/avg/max/stddev: 4999/13 4999/1670 4999/4983 0/2343][Pkt Len c2s/s2c min/avg/max/stddev: 70/1430 70/1430 70/1430 0/0][Risk: ** Malformed Packet **** Suspicious DNS Traffic **** Large DNS Packet (512+ bytes) **][Risk Score: 160][Risk Info: DNS Record with zero TTL / Invalid DNS Query Lenght / 1388 Bytes DNS Packet][PLAIN TEXT (AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA)][Plen Bins: 33,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,66,0,0,0,0] |