Jump to content
Sign in to follow this  
gnoo

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

Recommended Posts

Posted (edited)

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.

 

Edited by gnoo

Share this post


Link to post
Share on other 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.

Share this post


Link to post
Share on other sites

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.

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Sign in to follow this  

  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...