[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, 그러니까 어떤 값에 대한 응답인지 표현할때 다음과 같이 두 가지 방법이 있다.
압축(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()
'Dev-' 카테고리의 다른 글
[React-Native] 시작하기 (0) | 2020.03.31 |
---|---|
[Python] Bandizip Updater (3) | 2019.06.11 |
[C#] Adobe Flash Player 자동 업데이트 문제와 해결 (0) | 2018.07.29 |
[Python] Ubuntu+ Django + Gunicorn + Nginx + Postgres (2) | 2017.07.20 |
[VBScript] WScript Run과 Exec (2) | 2017.04.10 |
블로그의 정보
정윤상이다.
담배맛구마