광고 검증은 망가져 있다. 2023년 업계 감사에 따르면 주요 거래소를 통해 제공되는 프로그래매틱 광고 노출의 60~80%가 검증 봇에게는 실제 사용자와 다른 크리에이티브를 제공한다. 이것이 클로킹이며, 지금까지 읽은 모든 브랜드 안전 보고서를 무력화한다. 해결책은 검증 크롤러를 공격자처럼 취급하는 것이다. 즉, HTTP 프록시를 통해 라우팅하고, 핑거프린트를 다양화하며, 렌더링된 콘텐츠를 알려진 안전 기준과 비교하는 것이다.
클로킹이 대상을 선택하는 방법
클로킹은 세 가지 신호에 의존한다: User-Agent, X-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-Agent 또는 X-Forwarded-For로 재전송하여 광고 서버가 크리에이티브를 변경하는지 확인할 수 있다.
다음은 동일한 URL에 대해 서로 다른 사용자 에이전트로 두 요청을 보내 불일치를 기록하는 최소한의 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
두 파일을 비교하라. <script> 태그나 이미지 src 속성에 차이가 있다면 지리적 클로킹이 발생한 것이다.
모바일 vs 데스크톱 크리에이티브 차이
클로킹은 종종 모바일 트래픽을 대상으로 하는데, 모바일 사용자는 네트워크 요청을 검사할 가능성이 낮기 때문이다. 데스크톱 브라우저만 모방하는 검증 크롤러는 이를 완전히 놓친다. 두 가지 User-Agent 문자열로 요청을 보내고 응답을 비교해야 한다. 일반적인 패턴은 데스크톱 응답에는 표준 300x250 배너가 포함되고, 모바일 응답에는 피싱 페이지로 리디렉트되는 전체 화면 인터스티셜이 로드되는 것이다.
diff 또는 jq과 같은 도구를 사용하여 JSON 응답을 비교하라. HTML의 경우 htmlq 또는 pup을 사용하여 특정 요소를 추출하라. 핵심은 사용자 에이전트, IP 지역, 리퍼러의 매트릭스에 걸쳐 비교를 자동화하는 것이다. 내가 구축한 프로덕션 시스템은 광고 단위당 16개의 병렬 요청을 실행하고 5% 바이트 크기 임계값을 초과하는 모든 변형을 플래그 처리한다.
트레이드오프와 한계
이 접근 방식은 완벽하지 않다. 광고 서버는 프록시 IP 범위를 탐지하여 알려진 프록시 출구에 깨끗한 크리에이티브를 제공할 수 있다. 이는 검증 봇을 탐지하는 방식과 동일하다. 리지덴셜 프록시를 로테이션하면 도움이 되지만 지연 시간과 비용이 추가된다. 또한 일부 클로킹은 시간 기반이다. 악성 크리에이티브는 지연 후 또는 단순한 curl 요청으로 트리거할 수 없는 JavaScript 이벤트 후에만 나타난다. 이러한 경우 프록시 뒤에 헤드리스 브라우저(Puppeteer, Playwright)가 필요하며, 이는 복잡성과 핑거프린트 가능성을 증가시킨다.
그러나 핵심 원칙은 유효하다. 다양한 클라이언트 핑거프린트에서 정확히 동일한 응답을 재현할 수 없다면 해당 광고는 신뢰할 수 없다. HTTP 프록시는 이러한 핑거프린트를 프로그래밍 방식으로 구축할 수 있는 제어권을 제공한다. 간단한 mitmproxy 스크립트와 몇 개의 프록시 엔드포인트로 시작하라. 이만으로도 대부분의 저노력 클로킹 캠페인을 잡아낼 수 있으며, 비용은 Python 몇 줄에 불과하다.