DNS基础设施安全取证深度分析

DNS基础设施安全取证深度分析

DNS(Domain Name System)是互联网基础设施的核心协议,也是攻击者最常滥用的协议之一。据统计,2024年全球范围内超过42%的网络攻击涉及DNS层面的技术滥用——DNS隧道用于C2通信和数据外传,DGA用于僵尸网络域名生成,DNS劫持用于流量重定向和中间人攻击。对蓝队防御者而言,DNS层取证不仅是应急响应的重要环节,更是早期发现入侵的关键信号源。

与传统的Web流量或邮件流量分析不同,DNS取证具有独特的优势:DNS请求是几乎所有网络活动的前置步骤,即使攻击者使用HTTPS加密通信也无法完全绕过DNS解析;DNS日志通常记录轻量、保留周期长、数据量可控;攻击者在DNS层的异常活动往往在入侵链条的早期阶段就会被触发。这些特性使DNS取证成为蓝队"以逸待劳"的高杠杆分析手段。

本文从蓝队取证实战视角出发,系统性地覆盖DNS安全威胁的检测与取证方法,涵盖DNS劫持与缓存投毒、DNS隧道隐蔽通信、DGA域名生成算法、DoH加密DNS、被动DNS数据分析、域名前置C2隐蔽等核心技术。结合SolarWinds DNS劫持事件、TrickBot DGA基础设施演变等真实案例,构建完整的DNS层取证方法论。


0x01 DNS协议基础与安全威胁概述

DNS协议架构与解析流程

DNS系统采用分层分布式架构,其查询流程涉及多个层次的交互。理解这些交互机制是DNS取证的前提。

组件功能描述安全影响
存根解析器(Stub Resolver)客户端侧DNS解析库,发起查询请求容易被本地劫持修改
递归解析器(Recursive Resolver)代表客户端完成全量DNS查询缓存响应缓存投毒攻击的目标
权威名称服务器(Authoritative NS)持有特定域名的最终授权记录注册商劫持的目标
TLD名称服务器管理顶级域名的权威信息DNSSEC信任链起点
根名称服务器DNS层次结构的根节点全球DNS基础设施的核心节点

标准DNS解析流程中,客户端首先向递归解析器发起查询。如果递归解析器缓存中没有对应记录,则从根服务器开始依次向下查询(根→TLD→权威),直到获取目标域名的IP地址。这一链条中的每个环节都可能成为攻击面。

DNS报文格式与关键字段

DNS报文长度通常限制在512字节(UDP)或不超过65535字节(TCP + EDNS0),这一限制决定了DNS隧道的数据传输速率瓶颈。DNS报文中关键字段如下:

字段偏移量长度取证价值
Transaction ID0-12字节请求-响应匹配,DNS ID欺诈检测
Flags(QR/Opcode/AA/TC/RD/RA/Z/RCODE)2-32字节RCODE异常值检测(NXDOMAIN等)
Questions4-52字节查询数量异常
Answer RRs6-72字节响应记录数量异常
Query Name(QNAME)变长变长域名内容分析的核心字段
Query Type(QTYPE)变长2字节查询类型异常(TXT异常增多等)

攻击者常利用QNAME字段编解码数据(DNS隧道)、利用QTYPE选择不易被监控的记录类型(TXT/MX/AAAA等)、利用Transaction ID进行缓存投毒。

DNS安全威胁全景图

威胁类型攻击方式MITRE ATT&CK影响范围
DNS劫持篡改DNS响应/重定向流量T1557/T1558全局流量劫持
缓存投毒向递归服务器注入伪造DNS记录T1595.003影响大规模用户
DNS隧道在DNS查询中编码隐蔽通信T1572C2通信、数据外传
DGA算法生成大量域名躲避黑名单T1568.002C2弹性、检测规避
域名前置利用CDN的SNI/Host字段差异T1090.004C2通信隐蔽
DNS放大攻击伪造源IP发起反射攻击T1498DDoS攻击向量
域名Shadowing在合法域名下创建恶意子域名T1583.001持久化C2基础设施
DNSSEC滥用利用DNSSEC信任链进行DNS欺骗T1574信任链攻击

0x02 DNS劫持与缓存投毒取证分析

DNS劫持的主要类型

DNS劫持是指攻击者篡改DNS解析结果,将用户流量重定向到恶意目标的过程。从攻击层面区分,DNS劫持包含以下类型:

劫持类型攻击层面检测难度典型场景
本地Hosts篡改终端主机恶意软件修改 hosts 文件
路由器DNS劫持家庭/企业路由器路由器固件漏洞、弱密码
代理/DHCP劫持网络接入层Rogue DHCP服务器
ISP级劫持运营商网络运营商广告注入、监管干预
DNS响应篡改(MITM)中间网络节点中间人攻击
BGP/DNS协同劫持网络路由层极高国家级网络攻击(SolarWinds类)

本地Hosts劫持取证

攻击者修改操作系统的hosts文件是最基础的DNS劫持手段。不同操作系统的hosts文件路径如下:

操作系统hosts文件路径权限要求
WindowsC:\Windows\System32\drivers\etc\hosts管理员权限
Linux/etc/hostsroot权限
macOS/etc/hostsroot权限
Android/system/etc/hostsroot权限

取证检查命令:

# Windows - 检查hosts文件内容
type C:\Windows\System32\drivers\etc\hosts

# Linux/macOS - 检查hosts文件修改时间
stat /etc/hosts

# 对比hosts文件哈希与基线值
certutil -hashfile C:\Windows\System32\drivers\etc\hosts SHA256

# 使用diff对比基线版本(假设有备份)
diff /etc/hosts /var/backup/hosts.baseline

恶意软件常用的hosts劫持模式包括:重定向安全厂商域名到127.0.0.1(阻止安全更新和遥测)、重定向金融服务域名到钓鱼页面、重定向更新服务器到恶意服务器。

路由器级DNS劫持取证

攻击者通过路由器管理接口篡改DNS设置是最常见的大规模劫持方式。取证分析方法:

# Linux/macOS - 检查当前系统DNS配置
scutil --dns | grep 'nameserver'

# Windows - 检查网络接口DNS配置
ipconfig /all | findstr "DNS"

# 检查路由器默认DNS是否被篡改(通过SNMP或API)
dig @8.8.8.8 google.com +short

# 对比dig结果与本地DNS解析结果
dig google.com +short
nslookup google.com 2>&1 | grep Address

如果本地DNS返回的IP与公共DNS(如8.8.8.8)返回的IP不一致,则表明存在DNS劫持。

DNS缓存投毒检测

