LinuxにおけるDNSトンネリングの検知と防御:データ持ち出しと悪意あるC2通信を阻止する

Security tutorial - IT technology blog
Security tutorial - IT technology blog

課題:ポート53に潜む盲点

多くの境界ファイアウォールはインバウンド(受信)トラフィックを厳格に遮断する一方で、ポート80、443、そして53へのアウトバウンド(送信)UDP/TCPトラフィックは素通りさせがちです。システム管理者は、外部へのDNS通信を無害なバックグラウンド処理とみなしてしまうことが少なくありません。

サーバーがaptミラー、NTPプール、各種APIへ接続するにはドメイン名の名前解決が欠かせないためです。攻撃者はまさにこの盲点を突いてきます。エグレスファイアウォールによってSSH、HTTPS、生のTCP接続が直接ブロックされていても、DNSトンネリングを用いれば境界をすり抜けて企業の機密情報を持ち出したり、対話型のC2(Command and Control)シェルを実行したりすることが容易にできてしまいます。

再帰リゾルバが内部マシンに代わってクエリを中継するため、悪意あるパケットは何の障壁もなく内部VLANの境界を越えていきます。そこからリクエストは攻撃者が管理する権威ネームサーバーへと直接送られます。一般的なDPI(ディープパケットインスペクション)ファイアウォールはサブドメインを詳細に精査しないことが多く、ランダム化された膨大なクエリを通常の検索トラフィックと誤認してしまいます。

数年前の深夜2時、運用中のデータベースサーバーが激しいブルートフォース攻撃を受けました。このインシデントをきっかけに、私はSSHの堅牢化、sudo権限の制限、そして全ホストへのFail2ban導入を実施しました。これにより自動スキャナーは完全に遮断できました。しかし数か月後に外部への送信ネットワーク接続を監査した際、さらに深刻な問題に気づいたのです。デフォルトのリゾルバ設定のせいで、パブリックインターネットへ直接つながるエグレストラフィックの抜け道が完全に開いたままになっていたのです。

基本概念:DNSトンネリングの仕組み

DNSトンネリングは、BIND、Unbound、systemd-resolvedなどのソフトウェアの脆弱性を突くものではありません。DNSプロトコル自体の基本設計を悪用する攻撃です。ホストを守るには、まずトンネリングツールがどのようにデータをカプセル化し、C2セッションを維持しているかを理解する必要があります。

1. データ持ち出し(Exfiltration)の仕組み

DNS通信は低速です。スループットは通常1 KB/s〜100 KB/s程度にとどまります。それでも、APIトークン、SSH鍵、SQLダンプなどを盗み出すには十分すぎる速度です。攻撃者は機密ファイルを小さなバイト単位に分割し、Base32や16進数(hex)でエンコードした上で、カスタムドメインのサブドメインとして付加します:

# ルックアップ内に埋め込まれたエンコード済みの機密文字列
data-chunk-4d616c6963696f7573.c2-listener.example.com

ローカルホストは設定されたリゾルバにIPアドレスの問い合わせを行います。リゾルバはルートヒントやTLDサーバーを経由する標準の委任チェーンに従って名前解決を進めます。

最終的にクエリは、c2-listener.example.comの権威ネームサーバーへと転送されます。攻撃者のカスタムデーモンがクエリログからリクエストを捕捉し、ドメインサフィックスを削除してデコードしたバイト列を結合することで、元のファイルを復元します。ここで注目すべきは、侵害されたサーバーが攻撃者に対して直接ネットワークソケットを一度も確立していないという点です。

2. C2(Command and Control)チャネル

本格的なC2フレームワークは、DNSを双方向トンネルとして利用します。Linuxクライアントは数秒おきに攻撃者のドメインへポーリングを行い、カスタムネームサーバーはそれに応答してペイロードを含んだレコードを返します:

  • TXTレコード: 最も一般的に使用されるトランスポートです。単一のTXT文字列は最大255文字を保持でき、1つのレコード内に複数の文字列を格納することで、Base64エンコードされた数キロバイトのシェル出力を伝送できます。
  • CNAMEおよびAレコード: 簡易的なハートビート、ルーティングマーカー、コマンド実行の確認応答(ACK)などに使用されます。
  • NULLレコード: RFC 1035で定義されており、最大65,535バイトの生バイナリデータを格納できます。iodineなどの攻撃ツールは、スループットを稼ぐためにNULLレコードを好んで利用します。

