WAF JA4 봇 차단의 한계 — 헤드리스 브라우저

네트워킹3분조회

DDoS 성 봇넷 트래픽을 WAF 로 자동 차단하는 걸 만들었습니다. 처음엔 JA4 라는 TLS 지문 하나로 잘 막혔는데, 봇이 진화하면서 그 방법이 무너졌습니다.

JA4 — TLS 핸드셰이크 지문

JA4 는 TLS 핸드셰이크의 특징(암호 스위트, 확장 목록 등)을 해시한 지문입니다. 클라이언트가 TLS 를 맺는 방식이 라이브러리마다 다르기 때문에, 이 지문으로 “어떤 도구가 접속했는지” 를 대략 구분할 수 있습니다.

봇넷이 자체 TLS 스택을 쓰면 정상 브라우저와 다른 고유한 JA4 가 나옵니다. 그러면 그 JA4 하나만 막으면 됩니다.

1세대 — 지문 하나로 잡힌 단순 봇넷

초기 봇넷은 이런 특징이었습니다.

피처
Sec-Fetch-Site none (100%)
Accept-Language 비한국어 (96.5%)
Referer 없음 (100%)
JA4 100% 동일한 고유 지문

봇 전부가 똑같은 JA4 를 썼고, 그게 정상 브라우저엔 없는 값이었습니다. 그래서 ja4 = '<그 지문>' 하나로 오탐 0%, 커버리지 100% 로 막혔습니다.

2세대 — 진짜 브라우저를 쓰기 시작한 봇

다음 봇넷은 헤드리스 크롬에 클릭 시뮬레이션까지 붙였습니다.

봇의 JA4 가 정상 크롬과 똑같아졌습니다. 진짜 크롬 엔진을 쓰니 TLS 지문도 진짜 크롬입니다. JA4 로 막으면 정상 사용자까지 막힙니다. 실제로 JA4 단독 차단을 켰다가 정상 사용자 오탐으로 롤백했습니다.

지문 하나에 의존하는 방식의 한계가 여기서 드러났습니다. 봇이 정상 도구를 쓰는 순간, 그 도구의 지문으로는 구분이 불가능합니다.

지문 하나 대신 여러 피처를 AND로

JA4 를 버리고, 여러 피처를 조합했습니다. 봇은 진짜 브라우저를 흉내 내지만 행동 패턴까지 완벽히 똑같지는 않았습니다.

핵심 신호 하나가 Accept-Language 의 엔트로피였습니다. 봇넷은 IP 마다 다른 언어 헤더를 뿌리는데, 그 다양성(엔트로피)이 정상 사용자보다 훨씬 높았습니다. 정상 사용자는 자기 브라우저 언어가 고정이라 엔트로피가 낮습니다.

정상 사용자  Accept-Language 엔트로피 최대 2.5 정도
봇넷         엔트로피 8 이상 — 핵심 탐지 신호

여기에 요청 URI 패턴(대부분 / 만 때림), Sec-Fetch-Site 값을 AND 로 묶어 룰을 만들었습니다. 여러 피처를 동시에 만족해야 차단하니, 정상 사용자가 우연히 하나 걸려도 안 막힙니다. 오탐률이 0.03% 수준으로 떨어졌습니다.

이상 탐지로 룰 자동 생성

봇넷이 계속 바뀌는데 룰을 손으로 매번 새로 만들 수는 없어서, 이상 탐지를 붙였습니다. IsolationForest 로 “정상 트래픽에서 벗어난” 요청을 찾고, 거기서 공통 피처를 뽑아 룰을 자동 생성하는 방향입니다.

IsolationForest 는 정상 데이터의 분포를 학습해서, 거기서 멀리 떨어진 점을 이상치로 봅니다. 봇넷은 특정 피처 조합이 정상과 확 다르기 때문에 이상치로 뜹니다. 그 이상치 무리에서 공통점(예: 특정 URI + 언어 엔트로피)을 뽑아 WAF 룰로 만드는 겁니다.

지문 하나로는 부족 — 약한 신호 여러 개

봇 탐지에서 “지문 하나로 판별” 은 봇이 단순할 때만 통합니다. 봇이 정상 도구를 쓰기 시작하면 그 지문은 정상과 겹치고, 단일 피처는 무력해집니다.

그때부터는 하나의 결정적 신호 가 아니라 여러 약한 신호의 조합으로 가야 합니다. 각 피처만 보면 정상과 구분이 안 되지만, 여러 개를 동시에 보면 봇의 무리가 드러납니다. 그리고 봇이 계속 변하니, 룰을 고정하지 말고 트래픽에서 패턴을 계속 다시 뽑는 구조가 필요했습니다.

한 가지 덧붙이면, 취약점 스캐너처럼 성격이 다른 위협은 이 시스템의 대상이 아니었습니다. .env 같은 경로를 훑는 스캐너는 DDoS 봇넷과 완전히 다른 문제라, 탐지 범위를 명확히 나눠서 이 시스템은 봇넷에만 집중했습니다.

참고

  1. 불러오는 중