在DNS查询中,axfr类型是Authoritative Transfer的缩写,指请求传送某个区域的全部记录。
前面三篇日志中,笔者分别使用nmap、dig、nslookup来查询域传送记录。
本篇介绍自己动手,用python写一个简单的DNS客户端,仅实现axfr查询,并且只处理A记录。
DNS消息的格式
DNS请求和响应,都是由5个区块组成的,如下图所示:
+---------------------+
| Header |
+---------------------+
| Question | the question for the name server
+---------------------+
| Answer | RRs answering the question
+---------------------+
| Authority | RRs pointing toward an authority
+---------------------+
| Additional | RRs holding additional information
+---------------------+
1
2
3
4
5
6
7
8
9
10
11
+---------------------+
|Header|
+---------------------+
|Question|thequestionforthenameserver
+---------------------+
|Answer|RRsansweringthequestion
+---------------------+
|Authority|RRspointingtowardanauthority
+---------------------+
|Additional|RRsholdingadditionalinformation
+---------------------+
axfr请求的包,只填充header和Question区块就可以了。
Header区块的格式
Header的格式是这样的:
1 1 1 1 1 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ID |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR| Opcode |AA|TC|RD|RA| Z | RCODE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QDCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ANCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NSCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ARCOUNT |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
111111
0123456789012345
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|ID|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR|Opcode|AA|TC|RD|RA|Z|RCODE|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QDCOUNT|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|ANCOUNT|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|NSCOUNT|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|ARCOUNT|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
可以看到,每一行是16位,两个字节。整个Header的大小是12字节:
ID是随机值,2个字节,介于1到65535之间。客户端发送一个随机值,DNS服务器会原样返回该ID。
第二行是几个标记的集合:QR标记该消息是Query还是Response,查询的时候填充0,响应的时候填充1。而RCODE是Response Code(响应码),只有该值为0的时候才表示查询成功了。得到服务器响应之后应该检查这个值。
QDCOUNT填充Question的个数、ANCOUNT填充Answer的个数,NSCOUNT填充DNS权威服务器的个数,ARCOUNT填充附加记录的个数。对于本文要实现的axfr查询,分别填充: 1, 0, 0, 0。
下图是笔者用WireShak抓包得到一个axfr请求的Header:
图中Additional RRs的值为1,在尾部附加了一些额外的参数。编码的简化,可以不提供。
Question区块的格式
Question区块的基本格式是:
1 1 1 1 1 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| |
/ QNAME /
/ /
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QTYPE |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QCLASS |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
1
2
3
4
5
6
7
8
9
10
11
111111
0123456789012345
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
||
/QNAME/
//
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QTYPE|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QCLASS|
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
QNAME是需要查询的域名,比如nwpu.edu.cn。它由一个Lable序列来表示。
格式是这样的:
用点号“.”把域名分割为几个Label,每个label前面带上字符串长度,nwpu.edu.cn.cn最终变为:
4nwpu3edu2cn
结尾处跟一个字节的空白。笔者抓包截图如下:
04表示有4个字符,而6e 77 70 75正好是nwpu。
QTYPE是一个类型常量,axfr记录的值是252。
QCLASS设置为1,表示Inernet。
Python组合一个Query消息
介绍完上面的内容,我们已经可以组一个查询消息了。函数如下:
def gen_query(domain):
import random
TRANS_ID = random.randint(1, 65535) # random ID
FLAGS = 0; QDCOUNT = 1; ANCOUNT = 0; NSCOUNT = 0; ARCOUNT = 0
data = struct.pack(
'!HHHHHH',
TRANS_ID, FLAGS,QDCOUNT, ANCOUNT, NSCOUNT, ARCOUNT
)
query = ''
for label in domain.strip().split('.'):
query += struct.pack('!B', len(label)) + label.lower()
query += '\x00' # end of domain name
data += query
global LEN_QUERY
LEN_QUERY = len(query) # length of query section
q_type = 252 # Type AXFR = 252
q_class = 1 # CLASS IN
data += struct.pack('!HH', q_type, q_class)
data = struct.pack('!H', len(data) ) + data # first 2 bytes should be length
return data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
defgen_query(domain):
importrandom
TRANS_ID=random.randint(1,65535)# random ID
FLAGS=0;QDCOUNT=1;ANCOUNT=0;NSCOUNT=0;ARCOUNT=0
data=struct.pack(
'!HHHHHH',
TRANS_ID,FLAGS,QDCOUNT,ANCOUNT,NSCOUNT,ARCOUNT
)
query=''
forlabelindomain.strip().split('.'):
query+=struct.pack('!B',len(label))+label.lower()
query+='\x00'# end of domain name
data+=query
globalLEN_QUERY
LEN_QUERY=len(query)# length of query section
q_type=252# Type AXFR = 252
q_class=1# CLASS IN
data+=struct.pack('!HH',q_type,q_class)
data=struct.pack('!H',len(data))+data# first 2 bytes should be length
returndata
struct.pack用来格式化字符串,第一个参数中的惊叹号“!”表示字节序使用network (= big-endian)。
B代表Byte,一个字节。H代表unsigned short,两个字节。
消息开头的前两个字节,需要提供消息正文的长度。
因为在decode response时,还要用到Query的长度,所以我将它保存到了一个全局的LEN_QUERY变量中。