パケットとフロー情報の分析
観測点、通信量、検知条件、外部装置の措置を対応付けて評価します。
説明
NDRはネットワーク上の観測から接続や振る舞いを分析します。観測位置、暗号化、パケットの欠落によって得られる情報が変わるため、記録の項目が何を数えているかを確認します。
固定pcapをPythonで開き、アドレス、ポート、TCPデータ量を集計します。SuricataのHTTP・接続記録との対応と、NATやTLSによる見え方の違いも確認します。
観測点、通信量、検知条件、外部装置の措置を対応付けて評価します。
1取得したフレームを一覧にする
時刻、送信元、宛先、両ポート、TCPデータ長、フレーム長の順です。
from sp06 import *
rows=frames取得したフレームを一覧にする()
for row in rows: print(row)6枚のフレームがあります。データが0バイトでもTCPの接続確立や確認応答として意味があります。
2接続の両方向をそろえる
端点の組を順序によらずまとめます。
pairs={tuple(sorted接続の両方向をそろえる(((s,sp),(d,dp)))) for t,s,d,sp,dp,n,frame in rows}
print(sorted(pairs))2つの方向が一つの接続にまとまります。一般にはプロトコル、開始・終了、再利用されたタプルも含めて接続を分離します。
3送信方向のデータ量を数える
端末192.0.2.7からのTCPデータ長を合計します。
print(sum送信方向のデータ量を数える(n for t,s,d,sp,dp,n,f in rows if s=='192.0.2.7'))
print(sum(f for t,s,d,sp,dp,n,f in rows))TCPデータ量と全フレーム長の合計は異なります。ヘッダーを含む転送量とアプリのデータ量を同じ尺度として扱いません。
4HTTP記録とフローを結ぶ
実解析記録にあるIDの対応を調べます。
events=RESULTS['split-alert']
http_event=next(e for e in events if e['event_type']=='http')
flow_event=next(e for e in events if e['event_type']=='flow')
print(http_event['flow_idHTTP記録とフローを結ぶ']==flow_event['flow_id'])
print(http_event['http']['hostname'],http_event['http']['url'])同じflow_idで接続とHTTPの情報を対応付けられます。異なる製品の任意のID同士が同じ値になるという意味ではありません。
5NAT後の観測を端末へ対応付ける
固定の変換記録に内側と外側の端点を用意します。
mapping={('203.0.113.9',61000):('192.0.2.7',53000)}
print(mapping.getNAT後の観測を端末へ対応付ける(('203.0.113.9',61000)))
print(mapping.get(('203.0.113.9',61001)))61000は対応しますが61001は不明です。共有アドレスだけを同じ利用者へ割り当てず、時刻とポートを含む変換記録を使います。
6暗号化された区間の可視性を確認する
TLSの暗号文と、終端した側の平文を比較します。
from sp05 import tls_exchange暗号化された区間の可視性を確認する
version,wire,plain=tls_exchange()
print(version,b'Host:' in wire,b'Host:' in plain)観測点がTLSを終端しなければHTTPヘッダーはこの暗号文から読めません。接続先や量などの情報と本文の可視性を分けて扱います。
7欠落を含む記録の結論を確認する
後半欠落pcapのHTTP記録を開きます。
print([e['http'].get欠落を含む記録の結論を確認する('url') for e in RESULTS['gap-alert'] if e['event_type']=='http'])/lessという途中の値です。観測記録だけで完全なURLを補完して分類すると、根拠にない情報を足してしまいます。
まとめ
バイト数はどの層を数えた値か確認します。
接続IDとNAT記録で観測点間を照合します。
暗号化や欠落による未観測範囲を残します。
操作履歴と解説を振り返れます。
Python 3.15 / 固定pcap・Suricata 7.0.10解析記録
from sp06 import *
rows=frames()
for row in rows: print(row)(1700000001, '192.0.2.7', '198.51.100.20', 53000, 80, 0, 54) (1700000002, '198.51.100.20', '192.0.2.7', 80, 53000, 0, 54) (1700000003, '192.0.2.7', '198.51.100.20', 53000, 80, 0, 54) (1700000004, '192.0.2.7', '198.51.100.20', 53000, 80, 9, 63) (1700000005, '192.0.2.7', '198.51.100.20', 53000, 80, 40, 94) (1700000006, '198.51.100.20', '192.0.2.7', 80, 53000, 0, 54)
pairs={tuple(sorted(((s,sp),(d,dp)))) for t,s,d,sp,dp,n,frame in rows}
print(sorted(pairs))[(('192.0.2.7', 53000), ('198.51.100.20', 80))]print(sum(n for t,s,d,sp,dp,n,f in rows if s=='192.0.2.7'))
print(sum(f for t,s,d,sp,dp,n,f in rows))49 373
events=RESULTS['split-alert']
http_event=next(e for e in events if e['event_type']=='http')
flow_event=next(e for e in events if e['event_type']=='flow')
print(http_event['flow_id']==flow_event['flow_id'])
print(http_event['http']['hostname'],http_event['http']['url'])True demo.example.test /lesson
mapping={('203.0.113.9',61000):('192.0.2.7',53000)}
print(mapping.get(('203.0.113.9',61000)))
print(mapping.get(('203.0.113.9',61001)))('192.0.2.7', 53000)
Nonefrom sp05 import tls_exchange
version,wire,plain=tls_exchange()
print(version,b'Host:' in wire,b'Host:' in plain)TLSv1.3 False True
print([e['http'].get('url') for e in RESULTS['gap-alert'] if e['event_type']=='http'])['/less']
演習完了