Slowloris 공격은 2009년 것이다. 작은 스크립트 하나에 들어가고, 컴퓨터 한 대와 사실상 대역폭이 필요 없으며, 지금도 기본 설정 그대로의 Apache 서버를 멈춰 세운다 — Robert “RSnake” Hansen이 이걸 쓴 지 17년이 지난 오늘도. 같은 해 후반, 핵티비스트들이 대선 이후 시위 국면에서 이란 정부 사이트를 치는 데 이 도구를 집어 들었다. 봇넷도 굵은 회선도 필요 없다는 바로 그 이유 때문이었다. 형편없는 호텔 와이파이에 물린 노트북 한 대면 충분했다.
지난 두 글은 서비스 거부의 시끄러운 쪽을 다뤘다 — 초당 3억 9800만 요청을 쏘아댄 Rapid Reset, 단일 연결로 서버를 죽인 CONTINUATION Flood. 그것들은 소방 호스다. 이국적이고, 최신이며, HTTP/2의 새 기계장치 안에서 태어났다. Slowloris는 그 모든 것의 정반대다. 홍수도, 물량도, 프레임을 둘러싼 영리함도 없다. 서버가 예상하는 것보다 느리게 감으로써 이긴다. 그리고 HTTP/2가 존재하기도 전부터 그 방식으로 이겨 왔다. 바로 그 대조가 이 공격을 이해할 가치가 있는 이유다.
끝나지 않는 요청
여기 지루한 쪽의 HTTP/1.1 요청이 있다:
GET / HTTP/1.1\r\n
Host: example.com\r\n
User-Agent: Mozilla/5.0\r\n
\r\n
이 요청은 마지막 빈 줄 — 마지막 헤더 뒤의 맨 \r\n — 전까지 끝난 게 아니다. 그게 종결자다. 서버는 그걸 보기 전까지 헤더가 더 온다고 가정할 수밖에 없고, 그래서 연결을 열어 둔 채 워커 하나를 거기 붙여 나머지를 읽으려 기다린다.
Slowloris는 그냥 그 빈 줄을 영영 안 보낸다.
연결을 열고, 요청 라인과 Host 헤더를 보낸 뒤, 완성 한 타 직전에서 멈춘다. 소켓을 닫지도 않는다. 15초나 20초마다 — 서버가 허용하는 읽기 타임아웃 안쪽으로 넉넉히 — 의미 없는 헤더 하나를 흘린다. X-a: b\r\n, 오로지 시계를 리셋하려고. 헤더 내용은 무의미하다. 유일한 임무는 진행 중인 것처럼 보이는 것이다. 서버가 보기엔 느리지만 정직한 클라이언트가 아직 요청을 타이핑하는 중이다. 그래서 계속 기다린다.
이제 이걸 한 대에서 수백 개 병렬로 돌린다. 각 연결은 공격자에게 거의 공짜다 — 소켓 하나에 분당 바이트 몇 개. 각 연결은 서버에게 다른 누구에게도 못 쓰는 워커 하나를 물린다. 모든 워커를 반쯤 완성된 요청으로 채우면 서버는 진짜 요청에 답할 것이 하나도 안 남는다. 죽은 게 아니다. CPU도 대역폭도 바닥나지 않았다. 그저 완전히, 예의 바르게 점유돼 있을 뿐이다 — 영영 본론에 이르지 않을 대화로 가득 찬 방을 붙들고서.
서버 모델이 모든 것을 결정한다
이게 통하느냐는 하나의 아키텍처 선택으로 갈린다. 서버가 연결마다 스레드나 프로세스를 전담시키는가, 아니면 이벤트 루프에서 여러 연결을 저글링하는가.
Apache의 전통적 모델 — prefork와 worker MPM — 은 연결에 워커를 배정하고, 그 워커가 읽는 동안 블록되도록 둔다. 깔끔하고 읽기 쉬운 설계이며, 동시에 서비스할 수 있는 연결 수가 설정한 워커 수에서 상한이 걸린다는 뜻이기도 하다. 기본값으로 그 천장은 낮다, 수백 개. Slowloris는 바로 그 수만큼의 슬롯을 결코 완성되지 않는 연결로 채우려고 존재한다. 큰 숫자를 소진시킬 필요가 없다. 작은 숫자를 소진시키고, 그걸 붙들고 있으면 된다.
이벤트 구동 서버는 그렇게 동작하지 않는다. nginx, 그리고 그 전의 lighttpd는 워커 프로세스 몇 개가 각각 수천 개의 연결을 동시에 지켜보다가, 연결에 실제로 읽을 데이터가 준비됐을 때만 일한다. 분당 헤더 하나를 보내며 놀고 있는 반쯤 열린 연결은 그들에겐 거의 공짜다 — 폴 집합 안의 파일 디스크립터 하나일 뿐, 통째로 붙들린 워커가 아니다. 그래서 지난 10년간의 표준 조언은 보호하려는 대상 앞에 nginx를, 또는 버퍼링하는 리버스 프록시를 두라는 것이었다. nginx가 마법이어서가 아니라, 연결을 붙드는 이 수법은 연결마다 희소한 무언가를 배정하는 서버를 필요로 하는데 이벤트 루프는 그걸 거부하기 때문이다.
이건 Rapid Reset 글의 교훈을 반대편에서 본 것이다. 거기서는 HTTP/1.1의 연결당 한 요청이라는 굼뜸이 공격자를 스로틀하며 조용히 보안 일을 하고 있었다. 여기서는 Apache의 연결당 워커라는 명료함이, 각 연결을 수백 개면 치명적일 만큼 비싸게 만들어 조용히 취약점을 만들어 내고 있다. 추론하기 가장 쉬운 아키텍처가 종종 악용 가능한 모서리가 가장 많은 아키텍처다.
해법은 스톱워치다
Slowloris에는 패치할 프로토콜 구멍이 없다. HTTP를 위반하는 게 하나도 없기 때문이다. 헤더를 느리게 보내는 건 합법이다. 사이에 쉬는 것도 합법이다. 이 공격은 그저 클라이언트가 느릴 권리를 행사하는 것이다 — 대규모로, 끝낼 의사 없이. 그러니 방어는 “느린 클라이언트를 금지”할 수 없다. “그렇게 참을성 있게 기다리기를 멈추기”여야 한다.
Apache는 정확히 그걸 mod_reqtimeout으로 내놨고, 2010년 2.2.15 버전부터 기본 활성화돼 있다. 요청 헤더와 본문을 받는 데 마감 시한을, 그리고 클라이언트가 유지해야 할 최소 데이터 전송률을 건다. 분당 헤더 하나로 연명하던 연결은 이제 한계에 걸려 영구 좌석 대신 408 Request Timeout을 받는다. 그 모듈 하나가 Slowloris를 “기본 Apache를 죽인다”에서 “일부러 잘못 설정한 Apache를 죽인다”로 바꿔 놓았다. 다른 모듈 무리 — mod_qos, mod_antiloris, mod_limitipconn, mod_evasive — 는 연결 수 쪽에서 공격을 친다. 한 IP가 동시에 붙들 수 있는 연결 수에 상한을 걸며, 이것이 공격이 의존하는 또 다른 축이다.
MaxRequestWorkers를 올리는 것 — 서버에 슬롯을 더 주는 것 — 은 목록에 없다는 데 주목하라. 해법처럼 느껴지지만 함정이다. 공격자는 그냥 공짜인 연결을 더 열어 새 슬롯을 채우고, 그 슬롯은 당신의 메모리를 먹는다. 단위 비용이 소켓 하나에 분당 바이트 하나인 공격을 증설로 이길 수는 없다. 유일하게 통하는 것은 실질적 진행을 안 하는 클라이언트에게 슬롯 붙들기를 거부하는 것이다.
이 가족은 전부 속삭인다
Slowloris는 요청 헤더에서 서버를 굶긴다. 그 형제들은 교환의 다른 부분에서 같은 수법을 쓴다. RUDY — “R-U-Dead-Yet” — 는 큰 Content-Length를 선언한 POST를 보낸 뒤 본문을 한 바이트씩 흘리며, 분당 한 바이트로 도착하는 페이로드를 기다리는 서버를 붙든다. Slow Read 공격은 뒤집는다. 큰 것을 요청한 뒤 작은 TCP 수신 윈도를 광고하고 응답을 지독하게 느리게 읽어 서버의 송신 버퍼를 못 박는다. 매번 같은 모양이다 — 의도를 선언하고, 그걸 이행하는 데 영원을 끌며, 그 내내 서버가 상태를 붙들게 한다. 통틀어 슬로우 HTTP 공격이고, 완화책은 늘 같은 가족이다. 타임아웃, 최소 데이터 전송률, 연결당·IP당 상한, 그리고 오리진을 건드리기 전에 요청 전체를 스스로 읽어 버리는 버퍼링 프록시.
네 편의 글을 관통하는 실은 이렇다. Rapid Reset은 되돌릴 수 없는 RST_STREAM 전에 서버가 일을 하게 만든다. CONTINUATION Flood는 결코 오지 않는 END_HEADERS를 기다리며 헤더 조각을 붙들게 만든다. Slowloris는 결코 오지 않는 빈 줄을 기다리며 워커를 세워 두게 만든다. 다른 시대, 다른 프로토콜 버전, 천차만별인 대역폭 — 그러나 매번 같은 근본적 사기다. 서버가, 클라이언트가 지킬 의사 없는 약속에 희소한 무언가를 걸도록 만드는 것. 이국적인 HTTP/2 홍수와 17년 된 Perl 스크립트는, 본질에서 같은 수다.
방어가 계속 운을 맞추는 이유도 그래서다. 이들 하나하나가 같은 멋없는 본능으로 격퇴된다. 완성되지 않은 요청에 무한한 참을성을 베풀지 말 것. 마감을 걸 것. 진행을 요구할 것. 본론에 결코 이르지 않으면서 계속 떠드는 연결은 끊어 버릴 것. 인터넷은 서버를 더 빠르고 효율적으로 만드는 데 막대한 노력을 쏟지만, 언제 기다림을 멈춰야 하는지 가르치는 데는 거의 쓰지 않는다 — 그리고 기다림을 멈추는 것이야말로 속삭임과 소방 호스가 둘 다 두려워하는 유일한 방어다.