3. トンネリングトラフィックの統計的特徴

通常のリゾルバトラフィックは人間の活動習慣や予測可能なWeb依存関係に沿った動きを見せます。しかし統計的な観点から分析すると、トンネリングトラフィックは全く異なる様相を呈します:

  • 高いシャノンエントロピー: 正当なホスト名に含まれる英単語のエントロピーは通常2.2〜3.0程度です。一方、暗号化またはBase64エンコードされたサブドメインは3.8〜4.6に達し、文字分布がランダムに近いことを示します。
  • 肥大化したラベル長: 通常のホスト名検索でラベルが25文字を超えることは稀です。トンネリングエージェントは1回のクエリに可能な限り多くのデータを詰め込もうとするため、RFC 1035の制限(ラベルあたり最大63文字、ドメイン名全体で最大253文字)の限界近くまで使用します。
  • キャッシュバスター: リゾルバはレイテンシを短縮するためにレコードをキャッシュします。しかしトンネリングペイロードは毎回一意なサブドメイン文字列を使用するため、再帰リゾルバのキャッシュミス率はほぼ100%に達し、1時間に数万件もの固有クエリが発生します。

実践手順:検知・分析・防御

ステップ1: Linux上でのDNSトラフィックのキャプチャと検査

まず、tsharkを使用してプライマリインターフェース(eth0やens192など)上の送信DNSトラフィックをキャプチャします。送信クエリをフィルタリングし、ワークロードが具体的にどのホスト名をリクエストしているかを確認します。

# 送信DNSクエリをキャプチャし、リクエストされたホスト名を表示
sudo tshark -i eth0 -f "src port 53 or dst port 53" -Y "dns.flags.response == 0" -T fields -e ip.src -e dns.qry.name

侵入者がdnscat2やiodineなどのトンネリングエージェントを稼働させている場合、同一のルートドメイン宛てに高エントロピーで長いリクエストが何百件も押し寄せ、ターミナルが埋め尽くされます:

10.0.0.15   01a3f89c2b0e7741.tunnel.badactor.io
10.0.0.15   02e4d91a8c33902f.tunnel.badactor.io
10.0.0.15   03c801ba92d4f16a.tunnel.badactor.io

ステップ2: Pythonを用いた高エントロピークエリの検知

送受信されるクエリラベルのシャノンエントロピーを計算することで、データ流出をリアルタイムに検知できます。エントロピーとラベル長が同時に急増する現象は、トンネリング活動の確度の高い特徴です。

import math
from collections import Counter

def calculate_entropy(text: str) -> float:
    """入力文字列のシャノンエントロピーを計算する"""
    if not text:
        return 0.0
    length = len(text)
    frequencies = Counter(text)
    entropy = -sum((count / length) * math.log2(count / length) for count in frequencies.values())
    return round(entropy, 2)

def analyze_query(domain: str):
    labels = domain.strip(".").split(".")
    if len(labels) < 2:
        return
    
    subdomain_payload = labels[0]
    entropy = calculate_entropy(subdomain_payload)
    length = len(subdomain_payload)
    
    # 正規のラベルが30文字を超え、エントロピーが3.8を上回ることは稀
    if length > 30 and entropy > 3.8:
        print("[警告] 不審なDNSクエリを検知しました!")
        print(f"  対象FQDN: {domain}")
        print(f"  第1ラベル: {subdomain_payload[:24]}... ({length} 文字)")
        print(f"  エントロピー: {entropy} (しきい値: >3.8)\n")

# 実際のサンプルでテスト
analyze_query("4d616c6963696f75735061796c6f616444617461.tunnel.badactor.io")
analyze_query("sentry-cdn-origin-east.internal-infra.net")

1つ目のクエリは即座に警告を発生させます。2つ目のクエリは22文字ありますが、可読性のある単語で構成されているためエントロピーは2.9前後に抑えられ、誤検知(フォールスポジティブ)を防ぐことができます。

