あなたがブラウザに bank.example と打ち込んだその瞬間、最初に起きるのは名前解決——ドメイン名をIPアドレスに変換する DNS の問い合わせです。もしこの一手を攻撃者に乗っ取られたら? URL バーには正しいドメインが表示されたまま、接続先だけが攻撃者のサーバーにすり替わります。これが DNSスプーフィングであり、リゾルバのキャッシュごと毒するキャッシュポイズニングは、一度の成功で多数の利用者を巻き込む、極めて影響範囲の広い攻撃です。
この記事は、ネットワークペンテストの全体像に続き、DNS 攻撃の原理を歴史的な Kaminsky 攻撃から解き、RFC 5452 と DNSSEC による防御を設定とコードで示します。根本にあるのは、初期DNSが UDP上の「認証なき応答」を信じてしまうという、TCP/IP の信頼の前提です。
安全地帯の徹底:本記事の検証は、すべて隔離ラボの**自分が立てたDNSリゾルバ(例 BIND/Unbound のローカル実験機)**に対してのみ行います。公開リゾルバや他者のDNSへ偽答を注入する行為は、不正アクセス禁止法違反であり、社会インフラへの攻撃です。検証してよいのは自分の資産だけ——法律の記事を必ず先に。
1. DNS が騙される仕組み — 「速く・それらしい偽答」
キャッシュDNSリゾルバが bank.example の問い合わせを受け、まだキャッシュにない場合、上位の権威サーバーへ問い合わせます。攻撃者の狙いは、正規の権威サーバーの応答より先に、偽の応答をリゾルバに信じさせることです。
リゾルバが偽答を受け入れてしまうには、偽答が次をすべて満たす必要があります。
正規応答とマッチさせるべき要素:
1. トランザクションID(16ビット)が問い合わせと一致
2. 送信元/宛先ポートが一致
3. 質問セクション(QNAME/QTYPE)が一致
4. 正規応答より「速く」届く(レース)
かつてリゾルバは送信元ポートを固定し、トランザクションIDも予測しやすい実装がありました。すると攻撃者が当てるべきは実質**16ビットのID(65,536通り)**だけ。さらにキャッシュ済みなら次のTTLまで待つ必要がありますが——
1.1 Kaminsky 攻撃(2008)— DNS を作り直させた一撃
2008年、Dan Kaminsky は決定的な手法を公表しました。存在しないサブドメイン(random123.bank.example)を大量に問い合わせ、それぞれに対して「bank.example の権威サーバーは攻撃者のIPだ」という委任(NS/glue)を含む偽答を撃ち込む。存在しない名前なのでキャッシュのTTL待ちを回避でき、何度でも高速に試行できる。一度当たれば、ドメイン全体の権威がキャッシュに毒として刻まれます。
これは「16ビットIDの推測は現実的に破れる」ことを示し、DNS の防御を根本から見直させました。その答えが次の RFC 5452 と DNSSEC です。
2. 防御①:RFC 5452 — 推測を「非現実的」にする
RFC 5452(Measures for Making DNS More Resilient against Forged Answers, 2009)は、偽答を受け入れにくくする実装ルールを標準化しました。中心はエントロピーの追加です。
- 送信元ポートのランダム化:問い合わせごとに送信元UDPポートをランダムに選ぶ。攻撃者はIDに加えてポート(最大16ビット)も当てねばならず、組み合わせのエントロピーが実質約32ビットに跳ね上がる。総当たりは非現実的になる。
- トランザクションIDのランダム化:予測可能なIDを避け、暗号的に乱数化する。
- 0x20 エンコーディング(補助):QNAME の英字の大小をランダム化し、応答でそれが保たれるかを追加の検証材料にする。
# 自分のリゾルバが送信元ポートをランダム化しているかを確認(自分の資産で)
# 良い例:問い合わせごとに source port がばらける(高エントロピー)
dig +short porttest.dns-oarc.net TXT @127.0.0.1
# → "GREAT" 等(リゾルバのポートランダム性の評価が返る)
重要な限界:RFC 5452 は「推測を困難にする」確率的防御であって、「不可能にする」ものではありません。経路上にいる(MITM 済みの)攻撃者にはエントロピーが意味をなさない。だから次の DNSSEC が要ります。
3. 防御②:DNSSEC — 「言った者勝ち」を暗号で終わらせる
DNSSEC(RFC 4033/4034/4035)は、DNS 攻撃への本質的な答えです。発想は明快——権威サーバーが応答にデジタル署名し、リゾルバがその署名を検証する。偽答は正しい署名を作れないため、検証で弾かれます。
DNSSEC の信頼の連鎖(chain of trust):
ルート(.) の鍵 ──署名──► .example の鍵 ──署名──► bank.example のレコード
▲ │
└──── リゾルバはルートの鍵(トラストアンカー)から ◄────┘
署名を辿って検証。1つでも署名が合わなければ SERVFAIL(拒否)
- RRSIG:各レコードセットへの署名。
- DNSKEY:検証に使う公開鍵。
- DS:親ゾーンに置かれ、子ゾーンの鍵を保証する(連鎖の輪)。
検証が成功すると、応答に AD(Authenticated Data)ビットが立ちます。偽答は署名検証に失敗し、リゾルバは結果を返しません。
# DNSSEC 検証が効いているかを確認(AD フラグが立つか)
dig +dnssec bank.example @127.0.0.1
# ヘッダの flags に "ad" があれば、署名検証済みの真正な応答
# RRSIG レコードが応答に含まれる
3.1 アプリ/リゾルバ側で検証を「要求」する
権威ゾーンに署名してあっても、リゾルバが検証しなければ意味がありません。検証するリゾルバを使うことが利用者側の責務です。
import { Resolver } from "node:dns/promises";
/**
* DNSSEC 検証を行うリゾルバ(例:ローカルの validating resolver)を明示的に指定する。
* OS のデフォルトに任せず「検証するリゾルバ」へ向けることで、毒入りキャッシュを避ける。
* 失敗(SERVFAIL)は握りつぶさず例外として扱い、安全側(接続しない)に倒す。
*/
export async function resolveValidated(hostname: string): Promise<readonly string[]> {
const resolver = new Resolver();
resolver.setServers(["127.0.0.1"]); // 検証する自前/信頼リゾルバ(Unbound 等)
try {
return await resolver.resolve4(hostname);
} catch (err) {
// SERVFAIL は「署名検証に失敗した可能性」を含む。曖昧なまま接続しない。
throw new Error(`DNS resolution failed (possible DNSSEC failure) for ${hostname}: ${(err as Error).message}`);
}
}
4. 防御③:DoH / DoT — 経路を暗号化する
DNSSEC は応答の**真正性(改ざんされていないか)**を守りますが、機密性(誰が何を引いたかを盗み見られない)は守りません。また、利用者からリゾルバまでの経路はなお平文です。ここを埋めるのが通信路の暗号化です。
- DoT(DNS over TLS, RFC 7858):DNS を TLS(853番)で運ぶ。
- DoH(DNS over HTTPS, RFC 8484):DNS を HTTPS(443)で運ぶ。検閲・盗聴に強い。
役割分担(両方が要る):
DNSSEC … 応答の真正性(偽答を弾く) ← 権威〜リゾルバの「中身」を守る
DoH/DoT … 経路の機密性・完全性(盗聴/改ざん) ← 利用者〜リゾルバの「経路」を守る
DNSSEC と DoH/DoT は競合ではなく補完です。前者は「答えが本物か」、後者は「経路が覗かれないか」。本番では両方を組み合わせます。
5. 設計者のためのチェックリスト
自社ドメインと名前解決を守るために、設計・運用で確認すべき点です。
- 自社の権威ゾーンを DNSSEC 署名しているか(ドメインレジストラ/DNSサービスで DS を親に登録)。
- アプリが向くリゾルバが DNSSEC 検証を行うか(マネージドリゾルバなら検証ポリシーを確認)。
- リゾルバが送信元ポートをランダム化しているか(RFC 5452 準拠)。
- クライアント〜リゾルバ間で DoH/DoT を使えているか。
- DNS 解決失敗(SERVFAIL)を握りつぶさず、安全側(接続中止)に倒しているか。
- そもそも最終防壁は TLS の証明書検証——名前解決が毒されても、正規証明書を持たない攻撃者サーバーには TLS が繋がらない(MITM 記事参照)。
多層で考える:DNS が毒されても TLS で守られ、TLS を狙う MITM は mTLS とゼロトラストで守られる。単一の防御に依存しないのがネットワークセキュリティの一貫した姿勢です。
6. まとめ
- DNSスプーフィング/キャッシュポイズニングは名前解決を乗っ取る。Kaminsky 攻撃が16ビットID推測の危険を現実化し、DNS の防御を作り直させた。
- RFC 5452:送信元ポート+IDのランダム化でエントロピーを約32ビット級へ。推測を非現実的にする確率的防御(経路上の攻撃者には無力)。
- DNSSEC(RFC 4033-4035):署名と信頼の連鎖で偽答を弾く本質的防御。AD ビットが真正性の証。
- DoH/DoT(RFC 7858/8484):経路の暗号化。DNSSEC(真正性)と補完関係で両方要る。
- 最後はやはり TLS:名前解決が毒されても、証明書検証が攻撃者サーバーを拒む。多層防御。
次は、確立済みのTCP接続そのものを奪う**TCPセッションハイジャック / RSTインジェクション / IPスプーフィング**を、RFC 5961 の防御まで扱います。
私(友田 陽大)は、本番システムの DNS 設計(DNSSEC・プライベートDNS・解決失敗時のフェイルセーフ)と、TLS/証明書運用を含む“名前解決を信頼しきらない”設計を実装してきました。「自社ドメインを DNSSEC 署名したい」「DNS 解決の失敗が障害につながっている」「DoH/DoT を導入したい」——こうした名前解決の堅牢化を、攻撃者の視点で診断し、真正性と機密性の両輪で実装します。お気軽にご相談ください。