DNS缓存投毒(Cache Poisoning)通过向递归解析器注入伪造的DNS记录,使后续所有查询该域名的客户端都被重定向到恶意IP。Kaminsky漏洞(2008年发现)是缓存投毒的经典案例。

取证检测方法:

# 检查DNS缓存记录
# Windows
ipconfig /displaydns

# Linux(使用systemd-resolved)
resolvectl statistics
resolvectl query example.com

# Linux(使用dnsmasq)
kill -USR1 $(pidof dnsmasq)  # 向dnsmasq发送信号获取统计

# 检查DNS响应中的异常TTL值
dig +nocmd +nocomment +noquestion example.com any | grep -E "^[a-z]"

# 使用dnstop监控实时DNS流量
dnstop -s eth0

缓存投毒的取证关键指标包括:异常的TTL时间、不匹配的权威服务器、多条不相关的A记录、RCODE 0但答案部分异常的响应。

大规模DNS劫持的关联分析

涉及BGP/DNS协同劫持的取证需要跨多个数据源:

# 检查BGP路由变动历史
bgpdump /var/log/bgp/updates.log | grep -E "2026-07-.*103.102.100"

# 通过公开BGP监控API查询AS路径
curl -s "https://stat.ripe.net/data/bgplay/data.json?resource=103.102.100.0/24"

# 对比多地区的DNS解析结果
# 使用Python脚本跨区域查询
python3 -c "
import dns.resolver
import json
providers = {
    'google': '8.8.8.8',
    'cloudflare': '1.1.1.1',
    'opendns': '208.67.222.222',
    'quad9': '9.9.9.9'
}
results = {}
target = 'paypal.com'
for name, server in providers.items():
    resolver = dns.resolver.Resolver()
    resolver.nameservers = [server]
    answers = resolver.resolve(target, 'A')
    results[name] = [str(r) for r in answers]
print(json.dumps(results, indent=2))
"

0x03 DNS隧道隐蔽通信取证分析

DNS隧道原理与工作机制

DNS隧道是一种将数据协议封装在DNS查询和响应中的隐蔽通信技术。由于DNS协议在大多数网络环境中默认放行、很少被深度检测,攻击者利用它穿越防火墙、代理服务器和NAT设备。

DNS隧道的工作流程:

阶段操作数据流方向编码方式
初始连接客户端发送特殊格式DNS查询内→外Base32/Base64编码
数据传输编码数据放在子域名或TXT记录中双向自定义编码
会话管理通过Transaction ID或自定义字段维护会话双向SEQ/ACK序列
隧道终止发送终止信号释放资源内→外特殊标志位

主流DNS隧道工具对比

工具语言编码方式特征最大速率
iodineCBase32支持多QTYPE、分片、密码1-5 Mbps
dnscat2C自定义编码命令shell、文件传输、加密0.5-2 Mbps
DNSExfiltratorC#Base64Windows原生、文件外传0.1-1 Mbps
dns2tcpCBase32/BinaryTCP over DNS、RPC over DNS0.5-3 Mbps
ChashellGoBase64 + AES反向shell、多会话1-4 Mbps
HeyokaC++Base64利用询问侧信道0.5-2 Mbps

DNS隧道流量特征与检测

检测维度正常DNS特征DNS隧道特征检测方法
域名长度平均15-25字符子域名超过52字符检查FQDN总长度
查询频率间歇性、不稳定固定间隔、高频率时间序列分析
QTYPE分布A/AAAA占80%+TXT/MX占比异常增高QTYPE比例统计
域名熵值自然语言特征高随机性、类Base32字符分布熵分析
数据包大小≤100字节持续接近512或1500字节包长度分布统计
响应RCODE0为主高比例NXDOMAIN(3)RCODE分布统计
域名覆盖集中式大量唯一域名域名基数统计

Python DNS隧道检测脚本

import dpkt
import socket
from collections import defaultdict, Counter
import math
import statistics

def calculate_entropy(data):
    if not data:
        return 0
    entropy = 0
    for x in range(256):
        p_x = data.count(x) / len(data)
        if p_x > 0:
            entropy += -p_x * math.log2(p_x)
    return entropy

def extract_dns_queries(pcap_file):
    queries = []
    with open(pcap_file, 'rb') as f:
        pcap = dpkt.pcap.Reader(f)
        for ts, buf in pcap:
            try:
                eth = dpkt.ethernet.Ethernet(buf)
                ip = eth.data
                if isinstance(ip, dpkt.ip.IP) and ip.p == dpkt.ip.IP_PROTO_UDP:
                    udp = ip.data
                    if udp.dport == 53 or udp.sport == 53:
                        dns = dpkt.dns.DNS(udp.data)
                        if len(dns.qd) > 0:
                            q = dns.qd[0]
                            qname = q.name.decode('utf-8', errors='ignore')
                            qtype = q.type
                            src_ip = socket.inet_ntoa(ip.src)
                            dst_ip = socket.inet_ntoa(ip.dst)
                            queries.append({
                                'timestamp': ts,
                                'src_ip': src_ip,
                                'dst_ip': dst_ip,
                                'qname': qname,
                                'qtype': qtype,
                                'length': len(buf)
                            })
            except Exception:
                continue
    return queries

def detect_dns_tunnel(queries, threshold_entropy=4.5, threshold_freq=100):
    alerts = []
    qname_entropy = {}
    domain_count = Counter()
    qtype_dist = Counter()
    length_dist = []

    for q in queries:
        labels = q['qname'].rstrip('.').split('.')
        if len(labels) >= 3:
            subdomain = '.'.join(labels[:-2])
            entropy = calculate_entropy(subdomain.encode())
            qname_entropy[q['qname']] = entropy
        domain_count[q['src_ip']] += 1
        qtype_dist[(q['src_ip'], q['qtype'])] += 1
        length_dist.append(q['length'])

    for src_ip, count in domain_count.most_common():
        if count > threshold_freq:
            high_entropy = []
            for q in queries:
                if q['src_ip'] == src_ip:
                    labels = q['qname'].rstrip('.').split('.')
                    if len(labels) >= 3:
                        subdomain = '.'.join(labels[:-2])
                        ent = calculate_entropy(subdomain.encode())
                        if ent > threshold_entropy:
                            high_entropy.append(q['qname'])
            if len(high_entropy) > count * 0.3:
                alerts.append({
                    'src_ip': src_ip,
                    'query_count': count,
                    'high_entropy_ratio': len(high_entropy) / count,
                    'sample_queries': high_entropy[:5],
                    'avg_length': statistics.mean(length_dist) if length_dist else 0
                })

    return alerts

if __name__ == '__main__':
    import sys
    pcap_file = sys.argv[1] if len(sys.argv) > 1 else 'capture.pcap'
    queries = extract_dns_queries(pcap_file)
    alerts = detect_dns_tunnel(queries)
    print(f'Total queries: {len(queries)}')
    print(f'DNS tunnel alerts: {len(alerts)}')
    for alert in alerts:
        print(f'  Source: {alert["src_ip"]}')
        print(f'  Queries: {alert["query_count"]}')
        print(f'  High entropy ratio: {alert["high_entropy_ratio"]:.2%}')
        print(f'  Sample: {alert["sample_queries"][:2]}')

iodine工具通信特征取证

iodine是最成熟的DNS隧道工具之一,其流量特征包括:使用TYPE NULL或TYPE TXT查询、子域名长度固定为38字符(Base32编码)、DNS请求间隔恒定、数据包的ID字段为递增序列。

取证检测命令:

# 检查子域名长度异常
tcpdump -r traffic.pcap -nn port 53 | awk '{print $NF}' | \
  awk -F. '{print $1}' | awk '{print length}' | sort -n | tail -10

# 统计TXT查询占比
tcpdump -r traffic.pcap -nn port 53 | grep "TXT" | wc -l

# 查看特定域名下的TXT查询
tshark -r traffic.pcap -Y "dns.qry.type == 16" -T fields -e dns.qry.name | \
  sort | uniq -c | sort -rn | head -20

0x04 DGA域名生成算法检测与分析

DGA基本原理与分类

域名生成算法(Domain Generation Algorithm, DGA)是恶意软件用于规避静态域名黑名单检测的核心技术。DGA通过种子值(如当前日期、Twitter趋势话题、TLD列表等)和加密算法生成大量伪随机域名,恶意软件逐一尝试连接,只要其中一个被注册为C2服务器即可建立通信。

DGA类型种子来源算法复杂度DNS查询模式代表家族
时间基础型日期/时间戳每日批量生成Conficker, Locky
加密算法型自定义加密函数持续生成TrickBot, Emotet
网络数据型Twitter趋势/股票价格条件触发GameOver Zeus, Kraken
词典组合型单词列表组合低频生成Suppobox, Matsnu
混合型多种子混合自适应动态Cryptolocker, Torpig

主要DGA家族技术特征对比

家族每日域名数域名长度字符集TLD偏好是否公开
Conficker2504-10字母+数字.com/.net/.org是(1-3变种)
TrickBot100-30012-25字母+数字.com/.org/.info
Emotet500-100015-30字母+数字.com/.net/.biz
Dridex500-10008-20字母.com/.net部分公开
QakBot300-50015-35字母+数字.com部分公开
Ryuk200-50010-20字母+数字.com/.org部分公开
LockBit可变10-25字母+数字多个TLD

DGA逆向分析方法

DGA逆向的核心步骤包括:从样本中提取DGA算法逻辑、确定种子生成机制、计算预测域名列表、验证已注册域名的关联性。

import datetime
import hashlib
import argparse
import requests

def dga_conficker_b(date_str=None):
    if date_str is None:
        date = datetime.datetime.now()
    else:
        date = datetime.datetime.strptime(date_str, '%Y-%m-%d')
    year = date.year
    month = date.month
    day = date.day
    tlds = ['com', 'net', 'org', 'info', 'biz', 'ws', 'us', 'cc']
    domains = []
    for tld in tlds:
        seed = f'{year}{month:02d}{day:02d}{tld}'
        for i in range(250):
            data = f'{seed}{i}'
            md5 = hashlib.md5(data.encode()).hexdigest()
            length = (int(md5[:2], 16) % 6) + 4
            domain_chars = []
            for j in range(length):
                char_val = int(md5[2+j*2:4+j*2], 16) % 36
                if char_val < 26:
                    domain_chars.append(chr(ord('a') + char_val))
                else:
                    domain_chars.append(chr(ord('0') + char_val - 26))
            domain = ''.join(domain_chars)
            domains.append(f'{domain}.{tld}')
    return domains

def dga_trickbot(date_str=None):
    if date_str is None:
        date = datetime.datetime.now()
    else:
        date = datetime.datetime.strptime(date_str, '%Y-%m-%d')
    year = date.year
    month = date.month
    day = date.day
    domains = []
    tlds = ['com', 'net', 'org', 'info']
    charset = 'abcdefghijklmnopqrstuvwxyz0123456789'
    for tld in tlds:
        seed = f'{year}{month:02d}{day:02d}{tld}'
        for i in range(100):
            data = seed + str(i) * 3
            md5 = hashlib.md5(data.encode()).hexdigest()
            length = (int(md5[-4:], 16) % 16) + 12
            domain = ''
            for j in range(length):
                idx = int(md5[j*2:(j+1)*2], 16) % len(charset)
                domain += charset[idx]
            domains.append(f'{domain}.{tld}')
    return domains

def check_domain_availability(domains, sample_size=100):
    import socket
    import concurrent.futures
    results = {'resolved': [], 'nxdomain': [], 'error': []}
    test_domains = domains[:sample_size]

    def check(domain):
        try:
            ip = socket.gethostbyname(domain)
            return (domain, 'resolved', ip)
        except socket.gaierror:
            return (domain, 'nxdomain', None)
        except Exception as e:
            return (domain, 'error', str(e))

    with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
        for result in executor.map(check, test_domains):
            domain, status, extra = result
            results[status].append((domain, extra))
    return results

def compute_ngram_features(domains):
    from collections import Counter
    bigrams = Counter()
    trigrams = Counter()
    lengths = []
    for domain in domains:
        name = domain.split('.')[0]
        lengths.append(len(name))
        for i in range(len(name) - 1):
            bigrams[name[i:i+2]] += 1
        for i in range(len(name) - 2):
            trigrams[name[i:i+3]] += 1
    total_bigrams = sum(bigrams.values())
    total_trigrams = sum(trigrams.values())
    return {
        'avg_length': sum(lengths) / len(lengths),
        'unique_bigram_ratio': len(bigrams) / total_bigrams if total_bigrams > 0 else 1,
        'unique_trigram_ratio': len(trigrams) / total_trigrams if total_trigrams > 0 else 1,
        'top_bigrams': bigrams.most_common(5)
    }

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='DGA Domain Generator & Analyzer')
    parser.add_argument('--family', choices=['conficker', 'trickbot'], default='conficker')
    parser.add_argument('--date', help='Date in YYYY-MM-DD format')
    parser.add_argument('--check', action='store_true', help='Check domain resolution')
    args = parser.parse_args()

    if args.family == 'conficker':
        domains = dga_conficker_b(args.date)
    else:
        domains = dga_trickbot(args.date)

    print(f'Generated {len(domains)} domains for {args.family}')
    features = compute_ngram_features(domains)
    print(f'Average length: {features["avg_length"]:.1f}')
    print(f'Bigram uniqueness: {features["unique_bigram_ratio"]:.3f}')
    print(f'Top bigrams: {features["top_bigrams"]}')

    if args.check:
        results = check_domain_availability(domains)
        print(f'Resolved: {len(results["resolved"])}')
        print(f'NXDOMAIN: {len(results["nxdomain"])}')
        for domain, ip in results['resolved'][:10]:
            print(f'  {domain} -> {ip}')

