XDP实现DNS Query Name Blacklist Filter(一)

怀宇
2023-12-01

原理

Query Name Blacklist是抵抗对DNS服务器的DDoS攻击的一种filter,攻击者在发起攻击时通常使用形如www.FIXED-QUERY.com或RANDOM-STRING.FIXED-SUFFIX.com的域名,即这些攻击域名中存在一定共同部分,通过发现这些共同部分,可以将攻击请求在到达DNS服务器真正被处理之前过滤掉,从而达到抵抗DDoS的目的。Query Name Blacklist的思路来自DDIDD项目DDoS Defense In Depth for DNS (DDIDD)

思路

分为三个环节

  1. 捕捉一定时间的DNS报文,提取其中的QNAME字段,并保存
  2. 分析这段时间内的QNAME,提取出要进行过滤的模式
  3. 使用提取出的模式部署过滤器

存在以下问题

  1. 使用什么技术捕捉报文,可选项libpcap,AF_XDP
  2. 使用xdp如何将QNAME从内核中传递出来,是否保存到本地(出于应记录log,内存不够大,需要保存)
  3. 如何提取模式
  4. 使用什么技术实现过滤器,可选项iptables,xdp
  5. 使用xdp如何即时修改过滤的目标,自动化编辑源文件并重新编译/使用map,考虑到域名长度有上限,可能可行
  6. 如何设计实现专为QNAME匹配使用的算法
  7. 搭建测试环境及生成模仿DDoS攻击流量

实现

DDoS模拟

首先尝试实现DDoS的模拟,

方案一:使用trafgen,配置文件如下

# full random
{
    0x02, 0x42, 0xac, 0x11, 0x00, 0x02 # MAC Destination
    drnd(6), # MAC Source
    
    const16(0x0800),
    /* IPv4 Version, IHL, TOS */
    0b01000101, 0,
    /* IPv4 Total Len */
    const16(56),
    /* IPv4 Ident */
    drnd(2),
    
    /* IPv4 Flags, Frag Off */
    0b01000000, 0,
    /* IPv4 TTL */
    64,
    /* Proto UDP */
    0x11,
    /* IPv4 Checksum (IP header from, to) */
    csumip(14, 33),
    
    /* NEED ADJUST */
    172, 17, 0, 3,  # Source IP
    172, 17, 0, 2, # Dest IP

    # UDP
    drnd(2), const16(33340),
    const16(36), csumudp(14,34),
    # DNS ID
    drnd(2),
    # Flags
    0x01, 0x00,
    # Questions
    0x00, 0x01,
    # Answer RRs
    0x00, 0x00,
    #Authoritative RRs
    0x00, 0x00,
    #Aditional RRs
    0x00, 0x00,
    # Questions Start
    0x03, "www",
    0x07, drnd(7),
    0x00, 0x01,
    0x00, 0x01
}
# name fixed
{
    0x02, 0x42, 0xac, 0x11, 0x00, 0x02 # MAC Destination
    0x02, 0x42, 0xac, 0x11, 0x00, 0x03, # MAC Source
    
    const16(0x0800),
    /* IPv4 Version, IHL, TOS */
    0b01000101, 0,
    /* IPv4 Total Len */
    const16(56),
    /* IPv4 Ident */
    drnd(2),
    
    /* IPv4 Flags, Frag Off */
    0b01000000, 0,
    /* IPv4 TTL */
    64,
    /* Proto UDP */
    0x11,
    /* IPv4 Checksum (IP header from, to) */
    csumip(14, 33),
    
    /* NEED ADJUST */
    drnd(4),  # Source IP
    172, 17, 0, 2, # Dest IP

    # UDP
    drnd(2), const16(33340),
    const16(40), csumudp(14,34),
    # DNS ID
    drnd(2),
    # Flags
    0x01, 0x00,
    # Questions
    0x00, 0x01,
    # Answer RRs
    0x00, 0x00,
    #Authoritative RRs
    0x00, 0x00,
    #Aditional RRs
    0x00, 0x00,
    # Questions Start
    0x03, "www",
    0x07, "fxddmnm",
    0x03, "com",
    0x00, 0x01,
    0x00, 0x01
}

