10년 동안, SSH 서버를 어떻게 하드닝하느냐고 물으면 답에는 암호(cipher)에 관한 줄이 들어 있었다. 낡은 CBC 모드를 끄고 chacha20-poly1305@openssh.com을 우선하라. 좋은 조언이었다. ChaCha20-Poly1305는 현대적인 인증 암호로, 빠르고, 상수 시간이며, CBC 모드를 반복되는 버그의 원천으로 만들었던 날카로운 모서리가 없다. 모든 체크리스트 상단 근처에 이게 있었다.

2023년 12월, 독일 보훔 루르대학교(Ruhr University Bochum)의 연구자 세 명 — 파비안 보이머(Fabian Bäumer), 마르쿠스 브링크만(Marcus Brinkmann), 외르크 슈벤크(Jörg Schwenk) — 이 Terrapin이라는 공격을 발표했고, 핵심은 chacha20-poly1305@openssh.com이 이 공격이 가장 깔끔하게 통한 암호였다는 것이다. ChaCha20이 약해서가 아니다. 결함이 애초에 암호에 있지 않았기 때문이다. 결함은 어떤 암호도 책임지지 않는 SSH의 한 부분에 있었고, “좋은 암호”와 “나쁜 암호”가 — 서로 다른 구조적 이유로, 같은 결과로 — 둘 다 노출돼 있었다.

Terrapin은 네트워크 공격자가, 암호화되고 인증된 프로토콜에서는 불가능해 보이는 일을 할 수 있게 했다. 연결 시작 부분에서 메시지 하나를 깔끔하게 삭제하고, 양쪽 어느 끝도 눈치채지 못하게 하는 것. 복호화가 아니다. 읽는 것도 아니다. 삭제하는 것이고, 뭔가 사라졌다는 흔적조차 남기지 않는 것이다.

SSH가 세면서도 결코 확인하지 않는 숫자

어떻게 가능한지 보려면, SSH가 데이터를 어떻게 프레이밍하는지 작은 사실 하나를 알아야 한다. 암호화 아래에서 SSH는 패킷으로 말한다 — 바이너리 패킷 프로토콜(Binary Packet Protocol) — 그리고 모든 패킷에 시퀀스 번호가 붙는다. 연결의 첫 패킷은 0번, 다음은 1번, 그렇게 단조 증가하며 영원히 이어진다. 이 번호는 회선에 실려 나가지 않는다. 양쪽이 그저 각자 독립적으로 세면서, 서로의 카운터가 일치한다고 믿는다.

이 시퀀스 번호는 장식이 아니다. 각 암호화 패킷을 보호하는 메시지 인증 코드(MAC)의 입력이다. 수신자가 패킷의 MAC를 검사할 때 시퀀스 번호가 계산에 들어가고, 이것이 공격자가 옛 패킷을 재전송하거나 스트림 순서를 바꾸는 것을 막는다. 패킷의 위치를 바꾸면 MAC가 더는 검증되지 않기 때문이다. 카운터는 대화에서 지금 어디쯤인지에 대한 프로토콜의 감각이다.

25년간 유지됐지만 틀린 것으로 드러난 가정이 여기 있다. MAC가 모든 암호화 패킷에서 시퀀스 번호를 덮으니 카운트 자체도 처음부터 끝까지 신뢰할 수 있다는 것. MAC는 당신이 보낸 각 패킷을 덮는다. 정작 중요한 카운팅이 시작되기 전에 당신이 제거한 패킷에 대해서는 아무 말도 하지 않는다.

트릭

SSH 연결은 평문으로 시작한다. 양쪽이 지원 알고리즘을 교환하고, 키 교환을 돌리고, 그런 다음에야 암호화를 켠다. 초기 핸드셰이크 패킷은 암호화되지 않았고 — 이게 하중을 지탱하는 핵심이다 — 키 교환이 인증하는 서명된 트랜스크립트에 전부 접혀 들어가지도 않는다. 어떤 메시지 타입은 설계상 무시해도 되는 것이다. SSH_MSG_IGNORE는 이름 그대로다. 수신자가 반드시 버려야 하는 no-op 패킷. 패딩과 킵얼라이브를 위해 존재한다. 아무것도 안 하니 아무도 깊이 생각하지 않았다.

그런데 딱 한 가지는 한다. 이걸 받으면 수신자의 시퀀스 카운터가 1 올라간다.

그래서 회선 위에 앉은 중간자(man-in-the-middle)의 공격은 이렇게 된다. 암호화되지 않은 핸드셰이크 동안, 한쪽으로 SSH_MSG_IGNORE 패킷 하나를 주입한다. 피해자는 규정대로 그 내용을 버리지만 — 그 카운터는 이제 반대편이 생각하는 것보다 1 앞서 있다. 주입된 패킷은 서명된 키 교환 트랜스크립트에 아무 지문도 남기지 않았으므로, 핸드셰이크는 여전히 완벽하게 검증된다. 양쪽 모두 모든 게 정상이라 확신하며 키 교환을 마친다.

