» » » Составляем DNS-запрос вручную

 

Составляем DNS-запрос вручную

Автор: admin от 5-01-2018, 15:35, посмотрело: 44

Об авторе. Джеймс Рутли — бэкенд-разработчик в компании Monzo.



В этой статье мы изучим двочиный формат сообщений Domain Name Service (DNS) и напишем вручную одно сообщение. Это больше, чем вам нужно для использования DNS, но я подумал, что для развлечения и в образовательных целях интересно посмотреть, что находится под капотом.



Мы узнаем, как:




  • Написать запросы DNS в двоичном формате

  • Отправить сообщение в теле датаграммы UDP с помощью Python

  • Прочитать ответ от DNS-сервера



Писать в двоичном формате кажется сложным, но в реальности я обнаружил, что это вполне доступно. Документация DNS хорошо написана и понятна, а писать мы будем маленькое сообщение — всего 29 байт.

обычно отправляются по протоколу UDP.



Стандарт DNS описан в RFC 1035. Все диаграммы и бoльшая часть информации для этой статьи взята в данном RFC. Я бы рекомендовал обратиться к нему, если что-то непонятно.



В этой статье мы используем шестнадцатеричный формат для упрощения работы с бинарником. Ниже я добавил краткое пояснение, как они переводятся друг в друга [1]RFC1035, раздел 4.1.1.



Для нас имеют значение следующие поля:




  • ID: Произвольный 16-битный идентификатор запроса. Такой же ID используется в ответе на запрос, так что мы можем установить соответствие между ними. Возьмём AA AA.

  • QR: Однобитный флаг для указания, является сообщение запросом (0) или ответом (1). Поскольку мы отправляем запрос, то установим 0.

  • Opcode: Четырёхбитное поле, которое определяет тип запроса. Мы отправляем стандартный запрос, так что указываем 0. Другие варианты:


    • 0: Стандартный запрос

    • 1: Инверсный запрос

    • 2: Запрос статуса сервера

    • 3-15: Зарезервированы для будущего использования


  • TC: Однобитный флаг, указывающий на обрезанное сообщение. У нас короткое сообщение, его не нужно обрезать, так что указываем 0.

  • RD: Однобитный флаг, указывающий на желательную рекурсию. Если DNS-сервер, которому мы отправляем вопрос, не знает ответа на него, он может рекурсивно опросить другие DNS-серверы. Мы хотим активировать рекурсию, так что укажем 1.

  • QDCOUNT: 16-битное беззнаковое целое, определяющее число записей в секции вопроса. Мы отправляем 1 вопрос.



Полный заголовок



Совместив все поля, можно записать наш заголовок в шестнадцатеричном формате:





AA AA - ID
01 00 – Параметры запроса
00 01 – Количество вопросов
00 00 – Количество ответов
00 00 – Количество записей об уполномоченных серверах
00 00 – Количество дополнительных записей



Для получения параметров запроса мы объединяем значения полей от QR до RCODE, помня о том, что не упомянутые выше поля установлены в 0. Это даёт последовательность 0000 0001 0000 0000, которая в шестнадцатеричном формате соответствует 01 00. Так выглядит стандартный DNS-запрос.



Вопрос



Секция вопроса имеет следующий формат:





0 1 2 3 4 5 6 7 8 9 A B C D E F
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| |
/ QNAME /
/ /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QTYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QCLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+




  • QNAME: Эта секция содержит URL, для которого мы хотим найти IP-адрес. Она закодирована как серия надписей (labels). Каждая надпись соответствует секции URL. Так, в адресе example.com две секции: example и com.



    Для составления надписи нужно закодировать каждую секцию URL, получив ряд байтов. Надпись — это ряд байтов, перед которыми стоит байт беззнакового целого, обозначающий количество байт в секции. Для кодирования нашего URL можно просто указать ASCII-код каждого символа.



    Секция QNAME завершается нулевым байтом (00).

  • QTYPE: Тип записи DNS, которую мы ищем. Мы будем искать записи A, чьё значение 1.

  • QCLASS: Класс, который мы ищем. Мы используем интернет, IN, у которого значение класса 1.



Теперь можно записать всю секцию вопроса:





07 65 – у 'example' длина 7, e
78 61 – x, a
6D 70 – m, p
6C 65 – l, e
03 63 – у 'com' длина 3, c
6F 6D – o, m
00 - нулевой байт для окончания поля QNAME
00 01 – QTYPE
00 01 – QCLASS



В секции QNAME разрешено нечётное число байтов, так что набивка байтами не требуется перед началом секции QTYPE.



Отправка запроса



Мы отправляем наше DNS-сообщение в теле UDP-запроса. Следующий код Python возьмёт наш шестнадцатеричный DNS-запрос, преобразует его в двоичный формат и отправит на сервер Google DNS по адресу 8.8.8.8:53.



import binascii
import socket