缺点是随机功能不好用,无法限制范围,导致源mac随机的结果可能非法,不能发送,DNS Question内容也很大可能非法。优点是高性能,明确使用多核。

方案二:python scapy

RandMAC,RandIP,RandString函数解决了trafgen随机的缺陷之处,一个简单的将重复次数和随机部分长度作为参数的py脚本见下

from scapy.all import Ether, IP, UDP, sendp, hexdump, RandMAC, RandIP
from scapy.layers.dns import DNS, DNSQR
from scapy.volatile import RandString, RandShort
import getopt
import sys
import time

t1 = time.perf_counter()

n = 3
l = 7
try:
    opts, args = getopt.getopt(sys.argv[1:],"n:l:")
except getopt.GetoptError:
    print("Illegal Opts")
    sys.exit(2)
for opt, arg in opts:
    if opt == '-n':
        n = int(arg)
    elif opt == '-l':
        l = int(arg)

pack_list = []
for i in range(n):
    randdns = \
    Ether(dst='02:42:ac:11:00:03', src=RandMAC())/\
        IP(dst='172.17.0.3', src=RandIP())/\
            UDP(dport=33340)/\
                DNS(id=1, qr=0, opcode=0, tc=0, rd=1, qdcount=1,ancount=0,nscount=0,arcount=0,
                qd=DNSQR(qname='www.'+str(RandString(l))+'.com',qtype=1,qclass=1))
    pack_list.append(randdns)

t2 = time.perf_counter()

print("Pack Gen Interval:", (t2-t1)*1000000)

sendp(pack_list)

t3 = time.perf_counter()

print("Pack Send Interval:", (t3-t2)*1000000)
print("Total Interval:",  (t3-t1)*1000000)

需要注意的是RandXXX函数返回的是scapy的volatile对象,每次取值(包括对构建时使用RandXXX的包调用send/sendp)值都会变化。

性能测试

fullrand发送1000个包(70Byte/packet)

trafgen:32us == 17500Gb/s

scapy:451624us == 1.24Gb/s

发送10000个包

trafgen:2379us == 2353.9Gb/s

scapy:3325426us == 1.69Gb/s

发送100000个包

trafgen:32108us == 1744.1Gb/s

scapy:33156602us == 1.69Gb/s

scapy提升性能可能需要引入多线程

捕包存储QNAME

使用libpcap实现

/* qname_cap.c */

#include <pcap.h>
#include <time.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include <arpa/inet.h>
#include <netinet/if_ether.h>
#include <netinet/ip.h>
#include <netinet/udp.h>
#include "dns.h"

// save函数可修改为存入文件等
void save(const char * str, int len){
  printf("%s|%d\n", str, len);
}

void save_qr_raw(const u_char *packet, int packlen){
  void * ptr = (void *)packet;
  
  // ETH Header
  uint16_t e_type;

  struct ethhdr *eth;
  eth = (struct ethhdr *)ptr;
  e_type = ntohs(eth->h_proto);

  ptr = (void *)((struct ethhdr *)ptr + 1);

  if (e_type != ETH_P_IP) {
    return;
  }   

  // IP Header
  struct iphdr *ip;
  ip = (struct iphdr *)ptr;
  e_type = ntohs(ip->protocol);

  ptr =  ptr + ip->ihl*4;

  // UDP Header
  struct udphdr *udp;
  udp = (struct udphdr *)ptr;

  ptr = (void *)((struct udphdr *)ptr + 1);

  // DNS
  struct dnshdr *dns;
  dns = (struct dnshdr *)ptr;
  dnsqr_list ql = get_qr_list(dns);
  dnsqr_list head = ql;
  if(!ql){
    return;
  }

  while(ql){
    save(ql->qn.start, ql->qn.len);
    ql = ql->next;
  }

  free_qr_list(head);
}
 