N-gram分析检测方法

DGA域名与合法域名在字符分布上的统计差异是检测的核心依据:

特征合法域名DGA域名检测效果
平均长度8-15字符10-25字符中等
元音比例35%-50%15%-30%良好
2-gram覆盖率低(可读)高(随机)优秀
字符类分布字母为主混合字符中等
频率分布Zipf分布均匀分布优秀

Sigma规则 - DGA域名的NXDOMAIN洪水检测

title: DGA Domain NXDOMAIN Flood Detection
id: bf8c3a21-e25d-4a5c-9f17-8b4d5e6f7a8b
status: experimental
description: Detects high volume of NXDOMAIN responses from a single client, indicating potential DGA domain generation and connection attempts
author: BlueTeam Analyst
date: 2026-07-08
tags:
  - attack.t1568.002
  - attack.ta0011
logsource:
  category: dns
  product: windows
  service: dns-server
detection:
  selection:
    EventID: 264
    RCODE: 3
  timeframe: 5m
  condition:
    selection | count() > 100 by SourceIp
falsepositives:
  - Misconfigured applications
  - Vulnerability scanners
  - DNS resolver testing
level: medium

0x05 DNS over HTTPS/加密DNS取证挑战

DoH/DoT协议原理与技术对比

加密DNS协议传输层默认端口标准化可观测性主流支持
DNS over HTTPS (DoH)HTTP/2, HTTP/3443RFC 8484极低(混入HTTPS流量)Firefox, Chrome, Edge, Windows
DNS over TLS (DoT)TCP-TLS853RFC 7858低(独立端口但加密)Android, iOS, Linux
DNSCryptUDP/TCP443非标准化OpenDNS客户端
DNS over QUICQUIC443RFC 9250极低AdGuard, Surfshark

加密DNS对取证的影响

取证挑战传统DNS加密DNS应对策略
查询内容可见性明文可见加密不可见终端DLP代理采集
日志记录完整性服务器端记录无法在代理层记录本地DNS代理/日志
C2通信检测Signature匹配难以检测ML行为分析
数据外传检测DNS隧道流量分析正常HTTPS流量伪装容量分析+终端联动

企业环境DoH管控方案

# Windows - 通过组策略禁用DoH
# 设置: Computer Configuration > Administrative Templates > Network > DNS Client
# 策略: "Turn off DoH" -> Enabled

# macOS - 通过配置文件禁用DoH
# /etc/dnssec/dns.conf 配置

# Linux - 本地DNS代理强制重定向
iptables -t nat -A OUTPUT -p udp --dport 853 -j REDIRECT --to-port 5353
iptables -t nat -A OUTPUT -p tcp --dport 853 -j REDIRECT --to-port 5353

# 使用dnsmasq作为本地DNS代理
cat > /etc/dnsmasq.d/block-doh.conf << 'DNSMASQ_CONF'
# 拦截已知DoH服务器域名
server=/cloudflare-dns.com/0.0.0.0
server=/mozilla.cloudflare-dns.com/0.0.0.0
server=/dns.google/0.0.0.0
server=/dns.quad9.net/0.0.0.0
server=/dns.adguard.com/0.0.0.0
DNSMASQ_CONF

# 检测网络中DoH流量(SNI分析)
tcpdump -ni eth0 -A 'tcp port 443' | grep -E "cloudflare-dns|dns.google|dns.quad9"

TLS指纹与JA3分析

TLS握手中的Client Hello包含丰富的指纹信息,可用于区分DoH应用和普通HTTPS流量:

# 使用tshark提取TLS Client Hello指纹
tshark -r capture.pcap -Y "tls.handshake.type == 1" -T fields \
  -e tls.handshake.ciphersuite \
  -e tls.handshake.extensions_server_name \
  -e tls.handshake.extensions_supported_group

# JA3指纹匹配(通过Python)
python3 -c "
import json
doh_fingerprints = {
    '785f5b62c12521a11a10a31a8d5d6c89': 'Firefox DoH',
    '51c2f0e5e7e2cc7a8e7c9f3b1a2d4e6f': 'Chrome DoH',
    'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6': 'curl DoH'
}
target = 'your_ja3_here'
if target in doh_fingerprints:
    print(f'Detected: {doh_fingerprints[target]}')
else:
    print('Unknown fingerprint, may need analysis')
"

0x06 被动DNS与DNS日志关联分析

被动DNS工作原理

被动DNS(Passive DNS)通过监听递归解析器的DNS流量或采集权威服务器的日志,持续记录DNS查询和响应数据,构建历史域名解析数据库。与传统主动扫描不同,被动DNS依赖真实的用户查询,因此不产生额外网络负载,且能捕获到短期存活的恶意域名。

数据源数据采集方式覆盖范围典型延迟
递归解析器日志直接采集企业/ISP内部实时
被动网络监听端口镜像/tap本地网络实时
公共DNS平台API查询全局分钟级
安全厂商聚合多源汇聚全局小时级

主流被动DNS平台对比

平台API类型免费配额支持数据类型查询延迟
VirusTotalREST API500/天域名/IP/URL解析历史秒级
SecurityTrailsREST API50/月域名解析历史、子域名秒级
PassiveTotalREST API有限免费域名/IP/SSL/WHOIS关联秒级
CIRCL PDNSREST API免费DNS查询历史秒级
RiskIQREST API付费域名/IP/证书多维度秒级

被动DNS取证分析方法

import json
import requests
import datetime
from collections import defaultdict, Counter

