2006년 9월, 한 데비안 관리자가 메모리 검사기의 경고 하나를 없애려다가, 자기 사용자들의 컴퓨터가 만들 수 있는 서로 다른 SSH 키의 수를 천문학적으로 많던 것에서 32,767개로 조용히 줄였습니다. 수천 배 줄어든 게 아닙니다. 텍스트 파일 하나에 다 적을 수 있는 숫자로요. 이후 스무 달 동안, 데비안이나 우분투에서 갓 만든 2048비트 RSA 키는 고작 수만 개 후보 중 하나였고, 누구든 그 전체 집합을 미리 계산해 둘 수 있었습니다.

데비안의 루치아노 벨로(Luciano Bello)가 2008년 5월에 이를 발견하면서 CVE-2008-0166과 데비안 권고문 DSA-1571이 붙었습니다. 하지만 사람들이 기억하는 건 그 숫자입니다. 아키텍처·키 종류·키 길이마다 3만 2,767개. 인터넷에서 가장 보안에 예민한 운영체제가 거의 2년 동안. 겉보기에 똑같은 C 코드 두 줄이 어떻게 그런 일을 저질렀는가 하는 이야기는 “잘못돼 보이는 코드”와 “구조를 떠받치는 코드” 사이의 간극에 관해 제가 아는 최고의 교훈입니다.

무작위성은 초기화 안 된 메모리에서, 그것도 일부러 나왔다

이 버그를 이해하려면 먼저 불편한 사실 하나를 삼켜야 합니다. OpenSSL의 난수 생성기는 일부러 초기화되지 않은 메모리를 읽었습니다.

암호용 PRNG는 스스로를 시드(seed)하기 위해 예측 불가능한 비트 풀이 필요합니다. OpenSSL은 여느 곳에서 엔트로피를 모았는데, 거기에 더해 덤으로 초기화되지 않은 버퍼 — 할당은 했지만 한 번도 쓴 적 없는 메모리 — 의 내용을 섞어 넣었습니다. 그 안의 값은 직전 주인이 남긴 무엇이든입니다. 쓰레기값이지만, 매번 달라지는 쓰레기값이죠. 실제 엔트로피엔 거의 기여하지 못했고 좋은 생각이라 보기도 어렵지만, 어쨌든 의도된 것이었습니다.

문제는 초기화 안 된 메모리를 읽는 행위가 바로 Valgrind나 Purify 같은 도구가 비명을 지르라고 존재하는 대상이라는 겁니다. 메모리 검사기에게 “초기화되지 않은 값 사용”은 실제 버그 부류 전체를 알리는 빨간 깃발이고, 여기서는 무엇이든 RNG를 건드릴 때마다 OpenSSL에서 그 경고가 떴습니다 — 관리자가 정작 신경 쓰던 경고들을 파묻어 버리는, 끊임없고 시끄러운 거짓 경보였죠. 그래서 그는 이 소음을 일으키는 줄을 찾아 나섰습니다.

똑같이 생긴 두 줄, 그중 하나가 전부를 떠받치고 있었다

md_rand.c에는 완전히 똑같아 보이는 호출이 두 개 있었습니다.

MD_Update(&m, buf, j);

같은 함수, 같은 인자, 글자 하나까지 동일. 하나는 풀에서 난수 바이트를 꺼내는 루틴 안에 있으면서 그 초기화 안 된 버퍼를 섞고 있었습니다 — 지워도 무해한 쪽이죠. 다른 하나는 ssleay_rand_add, 즉 새 엔트로피를 풀에 섞어 넣는 것이 유일한 임무인 루틴 안에 있었습니다. OpenSSL이 시스템에서, 프로세스 상태에서, 호출자에게서 모은 모든 바이트가 바로 그 줄을 통과했습니다.

둘은 눈으로 구별할 수 없었습니다. 그리고 관리자는 겉보기에 쓰레기값을 읽는 것 같은 암호 코드를 함부로 바꿔도 되는지 확신이 안 서서, 책임 있는 행동을 합니다. 상류(upstream)에 물어본 거죠. 2006년 openssl-dev 메일링 리스트의 한 스레드(“Random number generator, uninitialised data and valgrind”)에서 그는 이 줄들을 가리키며 지워도 되겠느냐고 물었습니다. OpenSSL 개발자 울프 묄러(Ulf Möller)가 답했습니다. “디버깅에 도움이 된다면, 저는 지우는 데 찬성입니다.”

바로 이것이 이 재앙의 경첩입니다. 관리자는 이를 청신호로 읽었습니다. 하지만 openssl-dev는 바쁜 공개 리스트였지 보안 팀이 아니었고, 그 답은 단편적인 코드 조각에 대한 질문에 던진 빠른 동의였을 뿐이며, 답한 사람은 그 두 줄 중 하나가 실제 엔트로피가 풀에 닿는 유일한 통로라는 걸 거의 확실히 몰랐습니다. 그 대화에서 누구도 두 호출 지점과 그 주변 함수를 동시에 보고 있지 않았습니다. 맥락이 빠진 질문에 대한 한 줄짜리 승인이, 끝내 이뤄지지 않은 리뷰를 대신한 겁니다.

두 줄 다 빠졌습니다. 한쪽 제거는 사실상 아무 일도 아니었고, 다른 한쪽은 그렇게 정성껏 모은 엔트로피와 그걸 써야 할 생성기 사이의 연결을 끊어 버렸습니다.

남은 것은 프로세스 ID였다

ssleay_rand_add가 무력화되자, OpenSSL이 계속 모으던 엔트로피는 갈 곳이 없어졌습니다. 여전히 모으긴 했는데, 더는 섞이지 않았죠. 시드에 닿는 유일한 가변 입력은 현재 **프로세스 ID(PID)**였습니다.