void getPacket(u_char * arg, const struct pcap_pkthdr * pkthdr, const u_char * packet)
{
  int * id = (int *)arg;
 
  printf("id: %d\n", ++(*id));
  printf("Packet length: %d\n", pkthdr->len);
  printf("Number of bytes: %d\n", pkthdr->caplen);
  printf("Recieved time: %s", ctime((const time_t *)&pkthdr->ts.tv_sec));
 
  int i;
  for(i=0; i<pkthdr->len; ++i)
  {
    printf(" %02x", packet[i]);
    if( (i + 1) % 16 == 0 )
    {
      printf("\n");
    }
  }
 
  printf("\n\n");

  save_qr_raw(packet, pkthdr->caplen);

  printf("\n\n");
}
 
int main()
{
  char errBuf[PCAP_ERRBUF_SIZE];
  pcap_if_t *it;
  int r;

  /* get a device */
  r = pcap_findalldevs(&it, errBuf);

  if(r == -1){
    return -1;
  }

  while(it){
    if(!strcmp(it->name, "eth0")){
      break;
    }
    it = it->next;
  }

  if(!it)
    return 0;
 
  /* open a device, wait until a packet arrives */
  //pcap_t * device = pcap_open_offline("test.pcap",errBuf);     //读取本地文件作为网络包数据
  pcap_t * device = pcap_open_live(it->name, 65535, 1, 5000, errBuf);
 
  if(!device)
  {
    printf("error: pcap_open_live(): %s\n", errBuf);
    exit(1);
  }
  
  printf("Capturing on dev %s\n", it->name);

  /* construct a filter */
  struct bpf_program filter;
  pcap_compile(device, &filter, "udp port 33340", 0, 0);
  pcap_setfilter(device, &filter);
 
  /* wait loop forever */
  int id = 0;
  pcap_loop(device, -3, getPacket, (u_char*)&id);
 
  pcap_close(device);
 
  return 0;
}
/* dns.h 实现dns相关函数,提取包头和QR等 */
#ifndef DNS
#define DNS

#include <stdint.h>
#include <sys/types.h>

struct dnshdr
{
    uint16_t id;
    uint16_t flags;
    uint16_t QRn;
    uint16_t AnRn;
    uint16_t ArRn;
    uint16_t AdRn;
};

typedef struct qname{
  int len;
  char* start;
}qname_t;

struct dnsqr
{
    qname_t qn;
    
    uint16_t type;
    uint16_t dclass;
    struct dnsqr* next;
};

typedef struct dnsqr * dnsqr_list;

int get_qr(u_char *start_pos, struct dnsqr *qr){
    u_char *ptr = start_pos;
    uint16_t ttlen = 0;
    int dcnt;
    while(*ptr){
        if(*ptr > 63)
        return -1;
        ttlen += (*ptr) + 1;
        ptr += (*ptr) + 1;
    }
    qr->type = ntohs(*(uint16_t *)(++ptr));
    ptr += 2;
    qr->dclass = ntohs(*(uint16_t *)ptr);

    qr->qn.len = ttlen;
    qr->qn.start = malloc(ttlen);
    ptr = start_pos;

    char* writer = qr->qn.start;

    while(*ptr){
        dcnt = *ptr;
        while(dcnt--){
            *(writer++) = *(++ptr);
        }
        *(writer++) = '.';
        ++ptr;
    }

    *(writer-1) = '\0';
    return ttlen+4;
}

dnsqr_list get_qr_list(struct dnshdr *dns){
    dnsqr_list begin = NULL;
    dnsqr_list end = NULL;
    int qrn = ntohs(dns->QRn);
    u_char *qstart = (u_char *)(dns+1);
    
    int qrlen = 0;
    dnsqr_list qr;
    while(qrn--){
        qr = malloc(sizeof(struct dnsqr));
        qr->next = NULL;
        qrlen = get_qr(qstart, qr);
        if(qrlen < 0){
            return NULL;
        }

        if(!end){
            end = qr;
            begin = qr;
        }else{
            end->next = qr;
        }

        qstart += qrlen;
    }
    return begin;
}

void free_qr_list(dnsqr_list ls){
    dnsqr_list nx;
    while(ls){
        nx = ls->next;
        free(ls->qn.start);
        free(ls);
        ls = nx;
    }
}

#endif

过滤器部分见下一篇

 类似资料: