【実務・中級編】 TLSハンドシェイクにおけるJA3/JA3Sフィンガープリンティング:暗号化されたC2通信のプロファイル識別 – サイバーセキュリティとプライバシー保護実践ガイド

暗号化の裏側を暴く:JA3フィンガープリンティングでC2通信を狩り出す技術

ネットワークエンジニアとして現場に立っていると、「HTTPSで通信しているから安全だ」という言葉がいかに脆い幻想か、痛感させられる瞬間がある。現代の攻撃者は、TLSという強力な暗号化の盾を悪用し、C2(Command & Control)サーバーへの指示出しを「正規のHTTPSトラフィック」の中に巧妙に隠蔽する。

ファイアウォールでドメインを遮断しても、IPをブロックしても、彼らは次々と新しいインフラへ逃げ込む。この「いたちごっこ」に終止符を打つため、我々守備側が頼るべきは「通信の中身」ではなく、「通信の作法(フィンガープリント)」だ。

今回は、TLSハンドシェイクの機微を突く「JA3/JA3S」という技術に焦点を当て、実務の現場でどう活用すべきかを紐解いていこう。

—

なぜ「暗号化の中身」を見ずに識別できるのか?

TLSハンドシェイクの冒頭、クライアントはサーバーに対して「私はこんな暗号スイートに対応していて、こんな拡張機能が使えますよ」という意思表示を行う。これが ClientHello メッセージだ。

面白いことに、OS標準のブラウザと、攻撃者が自作したマルウェアの通信ライブラリでは、この ClientHello の組み立て方が微妙に異なる。

  • サポートしている暗号スイートの順序
  • 使用するTLSバージョンの範囲
  • 拡張フィールド(Server Name Indication や Supported Groups など)の構成

これらを特定の文字列として連結し、MD5ハッシュ化したものが JA3 だ。たとえ通信内容が強力に暗号化されていようと、この「握手の癖」までは隠せない。これこそが、C2通信をプロファイリングする強力な武器となる。

—

実際に指紋を採取してみる:Pythonでの実装例

理屈はわかった。では、どうやってこの「指紋」を採取するのか。難しく考える必要はない。scapy を使ってパケットをキャプチャし、ハンドシェイクのフィールドを抽出するスクリプトを書いてみよう。

from scapy.all import *
from scapy.layers.tls.all import *

# 現場で使うなら、特定のインターフェースを監視するスニファを作る
def extract_ja3(pkt):
    if pkt.haslayer(TLSClientHello):
        # 必要なフィールドを抽出
        version = pkt[TLSClientHello].version
        ciphers = [c.val for c in pkt[TLSClientHello].ciphers]
        extensions = [e.type for e in pkt[TLSClientHello].ext]
        
        # JA3の仕様に従いカンマ区切りで連結
        # フォーマット: Version,Ciphers,Extensions,Groups,EcPointFormats
        ja3_string = f"{version},{','.join(map(str, ciphers))},{','.join(map(str, extensions))}"
        
        # 最後にMD5ハッシュ化
        import hashlib
        ja3_hash = hashlib.md5(ja3_string.encode()).hexdigest()
        print(f"Detected JA3 Hash: {ja3_hash}")

# ens33インターフェースでパケットをキャプチャ開始
sniff(iface="ens33", filter="tcp port 443", prn=extract_ja3)

このコードを動かせば、ブラウザの通信と、curl や Pythonの requests が生成する通信で、全く異なるハッシュ値が出ることがわかるはずだ。マルウェアが WinHTTP を使っているのか、あるいは軽量な mbedTLS で構築されているのか。その「匂い」が、このハッシュ値に凝縮されている。

—

実務で直面する「JA3」の死角と限界

JA3は強力だが、銀の弾丸ではない。エンジニアとして、以下の限界を理解しておく必要がある。

1. ハッシュの衝突: 異なるライブラリでも、たまたま同じ設定値を持っていれば同じJA3値になることがある。
2. アップデートによる変動: ライブラリのバージョンが上がると、対応する暗号スイートが変わる可能性がある。つまり、一度登録した「悪意のあるハッシュ」が永遠に使えるわけではない。
3. JA3Sとの組み合わせ: クライアント側のJA3だけでなく、サーバー側からのレスポンスの癖を捉える JA3S とセットで判断するのが定石だ。通信は「対話」である。クライアントとサーバー、両方の指紋が一致したとき、その信頼性は飛躍的に高まる。

—

運用への落とし込み:IDS/IPSとログ分析

現場のインフラ運用においては、個別のスクリプトを回すだけでなく、既存のセキュリティスタックにこの知見を組み込むことが重要だ。

Zeek (旧Bro) のようなネットワークIDSを使っているなら、標準でJA3がログ出力される。

# Zeekのログファイル(conn.logやssl.log)から確認できるフィールド例
# JA3ハッシュを抽出して、既知のマルウェアDBと照合する
event ssl_client_hello(c: connection, version: count, record_version: count, possible_ts: time, client_hello: table[count] of count, cipher_suites: vector of count, compression_methods: vector of count, extensions: table[count] of string)
{
    # ここで抽出した情報をインテリジェンス・フィードと突き合わせる
    # 怪しいハッシュならアラートを飛ばす運用を構築する
}

最後に:ネットワークの「解像度」を上げろ

ネットワークエンジニアの仕事は、単に「パケットを通すこと」ではない。パケットが何を語っているか、その行間を読むことだ。

JA3フィンガープリンティングは、暗号化という「不可視の壁」に対して、我々が持ちうる数少ない「可視化のレンズ」の一つだ。まずは手元のサーバーやコンテナから出る通信をハッシュ化し、普段のトラフィックがどんな「指紋」を描いているのかを確認することから始めてほしい。

異常を検知する力は、正常を誰よりも深く知っている者にこそ宿る。あなたのネットワークの守りを、一歩先へ進めていこう。

コメント

タイトルとURLをコピーしました