리눅스에서 PID의 기본 최댓값은 32,768입니다. 그래서 PRNG 전체의 시드 공간 — ssh-keygen, TLS 키를 만드는 OpenSSL 명령, OpenVPN, 데비안 OpenSSL에 무작위성을 요청한 그 무엇이든 — 은 주어진 아키텍처·키 종류·키 길이에 대해 최대 32,767개 값으로 쪼그라들었습니다. 애써야 겨우 맞힐 수 있는 취약한 키는 위험입니다. 하지만 공격자가 미리 전부 열거할 수 있는 32,767개 집합에서 뽑힌 키는 아예 키가 아닙니다. 가능한 모든 조합이 색인 카드 한 장에 들어가는 자물쇠죠.

수학을 깰 필요가 없었다 — 목록만 있으면 됐다

공개 며칠 만에 HD 무어(HD Moore)를 비롯한 이들이 뻔하고도 파괴적인 일을 했습니다. 가능한 키를 전부 만들어 버린 겁니다 — 망가진 데비안 상자가 뽑을 수 있는 흔한 크기의 RSA·DSA 키 전부를, 미리 계산해 다운로드용으로 묶어서. 어떤 서버의 키가 취약한지 확인하는 일은 조회 한 번이 됐습니다. 그런 키를 쓰는 서버에 들어가는 일은, 후보 개인키 32,767개를 그 서버의 SSH 로그인에 하나씩 넣어 보다 하나가 통할 때까지 — 몇 분이면 되고, 암호 해독도 재간도 필요 없는 — 일이 됐습니다.

그리고 “데비안 서버가 노출됐다”보다 더 나빴습니다. 독은 운영체제가 아니라 키를 따라 옮겨 다녔거든요. 2007년에 데비안 노트북에서 인증서 서명 요청(CSR)을 만들어, 그 인증서를 튼튼한 FreeBSD 서버에 설치했다 해도, 그 키는 여전히 32,767개 중 하나였습니다. 우분투에서 SSH 키쌍을 만들어 공개키를 아무 운영체제나 도는 서버 백 대에 복사했다면, 그 백 대 전부가 이제 추측 가능한 키를 신뢰하게 된 겁니다. 데비안은 차단 목록 패키지를 배포하고 OpenSSH가 알려진 취약 키를 아예 거부하도록 가르쳐야 했습니다. OpenSSL을 패치해서 고칠 수 있는 문제가 아니었으니까요 — 나쁜 키들은 이미 인터넷 곳곳에, 데비안을 한 번도 돌린 적 없는 시스템에까지 배포돼 있었습니다. 그 키들은 이후로도 몇 년간 계속 튀어나왔습니다. 권고문이 나오고 7년 뒤인 2015년, 깃허브 사용자들의 SSH 공개키를 모아 본 한 연구자가 그 안에서 여전히 데비안 취약 키를 찾아냈고 — 일부는 유명 프로젝트에 푸시 권한이 있었습니다 — 깃허브가 그 키들을 폐기해야 했습니다.

정리 작업과 재앙은 같은 편집이었다

제가 계속 곱씹게 되는 지점이 여기입니다. 악의도 없었고, 흔히 말하는 무능도 없었고, 놓친 뻔한 단계도 없었습니다. 관리자는 존중받는 도구가 내는 진짜로 불안한 경고를 봤습니다. 그게 초기화 안 된 메모리를 읽는 데서 온다는 걸 정확히 짚었고, 그건 없앨 만한 진짜 버그 패턴이 맞습니다. 혼자 암호 코드를 바꾸는 걸 못 미더워해서 상류 프로젝트에 물었습니다. 실제 OpenSSL 개발자에게서 긍정의 답을 받았습니다. 개별 동작 하나하나는 다 책임 있는 선택이었습니다. 그 결과가 주요 배포판 역사상 최악의 암호 실패였습니다.

함정은, 두 MD_Update 줄이 글자 그대로 똑같으면서도 의미상 정반대였다는 것입니다 — 하나는 소음, 하나는 구조를 떠받치는 벽. 그런데 코드도, 도구도, 대화도 그 차이를 드러내 주지 않았습니다. Valgrind는 어떤 줄이 초기화 안 된 메모리를 읽는다고는 말해 줍니다. 하지만 세 함수 떨어진 곳의 똑같이 생긴 줄이 당신의 엔트로피 풀을 먹이는 유일한 통로라는 건 말해 주지 못합니다. 메모리 검사기는 코드의 한 속성을 재고 있었고, 정작 중요한 속성은 그 검사기에 보이지 않았습니다. 그리고 이를 잡아낼 수도 있었던 사람의 리뷰는, 누구도 전체를 보지 못한 조각에 대한 메일링 리스트 한 줄로 압축돼 버렸습니다.

사람들이 흔히 끌어내는 교훈은 “배포판이 상류 암호 코드를 함부로 패치하면 안 된다”와 “린터를 맹목적으로 따르지 마라”이고, 둘 다 그 자체로는 맞습니다. 하지만 더 깊은 교훈은 위험이 실제로 어디에 사는가에 관한 것입니다. 위험은 알고리즘에 없었습니다 — RSA는 멀쩡했고, 수학도 멀쩡했습니다. 위험은, 관여한 모두가 사소하게 취급할 만큼 작은 변경 안에 있었습니다. 보안 리뷰를 붙이기엔 너무 사소하고, 다시 확인하기엔 너무 뻔하고, 구별하기엔 너무 똑같이 생긴 변경. 가장 위험한 편집은 위험해 보이는 편집인 경우가 드뭅니다. 그건 정리 정돈처럼 보이는 편집입니다.