하트블리드의 가장 나쁜 점은 유출된 메모리가 아니었다. 사건이 끝난 뒤에도, 자기 메모리가 유출됐는지 아닌지를 아무도 확인할 수 없었다는 것이다.
2014년 4월 7일, 두 팀이 같은 버그를 같은 날 공개했다 — 구글의 닐 메타(Neel Mehta)가 발견했고, 코데노미콘(Codenomicon)이라는 핀란드 회사가 독립적으로 찾아내 하트블리드라는 이름과 로고와 웹사이트까지 붙였다. 덕분에 메모리 안전성 버그 하나가 웬만한 스타트업보다 나은 브랜딩을 갖게 됐다. 버그는 OpenSSL 안에 있었다. 웹의 ‘보안’ 서버 상당수에서 TLS를 종단하는 그 라이브러리다. 이 버그는 인터넷상의 누구든 서버의 살아 있는 메모리를 최대 64킬로바이트씩, 몇 번이고 반복해서 읽게 해줬다 — 서버가 완전히 일상적인 것으로 취급하는 요청 하나로. 개인키, 세션 쿠키, 전송 중인 비밀번호, 힙에 아직 남아 있던 남의 요청까지, 근처에 있던 것은 무엇이든.
그리고 그 읽기는 아무 흔적도 남기지 않았다. 붙들고 있어야 할 문장은 이거다. 나머지는 전부 여기서 따라 나온다.
거의 아무도 필요로 하지 않은 기능
TLS에는 하트비트 확장이 있다. RFC 6520에 정의돼 있다. 발상은 작고 합리적이다: 연결을 재협상 없이 살려두기 위해, 한쪽이 작은 페이로드를 보내며 “이걸 나한테 되돌려줘”라고 하면 상대가 그대로 메아리처럼 돌려준다. 주로 유용한 건 DTLS, 즉 데이터그램 버전 TLS다. 상대가 아직 살아 있는지 확인하고 경로 MTU를 탐색하는 데 쓴다. TCP 위의 평범한 HTTPS에서는 딱히 필요한 일을 거의 하지 않는다. 그런데도 추가됐다.
이 요청에는 중요한 세 가지가 담긴다: 타입 바이트, 16비트 길이 필드, 그리고 페이로드 자체다. 응답하는 쪽은 버퍼를 할당해 페이로드를 그대로 복사해 돌려줘야 한다. 대략 OpenSSL이 한 일은 이랬다:
/* p는 들어온 하트비트 메시지를 가리킨다 */
hbtype = *p++;
n2s(p, payload); /* payload = 클라이언트가 '주장한' 길이 */
pl = p; /* pl은 클라이언트의 실제 데이터를 가리킨다 */
/* ... 응답을 할당하고 payload 바이트만큼 복사 ... */
buffer = OPENSSL_malloc(1 + 2 + payload + padding);
memcpy(bp, pl, payload); /* 주장한 길이를 믿고 그만큼 복사한다 */
memcpy 줄을 다시 읽어보자. payload는 클라이언트가 보낸 숫자다. 코드는 클라이언트 데이터에서 시작해 그 숫자만큼 바이트를 복사한다 — 그런데 클라이언트가 정말로 그만큼 보냈는지는 결코 검사하지 않는다. 그래서 페이로드는 1바이트만 담고 길이 필드에는 65,535라고 적어 하트비트를 보내면, OpenSSL은 충실하게 65,535바이트를 돌려준다: 당신의 1바이트, 그리고 힙에서 바로 옆에 있던 무엇이든 64KB. 당신이 보낸 적 없는 메모리를 넘겨준다. 당신이 숫자를 불러줬고, 그게 그걸 믿었기 때문이다.
이게 버그의 전부다. 교과서적인 버퍼 오버리드(over-read): “주장한 길이가 실제로 우리가 받은 레코드 안에 들어가는가?”라고 물었어야 할 방어선이 그냥 없었던 것이다.
고치는 데는 세 줄이면 됐다
OpenSSL이 공개와 같은 날 1.0.1g를 내놓았을 때, 패치는 허무할 만큼 간단했다. 빠져 있던 검사를 추가했을 뿐이다:
/* 레코드가 주장한 내용을 담기엔 너무 짧으면 조용히 폐기 */
if (1 + 2 + 16 > s->s3->rrec.length)
return 0;
if (1 + 2 + payload + 16 > s->s3->rrec.length)
return 0;
주장한 길이를, 실제로 도착한 레코드의 길이와 비교한다. 메시지가 자기가 말한 내용을 도저히 담을 수 없다면, 버리고 답하지 않는다. 그게 다다. “인터넷 개인키 상당수를 누구나 읽을 수 있다”와 “아니다” 사이의 차이가, 코드가 이미 구조체 안에 들고 있던 길이 값과의 비교 한 번이었다.
이 버그는 2012년 3월 14일 OpenSSL 1.0.1에 실려 배포됐다. 2011년 12월 31일 섣달그믐에 작성된 커밋에서 왔고, 빠진 검사를 잡아내지 못한 메인테이너가 병합했다. 2년 동안 프로덕션에 앉아 있었다. 1.0.1부터 1.0.1f까지 모든 버전이 악용 가능했다. 공개 시점 넷크래프트(Netcraft) 추정으로는 신뢰받는 HTTPS 사이트의 약 17%, 즉 인증서 약 50만 개가 위험했다. 그 크기의 구멍이 열려 있기에 2년은 긴 시간이다. 그리고 여기, 2년을 단지 나쁜 게 아니라 견딜 수 없게 만든 것이 있다.
피해를 감사(監査)할 수 없었다
정상적인 익스플로잇은 증거를 남긴다. 뭔가를 죽이거나, 로그 한 줄을 건드리거나, 필터가 기록하는 잘못된 입력을 보내거나, 경보를 울린다. 하트블리드는 그중 아무것도 하지 않았다. 설계상 그랬다. 악의적 하트비트는 애초에 잘못된 게 아니기 때문이다. 악성 하트비트는 유효한 하트비트다. 타입 바이트도 맞고, 길이 필드도 프로토콜 관점에선 맞고, 서버는 정당한 요청을 처리하는 것과 똑같이 처리한다. 오류도, 예외도, 이상 징후도 없다. 공격자가 잘 짜인 질문을 던지면 서버가 잘 짜인 답을 주는데, 그 답에 자기 비밀이 섞여 있을 뿐이다.
OpenSSL은 하트비트 내용을 로그로 남기지 않았다 — 남길 이유가 있었겠나? 그래서 압도적 다수의 서버에는 누가 하트비트를 보냈는지, 몇 개나, 얼마나 크게 보냈는지에 대한 기록이 없었다. 그 말은, 4월 7일 이후 취약 버전을 돌리던 모든 운영자가 답을 구할 수 없는 질문 앞에 섰다는 뜻이다: 우리가 털렸나, 털렸다면 무엇이 빠져나갔나? 확인할 수 없었다. 그걸 알려줄 로그는 애초에 쓰인 적이 없었다. 이 트래픽을 기록할 가치가 있다고 아무도 생각한 적이 없었으니까.
털리지 않았음을 증명할 수 없을 때는, 털렸다고 가정해야 한다. 이 인식론적 사실 하나가 세 줄짜리 패치를 전 지구적 수 주간의 작업으로 바꿔놓았다. 패치는 미래의 유출을 즉시 멈췄다. 과거에 대해선 아무 말도 하지 못했고, 그 과거는 2년 길이였다.
아무도 원치 않은 청산: 폐기
서버 메모리가 읽혔다고 가정해야 한다면, 그 메모리에 사는 TLS 개인키도 유출된 것에 포함됐다고 가정해야 한다. 처음엔 키 자체가 추출 가능한지 의심하는 사람들도 있었다. 도달 가능한 힙에 놓여 있지 않을 거라는 주장이었다. Cloudflare가 일부러 취약하게 만든 서버를 세워두고 인터넷 전체에게 키를 훔쳐보라고 초대하며 논쟁을 끝냈다. 2014년 4월 11일, 표도르 인두트니(Fedor Indutny)라는 소프트웨어 엔지니어가 해냈고, 뒤이어 다른 사람들도 해냈다. 최악의 시나리오가 확인됐다: 키는 빠져나온다.
그래서 모든 운영자는 인증서 폐기라는, 존재는 하지만 대규모로는 거의 발동되지 않는 절차를 실행해야 했다: 새 키를 만들고, 새 인증서를 받고, 옛 것을 폐기한다. 재발급 부분은 잘 굴러갔다. 폐기 부분에서 업계가 거울을 봤고, 비친 모습이 마음에 들지 않았다. 2014년의 폐기는 깨진 두 메커니즘에 얹혀 있었다 — 폐기된 일련번호의 거대한 목록인 CRL은 브라우저가 점점 참을성 없이 다운로드를 꺼렸고, 실시간 조회인 OCSP는 브라우저가 “소프트 페일(soft-fail)“한다. 즉 조회가 시간 초과되면 어깨 한번 으쓱하고 인증서가 멀쩡한 것처럼 진행한다는 뜻이다. 훔친 키를 쥔 공격자는 그냥 OCSP 조회를 막아버리면 유유히 통과한다. 하트블리드는 웹이 겪은 가장 큰 폐기 이벤트 중 하나를 만들어냈고, CRL은 부풀었고, 불편한 결론은 이것이었다: 인증서를 폐기해도 누군가가 그걸 쓰는 걸 확실히 막지 못한다. 이 청산은 몇 년 뒤 생태계가 OCSP를 아예 꺼버린 이유, 그리고 진짜 해답이 인증서 수명을 폐기가 거의 무의미할 만큼 짧게 만드는 것으로 판명된 이유로 곧장 이어진다. 하트블리드가 폐기를 망가뜨린 게 아니다. 모두가 폐기를 한꺼번에 쓰게 만들었을 뿐이고, 그 과정에서 그게 애초에 제대로 작동한 적이 없다는 걸 발견한 것이다.
2,000달러짜리 각주
로고보다 주목은 덜 받았지만 더 중요한 후일담이 있다. 사람들이 OpenSSL을 정확히 누가 유지보수하는지 들여다봤을 때 — 웹의 큰 비중에서 자물쇠 아이콘 아래 깔려 있는 이 라이브러리 말이다 — 답은 이랬다: 사실상 아무도 없다. OpenSSL 소프트웨어 재단을 이끌던 스티브 마퀘스(Steve Marquess)는 이 프로젝트가 연간 약 2,000달러의 후원을 받았고, 사실상 한 사람만이 풀타임으로 일할 수 있었다고 밝혔다. 인터넷의 암호학적 바닥이, 괜찮은 중고차 한 대 값의 예산으로 굴러가는 자원봉사 프로젝트였던 것이다.
몇 주 안에 리눅스 재단이 Core Infrastructure Initiative를 발표했고, 아마존 웹 서비스, 시스코, 델, 페이스북, 후지쯔, 구글, IBM, 인텔, 마이크로소프트, 넷앱, 랙스페이스, VMware가 바로 이런 종류의 — 하중을 떠받치지만 보이지 않고 화려하지 않은 — 코드를 위한 개발자 후원에 실제 돈을 댔다. 모두가 의존하던 것을 아무도 지켜보고 있지 않았다는 걸 알아차리는 데 전 지구적 공황이 필요했다 — 10년 뒤 무급 메인테이너 한 명의 소진이 주요 리눅스 배포판의 SSH 서버에 백도어를 실을 뻔했을 때 다시 착지할 바로 그 교훈이다. 우리는 ‘핵심적’과 ‘후원받는’이 무관한 속성이라는 걸 자꾸 다시 배운다.
하트블리드가 우리에게 가르쳤어야 할 것
깔끔한 교훈은 “메모리 안전 언어를 써라”이고, 틀린 말은 아니다 — 이런 오버리드는 러스트(Rust)에선 조용한 유출이 아니라 크래시이고, rustls 같은 메모리 안전 TLS 스택이 바로 이 이유로 생겨났다. 하지만 오래 남는 교훈은 언어에 관한 게 아니다. 읽기 버그가 치르게 하는 대가에 관한 것이다.
우리는 보안 주의력의 대부분을 쓰기에 쓴다 — 코드 실행, 인젝션, 공격자가 기계로 하여금 무언가를 하게 만드는 것. 하트블리드는 아무것도 하지 않았다. 그저 봤을 뿐이다. 그리고 오직 보기만 하는 버그가, 충분히 조용하게 본다면, 행동하는 버그보다 겪기에 더 나쁘다. 행동은 조사할 수 있는 흔적을 남기지만 보기는 그렇지 않기 때문이다. 하트블리드의 피해는 어떤 공격자가 실제로 가져간 것으로 한정되지 않았다. 그들이 가져갔을 수도 있는 것으로 한정됐고, 아무도 그걸 측정할 수 없었기에 모두가 최댓값의 대가를 온전히 치러야 했다. 키를 새로 만든 운영자들은 침해됐다고 알려져서가 아니라, 침해되지 않았다고 알 수가 없어서 그렇게 했다. 그리고 많은 곳은 끝내 하지 않았다: 공개 한 달 뒤 넷크래프트는 영향받은 사이트 대부분이 인증서를 재발급도, 폐기도 하지 않았다고 집계했다.
“우리가 당했나?”에 답할 수 있는 시스템을 만들어라 — 옳은 것을 로그로 남기고, 악용이 정상 사용과 달라 보이게 만드는 시스템 말이다. 세 줄짜리 패치는 오후 한나절에 구멍을 막았다. 그 뒤에 있던 2년간의 실명(失明)은 우리가 끝내 되찾지 못한 부분이다.