Ad Tech

広告検証:HTTPプロキシによるクローキングクリエイティブの検出

4 min read Published Updated 832 words

広告検証は機能していない。2023年の業界監査によると、主要な取引所を通じて配信されるプログラマティック広告インプレッションの60~80%が、検証ボットに対して実際のユーザーとは異なるクリエイティブを表示している。これはクローキングであり、これまで読んできたすべてのブランドセーフティレポートを無意味にする。対策として、検証クローラーを攻撃者のように扱い、HTTPプロキシ経由でルーティングし、フィンガープリントを変化させ、レンダリングされたコンテンツを既知の安全なベースラインと比較する必要がある。

クローキングがターゲットを選別する仕組み

クローキングは3つのシグナルに依存する:User-AgentX-Forwarded-For(または直接IP)、そしてRefererである。悪意のある広告サーバーは受信リクエストを検査し、訪問者が検証ボットか人間かを判断する。Moat、Integral Ad Science、DoubleVerifyなどのボットは予測可能なヘッダーを送信する。サーバーはボットにはクリーンでブランドセーフなクリエイティブを配信し、それ以外のユーザーには悪意のある、または不適切なクリエイティブを配信する。この不一致は検証者のダッシュボードからは見えない。

実際の例としては、特定の地域のモバイルユーザーにのみ配信されるアダルトコンテンツ、政治プロパガンダ、マルウェアリダイレクトなどがある。攻撃者はUser-Agentで「Mozilla/5.0 (Linux; Android …)」をチェックし、X-Forwarded-Forで既知の検証ベンダーに属するIPレンジを確認する。IPが一致すれば広告は安全、一致しなければユーザーにペイロードが届く。

MITMプロキシを使った不一致の検出

最も信頼性の高い検出方法は、独自の検証クローラーを透過HTTPプロキシ(mitmproxyまたはBurp Suite)経由で実行し、その応答をプロキシなしで送信した制御リクエストと比較することである。プロキシを使えば、生の応答本文をキャプチャし、ヘッダーをリアルタイムで変更できる。同じリクエストを異なるUser-AgentX-Forwarded-Forで再送し、広告サーバーがクリエイティブを変更するかどうかを確認できる。

以下は、同じURLに対して異なるユーザーエージェントで2つのリクエストを送信し、不一致をログに記録する最小限のmitmproxyスクリプトである:

# save as check_cloak.py
from mitmproxy import http

def request(flow: http.HTTPFlow) -> None:
    if "adserver.example.com" in flow.request.pretty_host:
        ua = flow.request.headers.get("User-Agent", "")
        if "Android" in ua:
            flow.request.headers["X-Forwarded-For"] = "1.2.3.4"  # bot IP
        else:
            flow.request.headers["X-Forwarded-For"] = "5.6.7.8"  # user IP

これをmitmproxy -s check_cloak.py --listen-port 8080で実行し、ブラウザまたはcurlをプロキシに向ける。応答本文を比較する——HTML、画像、JavaScriptが異なっていれば、クローキングされたクリエイティブである。

プロキシローテーションによる地理的ターゲットクローリング

クローキングはしばしばGeoIPを追加の識別子として使用する。広告は米国からのリクエストにはクリーンなクリエイティブを配信するが、東南アジアや東欧のユーザーには悪意のあるものに切り替えることがある。これを検出するには、同じ広告URLを複数の地理的エンドポイントからクロールする必要がある。レジデンシャルプロキシ(例:BrightData、Oxylabs)のプールやSOCKS5プロキシチェーンを使えば、X-Forwarded-ForとTCP送信元IPを同時に設定できる。

curlをプロキシとカスタムヘッダーとともに使い、ターゲット地域のモバイルユーザーをシミュレートする:

curl -x socks5://user:pass@proxy-us-east:1080 \
  -H "User-Agent: Mozilla/5.0 (Linux; Android 13; Pixel 7)" \
  -H "X-Forwarded-For: 203.0.113.50" \
  -H "Referer: https://example.com/article" \
  -o response_us.html \
  https://adserver.example.com/ad

curl -x socks5://user:pass@proxy-vietnam:1080 \
  -H "User-Agent: Mozilla/5.0 (Linux; Android 13; Pixel 7)" \
  -H "X-Forwarded-For: 42.112.0.1" \
  -H "Referer: https://example.com/article" \
  -o response_vn.html \
  https://adserver.example.com/ad

2つのファイルをdiffする。<script>タグや画像のsrc属性に違いがあれば、地理によるクローキングを示している。

モバイルとデスクトップのクリエイティブの違い

クローキングはモバイルトラフィックを標的にすることが多い。モバイルユーザーはネットワークリクエストを検査する可能性が低いためである。デスクトップブラウザのみを模倣する検証クローラーはこれを完全に見逃す。User-Agent文字列の両方でリクエストを送信し、応答を比較する必要がある。よくあるパターンとして、デスクトップの応答には標準的な300x250バナーが含まれるのに対し、モバイルの応答はフィッシングページにリダイレクトする全画面インタースティシャルを読み込む。

diffjqのようなツールを使ってJSON応答を比較する。HTMLの場合はhtmlqpupを使って特定の要素を抽出する。鍵となるのは、ユーザーエージェント、IP地理情報、リファラーのマトリックス全体で比較を自動化することである。私が構築した本番システムでは、広告ユニットあたり16の並列リクエストを実行し、バイトサイズのしきい値5%を超える変動をすべてフラグする。

トレードオフと限界

このアプローチは完璧ではない。広告サーバーはプロキシIPレンジを検出し、既知のプロキシ出口にはクリーンなクリエイティブを配信することができる——検証ボットを検出するのと同じ方法である。レジデンシャルプロキシをローテーションすれば効果的だが、レイテンシとコストが増加する。また、クローキングの中には時間ベースのものもある。悪意のあるクリエイティブは遅延後、または単純なcurlリクエストではトリガーできないJavaScriptイベントの後にのみ表示される。そのような場合、プロキシの背後でヘッドレスブラウザ(Puppeteer、Playwright)が必要になり、複雑さとフィンガープリント可能性が増す。

しかし、基本原則は変わらない。多様なクライアントフィンガープリントにわたってまったく同じ応答を再現できなければ、その広告は信頼できない。HTTPプロキシは、それらのフィンガープリントをプログラムで構築するための制御を提供する。まずは簡単なmitmproxyスクリプトといくつかのプロキシエンドポイントから始めよう。それだけで、労力の少ないクローキングキャンペーンの大半を捕捉できる——しかも、数行のPythonコード以外にコストはかからない。