ファイナンス、情報通信技術のスキル・アグリゲーション・サイト

' . iseeit.jp 情報通信技術 . '
 
MongoDB / Access Log ③

攻撃者は、パターンで浮かぶ
ログから異常を読む

User-Agent は「Mozilla」と名乗れる。名乗りはいくらでも偽装できます。ならば何で見分けるか—— 振る舞いです。高頻度・分散・パス走査・名乗りとの矛盾。正常な訪問者を基準に、 そこから外れるパターンで攻撃者を炙り出します。

異常検知 4つの観点 スコアリング 検出→appwで遮断

§ 0 — 導入

UAは Mozilla。でも、明らかにおかしい

ある日、ログでこんな1件を見ました。User-Agent はごく普通の Mozilla/5.0。 なのに、その日いちばんアクセスが多いIP。人間なら、そんなに読めません。

観測 / UAは普通のブラウザを名乗る。しかし当日最多アクセス・非人間的な速度・存在しないパスへの試行。

「名乗り」は正常に偽装できます。ならば、何で見分けるか。答えは振る舞い(パターン)です。 前回「正常な訪問者」を掴みました。今度はその裏——正常から外れるものを掴みます。

IP・UA はすべてマスク済み。 本文とデモの IP はドキュメント用予約レンジ(203.0.113.x / 198.51.100.x / 192.0.2.x)で、実在のアクセス元ではありません。
§ 1 — 発想

正常を知れば、異常が浮かぶ

これは TensorFlow.js の異常検知とまったく同じ構造です。 AutoEncoder が「正常を学べば、異常は再構成できずに浮かぶ」ように、 正常なアクセスの姿を基準にすれば、逸脱が測れます

正常なアクセスとは、前回のセッション像そのもの。人間はページを読み、 適度に間隔をあけ実在するページを辿ります。 攻撃者は、そのどれからも外れます。この逸脱を、4つの観点に分けて測ります。

§ 2 — 観点①

同一IPの高頻度 — 速すぎる

人はページを読むのに時間がかかります。秒間に何十リクエストも投げるのは、人間業ではありません。 同一 addr の単位時間あたりアクセス数を数えれば、これは一発で浮かびます。

// ① 同一IPの単位時間あたりアクセス数
const count = log.filter(e =>
  e.addr === ip && now - e.t < 60_000).length;
if (count > 30) score.freq = ;   // 30req/60sec 超は要注意

前回のセッション化があるから、「1人あたり」の頻度がきちんと測れる、という土台も効いています。

§ 3 — 観点②

同一UAの複数IP分散 — 散らばりすぎ

1つのIPを弾かれないよう、同じツール(同じ uah)が多数のIPに散ることがあります。 1IPあたりの頻度は低く抑えられていても、UAを軸に横断集計すると、不自然な分散が見えます。

// ② 同一UAが、いくつの異なるIPから来たか
const ips = new Set(
  log.filter(e => e.uah === ua).map(e => e.addr)
);
if (ips.size > threshold) score.dist = ;
スキーマレスの恩恵。 1本目で uahaddr を1ドキュメントに持たせたから、この交差集計ができます。 「後から足せる器」が、こういう分析の自由度を生みます。
§ 4 — 観点③

多ページ走査 & 存在しないパス — 探っている

正常な訪問は、実在するページを辿ります。攻撃者は違う。当たりを探して、 /wp-admin/.env/phpmyadmin のような 一般によく狙われるパスを総当たりします。これらはこのサイトに存在しないので、 404 への集中それ自体が、強いシグナルになります。

// ③ 404率 と ページ横断の広さ
const reqs = log.filter(e => e.addr === ip);
const rate404 = reqs.filter(e => e.status === 404).length
              / reqs.length;
// 「読む」ではなく「探る」= 実在しないパスへの集中
§ 5 — 観点④

名乗りと振る舞いの矛盾 — UAは自己申告

導入の1件がこれでした。Mozilla/5.0 は誰でも名乗れます。だから、名乗りと行動のズレを突きます。

ブラウザを名乗るなら、普通は CSS も JS も画像も読む。ところが攻撃ツールは、 HTML だけを非人間的な速度で舐めて、資産(css/js/画像)を一切読まない。robots も無視する。 名乗りと振る舞いが噛み合いません。

