WAF JA4 봇 차단의 한계 — 헤드리스 브라우저
서비스 앞단에는 들어오는 요청을 보고 룰에 걸리면 막아 주는 WAF 를 두고 있고, 평소에는 기본 룰셋만 켜 두고 크게 손댈 일이 없습니다.
그러다 DDoS 성 봇넷 트래픽이 들어오기 시작했고, 들어오는 IP 가 계속 바뀌어서 로그를 보고 하나씩 차단 목록에 넣는 걸로는 따라갈 수가 없었습니다. 그래서 요청 자체에서 봇에만 있는 공통 특징을 찾아 그걸 WAF 룰로 만들어 자동으로 막는 쪽으로 갔습니다. 특징만 정확하면 IP 가 몇 개로 늘어나든 상관이 없어지니 그쪽이 맞다고 봤습니다.
처음 봇넷은 JA4 라는 TLS 지문 하나로 깨끗하게 막혔습니다. 룰을 한 줄 넣는 걸로 끝났고 정상 사용자가 걸린 것도 없어서, 앞으로도 이 방식이면 되겠다고 봤습니다. 그런데 다음 봇넷이 오면서 그 방법이 통째로 무너졌습니다.
JA4 — TLS 핸드셰이크 지문
JA4 는 TLS 핸드셰이크의 특징(암호 스위트, 확장 목록 등)을 해시한 지문입니다. 클라이언트가 TLS 를 맺는 방식이 라이브러리마다 다르기 때문에, 이 지문으로 “어떤 도구가 접속했는지” 를 대략 구분할 수 있습니다.
봇넷이 자체 TLS 스택을 쓰면 정상 브라우저와 다른 고유한 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 분포를 재야 나옵니다. 봇넷은 IP 마다 다른 언어 헤더를 뿌려서 그 분포가 수백 갈래로 흩어지지만, 정상 사용자 쪽은 브라우저 언어가 고정이라 몇 개로 쏠립니다.
정상 사용자 Accept-Language 엔트로피 최대 2.5 정도
봇넷 엔트로피 8 이상 — 핵심 탐지 신호
여기에 요청 URI 패턴(대부분 / 만 때림), Sec-Fetch-Site 값을 AND 로 묶어 룰을 만들었습니다.
세 가지를 동시에 만족해야 차단하도록 걸어 두면 정상 사용자가 우연히 하나에 걸려도 안 막힙니다.
그렇게 바꾸고 나서 오탐률이 0.03% 수준까지 떨어졌습니다.
이상 탐지로 룰 자동 생성
봇넷이 계속 바뀌는데 룰을 손으로 매번 새로 만들 수는 없어서 이상 탐지를 붙였습니다. IsolationForest 로 “정상 트래픽에서 벗어난” 요청을 찾고, 거기서 공통 피처를 뽑아 룰을 자동으로 만들게 했습니다.
IsolationForest 는 정상 데이터의 분포를 학습해서 거기서 멀리 떨어진 점을 이상치로 봅니다. 봇넷은 특정 피처 조합이 정상과 확 다르기 때문에 이상치로 뜹니다. 그 무리에서 공통점(예: 특정 URI + 언어 엔트로피)을 뽑아 WAF 룰로 만듭니다.
지문 하나로는 부족 — 약한 신호 여러 개
지문 하나로 판별하는 방식은 봇이 자기 도구를 쓸 때만 통하고, 정상 브라우저를 그대로 띄워서 오기 시작하면 지문이 정상과 겹쳐 버려서 피처 하나로는 갈라낼 방법이 없어집니다.
그때부터는 결정적인 신호 하나가 아니라 여러 약한 신호의 조합으로 가야 합니다. 각 피처만 보면 정상과 구분이 안 되지만 여러 개를 동시에 보면 봇의 무리가 드러납니다. 봇이 계속 변하니 룰을 고정해 두지 말고, 트래픽에서 패턴을 다시 뽑는 구조까지 같이 만들어야 했습니다.
댓글 0