通信の頻度と量に基づく異常検知
観測点、通信量、検知条件、外部装置の措置を対応付けて評価します。
説明
周期的な通信や送信量の急増は調査の手掛かりになりますが、更新確認やバックアップでも起こります。特徴の計算、基準との比較、調査後の措置を分けて考えます。
固定の接続記録で通信間隔と基準値を計算します。基準に含める期間を変えた後、通知から外部ファイアウォールへの適用までを追います。
観測点、通信量、検知条件、外部装置の措置を対応付けて評価します。
1端末別の接続を読む
時刻は開始からの秒、bytes_outは送信TCPデータ量です。
from sp06 import *
from sp02 import show端末別の接続を読む
db=connect()
show(db,"SELECT * FROM conn ORDER BY src,t")端末7は最後だけ900バイト、端末8は通信間隔が不均一です。いずれも固定の比較資料です。
2前回からの間隔を計算する
LAGで同じ端末の直前の時刻を取得します。
show(db,"SELECT src,t,t-LAG前回からの間隔を計算する(t) OVER(PARTITION BY src ORDER BY t) AS interval FROM conn ORDER BY src,t")端末7は60秒間隔、端末8は67秒と74秒です。最初の行には前回がないためNULLになります。
3間隔のばらつきを求める
端末7の時刻列から差分と標準偏差を計算します。
times=[r[0] for r in db.execute("SELECT t FROM conn WHERE src='192.0.2.7' ORDER BY t")]
deltas=[b-a for a,b in zip(times,times[1:])]
print(deltas,statistics.pstdev間隔のばらつきを求める(deltas))標準偏差は0です。周期の一致だけでは、正常な定期処理かどうかは確定しません。
4変化前の基準値を計算する
240秒より前の端末7だけを使います。
base=[r[0] for r in db.execute("SELECT bytes_out FROM conn WHERE src='192.0.2.7' AND t<240")]
print(statistics.mean変化前の基準値を計算する(base),statistics.median(base))
print(900/statistics.mean(base))平均も中央値も100、最後は9倍です。基準期間を記録すると判定を後から再現できます。
5評価対象を基準へ混ぜた影響を見る
900バイトを含めて平均を求めます。
mixed=base+[900]
print(statistics.mean評価対象を基準へ混ぜた影響を見る(mixed),round(900/statistics.mean(mixed),2))平均260、比は約3.46まで下がります。調査対象自身を基準へ含めると急増が小さく見えます。
6通知と措置の時刻を照合する
同じ調査対象への固定対応記録です。
show通知と措置の時刻を照合する(db,"SELECT * FROM response ORDER BY t")
show(db,"SELECT * FROM arrival ORDER BY t")241秒に通知しても、248秒の通信は到達しています。外部装置の適用は250秒です。
7適用前後の到達数を集計する
適用時刻を境に観測を分けます。
show適用前後の到達数を集計する(db,"SELECT CASE WHEN t<250 THEN 'before' ELSE 'after' END AS phase,SUM(accepted) AS accepted,COUNT(*) AS observations FROM arrival GROUP BY phase ORDER BY phase")適用前2件は到達、適用後の観測1件は不達です。観測件数を添えると、どの範囲まで確認した結果か分かります。
まとめ
周期や量は調査の手掛かりとして使います。
評価対象と基準期間を分けて計算します。
外部装置の適用と到達結果を確認します。
操作履歴と解説を振り返れます。
Python 3.15 / 固定pcap・Suricata 7.0.10解析記録
1
from sp06 import *
from sp02 import show
db=connect()
show(db,"SELECT * FROM conn ORDER BY src,t")id | src | t | bytes_out | dst a1 | 192.0.2.7 | 0 | 100 | 198.51.100.20 a2 | 192.0.2.7 | 60 | 110 | 198.51.100.20 a3 | 192.0.2.7 | 120 | 90 | 198.51.100.20 a4 | 192.0.2.7 | 180 | 100 | 198.51.100.20 a5 | 192.0.2.7 | 240 | 900 | 198.51.100.20 b1 | 192.0.2.8 | 0 | 100 | 198.51.100.20 b2 | 192.0.2.8 | 67 | 100 | 198.51.100.20 b3 | 192.0.2.8 | 141 | 100 | 198.51.100.20
2
show(db,"SELECT src,t,t-LAG(t) OVER(PARTITION BY src ORDER BY t) AS interval FROM conn ORDER BY src,t")src | t | interval 192.0.2.7 | 0 | NULL 192.0.2.7 | 60 | 60 192.0.2.7 | 120 | 60 192.0.2.7 | 180 | 60 192.0.2.7 | 240 | 60 192.0.2.8 | 0 | NULL 192.0.2.8 | 67 | 67 192.0.2.8 | 141 | 74
3
times=[r[0] for r in db.execute("SELECT t FROM conn WHERE src='192.0.2.7' ORDER BY t")]
deltas=[b-a for a,b in zip(times,times[1:])]
print(deltas,statistics.pstdev(deltas))[60, 60, 60, 60] 0.0
4
base=[r[0] for r in db.execute("SELECT bytes_out FROM conn WHERE src='192.0.2.7' AND t<240")]
print(statistics.mean(base),statistics.median(base))
print(900/statistics.mean(base))100 100.0 9.0
5
mixed=base+[900]
print(statistics.mean(mixed),round(900/statistics.mean(mixed),2))260 3.46
6
show(db,"SELECT * FROM response ORDER BY t")
show(db,"SELECT * FROM arrival ORDER BY t")t | event | device 241 | alert | ndr 245 | requested | fw-a 250 | applied | fw-a t | accepted 242 | 1 248 | 1 252 | 0
7
show(db,"SELECT CASE WHEN t<250 THEN 'before' ELSE 'after' END AS phase,SUM(accepted) AS accepted,COUNT(*) AS observations FROM arrival GROUP BY phase ORDER BY phase")phase | accepted | observations after | 0 | 1 before | 2 | 2
演習完了