그다음 암호화가 켜지고, 첫 실제 암호화 메시지가 건너간다. 현대 SSH에서 그 첫 메시지는 보통 SSH_MSG_EXT_INFO — 확장 협상이다. 공격자는 그냥 그걸 삭제한다.

암호화 패킷을 삭제하는 건 자살행위여야 한다. 다음 패킷의 MAC가 그 시퀀스 번호를 포함하므로, 패킷을 하나 떨구면 수신자의 카운트와 송신자의 카운트가 어긋나고, 바로 다음 MAC 검증이 실패하며, 연결이 무너진다. 시퀀스 번호를 MAC에 넣는 이유가 바로 그거다. 하지만 공격자는 이미, 평문 상태에서, 그 무시된 패킷으로 카운트를 1 어긋내 두었다. 이제 암호화 패킷 하나를 삭제하면 두 카운터가 딱 맞아떨어진다. 수신자는 송신자가 쓴 바로 그 시퀀스 번호로 다음 MAC를 계산한다. 검증된다. 연결은, 원래보다 메시지 하나 짧은 채로, 아무 데서도 문제 보고 없이 계속 나아간다.

이게 공격 전부다. 보이지 않는 곳에 패킷 하나를 더하고, 잡혔을 곳에서 패킷 하나를 빼서, 둘이 상쇄되게 하는 것. 프리픽스 절단(prefix truncation). chacha20-poly1305@openssh.com이나 Encrypt-then-MAC 모드의 CBC 암호가 필요한 이유는, 그 구조들이 패킷의 무결성을 시퀀스 번호에 묶는 방식이 이 더하기-1 빼기-1 맞바꿈을 일관되게 남겨두기 때문이다. AES-GCM은 마침 논스를 시퀀스 번호에 다르게 묶어서 아귀가 안 맞고 — 그래서 GCM은 취약하지 않았다. “현대적이고 권장되는” 암호는 취약했고, 사람들이 옮겨가던 암호 중 하나는 안 취약했다. 하드닝 가이드가 준 암호 선택 조언은, 여기서 도움이 되기엔 잘못된 계층을 겨냥하고 있었다.

그 대가로 실제로 얻는 것

대가에 대해 정직하고 싶다. “SSH에서 메시지를 조용히 삭제한다”와 그게 공격자에게 사주는 것 사이의 간극에서, 많은 보도가 숨넘어가게 흥분했기 때문이다.

OpenSSH 상대로 SSH_MSG_EXT_INFO를 삭제하는 건 탈취가 아니라 다운그레이드다. 확장 협상(RFC 8308)은 양쪽이 최신 기능을 합의하는 곳이다 — 클라이언트가 더 강한 서명 알고리즘을 안전하게 쓰게 해주는 server-sig-algs, 그리고 관찰자가 패킷 타이밍으로 당신이 무엇을 치는지 추론하지 못하도록 타이핑 타이밍을 패딩하는 키스트로크 타이밍 은폐 기능이 여기 포함된다. EXT_INFO를 벗겨내면 이것들이 협상 이전 기본값으로 되돌아간다. 프라이버시 기능 하나를 조용히 꺼버리고, 연결을 더 약한 선택 쪽으로 슬쩍 미는 것이다. 실재하지만, 미미하다 — 게다가 공격자가 이미 네트워크상 중간자로 자리 잡고 있어야 하고, 연결이 이미 취약한 암호를 쓰고 있어야 한다. 원격 루트가 아니다. OpenSSH 상대로는 메스이고, 그것도 얕은 메스다.

더 날카로운 모서리는 특정 구현에 있었다. 파이썬 SSH 라이브러리 AsyncSSH에는 추가 버그 두 개(CVE-2023-46445, CVE-2023-46446)가 있었고, Terrapin의 원시 도구가 이를 훨씬 나쁜 것으로 — 피해자의 클라이언트를 공격자가 통제하는 계정으로 몰래 로그인시켜, 피해자가 입력하는 모든 것이 공격자의 세션으로 가게 만드는 ‘불량 세션(rogue session)’ 공격을 포함해 — 촉발할 수 있었다. 이게 “정말 아플 수 있다”는 실전 사례이고, 조작된 핸드셰이크를 다루는 한 라이브러리의 처리에 있었지 프로토콜 결함 자체에 있지 않았다. 프로토콜 결함은 대표 CVE인 CVE-2023-48795를 받았지만, 무서운 결과는 딛고 설 구현 버그가 필요했다.

그래서 그 무서운 숫자 — 연구자들이 인터넷을 스캔해 SSH 서버의 77%가 취약한 모드를 지원하고, 57%가 그것을 선호 선택으로 올려두었음을 발견했고, 몇 주 뒤 Shadowserver 재단은 여전히 노출된 인터넷 서버를 1,100만 대 가까이 셌다 — 는 “거의 모든 것이 그 도구에 하나의 패킷으로 침해당하기 직전”이 아니라 “거의 모든 것이 그 원시 도구에 구조적으로 노출돼 있었다”로 읽는 게 맞다. 두 문장 다 참이다. 매우 다른 문장이다.

해법은 거의 모욕적일 만큼 단순하다

결함을 보고 나면 해법은 저절로 쓰인다. 실제로 그랬다. strict key exchange — “strict kex” — 라 불리고, 공격이 공개된 바로 그날 OpenSSH 9.6에 실렸으며, 이후 PuTTY, libssh 등으로 퍼졌다.

strict kex는 규칙 두 개를 더한다. 첫째: 예상치 못한 메시지 — SSH_MSG_IGNORE든, 키 교환의 엄격한 일부가 아닌 무엇이든 — 가 초기 핸드셰이크 중에 나타나면, 버리고 넘어가지 마라. 연결 전체를 중단하라. 공격이 의존하는 그 주입 패킷이 이제 보이지 않는 대신 치명적이 된다. 둘째: 첫 키 교환이 끝나면 양쪽 시퀀스 카운터를 0으로 재설정하라. 공격자가 핸드셰이크 중 몰래 끼워 넣은 어떤 오프셋도 그 경계에서 증발한다. 나중의 삭제를 상쇄하러 암호화 채널로 넘어갈 수 없게 된다.

카운터를 0으로 재설정하라. 그게 핵심이다. 사반세기 묵은 설계 틈을, 양쪽이 조용히 신뢰해오던 그 숫자를 연결이 보호할 가치가 있는 무언가가 되는 순간 알려진 값으로 다시 동기화하라고 못 박음으로써 막은 것이다. 유일한 걸림돌은 양쪽 끝이 모두 지원해야 한다는 것 — strict kex는 클라이언트와 서버가 둘 다 제안해야만 작동한다 — 이라서, 업데이트되지 않은 임베디드 장비·어플라이언스·네트워크 장비의 긴 꼬리는 앞으로 몇 년간 옛 방식으로 계속 협상할 것이다.

아무도 지키지 않던 이음매

OpenSSH 피해가 가벼웠음에도 Terrapin이 당신의 주의를 받을 가치가 있는 이유, 그게 실제로 무엇에 관한 것인지가 여기 있다.

SSH에는 모두가 죽도록 뜯어본 두 가지가 있었다. 핸드셰이크: 서명되고, 트랜스크립트로 인증되고, 25년간 암호학자들이 쑤셔댄 것. 그리고 데이터 채널: 모든 패킷이 암호화되고 MAC로 보호되며, 시퀀스 번호까지 포함된 것. 둘 다, 각자의 기준에서는, 올바랐다. 버그는 그 어느 쪽에도 없었다. 버그는 이음매 — 연결이 인증된-핸드셰이크 세계에서 인증된-채널 세계로 넘겨지는 순간 — 에 있었고, 그 이음매는 아무도 책임자를 지정하지 않은 유일한 곳이었다. 핸드셰이크는 무시된 패킷을 세지 않았다. 채널은 물려받은 카운터를 신뢰했다. 각 구성요소는 자기 내용물을 검증하고, 이음매 반대쪽은 누군가 다른 사람의 문제라고 가정했다.

이게 이전 가능한 교훈이고, SSH에만 국한된 게 아니다. 안전한 조각들로 안전한 시스템을 조립할 때, 조각들의 안전이 전체의 안전으로 합산되지는 않는다. 이음매는 그 자체가 공격 표면이고, 하필 각 인접 구성요소가 고립돼서는 올바르게 보이기 때문에, 테스트도 감사도 책임자도 가장 없을 부분이다. Terrapin은 SSH의 암호학을 깨지 않았다. 각각 잠겨 있고, 아무도 문을 달 생각을 안 한 두 방 사이의 문으로 걸어 들어갔다.

그리고 chacha20-poly1305@openssh.com을 우선하라던 그 체크리스트? 암호에 대해서는 틀리지 않았다. 다만 — 어떤 암호화 알고리즘이 가장 강한가 — 라는, 실제 약점이 있던 곳과는 아무 상관 없는 질문에 답하고 있었다. 좋은 조언이, 도움이 되기엔 한 계층 위를 겨냥한 것이다. 이 교훈의 불편한 버전은, 전부 옳은 진술로만 이루어진 하드닝 가이드조차 당신을 노출된 채로 둘 수 있다는 것이다. 결함이, 그 진술들이 다루던 어느 상자 안에도 애초에 없었기 때문이다.