.

[Python] DNS Packet 구조 코드화

by 담배맛구마

DNS 패킷 파싱하다보니까 정리해볼까하고... 건드렸다가 고통받았기에 기록해둠



1. DNS Packet 공통 구조


DNS Request든 Response든 공통적으로 다음과 같은 구조가지고 Response의 경우 정보가 뒤에 추가로 다닥다닥 붙는다.

* DNS Request와 Response는 Flags의 첫 번째 1bit에서 구분가능하니까 이상할건 없다.


Header

• Transaction ID : 그냥 시퀀스값인데 특정 DNS Request가 A라는 값이면 그에 대한 Response에서도 A라는 값가진다.

• Flags : 이런저런 플래그값. 생략

• Number of Questions : 이게 웃긴게 프로토콜 정의상 한 번에 2개 이상의 내용을 요청할수있다는데... 실제론 지원안함

• Number of Answer Section : 걍 이름값함

• Number of Authority Section : 걍 이름값함

• Number of Answer Section : 걍 이름값함



QUERY

• Query Name : 가변길이다. 특이한 포맷을 가진다. 도메인에서 '.'에 해당하는 부분이 다음의 문자열 갯수를 말한다.

  예) www.naver.com은 표현되기로 0x03 0x77 0x77 0x77 0x05 0x6E 0x61 0x76 0x65 0x72 0x03 0x63 0x6F 0x6D 0x00


그래서 0(\x00 aka null)으로 끝나는게 당연하다.




2. DNS Response

앞에서 Section이라고 했는데, PE구조마냥 Section이 다닥다닥 붙어있다. 딱히 구분자는 없다. 구분하는 방법은 Flags에서 Number of... 로 표현한 값들로 아 Answer가 3개니까 앞에서 3개까지는 Answer겠구나... 이렇게 해야된다.


DNS Reponse에서 QName, 그러니까 어떤 값에 대한 응답인지 표현할때 다음과 같이 두 가지 방법이 있다.


• DNS Request에서 Query부분 처럼 표현
• 압축(Compression)된 기법으로 표헌

뭘로 하든 상관없다. 하지만, 제대로된 프로그래밍을 한다면 두 가지 기법 모두 해석이 되야 하겠지.


압축(Compression)된 표현

첫 두 비트가 11이면 QName을 포인터로 가르킨다. 이 포인터는 DNS Hedaer부터 시작한다. 즉, Transaction ID 부분이 Pointer의 0이 된다.


예를 들어 보통 패킷을 스니핑하다보면 0xC0 0x0C로 표현이 많이되는데 풀어쓰면 포인터는 0x0C이고 12라는 값을 가진다.


• 0xC0 0x0C ----> 0b 1100 0000 0000 1100 ----> 0b 11 + 0b 1100


DNS Hedaer의 크기가 12Byte니까, Pointer가 12라는 값을 가지면 DNS Query의 Query Name과 동일한 값이다.


즉, DNS Query의 Query Name이 www.naver.com이고 Pointer가 12를 가리킨다면 QName도 www.naver.com이다.



근데 이런 표현이 QName에만 쓰이는게 아니다. RFC에 보면 다음과 같이 압축된 표현을 사용되는 곳을 정의해놨다.


• a pointer

• a sequence of labels ending in a zero octet : 어... 이거 뭔말이냐?

• a sequence of labels ending with a pointer : Pointer로 끝나는 시퀀스!!


즉, RData에서도 사용이 가능하다. RData에서 마지막에 0xC0 0x45이 오면 0xC0 0x45을 0x45에 해당하는 값으로 치환해서 보면 된다. 예를 들어


• 0x45 : 0x6e 0x61 0x76 0x65 0x72 (5 n a v e r)

• Length of RData : 0x06

• RData : 0x03 0x6e 0x73 0x33 0xC0 0x45  (3 n s 1 0xC0 0x45)


요렇게 되어있다면 RData는 결국 0x03 0x6e 0x73 0x33 0x6e 0x61 0x76 0x65 0x72 이되고 ns1.naver가 된다.


더 상세한 예는 https://tools.ietf.org/html/rfc1035#section-4.1.4 확인하거나 와이어샤크 떠보면 보인다.



DNS Request에서 Query부분 처럼 표현

첫 두 비트가 00이다. 그냥 QName을 표현하면된다. 쉬워서 생략!




3. DNS 패킷을 파씽하는 파이썬코드를 작성

아직 Header만 해놨고 데이터는 보류


import socket
import struct
from time import gmtime, strftime
import re