// ④ 名乗りと振る舞いの矛盾
const claimsBrowser = /Mozilla|Chrome|Safari/.test(ua);
const loadsAssets   = reqs.some(e =>
  /\.(css|js|png|jpg)$/.test(e.uri));
if (claimsBrowser && !loadsAssets)
  score.mismatch = ;   // ブラウザを名乗るのに資産を読まない
単独ではなく、重ねる。 1つの観点だけでは誤検出も出ます。だから複数観点のスコアを重ねて確度を上げる—— TF.js 記事の「2つのAIが一致すると確度が上がる」と、まったく同じ発想です。
§ 6 — 体感デモ

ログから、攻撃者を炙り出す

正常な訪問者と正規クローラに、数種類の攻撃者を混ぜてあります。「解析する」で4観点のスコアを算出し、 感度スライダーで閾値を動かしてください。攻撃者だけが複数観点で赤くなり、正常な訪問者は白のままです。

Interactive · Attacker scoring
ログストリーム → 攻撃者スコアリング
サーバー通信なし。サンプルログは固定です。IP・UA はマスク済み。数値は傾向を示す目安です。
検出感度(閾値)
← 感度 高(誤検出↑)感度 低(見逃し↑) →
攻撃者候補
誤検出(正常を誤り)
見逃し
「解析する」を押すと、各アクセス元の4観点スコアが算出されます。まず閾値=中(45)で見て、スライダーを両端に振ってみてください。

感度を上げすぎる(閾値を下げる)と、正規クローラや人間まで誤検出します。 下げすぎる(閾値を上げる)と、攻撃者を見逃します。 その中間に、攻撃者だけをきれいに拾う帯がある——単一観点ではなく、複数観点を重ねているからこそ、この帯が広く取れます。

§ 7 — その先

検出の、その先 — 遮断は別レイヤー

ここまでは「見つける」話でした。実際に「止める」のは、別の仕事です。 検出 ≠ 遮断。この2つを分けて考えるのが肝心です。

検出した攻撃者候補を実際に遮断するのは、サーバー側の役目。 .htaccess による IPv4 / IPv6 / UA ブロック、サブネット単位の遮断、APCu レート制限—— これらの遮断の実装は、appw.jp の運用記事に譲ります。

二段構え。 「合図を出す層(検出=この記事)」と「実際に止める層(遮断=appw)」を分ける。 これは RabbitMQ の cancel 記事で、 キューが合図を運び cancelled_sessions が実際に降りたのと、同じ設計思想です。
§ 8 — 総括

名乗りでは隠れられる。振る舞いでは浮かぶ

攻撃者は User-Agent を偽装して名乗りでは隠れられますが、振る舞い(パターン)では浮かびます。 高頻度・分散・走査/存在しないパス・名乗りとの矛盾。単独でなく、スコアで重ねることで、 正常な訪問者や正規クローラを巻き込まずに攻撃者を拾えます。

アクセスログ基盤シリーズ — 総括
  1. なぜログを MongoDB に
    器:後から育つデータにスキーマレスが効く
  2. PVからセッションへ
    正常:uqid を束ねて訪問者の姿を掴む
  3. 攻撃者は、パターンで浮かぶ(この記事)
    異常:正常からの逸脱で攻撃者を検出

器(MongoDB)→ 正常(セッション)→ 異常(攻撃者検出)。3段で、5サイト共通ログ基盤の「観測」側が完成しました。 検出したその先、実際に「止める」実装は、appw.jp へ続きます。

この記事のまとめ

  • UA(名乗り)は偽装できる。攻撃者は振る舞い(パターン)で見分ける。
  • 4観点:①高頻度 ②同一UAの分散 ③走査/404 ④名乗りとの矛盾
  • 「正常を知れば異常が浮かぶ」——TF.js の異常検知と同じ発想。
  • 単一観点は誤検出しうる。複数観点をスコアで重ねて確度を上げる。
  • 検出 ≠ 遮断。止める実装(.htaccess・APCu)は appw.jp の別レイヤー。

関連記事