Guest gnoo Posted May 20, 2019 at 10:43 PM Share Posted May 20, 2019 at 10:43 PM 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. Link to comment Share on other sites More sharing options...
fredericopissarra Posted May 21, 2019 at 01:43 AM Share Posted May 21, 2019 at 01:43 AM 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. Link to comment Share on other sites More sharing options...
Guest gnoo Posted May 21, 2019 at 08:19 AM Share Posted May 21, 2019 at 08:19 AM 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. Link to comment Share on other sites More sharing options...
Recommended Posts
Archived
This topic is now archived and is closed to further replies.