2018년 2월 28일 17시 21분(UTC), GitHub은 그때까지 누구도 기록한 적 없는 최대 규모의 서비스 거부 공격을 맞기 시작했다. 초당 1.35테라비트, 초당 1억 2,690만 패킷이 천 개가 넘는 네트워크의 수만 개 엔드포인트에서 쏟아졌다. GitHub 자체 엣지는 버티지 못했다. 17시 26분까지 그들은 인입 트래픽 전부를 Akamai의 스크러빙 센터로 재라우팅했고, 17시 30분에는 공격이 잦아들었다. 사이트는 약 10분간 저하됐다. 18시 이후 400Gbps쯤 되는 두 번째 스파이크가 있었지만 아무 일도 일으키지 못했다.
여기까지가 사건 전부다. 그리고 이건 흥미로운 대목이 아니다.
흥미로운 건, 아무도 이 무기를 만들지 않았다는 것이다. 봇넷은 없었다. 악성코드 캠페인도, 몇 년에 걸쳐 가정용 공유기와 카메라를 조용히 감염시킨 일도, 명령제어(C2) 인프라도 없었다. 당시 사상 최대의 공격은 남들이 열어둔 채 방치한 서버들로 몇 분 만에 조립됐다 — 그것도 공짜로.
15바이트를 750킬로바이트로 바꾸는 법
핵심은 memcached였다. 많은 웹 애플리케이션이 자신과 데이터베이스 사이에 두는 캐싱 서버다. 빠르고 단순하고 — 이 대목이 중요한데 — 수년간 UDP 11211 포트를 기본으로 열어둔 채, 어떤 인증도 없이 배포됐다. 거기에 닿을 수 있는 사람이면 누구든 말을 걸 수 있었다.
위험을 만드는 재료는 UDP다. UDP는 비연결형이다: 출발지 주소를 원하는 대로 사칭한 패킷을 보내면, 서버는 당신이 정말 그 주소에 사는지 확인도 하지 않고 그 주소로 응답한다. 그래서 공격자는 memcached 서버에 아주 작은 요청 — 캐시에 담긴 큰 값을 뱉으라는 요청 — 을 보내되 출발지 IP를 피해자의 것으로 위조한다. 서버는 순순히 그 응답을 GitHub에 쏜다.
무기로 만드는 건 이 비대칭이다. GitHub이 공격받기 하루 전 이 공격의 분석을 공개한 Cloudflare가 숫자를 박아 넣었다: 실제로 15바이트짜리 요청이 최대 750킬로바이트 응답을 유발할 수 있었다. 증폭률 약 51,000배 — CVE-2018-1000115에 적힌 수치(“1:50,000”)와 같은 자릿수다. 1바이트를 보내면 피해자는 5만 바이트를 받는다. 평범한 인터넷 회선을 쓰는 공격자 한 명이 테라비트급 소방호스를 표적에 겨눌 수 있다. 트래픽을 자기가 만드는 게 아니라, 낯선 이들의 서버 수천 대가 만들고, 그 각각이 공격자의 노력을 다섯 자릿수 배로 불려주기 때문이다.
아무도 침해당할 필요가 없었다. memcached 서버들은 해킹당하지 않았다. 그들은 만들어진 그대로 — 정상 요청과 똑같이 생긴 요청에, GitHub이 자기 데이터를 달라고 하는 것과 똑같이 생긴 주소로 — 정확히 자기 일을 했다.
두 개의 실패, 둘 다 수십 년 묵은 것
증폭 공격이 성립하려면 두 가지가 동시에 참이어야 한다. 우울한 건, 둘 다 수년 전에 이미 풀린 문제였다는 점이다.
첫째는 반사기(reflector): 요청하지도 않은 UDP에 훨씬 큰 응답을 돌려주는 서비스다. 공개 인터넷의 memcached는 화려한 사례였지만, 이 장르 자체는 오래됐다 — DNS, NTP, SSDP를 비롯한 십수 개가 같은 방식으로 악용돼 왔다. 여기서의 구체적 실패는 기본값이었다. memcached는 상자에서 꺼내자마자 UDP를 공개로 열었고, 수만 개 설치본이 아무도 원한다고 결정한 적 없는 그 기본값을 물려받았다. 유지보수 팀의 수정은, 나왔을 때, 거의 모욕적으로 작았다: 1.5.6 버전이 UDP 포트를 기본으로 껐다. 플래그 하나. 사상 최대 공격을 굴린 기능이 한 줄 변경으로 꺼졌고, 아쉬워한 사람은 거의 없었다.
둘째 실패가 정작 중요한 것이고, 아무도 안 고치는 것이다: 위조된 출발지 주소. 공격자가 패킷의 출처를 사칭하지 못하면 이 모든 게 성립하지 않는다. 그리고 사칭이 가능한 유일한 이유는, 대부분의 네트워크가 확인을 안 하기 때문이다. 그것에 대한 해법에는 이름과 날짜가 있다 — BCP38, 네트워크 인그레스 필터링, 2000년 RFC 2827로 발행. 내용은 뻔하다: 네트워크는 자기 소유가 아닌 출발지 주소를 단 나가는 패킷을 버려야 한다. 고객 트래픽이 GitHub의 IP 대역에서 왔다고 주장하는데 그 고객이 GitHub이 아니라면, 전달하지 마라.
이게 스푸핑에 대한 방어의 전부이고, 사반세기 동안 권장 관행이었다. 그런데도 여전히 보편적으로 배포돼 있지 않다. 어떤 RFC로도 못 고치는 인센티브 문제 때문이다: 필터링을 안 하는 네트워크는 피해를 입는 네트워크가 아니다. 필터링은 당신의 고객이 뿜는 위조 트래픽으로부터 남을 보호하려고 당신이 수고와 장비를 쓰는 일이다. 피해자는 언제나 인터넷 건너편 어딘가에 있다. 그래서 패킷은 계속 흐르고, 반사기는 계속 반사하고, 2001년에 이미 불가능했어야 할 공격이 2018년에 기록을 세웠다.
있어선 안 됐을 킬 스위치
이 전체를 무섭다가 우스꽝스럽게 만드는 대목이 하나 있다. 열린 memcached 서버는 아무의 명령이나 받으니, 공격받는 방어자가 반사기들에게 스스로를 무장해제하는 바로 그 명령을 보낼 수 있었다. Corero Network Security가 며칠 만에 시연했다: 악용된 서버들에 flush_all을 쏴서 공격자가 심어둔 캐시 페이로드를 지워버리면, 반사되던 응답이 아무것도 아닌 크기로 줄어든다. 서버가 받아준다면 shutdown 명령은 반사기를 통째로 끈다. 연구자들은 이걸 도구로 감싸 노출된 서버 목록에 겨눴고, 그 기록 파괴적 공격군 전체에 전원 스위치가 생겼다.
이게 무슨 뜻인지 보라. 공격자가 이 서버들을 무기화하게 해준 바로 그 ‘인증 없음’이, 방어자가 손을 뻗어 서버를 끄게도 해줬다. 서버가 너무 열려 있어서 양쪽 모두가 그걸 조작할 수 있었던 것이다. 취약점과 그 응급 완화책은 방향만 다르게 본 같은 결함이었다.
기록은 일주일도 못 갔다
이게 일회성이 아니라 구조적 문제였다는 가장 분명한 신호를 원한다면 달력을 보라. GitHub은 2월 28일에 1.35Tbps를 맞았다. 일주일이 안 돼 — 3월 5일자 글에서 — NETSCOUT의 Arbor 그룹이 미국의 한 서비스 제공업체 고객을 겨눈 1.7Tbps memcached 공격을 확인했다. 그들의 전 세계 센서가 그때까지 본 종전 기록은 2016년 650Gbps였다. 일주일 남짓 만에 memcached 증폭이 사상 최대 기록을 두 번 갈아치워, 종전 기록의 두 배 반이 넘는 곳까지 밀어 올렸다.
기록 파괴적 공격의 원자재가 그냥 거기 공짜로, CVE를 읽는 아무나를 기다리며 놓여 있을 때 벌어지는 일이 이거다. 이 공격들의 규모는 애초에 공격자의 자원으로 제한된 적이 없었다. 열린 반사기가 몇 개나 있느냐로만 제한됐고, 반사기는 넘쳐났다.
교훈은 memcached에 관한 게 아니다
이걸 “설정 잘못한 캐싱 서버, 이제 패치됨”으로 분류하고 넘어가고 싶어진다. 그건 핵심을 놓친다. memcached의 기본값은 첫 대형 공격 며칠 만에 바뀌었다. 그날그날의 반사기는 바뀐다 — 예전엔 NTP였고 다음엔 뭔가 다른 게 될 것이다 — 하지만 반사기는 누가 지목만 하면 찾기도 고치기도 싸다.
하중을 견디는 실패는 BCP38였고, BCP38은 여전히 배포돼 있지 않다. 2000년 이후 경제 논리가 바뀌지 않았기 때문이다. 출발지 주소 검증은 내 비용을 들여 낯선 이들에게 베푸는 호의다. 출발지에서 스푸핑을 막을 수 있는 사람들은 그 테라비트를 얻어맞는 사람들이 아니다. 그래서 안 하고, 몇 년마다 새 반사기가 나타나 인터넷이 아직도 반송 주소를 위조하게 놔둔다는 걸 상기시킨다. 우리는 매번의 증폭 기록을 반사기에 관한 이야기로 취급한다. 사실은 모두가 좋은 생각이라고 동의하면서 거의 아무도 굳이 돌리지 않는 반송 주소 검사에 관한 이야기다.
1.35Tbps 공격은 8분간 이어졌다. 그걸 가능하게 한 결함은, 고치는 법까지 문서로 나온 지 스물여섯 해가 됐다.