class packetSniff():
    def __init__(self):
        self.hostip = self.chooseInterace()
        self.regDNS = re.compile('[^a-zA-Z0-9-@:%._\+~#=]')

    def chooseInterace(self):
        hostipLst = socket.gethostbyname_ex(socket.gethostname())[2]
        print('[*] System Interface List')
        for idx, ip in enumerate(hostipLst):
            if idx + 1 != len(hostipLst):
                print(' ┣ [%d] %s' % (idx, ip))
            else:
                print(' ┗ [%d] %s' % (idx, ip))
        try:
            ip = hostipLst[int(input('[+] Choose One Interface : '))]
            return ip
        except:
            print('[-] Wrong Input.')
            return False

    def analDNSPacket(self, packet):
        now = strftime("%Y-%M-%d %H:%M:%S", gmtime())
        recvHeader = struct.unpack_from('!HHHHHH', packet[:12])
        recvData = packet[12:].split(b'\x00', 1)

        flag_QnA = recvHeader[1] & 0b1000000000000000
        queryType, queryClass = struct.unpack('!HH', recvData[1][:4])
        if flag_QnA == 0 :
            print('[DNS Query]------------------------------------')
        else :
            print('[DNS Response]---------------------------------')
        print('[*] Time : ' + now)
        print('[+] Transaction ID : ' + str(hex(recvHeader[0])))
        print('[+] Flags : ' + str(hex(recvHeader[1])))
        print('[+] Number of queries : ' + str(hex(recvHeader[2])))
        print('[+] Number of authoritative : ' + str(hex(recvHeader[3])))
        print('[+] Number of additional record : ' + str(hex(recvHeader[4])))
        print('[+] Query Name : ' + self.regDNS.sub('.', recvData[0].decode('ascii')))
        print('[+] Query Type : ' + str(hex(queryType)))
        print('[+] Query Class : ' + str(hex(queryClass)))
        print('-----------------------------------------------')

    def start(self):
        rawsock = socket.socket(family=socket.AF_INET, type=socket.SOCK_RAW, proto=17)
        rawsock.bind((self.hostip, 0))
        rawsock.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)
        print('[*] Start Sniffer Process!')

        while(True):
            packet, address = rawsock.recvfrom(65565)
            srcport = struct.unpack_from('!H', packet[20:22])[0]
            dstport = struct.unpack_from('!H', packet[22:24])[0]
            if(srcport == 53 or dstport == 53):
                now = strftime("%H:%M:%S", gmtime())
                self.analDNSPacket(packet[28:])

if __name__ == '__main__':
    obj = packetSniff()
    obj.start()



4. Fake DNS 서버를 만들어보자.

일단 Localhost의 53포트로 바인딩해서 어떤 값이든 192.168.1.1로 응답해보리기~


import socket
import struct
from time import gmtime, strftime
import re

class fakeDNS:
    def __init__(self, ip):
        self.fakeIp = ip

    def start(self):
        rawsock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        rawsock.bind(('', 53))
        print('[*] Start Fake DNS Server!!')
        regDNS = re.compile('[^a-zA-Z0-9-@:%._\+~#=]')
        while True:
            packet, address = rawsock.recvfrom(65565)
            recvHeader = struct.unpack_from('!HHHHHH', packet[:12])
            recvData = packet[12:].split(b'\x00', 1)

            # Make DNS Response Header
            TransactionID = recvHeader[0]
            Flag = 0x8180 # Standard DNS Response -> 1000 0001 1000 000
            Questions = 1 # Number of Question Seciont
            AnswerRRs = 1 # Number of Answer Section
            AuthorityRRs = 0 # Number of Authority Section
            AdditionalRRs = 0 # Number of Additional Section

            # Make DNS Query
            QueryName = recvData[0] + b'\x00'
            Type = recvData[1][0:2]
            Class = recvData[1][2:4]

            # Make DNS Answer
            Pointer = 0xC00C
            # Type, Class are already define
            TimeToLive = 0x0005
            Length = 0x0004
            RData = socket.inet_aton(self.fakeIp)

            response = struct.pack('!HHHHHH', TransactionID, Flag, Questions, AnswerRRs, AuthorityRRs, AdditionalRRs)
            response = response + QueryName + Type + Class
            response = response + struct.pack('!H', Pointer) + Type + Class
            response = response + struct.pack('!IH', TimeToLive, Length) + RData
            print(response)
            rawsock.sendto(response, address)

if __name__ == '__main__':
    obj = fakeDNS('192.168.1.1')
    obj.start()



반응형

블로그의 정보

정윤상이다.

담배맛구마

활동하기