class PassiveDNSAnalyzer:
    def __init__(self, virustotal_api_key=None):
        self.api_key = virustotal_api_key
        self.dns_history = defaultdict(list)

    def query_pdns(self, domain):
        query_url = f'https://otx.alienvault.com/api/v1/indicators/domain/{domain}/passive_dns'
        try:
            resp = requests.get(query_url)
            if resp.status_code == 200:
                records = resp.json().get('passive_dns', [])
                for record in records:
                    self.dns_history[domain].append({
                        'hostname': record.get('hostname'),
                        'address': record.get('address'),
                        'first_seen': record.get('first_seen'),
                        'last_seen': record.get('last_seen'),
                        'record_type': record.get('record_type')
                    })
                return True
        except Exception as e:
            print(f'Query failed for {domain}: {e}')
        return False

    def analyze_dns_stability(self):
        analysis = {}
        for domain, records in self.dns_history.items():
            ip_changes = []
            unique_ips = set()
            for r in records:
                if r['record_type'] in ['A', 'AAAA']:
                    if r['address']:
                        unique_ips.add(r['address'])
                        ip_changes.append({
                            'ip': r['address'],
                            'time': r.get('first_seen', r.get('last_seen'))
                        })
            ip_count = len(unique_ips)
            if ip_count > 0:
                ip_changes_sorted = sorted(ip_changes, key=lambda x: x['time'] or '')
                period_days = 0
                if len(ip_changes_sorted) > 1:
                    try:
                        t1 = datetime.datetime.fromisoformat(ip_changes_sorted[0]['time'])
                        t2 = datetime.datetime.fromisoformat(ip_changes_sorted[-1]['time'])
                        period_days = (t2 - t1).days
                    except Exception:
                        pass
                change_rate = ip_count / max(period_days, 1)
                analysis[domain] = {
                    'unique_ips': ip_count,
                    'ip_change_rate': f'{change_rate:.4f}/day',
                    'period_days': period_days,
                    'is_stable': ip_count <= 3
                }
        return analysis

    def find_shared_infrastructure(self, domains):
        ip_domain_map = defaultdict(list)
        for domain in domains:
            for record in self.dns_history.get(domain, []):
                if record['record_type'] in ['A', 'AAAA'] and record.get('address'):
                    ip_domain_map[record['address']].append(domain)
        shared = {}
        for ip, doms in ip_domain_map.items():
            if len(doms) > 1:
                shared[ip] = list(set(doms))
        return shared

    def cluster_by_ns_or_mx(self, domains):
        ns_cluster = defaultdict(list)
        mx_cluster = defaultdict(list)
        for domain in domains:
            for record in self.dns_history.get(domain, []):
                if record['record_type'] == 'NS':
                    ns_cluster[record['address']].append(domain)
                elif record['record_type'] == 'MX':
                    mx_cluster[record['address']].append(domain)
        return {
            'ns_clusters': {k: list(set(v)) for k, v in ns_cluster.items() if len(set(v)) > 1},
            'mx_clusters': {k: list(set(v)) for k, v in mx_cluster.items() if len(set(v)) > 1}
        }

if __name__ == '__main__':
    analyst = PassiveDNSAnalyzer()
    test_domains = ['malware1.com', 'malware2.net', 'malware3.org']
    for domain in test_domains:
        analyst.query_pdns(domain)
    stability = analyst.analyze_dns_stability()
    shared = analyst.find_shared_infrastructure(test_domains)
    clusters = analyst.cluster_by_ns_or_mx(test_domains)
    print('DNS Stability Analysis:')
    for domain, info in stability.items():
        status = 'STABLE' if info['is_stable'] else 'UNSTABLE'
        print(f'  {domain}: {info["unique_ips"]} IPs, {status}')
    print(f'Shared infrastructure: {len(shared)} IPs')

DNS日志与SIEM关联分析

# 提取DNS日志中的NXDOMAIN统计
cat dns.log | awk -F'\t' '$10 == 3 {print $7}' | \
  sort | uniq -c | sort -rn | head -20

# DNS日志与NetFlow关联(查询特定时间窗口的DNS和IP连接记录)
awk -F'\t' '$1 ~ "2026-07-08 0[0-5]:" {print $7, $9}' dns.log > dns_queries.txt
awk '{print $1}' dns_queries.txt | sort -u > resolved_ips.txt
grep -Ff resolved_ips.txt netflow.log | awk '{print $1, $3, $5}' | \
  sort | uniq -c | sort -rn | head -20

# 检测速变域名(Fast-Flux)
awk -F'\t' '$7 ~ /^[a-z0-9]{20,}\./ {print $1, $7, $9}' dns.log | \
  sort -k2 | uniq -c | sort -rn | head -30

0x07 域名前置与高级C2隐蔽技术

域名前置原理

域名前置(Domain Fronting)利用CDN的内容分发机制实现C2通信的隐蔽。攻击者在HTTPS请求的TLS SNI扩展中填写CDN合法的域名(如cloudflare.com),但在HTTP Host头中填写C2域名。CDN边缘节点根据HTTP Host头进行路由,中间网络设备只能看到TLS SNI中的合法域名。

技术名称TLS层HTTP层CDN处理逻辑检测难度
标准域名前置SNI = 合法域名Host = C2地址根据Host路由
SNI代理回退SNI = C2域名Host = C2域名正常路由
TLS多证书SNI = 合法域名Host = 合法域名同一域下路径路由极高

域名前置检测方法

检测维度检测方法实施复杂度有效性
SNI vs Host比对检查TLS握手中的SNI与HTTP Host是否一致高(非CDN环境)
CDN访问日志检查CDN源站回源流量中的Host字段
流量Behavio分析分析连到CDN IP后访问的域名分布
终端Egress监控在终端侧使用代理捕获完整HTTP请求
# 检查TLS SNI与HTTP Host不一致性
tshark -r capture.pcap -Y "tls.handshake.type == 1" -T fields \
  -e tls.handshake.extensions_server_name | sort -u > sni_hosts.txt

tshark -r capture.pcap -Y "http.request" -T fields \
  -e http.host | sort -u > http_hosts.txt

# 比对两者差异
comm -23 <(sort http_hosts.txt) <(sort sni_hosts.txt)

# Python实现SNI/Host比对检测
python3 -c "
import json
from collections import defaultdict

pairs = defaultdict(set)
# 模拟流量解析
traffic = [
    {'sni': 'www.cloudflare.com', 'host': 'evil-c2.attacker.com'},
    {'sni': 'www.cloudflare.com', 'host': 'www.cloudflare.com'},
    {'sni': 'www.akamai.com', 'host': 'c2-panel.evil.net'},
]
for t in traffic:
    pairs[t['sni']].add(t['host'])

for sni, hosts in pairs.items():
    if len(hosts) > 1:
        print(f'Domain Fronting detected on {sni}:')
        for h in hosts:
            print(f'  -> {h}')
"