ステップ3: Netfilterを用いた送信DNSアクセスの制限

本番ノードが8.8.8.8や1.1.1.1などのパブリックリゾルバと直接通信できるようにしておくのは危険です。侵害されたホストは、攻撃者が用意した不正な権威サーバーへ直接クエリを送信することで、ローカルのDNSフィルターを容易に回避してしまいます。

Linuxゲートウェイまたはローカルのiptablesファイアウォールを厳格に制限しましょう。すべてのホストに対し、指定した内部DNSサーバー(例:10.0.0.2および10.0.0.3)経由でのみ名前解決を行うよう強制します:

# 1. 承認された内部リゾルバへの送信DNSのみを許可
sudo iptables -A OUTPUT -p udp -d 10.0.0.2 --dport 53 -j ACCEPT
sudo iptables -A OUTPUT -p tcp -d 10.0.0.2 --dport 53 -j ACCEPT
sudo iptables -A OUTPUT -p udp -d 10.0.0.3 --dport 53 -j ACCEPT
sudo iptables -A OUTPUT -p tcp -d 10.0.0.3 --dport 53 -j ACCEPT

# 2. その他すべての送信ポート53トラフィックを即座にブロック
sudo iptables -A OUTPUT -p udp --dport 53 -j REJECT --reject-with icmp-port-unreachable
sudo iptables -A OUTPUT -p tcp --dport 53 -j REJECT --reject-with tcp-reset

これらのルールにより、外部への直接的な漏洩ルートが塞がれます。すべてのクエリは中央リゾルバを経由することになり、監視ツールでのパースやログ記録が可能になります。

ステップ4: 内部リゾルバの堅牢化(RPZとレートリミット)

集約された内部リゾルバ自体も保護する必要があります。BIND9を運用している場合は、レスポンスレート制限(RRL: Response Rate Limiting)を有効にして機械的なクエリバーストを抑制します。さらにレスポンスポリシーゾーン(RPZ: Response Policy Zone)を組み合わせることで、既知の悪意あるC2ドメインをシンクホールへ誘導します。

/etc/bind/named.conf.optionsを開き、設定を更新します:

options {
    directory "/var/cache/bind";

    # 再帰問い合わせを内部サブネットのみに制限
    recursion yes;
    allow-recursion { 127.0.0.1; 10.0.0.0/16; };

    # レスポンスレート制限(RRL)により連続するC2バースト通信を抑制
    rate-limit {
        responses-per-second 15;
        window 5;
    };

    # レスポンスポリシーゾーン(RPZ)のブロックリストを適用
    response-policy { zone "rpz.blocklist"; };
};

次に、悪意あるドメインへのクエリをドロップするため、/etc/bind/db.rpz.blocklistにシンクホールファイルを定義します:

; ファイル: /etc/bind/db.rpz.blocklist
$TTL 1H
@       IN      SOA     localhost. root.localhost. ( 2026102801 3H 1H 1W 1H )
        IN      NS      localhost.

; C2ドメインをNXDOMAINまたはループバックにシンクホール
*.badactor.io                   CNAME   .
badactor.io                     CNAME   .
*.c2-listener.example.com       A       127.0.0.1

sudo rndc reloadで設定を再読み込みします。感染したノードがbadactor.io配下のデータ持ち出し用アドレスを解決しようとしても、BINDは上流にパケットを一切送信せず、即座にNXDOMAINエラーを返します。

まとめ

宛先を制限せずにポート53を開放したままにすることは、意図せず自社のDNSリゾルバを攻撃者のプロキシとして提供してしまうことにつながります。Linuxにおける確実な防御は、異常な高エントロピークエリパターンの検知と、エンドポイントの通信先を堅牢化された内部リゾルバのみに絞るエグレス制限という2つのレイヤーによって成り立ちます。今すぐ10分時間を取って、ファイアウォールルールの監査とクエリログの確認を行ってみてください。ポート53がデータの漏洩口ではなく、正当なサービスの名前解決のためだけに使われていることを確認しましょう。

Share: