DNSログ流出分析 は何をしますか?
使用タイミング
このスキルは以下の場合に使用します:
- SOCチームがファイアウォール/プロキシ制御をバイパスするDNSトンネリングによるデータ流出を疑っている場合
- 脅威インテリジェンスが攻撃者によるDNSベースのC2チャネル(例:Cobalt Strike DNSビーコン)の使用を示している場合
- UEBAが特定のホストからの異常なDNSクエリ量を検出した場合
- マルウェア解析でDNS-over-HTTPS(DoH)またはDNSトンネリング機能が明らかになった場合
使用しないでください 標準的なDNSトラブルシューティングや可用性監視には使用しません。このスキルはセキュリティ関連のDNS悪用検出に焦点を当てています。
前提条件
- DNSクエリログが有効になっていること(Windows DNS Server、Bind、Infoblox、Cisco Umbrella)
- DNSログがSIEMに取り込まれていること(
Stream:DNS、dnssourcetypeを使用したSplunk、またはZeek DNSログ) - 過去のドメイン解決分析のためのパッシブDNSデータ
- 正常なDNS動作のベースライン(クエリ量、ドメイン分布、TXTレコード頻度)
- Pythonの
mathおよびcollectionsライブラリ(エントロピー計算用)
ワークフロー
ステップ1:サブドメイン長分析によるDNSトンネリングの検出
DNSトンネリングはサブドメインラベルにデータをエンコードするため、異常に長いクエリが発生します:
index=dns sourcetype="stream:dns" query_type IN ("A", "AAAA", "TXT", "CNAME", "MX")
| eval domain_parts = split(query, ".")
| eval subdomain = mvindex(domain_parts, 0, mvcount(domain_parts)-3)
| eval subdomain_str = mvjoin(subdomain, ".")
| eval subdomain_len = len(subdomain_str)
| eval tld = mvindex(domain_parts, -1)
| eval registered_domain = mvindex(domain_parts, -2).".".tld
| where subdomain_len > 50
| stats count AS queries, dc(query) AS unique_queries,
avg(subdomain_len) AS avg_subdomain_len,
max(subdomain_len) AS max_subdomain_len,
values(src_ip) AS sources
by registered_domain
| where queries > 20
| sort - avg_subdomain_len
| table registered_domain, queries, unique_queries, avg_subdomain_len, max_subdomain_len, sources
ステップ2:高エントロピードメインクエリの検出(DGA検出)
ドメイン生成アルゴリズムはランダムに見えるドメインを生成します:
index=dns sourcetype="stream:dns"
| eval domain_parts = split(query, ".")
| eval sld = mvindex(domain_parts, -2)
| eval sld_len = len(sld)
| eval char_count = sld_len
| eval vowels = len(replace(sld, "[^aeiou]", ""))
| eval consonants = len(replace(sld, "[^bcdfghjklmnpqrstvwxyz]", ""))
| eval digits = len(replace(sld, "[^0-9]", ""))
| eval vowel_ratio = if(char_count > 0, vowels / char_count, 0)
| eval digit_ratio = if(char_count > 0, digits / char_count, 0)
| where sld_len > 12 AND (vowel_ratio < 0.2 OR digit_ratio > 0.3)
| stats count AS queries, dc(query) AS unique_domains, values(src_ip) AS sources
by query
| where unique_domains > 10
| sort - queries
Pythonを使用したDNSクエリのシャノンエントロピー計算:
import math
from collections import Counter
def shannon_entropy(text):
"""文字列のシャノンエントロピーを計算"""
if not text:
return 0
counter = Counter(text.lower())
length = len(text)
entropy = -sum(
(count / length) * math.log2(count / length)
for count in counter.values()
)
return round(entropy, 4)
# テスト例
normal_domain = "google" # 低エントロピー
dga_domain = "x8kj2m9p4qw7n" # 高エントロピー
tunnel_subdomain = "aGVsbG8gd29ybGQ.evil.com" # Base64エンコードデータ
print(f"通常: {shannon_entropy(normal_domain)}") # ~2.25
print(f"DGA: {shannon_entropy(dga_domain)}") # ~3.70
print(f"トンネル: {shannon_entropy(tunnel_subdomain)}") # ~3.50
# しきい値:サブドメインのエントロピー > 3.5 = トンネリング/DGAの可能性高い
Splunkでのエントロピースコアリング実装:
index=dns sourcetype="stream:dns"
| eval domain_parts = split(query, ".")
| eval check_string = mvindex(domain_parts, 0)
| eval check_len = len(check_string)
| where check_len > 8
| eval chars = split(check_string, "")
| stats count AS total_chars, dc(chars) AS unique_chars by query, src_ip, check_string, check_len
| eval entropy_estimate = log(unique_chars, 2) * (unique_chars / check_len)
| where entropy_estimate > 3.5
| stats count AS high_entropy_queries, dc(query) AS unique_queries by src_ip
| where high_entropy_queries > 50
| sort - high_entropy_queries
ステップ3:異常なDNSクエリ量の検出
異常なDNSトラフィックを生成するホストを特定します:
index=dns sourcetype="stream:dns" earliest=-24h
| bin _time span=1h
| stats count AS queries, dc(query) AS unique_domains by src_ip, _time
| eventstats avg(queries) AS avg_queries, stdev(queries) AS stdev_queries by src_ip
| eval z_score = (queries - avg_queries) / stdev_queries
| where z_score > 3 OR queries > 5000
| sort - z_score
| table _time, src_ip, queries, unique_domains, avg_queries, z_score
TXTレコードの悪用検出(一般的なトンネリング手法):
index=dns sourcetype="stream:dns" query_type="TXT"
| stats count AS txt_queries, dc(query) AS unique_txt_domains,
values(query) AS domains by src_ip
| where txt_queries > 100
| eval suspicion = case(
txt_queries > 1000, "CRITICAL — DNSトンネリングの可能性高い",
txt_queries > 500, "HIGH — DNSトンネリングの可能性あり",
txt_queries > 100, "MEDIUM — 異常なTXT量"
)
| sort - txt_queries
| table src_ip, txt_queries, unique_txt_domains, suspicion
ステップ4:既知のDNSトンネリングツールの検出
一般的なDNSトンネリングツールのシグネチャを検索します:
index=dns sourcetype="stream:dns"
| eval query_lower = lower(query)
| where (
match(query_lower, "\.dnscat\.") OR
match(query_lower, "\.dns2tcp\.") OR
match(query_lower, "\.iodine\.") OR
match(query_lower, "\.dnscapy\.") OR
match(query_lower, "\.cobalt.*\.beacon") OR
query_type="NULL" OR
(query_type="TXT" AND len(query) > 100)
)
| stats count by src_ip, query, query_type
| sort - count
ローカルDNSをバイパスするDNS over HTTPS(DoH)の検出:
index=proxy OR index=firewall
dest IN ("1.1.1.1", "1.0.0.1", "8.8.8.8", "8.8.4.4",
"9.9.9.9", "149.112.112.112", "208.67.222.222")
dest_port=443
| stats sum(bytes_out) AS total_bytes, count AS connections by src_ip, dest
| where connections > 100 OR total_bytes > 10485760
| eval alert = "DoHバイパスの可能性 — HTTPS経由で公開リゾルバーにDNSクエリ送信"
| sort - total_bytes
ステップ5:DNS調査結果とエンドポイントデータの相関
不審なDNSをプロセスデータとクロスリファレンスします:
index=dns src_ip="192.168.1.105" query="*.evil-tunnel.com" earliest=-24h
| stats count AS dns_queries, earliest(_time) AS first_query, latest(_time) AS last_query
by src_ip, query
| join src_ip [
search index=sysmon EventCode=3 DestinationPort=53 Computer="WORKSTATION-042"
| stats count AS connections, values(Image) AS processes by SourceIp
| rename SourceIp AS src_ip
]
| table src_ip, query, dns_queries, first_query, last_query, processes
ステップ6:データ流出量の推定
DNSクエリにエンコードされたデータ量を推定します:
index=dns src_ip="192.168.1.105" query="*.evil-tunnel.com" earliest=-24h
| eval domain_parts = split(query, ".")
| eval encoded_data = mvindex(domain_parts, 0)
| eval encoded_bytes = len(encoded_data)
| eval decoded_bytes = encoded_bytes * 0.75 -- Base64デコード係数
| stats sum(decoded_bytes) AS total_bytes_estimated, count AS total_queries,
earliest(_time) AS first_seen, latest(_time) AS last_seen
| eval estimated_kb = round(total_bytes_estimated / 1024, 1)
| eval estimated_mb = round(total_bytes_estimated / 1048576, 2)
| eval duration_hours = round((last_seen - first_seen) / 3600, 1)
| eval rate_kbps = round(estimated_kb / (duration_hours * 3600) * 8, 2)
| table total_queries, estimated_mb, duration_hours, rate_kbps, first_seen, last_seen
主要概念
| 用語 | 定義 |
|---|---|
| DNSトンネリング | DNSクエリ/レスポンス内にデータをエンコードしてデータを流出させたり、DNS経由でC2チャネルを確立する手法 |
| DGA | ドメイン生成アルゴリズム — C2の耐障害性のために疑似ランダムなドメイン名を生成するマルウェア技術 |
| シャノンエントロピー | 文字列のランダム性を測定する数学的尺度 — ドメイン名で高エントロピー(>3.5)はDGAまたはトンネリングを示す |
| TXTレコード悪用 | テキストデータ用に設計されたDNS TXTレコードを、データトンネリングの高帯域幅チャネルとして使用する行為 |
| DNS over HTTPS(DoH) | HTTPS(ポート443)上で暗号化されたDNSクエリ、従来のDNS監視をバイパスする |
| パッシブDNS | ドメインが時間の経過とともにどのIPに解決されたかを示す過去のDNS解決記録 |
ツールとシステム
- Splunk Stream: SIEM分析用に解析されたDNSクエリデータを提供するネットワークトラフィックキャプチャアドオン
- Zeek(Bro): 詳細なDNSトランザクションログを生成するネットワークセキュリティモニター
- Cisco Umbrella(OpenDNS): 悪意のあるドメインをブロックし、クエリデータをログに記録するクラウドDNSセキュリティプラットフォーム
- Infoblox DNS Firewall: RPZベースのブロックと詳細なクエリログを提供するDNSレイヤーセキュリティ
- Farsight DNSDB: 過去のドメイン解決ルックアップとインフラストラクチャマッピングのためのパッシブDNSデータベース
一般的なシナリオ
- Cobalt Strike DNSビーコン: C2ドメインへのエンコードされたペイロードを含む定期的なTXTクエリを検出
- データ流出: Base64/hexでエンコードされた盗難データを含む大量の一意のサブドメインクエリ
- DGAマルウェア: アルゴリズムで生成されたドメイン(高エントロピー、Webコンテンツなし)へのDNSクエリを検出
- DNS-over-HTTPSバイパス: 企業のDNSフィルタリングと監視をバイパスするためにDoHを使用する従業員
- 低速流出(Slow Drip Exfiltration): しきい値アラートを下回る低量のDNSトンネリング(ベースライン比較が必要)
出力形式
DNS流出分析 — WORKSTATION-042
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
期間: 2024-03-14 から 2024-03-15
送信元: 192.168.1.105 (WORKSTATION-042, 財務部)
検出結果:
[CRITICAL] evil-tunnel[.]com へのDNSトンネリングを検出
クエリ量: 18時間で12,847クエリ
平均サブドメイン長: 63文字(通常: <20)
平均エントロピー: 3.82(しきい値: 3.5)
クエリタイプ: TXT(89%)、A(11%)
推定データ量: DNS経由で約4.7MB流出
レート: 0.58 kbps(低速パターン)
[HIGH] DGAに類似したドメインが解決されました
一意のDGAドメイン: 247ドメインが解決されました
パターン: 15文字のランダム英数字.xyz TLD
エントロピー範囲: 3.6 - 4.1
プロセス特定:
プロセス: svchost_update.exe(偽装 — 正当なsvchostではない)
PID: 4892
親プロセス: explorer.exe
ハッシュ: SHA256: a1b2c3d4... (VT: 34/72 悪意あり — Cobalt Strikeビーコン)
封じ込め:
[完了] EDRによりホスト隔離
[完了] ドメイン evil-tunnel[.]com をDNSシンクホールに追加
[完了] インシデント IR-2024-0448 作成