Fast Flux技术取证

Flux类型DNS TTLIP变化频率域名数量平均存活时间
Single Flux30-180秒每次解析变化1-2个IP几分钟到几小时
Double Flux60-300秒NS+A同时变化大量IP池数天
Domain Flux1-60秒域名+IP同时变化大规模数小时到数天
import socket
import time
from collections import defaultdict

def detect_fast_flux(target_domain, samples=10, interval=30):
    ip_history = defaultdict(int)
    ns_history = defaultdict(int)

    for i in range(samples):
        try:
            answers = socket.getaddrinfo(target_domain, 80, socket.AF_INET)
            current_ips = set()
            for ans in answers:
                ip = ans[4][0]
                current_ips.add(ip)
                ip_history[ip] += 1
            print(f'Sample {i+1}: IPs={current_ips}')

            if i % 3 == 0:
                import dns.resolver
                ns_records = dns.resolver.resolve(target_domain, 'NS')
                for ns in ns_records:
                    ns_history[str(ns)] += 1

            time.sleep(interval)
        except Exception as e:
            print(f'Sample {i+1} failed: {e}')

    flux_score = {
        'ip_variety': len(ip_history) / max(sum(ip_history.values()), 1),
        'ns_variety': len(ns_history) / max(sum(ns_history.values()), 1),
        'is_fast_flux': len(ip_history) > 5 and max(ip_history.values()) < samples
    }
    return flux_score

if __name__ == '__main__':
    result = detect_fast_flux('suspicious-domain.example.com')
    print(f'Fast Flux Score: {result}')

0x08 证据强度分层与DNS异常分类

DNS取证的证据强度评估需要结合多种数据源进行交叉验证。以下分类框架帮助取证人员评估DNS相关发现的置信度。

🔴 确认恶意

证据类型具体发现确认条件典型TTL
已知恶意域名匹配威胁情报黑名单至少2个独立情报源确认永久有效
明确DGA匹配逆向分析确认的DGA域名算法验证+域名按时生成直到样本变更
DNS隧道确认提取到可解码的隧道数据成功提取到非DNS内容分析期间
已确认C2通信域名关联到已知恶意IPIP在沙箱分析中确认C2取证周期

🟡 高度可疑

证据类型具体发现需要进一步验证
高频NXDOMAIN单一源IP5分钟内超过100个NXDOMAIN排除扫描器/爬虫误报
高熵子域名子域名熵值超过4.5排除CDN/CMS生成域名
DNS隧道特征符合隧道工具特征但未解码提取更多样本分析
速变DNS模式TTL<60秒且频繁更换IP检查是否为CDN行为
SNI/Host不匹配域名前置模式验证CDN配置

🟢 需要关注

证据类型具体发现建议动作
DoH使用终端连接到已知DoH服务器检查企业策略配置
低频异常查询少数畸形DNS请求持续监控
新注册域名最近7天内注册的域名检查域名背景
低频TXT查询TXT查询占比有微小上升扩大采样窗口
长域名查询子域名偶尔超过40字符排除技术原因

证据交叉验证矩阵

单一证据单一证据强度+日志关联+流量捕获+终端取证
NXDOMAIN洪泛🟡🟡🟢🟢
高熵域名🟡🟡🔴🔴
域名前置SNI异常🟡🟡🟡🔴
Fast Flux特征🟡🔴🔴🔴
DNS隧道流量🟡🟡🔴🔴

0x09 自动化检测与Sigma/Bash/Python规则

Sigma YAML规则 - DNS隧道检测

title: DNS Tunneling Detection via High Query Rate and High Entropy
id: d9e4f5a6-b7c8-4d9e-0f1a-2b3c4d5e6f7a
status: experimental
description: Detects potential DNS tunneling activity characterized by high query rate from a single source combined with high-entropy subdomain queries
author: BlueTeam Analyst
date: 2026-07-08
tags:
  - attack.t1572
  - attack.ta0011
logsource:
  category: dns
  product: windows
  service: dns-server
detection:
  selection_high_rate:
    EventID: 264
    SourceIp: 
      - '10.0.0.0/8'
      - '172.16.0.0/12'
      - '192.168.0.0/16'
  selection_high_entropy:
    QueryName|re: '[a-z2-7]{30,}\.[a-z0-9]+\.[a-z]+'
    QueryType:
      - 'TXT'
      - 'MX'
      - 'NULL'
  timeframe: 5m
  condition: selection_high_rate and selection_high_entropy | count() > 50 by SourceIp
falsepositives:
  - CDN health check services
  - DNS zone transfer tools
  - Security scanners with TXT record verification
level: high

Sigma YAML规则 - DGA域名NXDOMAIN检测

title: DGA Domain NXDOMAIN Burst Detection
id: e0f1a2b3-c4d5-4e6f-7a8b-9c0d1e2f3a4b
status: experimental
description: Detects burst of NXDOMAIN responses from a single client, indicative of DGA domain generation and resolution attempts
author: BlueTeam Analyst
date: 2026-07-08
tags:
  - attack.t1568.002
  - attack.ta0011
logsource:
  category: dns
  product: linux
  service: bind
detection:
  selection:
    query_type: 'A'
    return_code: 'NXDOMAIN'
  timeframe: 10m
  condition: selection | count() > 200 by client_ip
falsepositives:
  - Web crawlers with malformed URLs
  - Misconfigured applications
  - Internal scanning tools
level: medium

Bash脚本 - DNS日志批量分析

#!/bin/bash
# DNS日志批量分析脚本
# 检测DNS隧道、DGA、异常查询模式

DNS_LOG="${1:-/var/log/named/named.log}"
ANALYSIS_DIR="dns_analysis_$(date +%Y%m%d)"
mkdir -p "$ANALYSIS_DIR"

echo "[*] Starting DNS log analysis on $DNS_LOG"
echo "[*] Results will be saved to $ANALYSIS_DIR/"

# 1. 提取所有查询域名并统计频次
echo "[1/6] Extracting query domains..."
grep -oP 'query: \K[^ ]+' "$DNS_LOG" | \
  sed 's/\.$//' | sort | uniq -c | sort -rn > \
  "$ANALYSIS_DIR/query_frequency.txt"
echo "  Total unique domains: $(wc -l < $ANALYSIS_DIR/query_frequency.txt)"

# 2. 检测长子域名
echo "[2/6] Detecting long subdomains..."
awk '{print $2}' "$ANALYSIS_DIR/query_frequency.txt" | \
  awk -F'.' '{
    if(length($1) > 40) print length($1), $0
  }' | sort -rn | head -50 > "$ANALYSIS_DIR/long_subdomains.txt"
