2024년 3월 말, Andres Freund라는 마이크로소프트 엔지니어가 거의 아무도 하지 않는 일을 하고 있었다: 0.5초에 신경이 쓰였던 것이다.
그는 데비안 테스팅 머신에서 PostgreSQL 벤치마크를 돌리다가, sshd — OpenSSH 데몬, 지구상 대부분 서버의 원격 로그인을 지키는 바로 그것 — 가 수상하게 CPU를 많이 먹는 걸 발견했다. 로그인이 있어야 할 것보다 500밀리초쯤 느렸다. 프로파일러는 자꾸 liblzma를 가리켰다. xz-utils 패키지의 압축 라이브러리인데, SSH 로그인 근처에 있을 이유가 전혀 없는 물건이었다. 대부분은 어깨를 으쓱하고 시끄러운 VM 탓을 하며 하던 일로 돌아갔을 것이다. Freund는 그 실을 끝까지 당겼다.
그가 찾아내 3월 29일 oss-security 메일링 리스트에 올린 것은, xz-utils 5.6.0과 5.6.1 버전에 고의로 심어진 백도어였다 — 적절한 종류의 머신에서 공격자에게 인증 이전에 sshd의 원격 코드 실행을 넘겨주는 백도어. 여기엔 CVE 번호 CVE-2024-3094와 가능한 최고 심각도 점수 10.0이 붙었다. 그리고 이게 어떻게 거기 들어갔는가 하는 이야기는, 코드보다 더 무섭다.
소스에는 없었다
이 공격을 특별하게 만드는 첫 번째 사실이자, 대부분의 분석이 얼버무리는 대목이다. 당신이 xz 깃 저장소를 클론해서 모든 줄을 읽었다 해도, 백도어를 찾지 못했을 것이다.
악성 페이로드는 릴리스 tarball — 배포판이 실제로 내려받아 빌드하는 그 .tar.gz 패키지 — 안에 있었지, 버전 관리 이력에는 없었다. 방아쇠는 autotools가 tarball에 생성해 넣지만 아무도 깃에 커밋하지 않는 빌드 스크립트에 묻혀 있었고, 페이로드 자체는 프로젝트의 테스트 파일 속에 숨어 있었다 — 손상된 압축 테스트 케이스로 분류된 바이너리 덩어리, 즉 읽을 수 없는 쓰레기여야 마땅한 파일이라 어떤 리뷰어도 절대 열어보지 않는 딱 그런 종류. 배포판 빌드 중, 적절한 아키텍처에서, 그 “테스트” 파일들이 복호화되고 이어 붙여져 컴파일된 라이브러리에 주입됐다.
그래서 당신이 볼 두 곳 — 깃허브의 소스, 그리고 프로젝트 메인테이너 자신이 서명한 릴리스 — 이 서로 어긋났고, 그중 하나만 오염돼 있었다. 지난 5년간 우리가 세운 “네 의존성을 알라”는 모든 반사작용은, 당신이 감사하는 산출물이 당신이 실행하는 산출물이라고 가정한다. 이 공격은 그 둘 사이에 쐐기를 박았다.
sshd는 xz를 쓰지도 않는다
두 번째 영리한 대목: OpenSSH는 애초에 liblzma에 링크되지 않는다. 업스트림 sshd는 압축 라이브러리와 아무 상관이 없다. 그런데 어떻게 압축 라이브러리 버그가 SSH 백도어가 됐을까?
여러 주요 배포판이 스스로 설치한 옆문을 통해서다. 데비안과 페도라는 OpenSSH가 systemd와 대화하도록 — 부팅 중 데몬이 “준비됐다”고 알리도록 — 패치하고, 그 패치가 sshd를 libsystemd에 링크하며, libsystemd는 의존성으로 liblzma를 끌어온다. 공격자는 OpenSSH가 xz를 신뢰하게 만들 필요가 없었다. 패키징이 xz를 sshd와 같은 프로세스로 끌어들이게 하면 됐고, 데비안과 레드햇 계열 시스템에서 실제로 그랬다.
일단 로드되자 백도어는 GNU 간접 함수(IFUNC) 리졸버 — 로드 시점에 최적화된 함수 버전을 고르는 정당한 메커니즘 — 를 이용해 동적 링커에 자신을 걸고, OpenSSH가 키를 검증하는 루틴을 가로챘다. 공격자만이 가진 Ed448 키로 서명된 적절한 페이로드를 제시한 클라이언트는 인증을 통째로 건너뛰고 서버 권한으로 명령을 실행했다. 다른 누가 쑤셔봐도 보이는 건 없었다: 새로 열린 포트도, 수상한 프로세스도 없이, 그저 하나의 특정 마스터 키에만 응답하는 얌전한 SSH 서버.
밤잠을 설치게 할 대목
코드는 우아하면서 끔찍하지만, 그게 무서운 부분은 아니다. 무서운 부분은 공격자가 어떻게 커밋 권한을 얻었는가다.
xz-utils는 수년간 사실상 한 사람이었다: Lasse Collin, 리눅스 세계 전체의 하중을 조용히 떠받치게 된 압축 라이브러리를 유지하던 무급 자원봉사자. 2021년경, “Jia Tan”이라는 이름(깃허브 핸들 JiaT75)을 쓰는 기여자가 패치를 보내기 시작했다. 유능했다. 계속 왔다. Jia Tan은 인내심 있고 도움이 됐으며, 지친 1인 메인테이너가 기도하며 바라는 딱 그런 기여자였다.
그러다 압박 캠페인이 시작됐다. 2022년 내내 “Jigar Kumar”, “Dennis Ens” 같은 이름의 계정들이 메일링 리스트에 나타나, xz가 정체돼 있다고, Collin이 병합을 충분히 빨리 안 한다고, 프로젝트엔 시간이 더 많은 새 메인테이너가 필요하다고 불평했다. 자기 업무량과 정신 건강 문제를 공개적으로 털어놨던 Collin은 반박했고 — 그러다 지쳐, 그동안 그토록 미덥던 그 도움 많은 신참에게 책임을 넘기기 시작했다. 2023년에는 Jia Tan이 릴리스를 만들고 있었다. 공동 메인테이너를 요구하던 그 소켓퍼펫들은, 하나 생기자마자 조용해졌다. 그들은 프로젝트가 고쳐지길 원한 게 아니었다. 열쇠를 원했다.
Jia Tan이 실제로 누구였는지는 아무도 모른다. 커밋 타임스탬프는 아시아·태평양처럼 그럴듯해 보이려 타임존을 편집한 사람처럼 이리저리 흔들리고, 그 운영 규율 — 신뢰를 한 번 쓰기 위해 수년간 정당한 작업으로 쌓아 올린 — 은 외로운 취미가가 아니라 자금 지원을 받는 전문 작전처럼 읽힌다. 커밋에 적힌 이름은 아무에게도 속하지 않는다. 모두가 신뢰한 그 메인테이너는 존재한 적이 없었다.
실제로 우리를 구한 건 운이었다
우리는 빠져나왔다. 오염된 버전들은 아직 테스팅·롤링 릴리스 배포판 — 데비안 sid, 페도라 Rawhide, openSUSE Tumbleweed, Kali — 을 통해 굴러가는 중이었고, 진짜 피해가 났을 안정·엔터프라이즈 릴리스에는 아직 닿지 않았다. 몇 주만 더 있었으면 5.6.x는 중요한 곳들에 들어가 있었을 것이다.
하지만 왜 빠져나왔는지 정직해지자. 스캐너가 아니었다. SBOM도 아니었다. 위협 모델링을 하는 보안팀도 아니었다. 우리가 기록한 가장 정교한 오픈소스 공급망 공격은, 꼼꼼한 엔지니어 한 명이 자기 벤치마크가 시끄러운 게 거슬려 그냥 넘어가길 거부했기 때문에 잡혔다. 그건 통제 수단이 아니다. 우연히 당첨된 복권이다.
그리고 우리의 표준 플레이북이 무엇을 했을지 보라. 소프트웨어 자재명세서(SBOM)는 당신이 xz 5.6.1을 — 실제 버전, 정당한 릴리스, 프로젝트 메인테이너 자신이 올바로 승인한 것을 — 돌리고 있다고 충실히 기록했을 것이다. 모든 출처(provenance) 검증이 통과했을 텐데, 출처가 진짜였기 때문이다. 공격자가 곧 메인테이너였다. 잘못된 사람을 신뢰하는 문제에서 검증으로 빠져나올 수는 없다. 서명은 열쇠를 쥔 손만큼만 좋다.
불편한 교훈은 “의존성을 더 열심히 감사하라”가 아니다. 인터넷의 충격적으로 많은 부분이 지친 자원봉사자 한 명이 여가 시간에 무급으로 유지하는 라이브러리 위에 얹혀 있다는 것, 그리고 그 인프라를 장악하는 가장 값싼 방법은 암호를 깨는 게 아니라 그 사람과 친구가 되어 기다리는 것이라는 사실이다. 우리는 20년을 코드를 단단히 하는 데 썼다. XZ 백도어는, 공급망 전체에서 가장 무르고, 가장 감시받지 않으며, 가장 많은 하중을 지는 부품이 메인테이너라는 것 — 그리고 어떤 적들은 그 메인테이너가 되기 위해 2년을 쓸 만큼 인내심이 있다는 것을 상기시켰다.