Ir para conteúdo

Análise de pacotes na prática [ Parte 5 ] - "DNS" - Raw Sockets python


Visitante gnoo

Posts Recomendados

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:

transferir.jpeg.c2c72a0767156baf33dca99d16518e96.jpeg

 

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

1902531048_Capturadeecr_2019-05-20_22-26-44.png.9cbee4d4c526571dbc251d110c915fd2.png

 

2019-05-20_22-34.thumb.png.6b44130e11391a2546a1829818101297.png

 

Descrição do campos de Header DNS

 

2019-05-20_22-36.thumb.png.e93f9ec40c7fe23008c8ea933819b47e.png

 

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:]

 

2019-05-20_22-43.png.a55aa510e577108011f8f551a0c56a6b.png

2019-05-20_22-44.png.7bef5efe38bca3f48c5f8893de2aa44e.png

2019-05-20_22-45.png.c95b776804c9b130ce01df9867cc5483.png

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:

2019-05-20_22-49.png.434a816adc49b9dfa9efedb8752dc75c.png

2019-05-20_22-53.thumb.png.c73b4a7c8baf3b9604cc17caabd5bca7.png

2019-05-20_22-59.png.e339d73b563e524de8f102693cb6aa4f.png

 

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

2019-05-20_23-03.png.ae4f51d4aab632546c23a8c6c1b40c27.png

 

2019-05-20_23-13.png.9462766ba9684aab712b42bd9f576c7b.png

 

 

2019-05-20_23-11.png.2acdad8ea0bcceee9314ce89b9546e42.png

 

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

2019-05-20_23-31.png.62b2dfc33f809bba4a07984718387587.png

 

2019-05-20_23-33.png.22aeb8385713b9a79f0ce68bceb96419.png

 

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 para o comentário
Compartilhar em outros sites

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 para o comentário
Compartilhar em outros sites

Arquivado

Este tópico foi arquivado e está fechado para novas respostas.

  • Quem Está Navegando   0 membros estão online

    • Nenhum usuário registrado visualizando esta página.
×
×
  • Criar Novo...