WAF JA4 봇 차단의 한계 — 헤드리스 브라우저
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 봇넷과 완전히 다른
문제라, 탐지 범위를 명확히 나눠서 이 시스템은 봇넷에만 집중했습니다.
댓글 0