def send_udp_message(message, address, port):
    """send_udp_message sends a message to UDP server

    message should be a hexadecimal encoded string
    """
    message = message.replace(" ", "").replace("n", "")
    server_address = (address, port)

    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        sock.sendto(binascii.unhexlify(message), server_address)
        data, _ = sock.recvfrom(4096)
    finally:
        sock.close()
    return binascii.hexlify(data).decode("utf-8")


def format_hex(hex):
    """format_hex returns a pretty version of a hex string"""
    octets = [hex[i:i+2] for i in range(0, len(hex), 2)]
    pairs = [" ".join(octets[i:i+2]) for i in range(0, len(octets), 2)]
    return "n".join(pairs)


message = "AA AA 01 00 00 01 00 00 00 00 00 00 " 
"07 65 78 61 6d 70 6c 65 03 63 6f 6d 00 00 01 00 01"

response = send_udp_message(message, "8.8.8.8", 53)
print(format_hex(response)) 


Можете запустить этот скрипт, скопировав код в файл query.py и запустив в консоли команду $ python query.py. У него нет никаких внешних зависимостей, и он должен работать на Python 2 или 3.



Чтение ответа



После выполнения скрипт выводит ответ от DNS-сервера. Разобьём его на части и посмотрим, что можно выяснить.



Заголовок



Сообщение начинается с заголовка, как и наше сообщение с запросом:





AA AA – Тот же ID, как и раньше
81 80 – Другие флаги, разберём их ниже
00 01 – 1 вопрос
00 01 – 1 ответ
00 00 – Нет записей об уполномоченных серверах
00 00 – Нет дополнительных записей



Преобразуем 81 80 в двоичный формат:





8 1 8 0
1000 0001 1000 0000



Преобразуя эти биты по вышеуказанной схеме, можно увидеть:




  • QR = 1: Это сообщение является ответом

  • AA = 0: Этот сервер не является уполномоченным для доменного имени example.com

  • RD = 1: Для этого запроса желательна рекурсия

  • RA = 1: На этом DNS-сервере поддерживается рекурсия

  • RCODE = 0: Ошибки не обнаружены



Секция вопроса



Секция вопроса идентична такой же секции в запросе:





07 65 – у 'example' длина 7, e
78 61 – x, a
6D 70 – m, p
6C 65 – l, e
03 63 – у 'com' длина 3, c
6F 6D – o, m
00 - нулевой байт для окончания поля QNAME
00 01 – QTYPE
00 01 – QCLASS



Секция ответа



У секции ответа формат ресурсной записи:





0 1 2 3 4 5 6 7 8 9 A B C D E F
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| |
/ /
/ NAME /
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| CLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| TTL |
| |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| RDLENGTH |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
/ RDATA /
/ /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+





C0 0C - NAME
00 01 - TYPE
00 01 - CLASS
00 00
18 4C - TTL
00 04 - RDLENGTH = 4 байта
5D B8
D8 22 - RDDATA




  • NAME: Этой URL, чей IP-адрес содержится в данном ответе. Он указан в сжатом формате:





    0 1 2 3 4 5 6 7 8 9 A B C D E F
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    | 1 1| OFFSET |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+



    Первые два бита установлены в значение 1, а следующие 14 содержат беззнаковое целое, которое соответствует смещению байт от начала сообщения до первого упоминания этого имени.



    В данном случае смещение составляет c0 0c или двоичном формате:





    1100 0000 0000 1100



    То есть смещение байт составляет 12. Если мы отсчитаем байты в сообщении, то можем найти, что оно указывает на значение 07 в начале имени example.com.

  • TYPE и CLASS: Здесь используется та же схема имён, что и в секциях QTYPE и QCLASS выше, и такие же значения.

  • TTL: 32-битное беззнаковое целое, которое определяет время жизни этого пакета с ответом, в секундах. До истечения этого интервала результат можно закешировать. После истечения его следует забраковать.

  • RDLENGTH: Длина в байтах последующей секции RDDATA. В данном случае её длина 4.

  • RDdata: Те данные, которые мы искали! Эти четыре байта содержат четыре сегмента нашего IP-адреса: 93.184.216.34.



Расширения



Мы увидели, как составить DNS-запрос. Теперь можно попробовать следующее:




  • Составить запрос для произвольного доменного имени

  • Запрос на другой тип записи

  • Отправить запрос с отключенной рекурсией

  • Отправить запрос с доменным именем, которое не зарегистрировано





Шестнадцатеричные числа (base 16) часто используются как удобная краткая запись для 4 битов двоичных данных. Вы можете конвертировать данные между этими форматами по следующей таблице:
















































































ДесятичныйHexДвоичныйДесятичныйHexДвоичный
000000881000
110001991001
22001010A1010
33001111B1011
44010012C1100
55010113D1101
66011014E1110
77011115F1111


Как видите, можно представить любой байт (8 бит) двумя шестнадцатеричными символами. ?

Источник: Хабрахабр

Теги: DNS Python UDP

Категория: Компании » Microsoft

Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *