1P by GN⁺ | ★ favorite | 댓글 1개
  • 2024년 3월 발견된 xz 백도어는 영향받는 sshd 환경에서 원격 코드 실행으로 이어질 수 있었고, NixOS 사례는 재현 가능한 빌드가 공급망 변조를 기계적으로 드러낼 수 있음을 보여줌
  • 악성 요소는 xz Git 저장소 전체가 아니라 5.6.0·5.6.1관리자 제공 릴리스 tarball에 숨어 있었고, 빌드 중 가짜 .xz 테스트 파일에서 셸 스크립트와 오브젝트 파일을 만들어냄
  • 공격은 glibcifunc를 이용해 liblzma 로딩 시 코드를 실행하고, sshdlibsystemd를 거쳐 liblzma에 전이 의존하는 Debian·Fedora 계열 환경을 노림
  • NixOS는 보통 GitHub 자동 생성 소스 아카이브를 선호하지만, xznixpkgs bootstrap 단계에 있어 autoconf 의존성 순환을 피하려면 관리자 tarball을 신뢰해야 했음
  • 제안된 방어법은 bootstrap 이후 GitHub 소스로 xz를 다시 빌드해 기존 tarball 산출물과 비교하는 방식이며, 조건을 활성화하자 liblzma.so 크기 차이와 _get_cpuid 심볼 추가가 드러남

xz 백도어가 숨어든 방식

  • xz는 Linux 배포판의 핵심 경로에서 소스 tarball을 풀 때 자주 쓰이는 압축·해제 소프트웨어임
  • 2024년 3월 xz에서 백도어가 발견됐고, 악성 관리자인 Jia Tan은 약 3년에 걸쳐 신뢰를 얻고 저장소 push 권한을 확보한 뒤 합법적인 기여 사이에 난독화된 코드를 조금씩 넣었음
  • Andres Freund는 여러 Debian unstable 머신에서 겪은 ssh의 500ms 성능 저하를 조사하다가 liblzma까지 추적했고, 백도어를 식별해 문서화함
  • 백도어의 목표는 ssh를 하이재킹해 특정 RSA 키로 로그인할 때 공격자가 피해자 머신에서 임의 명령을 실행할 수 있게 하는 것이었음

악성 tarball과 빌드 중 생성되는 오브젝트

  • 악성 구성요소는 xz Git 저장소 코드 전체에 직접 들어 있지 않았고, Jia Tan이 빌드·서명해 배포한 xz 5.6.05.6.1 릴리스 tarball에 포함됨
  • Git 저장소 안의 일부 테스트용 .xz 파일에는 머신 코드가 숨겨져 있었고, 릴리스 tarball에는 이를 꺼내도록 위장된 변경이 추가됨
  • 특히 m4/build-to-host.m4 변경은 겉보기에는 무해하고 주석도 달려 있었지만, 실제로는 여러 가짜 .xz 테스트 파일을 복호화·난독 해제하는 명령 체인을 숨김
  • 이 과정의 결과물은 두 가지임
    • xz 빌드 중 실행되는 셸 스크립트
    • 악성 바이너리 오브젝트 파일
  • 빌드 중 실행되는 셸 스크립트는 두 역할을 맡음
    • 특정 Linux 배포판, glibc 기능, ssh 설치 여부 등 백도어 실행 조건을 확인함
    • 정상 오브젝트인 liblzma_la-crc64_fast.o가 백도어 오브젝트 파일의 _get_cpuid 심볼을 쓰도록 수정함
  • Russ Cox의 xz 스크립트 분석은 빌드 중 악성 리소스가 만들어지는 과정을 자세히 다룸

sshd로 이어지는 ifunc 하이재킹

  • 동적 링크 프로그램은 실행 시점에 동적 로더가 공유 라이브러리를 메모리에 로드하고, 심볼 주소를 Global Offset Table(GOT) 에 채움
  • glibcifunc는 동적 로딩 시점에 같은 함수의 여러 구현 중 하나를 선택하기 위한 기능이며, xz 백도어는 이를 악성 코드 실행 경로로 사용함
  • Debian·Fedora 등 일부 배포판의 sshsystemd 알림 지원을 위해 libsystemd와 링크되고, libsystemd는 다시 liblzma와 링크됨
    • 이 환경에서는 sshdliblzma에 전이 의존함
  • sshd 실행 시 동적 로더가 libsystemdliblzma를 로드하고, 백도어가 설치된 경우 liblzma 로딩 중 악성 코드가 실행됨
  • 백도어는 ifunc 리졸버가 악성 _get_cpuid 심볼을 호출하게 만들고, 아직 읽기 전용이 아닌 GOT를 조작해 RSA_public_decrypt 주소를 악성 함수로 바꿈
  • Securelist의 상세 분석과 공격 벡터 및 완화책을 정리한 연구 글을 참고 자료로 볼 수 있음

신뢰할 수 있는 소스와 NixOS의 bootstrap 제약

  • 많은 배포판이 Git forge의 원본 소스 대신 관리자 제공 tarball로 xz를 빌드했기 때문에 이 공격이 효과를 가질 수 있었음
  • tarball 워크플로에는 역사적·실용적 이유가 있음
    • git보다 오래된 초기 Linux 배포판의 방식임
    • 릴리스 시점의 소스 상태를 자체 포함 아카이브로 보존함
    • manpage, configure 스크립트 같은 중간 산출물을 포함해 빌드 부담을 줄일 수 있음
    • 압축을 통해 저장 공간 효율을 얻을 수 있음
  • 보안 관점에서는 기술적으로 가능할 때 가장 신뢰할 수 있는 주체가 인증한 소스에서 빌드해야 함
    • GitHub에서 개발되는 프로젝트라면 GitHub가 릴리스마다 자동 생성하는 아카이브를 사용할 수 있음
    • Codeberg, SourceHut, GitLab 같은 신뢰할 수 있는 제3자 플랫폼에도 같은 논리를 적용할 수 있음
  • NixOS는 함수형 패키지 관리 모델을 쓰며, 패키지는 Nix 표현식으로 빌드 레시피를 정의함
  • NixOS 관리자는 가능한 경우 fetchFromGitHub로 GitHub 자동 생성 소스 아카이브를 쓰는 문화가 있지만, xz 패키지는 fetchurl로 수동 업로드된 관리자 tarball을 가져오고 있었음
  • 이유는 xznixpkgsbootstrap 단계에 포함되어 있기 때문임
    • bootstrap은 작은 seed binary 집합에서 nixpkgs 패키지 전체를 다시 빌드할 수 있게 하는 과정임
    • stdenv는 다른 패키지가 쓰는 기본 컴파일 환경임
    • stdenv는 런타임에 xz에 의존하고, coreutils 같은 패키지는 .tar.xz 소스 아카이브를 풀기 위해 xz가 필요함
  • GitHub 소스에서 xz를 빌드하려면 configure 스크립트를 만들기 위한 autoconf가 필요하지만, autoconfxz에 의존함
    • 관리자 제공 tarball에는 configure 스크립트가 이미 생성되어 있어 이 의존성 순환을 끊을 수 있음
    • 따라서 nixpkgs 그래프에서 xz가 빌드되는 지점에서는 GitHub 소스 아카이브를 쓰기 어렵고 관리자 tarball을 신뢰해야 했음

소스 비교보다 산출물 수렴 비교

  • 관리자 tarball과 GitHub 소스 tarball이 동일한지 비교하는 방식은 자연스러워 보이지만, 실무에서는 잘 맞지 않음
  • Daniel Stenberg은 릴리스 tarball이 소스와 다른 것이 기능이라고 설명함
    • manpage, configure 스크립트 같은 중간 산출물을 tarball에 포함할 수 있음
    • 배포판이 autoconf 의존성을 피하려는 경우 특히 유용함
  • 공급망 보안 관점에서는 이런 유연성이 관리자를 정직하게 신뢰해야 하는 부담으로 바뀜
  • 재현 가능한 빌드는 같은 조건에서 두 번 빌드했을 때 비트 단위로 동일한 산출물이 나오는 성질임
  • reproducible-builds 그룹은 가능한 많은 패키지를 재현 가능하게 만드는 것을 목표로 하며, Reproducible Builds: Increasing the Integrity of Software Supply Chains는 바이너리 산출물 배포 신뢰를 높이는 속성으로 이를 다룸
  • tarball 생성 자체를 재현 가능하게 만드는 접근은 PostgreSQL 프로젝트가 채택했음
    • 사용자가 관리자 tarball이 원본 소스에서 정직하게 생성됐는지 독립적으로 검증할 수 있음
    • 다만 upstream 프로젝트 관리자가 구현해야 하므로 FOSS 커뮤니티에서 채택 속도가 느릴 수 있음

NixOS에서 제안된 검출 절차

  • xz가 비트 단위로 재현 가능하고 관리자 tarball이 빌드 과정에 영향을 주는 변경을 담지 않았다면, GitHub tarball 빌드와 관리자 tarball 빌드는 같은 산출물을 내야 함
  • 제안된 방법은 xz를 bootstrap 이후 한 번 더 빌드하되, 이때는 fetchFromGitHub로 GitHub 소스 아카이브를 사용하고 기존 bootstrap 단계의 xz 산출물과 비교하는 방식임
  • 구현 예시는 pkgs/tools/compression/xz 아래에 after-bootstrap.nix를 두고 xz 5.6.1을 GitHub 소스에서 빌드함
    • 관리자 tarball에는 configure 스크립트가 이미 들어 있지만, GitHub 소스 빌드에서는 autoconf, libtool, automake 등을 사용해 ./autogen.sh를 실행해야 함
  • 비교 단계로 compareArtifacts를 추가해 빌드 마지막에 liblzma.so를 비교함
    • 예시에서는 백도어가 수정한 파일인 liblzma.so만 비교했지만, 모든 바이너리와 라이브러리 산출물로 일반화할 수 있음
  • 정상 master 리비전에서는 xz-after-bootstrap 빌드가 성공했고, 두 산출물이 동일했음
  • 백도어가 포함된 리비전 c53bbe3에서는 처음에는 동일하게 보였음
    • NixOS에서는 백도어가 활성화되지 않았고, 그 이유 중 하나는 설치 스크립트가 RPM_ARCH 변수 설정을 확인했기 때문임
    • env.RPM_ARCH = true;를 설정해 조건을 활성화하자 liblzma.so 바이너리 차이가 드러남
  • liblzma.so를 저장해 비교하자 백도어 포함 산출물은 258KB, bootstrap 이후 GitHub 소스 빌드 산출물은 210KB였고 약 48KB 차이가 있었음
  • nm으로 심볼을 비교하자 _cpuid, _get_cpuid, __tls_get_addr@GLIBC_2.3가 추가되어 있었고, _get_cpuid는 여러 xz 백도어 기술 보고서에서 문서화된 심볼임

적용 방안과 한계

  • 제안된 보호 장치는 bootstrap 단계에서 빌드되며 신뢰된 소스 아카이브를 쓰지 않는 모든 패키지에 적용하는 방식임
  • *-after-bootstrap 패키지를 channel blocker에 추가하면, 빌드 실패 시 관리자의 개입이 필요한 큰 경보로 만들 수 있음
  • xz 사례에 대한 개념 증명으로 nixpkgs 저장소에 pull request가 열림
  • 이 방법은 대상 패키지가 비트 단위 재현 가능하다는 가정이 필요함
    • 2017년부터 2023년까지 nixpkgs-unstable 리비전 17개를 표본으로 삼아 stdenv의 non-fixed-output derivation을 nix-build --check로 재빌드함
    • 모든 리비전에서 xz는 비트 단위로 재현 가능했음
    • 17개 중 12개 리비전에서는 빌드 가능하지만 재현 불가능한 패키지가 1~2개 있었고, 예를 들어 gcc는 2017~2021년, bash는 2019년까지 일관되게 재현 불가능했음
  • 모든 stdenv 패키지에 이 방법을 적용할 수는 없지만, 장기적으로 재현성이 좋은 패키지에는 선택적으로 활성화할 수 있음
  • Ken Thompson의 trusting trust 문제는 이 방식의 이론적 한계로 남음
    • bootstrap 단계의 신뢰할 수 없는 xzxz-after-bootstrap 빌드를 간접 오염시켜 산출물이 동일해 보이게 만들 수 없다는 가정이 필요함
    • 이런 공격은 매우 복잡할 수 있지만, 방법 자체는 이 가정을 전제로 함
  • 이 검출법은 tarball 변경이 최종 산출물에 영향을 줄 때만 탐지함
    • NixOS 실행 파일에는 백도어가 포함되지 않았기 때문에, 조건을 따로 활성화하지 않았다면 이 사례의 백도어를 발견하지 못했을 것임

댓글과 토론

Hacker News 의견들
  • NixOS재현 가능한 빌드가 xz 백도어를 탐지하지 못했다는 점은 짚고 가야 함. 실제로 NixOS도 악성 xz 빌드를 사용자에게 배포했지만, 악성코드가 NixOS를 겨냥하지 않아 동작하지 않았을 뿐임
    NixOS 개발자도 “백도어가 공개됐을 때 악성 버전의 xz가 우리 사용자에게 배포됐다는 사실에 놀랐다”고 했음. 늘 그렇듯 이론과 현실은 다르고, xz를 가능하게 한 건 기술적 취약점이라기보다 현실 세계의 인간 취약점이었음. 커뮤니티는 이런 문제를 더 나은 소프트웨어만으로 항상 패치할 수 없다는 사실을 잘 인정하지 못함

    • Nix의 선언적 구성은 여러 방식으로 공격 방어력을 높이는 데 꽤 유용하지만, 아직 활용되지 않은 가능성이 많음. 개인적으로는 세분화된 일회성 컨테이너를 최우선으로 구현하고 싶고, Guix에는 이미 이런 기능이 있음
      모든 프로세스를 제한된 권한으로 실행하고, 작업에 필요한 디렉터리를 제외하면 ~/에도 접근하지 못하게 만들 수 있으면, 예를 들어 악성 pip 패키지가 SSH 키를 훔치는 일을 막을 수 있음. 그래도 xz 백도어가 NixOS에서 동작하지 않은 이유는 NixOS의 특이한 비 FHS 파일시스템 구조 때문이라고 봄
    • 그래도 이 글은 좋았음. NixOS는 소스 코드와 분리된 빌드 산출물, 즉 재현 불가능한 산출물을 막는 기술적 해법을 내세우고 있고, xz 백도어는 바로 빌드 산출물 안에 숨어 있었기 때문임
    • 맞음. 제목은 Nix 방식이 백도어를 탐지했을 수도 있다는 뜻처럼 보였지만, 실제로는 Nix를 어떻게 바꾸면 그런 백도어를 탐지할 수 있을지 제안하려는 글에 가까움
    • 재현 가능한 빌드가 다른 발견 경로보다 먼저 xz 백도어를 실제로 잡아내지는 못했다는 사실을 강조해줘서 좋았음
      요즘은 실제 현실이 객관적 현실에 대한 개인의 감정이나 가치관과 맞지 않으면 자주 폄하됨. 개인적 가치관은 있지만, 실제 현실이 그것에 대한 내 감정보다 덜 중요하다고 보지는 않음. 즐겨 하던 말처럼, 이론과 현실의 차이는 이론상으로는 둘이 같지만 현실에서는 그렇지 않다는 데 있음. NixOS의 재현 가능한 빌드가 xz 공격을 잡아낼 수도 있었지만 그러지 못했다는 깨달음이, 앞으로 그런 빌드를 분석해 다른 공격을 더 빨리 찾는 발전으로 이어지길 바람
    • 이유가 좀 웃김. NixOS 부트스트랩은 소스 코드를 내려받는데, 그 소스가 xz로 압축된 tarball임
  • 글쓴이가 이번에 우연히 일어난 방식에 너무 시야가 좁아진 느낌임. Jia Tan 사건은 표본이 하나뿐이라, 그 방식만 가능하다고 보는 건 근시안적임
    여기서 제안한 방어가 통하지 않을 여러 시나리오를 충분히 상상할 수 있음. Nix 사용자로서도 NixOS가 이를 잡아냈을 가능성은 낮다고 봄. 실제로 잡아내지 못했으니까. 다음번에는 다르게 일어날 수 있다고 방금 말하긴 했지만, 증거 없이 Nix를 믿는 것도 어리석음

  • 여기서 NixOS는 별로 관련이 없음. xz 백도어는 구체적으로 Red Hat과 Debian을 겨냥했기 때문임
    같은 논리라면 xz 백도어가 Windows에는 영향을 주지 않았다고 말하는 것도 비슷하게 관련 있음. 아이러니하게도 이 백도어는 결국 Microsoft 직원이 발견했는데, 이 점은 자주 간과됨

    • 조금 개선된 NixOS나 Guix라면 이 백도어가 저장소에 들어온 순간 자동으로 잡아냈을 것임. 그래서 관련이 있음
  • 글에서는 배포판이 전통적인 설치용 tarball 대신 버전 관리 시스템, 예를 들어 Github에서 직접 소스 코드를 가져와야 한다고 함
    그런데 이게 무엇을 해결하는지 잘 모르겠음. 악의적인 관리자가 바이너리 덩어리를 소스 코드 저장소에 직접 추가하면 되는 것 아닌가? 글쓴이는 Github가 코드를 검증하는 것처럼 신뢰할 수 있다고 보지만, 당연히 Github는 그런 검증을 하지 않음

    • 이 방식이 해결하는 건 “검토된 것”과 “소프트웨어 빌드에 사용된 소스 코드”가 다른 경우가 많다는 문제임
      검증된 재현 가능한 빌드는 xz utils 침해, SolarWinds Orion 변조 같은 사례를 막는 데 도움이 될 수 있었고, 충분히 할 만한 가치가 있음
    • 그 비판은 조금 맞지 않음. 소스에서 빌드하면 본인과 모두가 소스를 검사할 수 있지만, 제공된 tarball에서 빌드하면 autoconf 과정이 파일을 바꾸기 때문에 원본 소스와 비교했을 때 본질적으로 달라짐
      Github 릴리스에서 바이너리를 내려받아 실행한다면 관리자를 완전히 믿는 수밖에 없음. Nix는 폐쇄 소스 패키지에서만 그렇게 함
  • 논지는 XZ 관리자가 Git에 체크인되지 않은 tarball 안에 악성 코드를 숨겼다는 사실에 기대고 있음
    글쓴이는 Nix가 Git에서 tarball을 생성해 바이너리 빌드에 넣도록 설정될 수 있음을 보여줌. 하지만 이게 왜 Nix나 NixOS가 필요한 기능인지는 모르겠음. RPM이나 Deb에 들어가는 것들을 포함해 어떤 빌드 시스템이든 중간 단계로 tarball을 생성하도록 설정할 수 있음. 실제로 Debian은 오래전부터 재현 가능한 빌드를 중요한 목표로 작업해 왔음. https://wiki.debian.org/ReproducibleBuilds

    • 글은 실제로 “Leveraging bitwise reproducibility” 절에서 reproducible-builds 프로젝트를 인용함. 이 글의 요지는 두 가지임
      첫째, NixOS 빌드 과정은 xz가 부트스트랩 초기에 너무 일찍 필요해서 xz의 전체 소스 빌드를 수행할 수 없었음. 둘째, 부트스트랩 초기에 필요한 nixpkgs 의존성이 침해됐는지 자동으로 감지하기 위한 nixpkgs 조정을 제안함. 다른 생태계도 물론 전체 소스 빌드를 시도해 불일치를 발견할 수 있음. 글의 핵심은 현재 nixpkgs가 그걸 할 수 없다는 데 있음
  • NixOS가 막을 수 있었던 사례에 집중하려면 CrowdStrike 사고를 봐야 함. 오늘의 설정이 동작하지 않으면 어제의 설정으로 부팅할 수 있다는 점만으로도 대부분의 피해를 완화했을 것임

    • 다만 그건 부팅 유연성이 없는 Windows 쪽 문제임. Ubuntu도 ZFS 위에서는 그렇게 할 수 있음
  • 신뢰하는 프레임워크를 쓰면 그 프레임워크가 공격받기 전까지는 안전함. xz 백도어는 발견됐을 수도 있지만, Nix 생태계에서 동작하게 만들 목적으로 설계된 건 아니었음
    언젠가 Nix 핵심 개발자가 스파이이거나 비슷한 상황이 되면 Nix 생태계를 겨냥한 공격도 나올 것임. Nix가 본질적으로 안전하다는 식으로 답하지 않았으면 함. 1~2년 안에 Nix가 성공적으로 공격당하면 찾아가서 틀렸다고 인정하게 만들고 싶어질 정도임

    • 기준은 과거에도, 앞으로도 절대적 보안이 아님. 그건 어떤 것도 만족할 수 없는 불가능한 기준임. 그럼에도 오늘날 소프트웨어가 30년 전보다 대체로 더 안전한 건 객관적으로 사실임
      더 강한 주장은 “Nix는 전통적인 빌드 시스템보다 공격하기 더 어렵고 비용이 많이 든다”는 것임. 그러니 Nix를 싸게 공격하는 방법을 찾으면 그때 찾아오면 됨. 그 전까지는 기술적 수준에서 Nix가 대안 시스템보다 공격하기 어렵다는 말은 적어도 그럴듯하고, 실제로도 매우 가능성이 높음
    • 백도어가 Nix를 겨냥한 건 아니었지만, 노출되지 않으려면 Nix에서 빌드되는 동안에도 의심을 사지 않아야 했음
  • NixOS가 조금 더 나아가긴 하지만, 대부분의 다른 배포판도 모든 것을 소스에서 컴파일하고, 사용하는 소스가 변조되지 않았는지 암호학적으로 검증하며, 패키지 간 버전 지정 의존성을 갖고 있음. Debian도 재현 가능한 빌드를 갖고 있음
    문제는 빌드 시스템이 소스에서 빌드하기 전에 미리 컴파일된 목적 파일을 제거하지 않았다는 데 있음. 그걸 고쳐도 아무도 소스 코드를 검사하지 않으면 원하는 만큼 백도어를 추가할 수 있고, NixOS든 다른 배포판이든 이를 막아줄 수 없음

  • 훌륭한 기술적 분석이지만 제목은 틀렸고 오해를 부름. “기술적으로는 맞다”고 할 수 있을지 몰라도, 좋게 봐도 백도어가 심긴 의미에 가까움
    유니언 파일시스템 계층을 넘어서는 빌드 관리자 도구가 필요하다는 점을 잘 보여줌. 예를 들어 테스트가 빌드 산출물을 오염시키지 못하도록 추적하고 강제해야 함. 빌드 과정에서 파일이 파일에 영향을 주는 인과 추적 그래프를 만들고, 그 그래프를 명시적으로 만든 뒤, 이를 강제하거나 이전 추적 그래프와의 차이를 보고하는 방식이 필요함

  • 맞음. 백도어를 숨기기는 확실히 더 어려워졌을 것임. 하지만 불가능과는 거리가 멂
    원한다면 언제든 소스 코드 안에 백도어를 숨길 수 있음. 그저 그럴듯한 버그처럼 보이게 만드는 데 더 많은 노력이 들고, 탐지될 가능성도 더 높아질 뿐임