Guest gnoo Posted May 20, 2019 Posted May 20, 2019 Saudações, Este conteúdo está sujeito a erros de interpretação por parte da minha pessoa, se vires algum erro ou achas que tens algo a acrescentar deixa nos comentários para ser corrigido/adicionado... Os conceitos de redes aqui apresentados foram retirados do livro: A informação disponibilizada sobre DNS retirada do livro acima indicado, vai estar em inglês porque não tenho tempo para estar a fazer tradução, quem tiver dificuldade no inglês tem de ter paciência e fazer a tradução, ou então procurar outra fonte de informação que lhe sirva melhor, este livro é uma boa fonte de informação para quem quer programar para redes, contém informação bastante detalhada das características dos protocolos que nele são abordados. NOTA : O script em python que vos vou deixar aqui deve ser visto como uma fonte de estudo e não uma ferramenta acabada, para aqueles que têm interesse em iniciar o estudo na análise de pacotes ou redes devem pegar neste script no intuito de formar uma estrutura de pensamento e critica analisar o fluxo de dados, e melhorar o que já está feito... montar e desmontar ( mexer no código ). No meu ponto de vista as melhorias a serem feitas no código passa por melhorar a análise de dados efetuada na função decode_label ( em algumas circunstâncias apenas ), uma vez que o número e o fluxo de pacotes UDP que nos chega pode ser elevado, deve ser implementado um queue FIFO (first in, first out), com um sistema de threads, e passa também por melhorar a estrutura de dados. Este script apenas suporta os seguintes tipo de DNS records: A AAA CNAME Se conseguires fazer a análise de dados destes 3 tipos de DNS Records, é muito mais fácil passar aos restantes. O processo de análise de pacotes DNS pode demorar algum tempo até que fique claro como todo o processo se desenrola na sua análise, não conseguir a primeira ou à décima tentativa é normal :P hehehehehehe Já percebemos no tópico anterior que o DNS nos chega em pacotes UDP Apesar de haver quem jure a pés juntos que também se desloca em TCP ( não sei porque nunca vi). Então o DNS vem no payload do UDP a imagem que segue representa o header do DNS Descrição do campos de Header DNS A função para no script que trata os dados header do dns é esta: def dns_header(payload): # Dados do header tupla_dados_header = struct.unpack('!HHHHHH', payload[:12]) ID = tupla_dados_header[0] flags_and_codes = tupla_dados_header[1] question_count = tupla_dados_header[2] answer_record_count = tupla_dados_header[3] authority_record = tupla_dados_header[4] additional_record_count = tupla_dados_header[5] return ID, flags_and_codes, question_count, answer_record_count, authority_record, additional_record_count, payload[12:] ATENÇÃO: Para análise do nome da questão, ou alguns tipos de DNS records tais como CNAME ou SOA, é importante perceber o conceito que segue: Todos os nomes / labels, no campo da questão e nos tipos de DNS records suportados por este script são tratados com a função que segue: def decode_label(payload, inicio_fatiamento, fim_fatiamento): # Vai buscar label à questão label_string = [] len_label = [] #Recebe dados a desde o inicio do header onde é feito o fatiamento até ao inicio do primeiro label objeto_iterador = iter(payload[inicio_fatiamento:fim_fatiamento]) # BYTE representa o valor do tamanho de cada label for BYTE in objeto_iterador: #se BYTE for zero indica o fim do label e termina iteração if BYTE == 0: break #Adiciona o valor de cada label à lista para calcular o tamanho total do label len_label.append(BYTE) #faz um fatiamento nos caracteres do label mediante o valor representado por BYTE #passando para valor do tipo lista adicionando à lista # 46 é o valor ascci que representa um ( . ) para completar o dominio for elemento in list(islice(objeto_iterador, BYTE)) + [46]: label_string.extend(chr(elemento)) # Apanha a o tamanho do dominio para fazer o fatiamento a partir desse tamanho para a frente # O +1 inclui o tamanho do byte zero que indica o fim do label que deve contar também # No tamanho para fazer o fatiamento nome = ''.join(label_string[:-1]) # +1 indica o último byte da compressão do label que é valor 0, este byte também conta no tamanho do label soma_tamanho_label = sum(len_label) + len(len_label) + 1 tamanho_nome = len(''.join(label_string[:-1])) return nome, soma_tamanho_label, tamanho_nome, label_string, len(len_label) Atenção: DNS pode conter múltiplas respostas No tratamento das respostas, no caso de serem duas ou mais passou por ser feito um fatiamento no bytearray, o tamanho do fatiamento feito com base no soma dos campos que formam a resposta somando o valor do Resource Data Lenght,e adicionando a uma lista até que o valor do tamanho do bytearray seja zero, pode ser visto neste ciclo de repetição while while len(resposta) != 0: Type, Class, _ ,TTL, R_d_len = struct.unpack("!HH2HH", resposta[2:12]) tamanho = 12 + R_d_len LISTA_DNS_RECORDS.append(resposta[:tamanho]) resposta = resposta[tamanho:] SCRIPT COMPLETO EM PYTHON #coding:utf-8 #!/usr/bin/env python3 from socket import * import binascii import struct from itertools import islice def ethernet_frame(raw_dados): mac_destino, mac_fonte, protocolo = struct.unpack('! 6s 6s H', raw_dados[:14]) return byte_to_hex_mac(mac_destino), byte_to_hex_mac(mac_fonte), htons(protocolo), raw_dados[14:] def byte_to_hex_mac(mac_em_bytes): endereco = binascii.hexlify(mac_em_bytes).decode("ascii") return ":".join([endereco[i:i+2] for i in range(0,12,2)]) def dados_pacote_ipv4(carga): tupla_dados_ipv4 = struct.unpack("!BBHHHBBH4s4s", carga[:20]) versao = tupla_dados_ipv4[0] header_len = versao >> 4 tipo_servico = tupla_dados_ipv4[1] tamanho_total = tupla_dados_ipv4[2] identificacao = tupla_dados_ipv4[3] offset_fragmento = tupla_dados_ipv4[4] tempo_vida_ttl = tupla_dados_ipv4[5] protocolos = tupla_dados_ipv4[6] checksum_cabecalho = tupla_dados_ipv4[7] ip_origem = inet_ntoa(tupla_dados_ipv4[8]) ip_destino = inet_ntoa(tupla_dados_ipv4[9]) tamanho_header_bytes = (versao & 15) * 4 return versao, header_len, tipo_servico, + \ tamanho_total, identificacao, offset_fragmento, + \ tempo_vida_ttl, protocolos, checksum_cabecalho, ip_origem, ip_destino, carga[tamanho_header_bytes:] def dados_pacote_udp(carga): tupla_dados_udp = struct.unpack('! H H H H', carga[:8]) porta_fonte = tupla_dados_udp[0] porta_destino = tupla_dados_udp[1] udp_len = tupla_dados_udp[2] udp_checksum = tupla_dados_udp[3] return porta_fonte, porta_destino, udp_len, udp_checksum, carga[8:] def dns_header(payload): # Dados do header tupla_dados_header = struct.unpack('!HHHHHH', payload[:12]) ID = tupla_dados_header[0] flags_and_codes = tupla_dados_header[1] question_count = tupla_dados_header[2] answer_record_count = tupla_dados_header[3] authority_record = tupla_dados_header[4] additional_record_count = tupla_dados_header[5] return ID, flags_and_codes, question_count, answer_record_count, authority_record, additional_record_count, payload[12:] def decode_label(payload, inicio_fatiamento, fim_fatiamento): # Vai buscar label à questão label_string = [] len_label = [] #Recebe dados a desde o inicio do header onde é feito o fatiamento até ao inicio do primeiro label objeto_iterador = iter(payload[inicio_fatiamento:fim_fatiamento]) # BYTE representa o valor do tamanho de cada label for BYTE in objeto_iterador: #se BYTE for zero indica o fim do label e termina iteração if BYTE == 0: break #Adiciona o valor de cada label à lista para calcular o tamanho total do label len_label.append(BYTE) #faz um fatiamento nos caracteres do label mediante o valor representado por BYTE #passando para valor do tipo lista adicionando à lista # 46 é o valor ascci que representa um ( . ) para completar o dominio for elemento in list(islice(objeto_iterador, BYTE)) + [46]: label_string.extend(chr(elemento)) # Apanha a o tamanho do dominio para fazer o fatiamento a partir desse tamanho para a frente # O +1 inclui o tamanho do byte zero que indica o fim do label que deve contar também # No tamanho para fazer o fatiamento nome = ''.join(label_string[:-1]) # +1 indica o último byte da compressão do label que é valor 0, este byte também conta no tamanho do label soma_tamanho_label = sum(len_label) + len(len_label) + 1 tamanho_nome = len(''.join(label_string[:-1])) return nome, soma_tamanho_label, tamanho_nome, label_string, len(len_label) def testa_ponteiro(fatiamento): # Analisa os dois últimos bytes da resposta se o plenultimo byte for 192 # é ponteiro, inicia fatiamneto do resto do nome CNAME PTR, offset = struct.unpack("!BB", fatiamento) return PTR, offset def dns_record_tipo_A(*args): recebe_resposta_A = d_record Type, Class, _ ,TTL, R_d_len, IPv4 = struct.unpack("!HH2HH4s", recebe_resposta_A[2:16]) print("Type: {}, Class: {}, TTL: {}, Resource_data_lenght: {}, IPv4: {}".format(Type, Class, TTL, R_d_len, inet_ntop(AF_INET, IPv4))) def dns_record_tipo_CNAME(*args): """ A variável payload carrega o buffer completo / DNS HEADER / QUESTÃO / RESPOSTA É utilizado para fatiamento com Ponteiro & offset... O Offset começa a contar a partir do campo ID do HEADER iniciando a contagem no primeiro byte com valor 0 (zero). """ recebe_payload = payload """ Recebe buffer com respostas após fatiamento... o fatiamento é feito com valores da soma total de label's QUESTÃO + 4 bytes do type e class também da QUESTÃO """ recebe_resposta_CNAME = d_record PTR, offset, Type, Class, _ ,TTL, R_d_len = struct.unpack("!BBHH2HH", recebe_resposta_CNAME[:12]) CNAME = recebe_resposta_CNAME[12:] if R_d_len > 2: join_nome_nome2 = [] nome, _, _, _, _ = decode_label(CNAME,inicio_fatiamento = 0, fim_fatiamento = None) # Analisa os dois últimos bytes da resposta se o plenultimo byte for 192 # é ponteiro, inicia fatiamneto do resto do nome CNAME PTR_CNAME, offset_CNAME = testa_ponteiro(CNAME[-2:]) if PTR_CNAME == 192: nome2, _ , _ , _ , _ = decode_label(payload,inicio_fatiamento = offset_CNAME, fim_fatiamento = None) join_nome_nome2.append(nome[:-1]) join_nome_nome2.append(nome2) print("Type: {}, Class: {}, TTL: {}, Resource_data_lenght: {}, CNAME: {}".format(Type, Class, TTL, R_d_len, "".join(join_nome_nome2))) else: print("Type: {}, Class: {}, TTL: {}, Resource_data_lenght: {}, CNAME: {}".format(Type, Class, TTL, R_d_len, nome)) elif R_d_len == 2: PTR_CNAME, offset_CNAME = testa_ponteiro(CNAME[-2:]) if PTR_CNAME == 192: nome, _ , _ , _ , _ = decode_label(payload,inicio_fatiamento = offset_CNAME, fim_fatiamento = None) print("Type: {}, Class: {}, TTL: {}, Resource_data_lenght: {}, CNAME: {}".format(Type, Class, TTL, R_d_len, nome)) def dns_record_tipo_AAA(*args): recebe_resposta_AAA = d_record Type, Class, _ ,TTL, R_d_len, IPv6 = struct.unpack("!HH2HH16s", recebe_resposta_AAA[2:28]) print("Type: {}, Class: {}, TTL: {}, Resource_data_lenght: {}, IPv6: {}".format(Type, Class, TTL, R_d_len,inet_ntop(AF_INET6, IPv6))) # As funções contidas neste dicionário são chamadas por referência chamada_funcao = { 1: dns_record_tipo_A, 5: dns_record_tipo_CNAME, 28: dns_record_tipo_AAA} # Esta tupla contém os tipos de DNS records suportados neste script DNS_record_suportado = (1, 5, 28) sock = socket(AF_PACKET, SOCK_RAW, ntohs(0x0003)) while True: raw_dados, addr = sock.recvfrom(65536) mac_destino, mac_fonte, type_ethernet, payload = ethernet_frame(raw_dados) # tipo de frame # Se for 8 vem ai pacote IP \0/ if type_ethernet == 8: """ Para desempacotar os valores da função dados_pacote_ipv4 com as variáveis aninhadas, necessáriamente elas têm que estar entre parênteses... """ ( versao, header_len, tipo_servico, tamanho_total, identificacao, offset_fragmento, tempo_vida_ttl, protocolos, checksum_cabecalho, ip_origem, ip_destino, payload ) = dados_pacote_ipv4(payload) # número protocolo # Se for 17 vem ai pacote UDP \0/ if protocolos == 17: porta_fonte, porta_destino, udp_len, udp_checksum, payload = dados_pacote_udp(payload) ID, flags_and_codes, question_count, answer_record_count, authority_record, additional_record_count, payload_12bytes_frente = dns_header(payload) if porta_fonte == 53 or porta_destino == 53: if question_count == 1 and answer_record_count == 0: print("-------- HEADER DNS --------") print("Identifier : {}".format(ID)) print("Flags & Codes : {}".format(flags_and_codes)) print("Question Count : {}".format(question_count)) print("Answer Record Count : {}".format(answer_record_count)) print("Authority Record : {}".format(authority_record)) print("Additional Record Count : {}\n".format(additional_record_count)) print("-------- QUESTÃO (dominio) --------") nome, soma_tamanho_label, tamanho_nome, label_string, len_label = decode_label(payload_12bytes_frente, inicio_fatiamento = 0, fim_fatiamento = None) tipo_class_questao = payload_12bytes_frente[soma_tamanho_label:] Type, Class = struct.unpack("!HH", tipo_class_questao[:4]) print("Nome Questão : {}".format(nome)) print("Tamanho do nome : {}".format(tamanho_nome)) print("Quantidade de Label's : {}".format(len_label)) print("Tipo (host address) : {}".format(Type)) print("Class : {}\n\n".format(Class)) print("################################################################################") elif question_count == 1 and answer_record_count >= 1: print("-------- HEADER DNS --------") print("Identifier : {}".format(ID)) print("Flags & Codes : {}".format(flags_and_codes)) print("Question Count : {}".format(question_count)) print("Answer Record Count : {}".format(answer_record_count)) print("Authority Record : {}".format(authority_record)) print("Additional Record Count : {}\n".format(additional_record_count)) print("-------- RESPOSTA (DNS RECORDS) --------") nome, soma_tamanho_label, tamanho_nome, label_string, len_label = decode_label(payload_12bytes_frente, inicio_fatiamento = 0, fim_fatiamento = None) print("Nome Questão : {}".format(nome)) print("Tamanho do nome : {}".format(tamanho_nome)) print("Quantidade de Label's : {}\n".format(len_label)) """ LISTA_DNS_RECORDS recebe multiplas respostas ( DNS Records ) encontradas no ciclo de repetição while que segue o buffer da resposta pode conter multiplas respostas, essas respostas ( DNS Records ) podem ser de varios tipos diferentes """ LISTA_DNS_RECORDS = [] # +4 inidica / soma o número de bytes do tipo e class da questão resposta = payload_12bytes_frente[soma_tamanho_label + 4:] while len(resposta) != 0: Type, Class, _ ,TTL, R_d_len = struct.unpack("!HH2HH", resposta[2:12]) tamanho = 12 + R_d_len LISTA_DNS_RECORDS.append(resposta[:tamanho]) resposta = resposta[tamanho:] for d_record in LISTA_DNS_RECORDS: ponteiro, offset, Type = struct.unpack("!BBH",d_record[:4]) if ponteiro == 192 and Type in DNS_record_suportado: #----- Argumentos que passam na chamada de função ----------- # payload_12bytes_frente : contém Questão e Respostas ( DNS Records ) # payload : Contém DNS Headers / Questão / Respostas ( DNS Records ) # d_record : Contém apenas Respostas ( DNS Records ) chamada_funcao[Type](d_record, payload_12bytes_frente, payload) print("##############################################################################") else: pass Resultados do script anterior O conteúdo aqui deixado é imprescindível, que seja complementado por informação contida no livro recomendado no início do post Abraço.
fredericopissarra Posted May 21, 2019 Posted May 21, 2019 Um aviso: nameservers retornam a resposta em pacotes TCP se essa for maior que um determinado tamanho (ajustado no bind). A requisição da consulta pode e deve ser feita por UDP, mas a resposta pode ser UDP ou TCP, dependendo do tamanho... Exemplo: O google retorna resposta em UDP para consultas de até 512 bytes: $ dig @8.8.8.8 www.google.com ; <<>> DiG 9.11.3-1ubuntu1.7-Ubuntu <<>> @8.8.8.8 www.google.com ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 10492 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 512 ;; QUESTION SECTION: ;www.google.com. IN A ;; ANSWER SECTION: www.google.com. 129 IN A 172.217.29.36 ;; Query time: 42 msec ;; SERVER: 8.8.8.8#53(8.8.8.8) ;; WHEN: Mon May 20 22:36:39 -03 2019 ;; MSG SIZE rcvd: 59 Note o comentário EDNS, acima... Já o cloudflare retorna em UDP pacotes com até 1452 bytes: $ dig @1.0.0.1 www.google.com ; <<>> DiG 9.11.3-1ubuntu1.7-Ubuntu <<>> @1.0.0.1 www.google.com ; (1 server found) ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 36721 ;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 1452 ;; QUESTION SECTION: ;www.google.com. IN A ;; ANSWER SECTION: www.google.com. 41 IN A 216.58.202.164 ;; Query time: 38 msec ;; SERVER: 1.0.0.1#53(1.0.0.1) ;; WHEN: Mon May 20 22:38:46 -03 2019 ;; MSG SIZE rcvd: 59 O "default" do bind é 4096 bytes. Quando o nameserver recursivo retorna respostas tão grandes? Quando existem muitos nomes associados à resposta! Um exemplo é o google.com, se pedirmos qualquer registro: $ dig +noall +answer www.google.com any www.google.com. 288 IN AAAA 2800:3f0:4001:814::2004 www.google.com. 161 IN A 172.217.29.228 Aqui só tem 2, mas poderiam ter vários registros A, vários AAAA, vários CNAMEs, ... consultas que também retornam a autoridade, e adicionais.
Guest gnoo Posted May 21, 2019 Posted May 21, 2019 Sim, eu já tinha visto que o DNS poderia vir em TCP, mas ainda não tinha tido oportunidade de analisar isso, fica a nota obrigado.
Recommended Posts
Archived
This topic is now archived and is closed to further replies.