echo "  Domains with >40 char subdomain: $(wc -l < $ANALYSIS_DIR/long_subdomains.txt)"

# 3. 检测NXDOMAIN错误
echo "[3/6] Detecting NXDOMAIN errors..."
grep -c "NXDOMAIN" "$DNS_LOG" > "$ANALYSIS_DIR/nxdomain_count.txt"
echo "  NXDOMAIN count: $(cat $ANALYSIS_DIR/nxdomain_count.txt)"

# 4. 提取高频查询的源IP
echo "[4/6] Extracting top query sources..."
grep -oP 'client \K[0-9.]+' "$DNS_LOG" | \
  sort | uniq -c | sort -rn | head -20 > \
  "$ANALYSIS_DIR/top_sources.txt"
echo "  Top sources saved"

# 5. 检测高熵域名(DGA候选)
echo "[5/6] Detecting high-entropy domains..."
while read -r count domain; do
  entropy=$(echo "$domain" | grep -oP '^[a-z0-9]+' | \
    python3 -c "
import sys, math
data = sys.stdin.read().strip()
if not data:
    exit(0)
entropy = 0
for c in range(256):
    p = data.count(chr(c)) / len(data)
    if p > 0:
        entropy -= p * math.log2(p)
print(f'{entropy:.2f}')
")
  if [ -n "$entropy" ] && [ "$(echo "$entropy > 4.0" | bc -l 2>/dev/null)" = "1" ]; then
    echo "$entropy $count $domain" >> "$ANALYSIS_DIR/high_entropy_domains.txt"
  fi
done < "$ANALYSIS_DIR/query_frequency.txt"

sort -rn "$ANALYSIS_DIR/high_entropy_domains.txt" -o "$ANALYSIS_DIR/high_entropy_domains.txt"
echo "  High-entropy domains: $(wc -l < $ANALYSIS_DIR/high_entropy_domains.txt)"

# 6. 汇总报告
echo "[6/6] Generating summary report..."
{
  echo "=== DNS Log Analysis Report ==="
  echo "Analysis date: $(date)"
  echo "Log file: $DNS_LOG"
  echo ""
  echo "--- Top 10 Most Queried Domains ---"
  head -10 "$ANALYSIS_DIR/query_frequency.txt"
  echo ""
  echo "--- Top 5 Long Subdomains ---"
  head -5 "$ANALYSIS_DIR/long_subdomains.txt"
  echo ""
  echo "--- Top 5 High-Entropy Domains (DGA candidate) ---"
  head -5 "$ANALYSIS_DIR/high_entropy_domains.txt"
} > "$ANALYSIS_DIR/summary_report.txt"

echo "[*] Analysis complete. Report: $ANALYSIS_DIR/summary_report.txt"

Python脚本 - DNS隧道数据提取与分析

#!/usr/bin/env python3
import dpkt
import socket
import base64
import struct
import sys
from collections import defaultdict

class DNSTunnelExtractor:
    def __init__(self, pcap_file):
        self.pcap_file = pcap_file
        self.tunnel_candidates = defaultdict(list)
        self.extracted_data = {}
        self.suspicious_ip_ranges = [
            ('10.0.0.0', '10.255.255.255'),
            ('172.16.0.0', '172.31.255.255'),
            ('192.168.0.0', '192.168.255.255')
        ]

    def ip_to_int(self, ip_str):
        parts = ip_str.split('.')
        return (int(parts[0]) << 24) + (int(parts[1]) << 16) + \
               (int(parts[2]) << 8) + int(parts[3])

    def is_private_ip(self, ip_str):
        ip_int = self.ip_to_int(ip_str)
        for start, end in self.suspicious_ip_ranges:
            start_int = self.ip_to_int(start)
            end_int = self.ip_to_int(end)
            if start_int <= ip_int <= end_int:
                return True
        return False

    def parse_dns_packets(self):
        with open(self.pcap_file, 'rb') as f:
            pcap = dpkt.pcap.Reader(f)
            for ts, buf in pcap:
                try:
                    eth = dpkt.ethernet.Ethernet(buf)
                    ip = eth.data
                    if isinstance(ip, dpkt.ip.IP) and ip.p == dpkt.ip.IP_PROTO_UDP:
                        udp = ip.data
                        if udp.dport == 53 or udp.sport == 53:
                            dns = dpkt.dns.DNS(udp.data)
                            if len(dns.qd) > 0:
                                q = dns.qd[0]
                                qname = q.name.decode('utf-8', errors='ignore')
                                qtype = q.type
                                src_ip = socket.inet_ntoa(ip.src)
                                dst_ip = socket.inet_ntoa(ip.dst)
                                if self.is_private_ip(src_ip):
                                    labels = qname.rstrip('.').split('.')
                                    if len(labels) >= 3 and len(labels[0]) > 30:
                                        tunnel_domain = '.'.join(labels[-3:])
                                        encoded_part = labels[0]
                                        self.tunnel_candidates[src_ip].append({
                                            'timestamp': ts,
                                            'domain': tunnel_domain,
                                            'encoded': encoded_part,
                                            'qtype': qtype,
                                            'dst_ip': dst_ip
                                        })
                except Exception:
                    continue

    def decode_tunnel_data(self):
        for src_ip, entries in self.tunnel_candidates.items():
            encoded_data = ''
            domain_group = defaultdict(list)
            for entry in entries:
                domain_group[entry['domain']].append(entry)
            for domain, group in domain_group.items():
                group.sort(key=lambda x: x['timestamp'])
                for entry in group:
                    try:
                        raw = entry['encoded'].replace('-', '/').replace('_', '+')
                        padding = 4 - len(raw) % 4
                        if padding != 4:
                            raw += '=' * padding
                        decoded = base64.b64decode(raw)
                        encoded_data += decoded.decode('utf-8', errors='replace')
                    except Exception:
                        continue
                if encoded_data:
                    self.extracted_data[domain] = {
                        'src_ip': src_ip,
                        'packet_count': len(group),
                        'total_bytes': len(encoded_data),
                        'decoded_preview': encoded_data[:200]
                    }

    def generate_report(self):
        print(f'[*] DNS Tunnel Analysis Report')
        print(f'[*] PCAP: {self.pcap_file}')
        print(f'[*] Tunnel candidates: {sum(len(v) for v in self.tunnel_candidates.values())}')
        print(f'[*] Unique source IPs: {len(self.tunnel_candidates)}')
        print()
        for domain, info in self.extracted_data.items():
            print(f'[!] Tunnel detected: {domain}')
            print(f'    Source: {info["src_ip"]}')
            print(f'    Packets: {info["packet_count"]}')
            print(f'    Data: {info["total_bytes"]} bytes')
            print(f'    Preview: {info["decoded_preview"][:80]}...')
            print()

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print(f'Usage: {sys.argv[0]} <pcap_file>')
        sys.exit(1)
    extractor = DNSTunnelExtractor(sys.argv[1])
    extractor.parse_dns_packets()
    extractor.decode_tunnel_data()
    extractor.generate_report()

0x0A 公开案例分析

案例一:SolarWinds供应链攻击中的DNS重定向技术

攻击链描述:SolarWinds攻击(2020年发现,又称UNC2452/SUNBURST)是迄今为止最复杂的供应链攻击之一。攻击者通过篡改SolarWinds Orion平台的软件更新,向18,000多个客户分发后门(SUNBURST)。在攻击链中,DNS重定向技术起到了关键的C2隐蔽通信和域名前置作用。

DNS相关的关键技术

DNS技术具体应用取证发现
域名前置SUNBURST使用api.solarwinds.com作为TLS SNICDN级别流量隐藏
DGA备选硬编码C2失效时使用DGA生成的备选域名多层次C2弹性
DNS解析延迟后门在感染后等待12-14天才开始C2通信规避沙箱检测
合法域名伪装C2域名使用与SolarWinds相似的模式欺骗网络监控

取证发现要点

  • SUNBURST后门使用合法的SolarWinds域名(solarwinds-update.com)作为初始C2
  • 攻击者通过伪造的代码签名证书签署恶意DLL,绕过Windows信任链验证
  • DNS解析的异常模式:在感染初期,恶意软件会进行大量看似正常的DNS查询用于铺垫
  • 攻击者注册了与SolarWinds官网域名极其相似的域名(solarwindsworld.com vs solarwinds.com)

IOC示例

域名: avsvmcloud[.]com(DGA C2)
域名: digitalcollege[.]org
域名: virtualnewspaper[.]net
IP: 13.59.140.44
IP: 54.193.121.176
签名证书: SolarWinds Worldwide, LLC

经验教训

  1. DNS遥测是检测供应链攻击的前线——code signing的信任链验证需要DNS支撑
  2. 域名相似性分析应成为企业DNS监控的标配能力
  3. 出站DNS请求的异常模式(如C2通信前的准备工作)是重要的早期预警信号
  4. 供应链攻击的DNS取证需要跨多个阶段的时间线分析

案例二:TrickBot DGA基础设施的演化追踪

攻击链描述:TrickBot最初作为银行木马(2016年出现),后演化为多功能的恶意软件即服务平台,为Ryuk/Conti勒索软件提供初始访问。TrickBot最显著的特征是其复杂的DGA机制,经过多次版本迭代。

DGA演化时间线

版本出现时间DGA算法特征每日生成数检测状况
TrickBot v12016基于日期种子+MD5,长15-20字符100-200逆向完全
TrickBot v22018加入TLD权重,增加基于国家代码的TLD200-300逆向完全
TrickBot v32020引入多种子机制,支持备用种子300-500部分逆向
TrickBot v42022使用BIP32类算法,种子长度增加500-800部分逆向

取证发现要点

  • TrickBot的DGA域名生成依赖TLSH(Trend Micro Local Sensitive Hash)算法计算域名概率得分
  • 攻击者注册了约15%-20%的DGA域名作为C2节点
  • 通过分析未注册DGA域名的NXDOMAIN模式,可以预测下一次C2域名更新
  • Bellingcat等调查组织通过DNS被动数据追踪到TrickBot运营者的基础设施关联

IOC示例

# TrickBot DGA 种子示例(2018年变种)
trickbot_dga_seeds = [
    'alltheplacesyoulike',
    'greatmistakesaremade',
    'futureislookinggood',
    'thistimereallyworks',
    'betterlatethanever',
]

经验教训

  1. DGA的逆向需要持续的样本收集和分析——算法随版本更新而演化
  2. NXDOMAIN统计可以作为DGA感染检测的可靠指标
  3. 被Blacklist掩盖的良性DGA域名需要结合被动DNS验证
  4. 单家族的DGA检测需要覆盖多个变种,非通用规则能覆盖

案例三:Cobalt Strike域名前置与DNS隐蔽通信

Cobalt Strike是红队和APT组织广泛使用的商业渗透测试框架,其DNS Beacon和域名前置功能在真实攻击中被频繁使用。

通信特征

通信模式检测方式难度实际案例
HTTPS Beacon + 域名前置SNI/Host比对APT29、APT41
DNS Beacon(TXT查询)高熵TXT查询检测多国APT
HTTPS Beacon(标准CDN)JA3指纹比对国家级APT
SMB Beacon(内网)进程间管道极高横向移动阶段

防御实践

  • CDN级别的域名前置可以通过检查回源流量的Host头来检测
  • DNS Beacon的TXT查询往往具有固定间隔(如60秒)
  • 结合EDR的进程创建日志,关联dns.exe的子进程异常
  • 对HTTPS流量实施SNI白名单策略

0x0B 参考资料

  1. DNS隧道工具 iodine 官方文档. https://code.kryo.se/iodine/
  2. dnscat2 项目说明 - DNS隧道建立隐蔽信道. https://github.com/iagox86/dnscat2
  3. MITRE ATT&CK - DNS相关技术(T1572, T1568, T1583). https://attack.mitre.org/techniques/T1572/
  4. SolarWinds供应链攻击深度分析(Mandiant). https://www.mandiant.com/resources/solarwinds-supply-chain-attack
  5. TrickBot DGA逆向分析(Intezer). https://www.intezer.com/blog/malware-analysis/trickbot-dga-analysis/
  6. DNS劫持事件分析(ICANN). https://www.icann.org/resources/pages/dnssec-2012-02-25-en
  7. Passive DNS数据采集与分析指南(FIRST). https://www.first.org/global/passive-dns
  8. Domain Fronting技术分析(Netflix TechBlog). https://netflixtechblog.com/domain-fronting-1-5c5da7b7a6a8
  9. DNS over HTTPS 安全影响分析(CISA). https://www.cisa.gov/uscert/ncas/analysis-reports/ar21-112a
  10. DGA域名检测机器学习方法论文(Security & Privacy 2023). https://www.ieee-security.org/TC/SP2023/
  11. 被动DNS数据库CIRCL PDNS API. https://www.circl.lu/services/passive-dns/
  12. 勒索软件DNS通信模式分析(Unit42). https://unit42.paloaltonetworks.com/ransomware-dns-communication/