1P by GN⁺ | ★ favorite | 댓글 1개
  • Linux 6.9부터 노트북 suspend 시 드라이브를 잠그는 도구가 조용히 실패해, LUKS 전체 디스크 암호화 키가 메모리에 남아 있었음
  • 원인은 2024년 5월 Linux 6.9에 들어간 블록 디바이스 접근 리팩터링과 암호화 코드의 예상 밖 상호작용이며, 제안된 수정은 한 줄짜리 패치임
  • 전체 종료에서는 문제가 드러나지 않았지만 suspend-to-RAM에서는 키가 남아, 전원이 켜진 노트북을 확보한 공격자가 RAM에서 키를 빼낼 수 있는 상태가 됨
  • 발견은 Debian cryptsetup-suspend의 NixOS 포트를 정리하던 중 /proc/keys 항목을 본 데서 시작됐고, QEMU 메모리 덤프로 지워졌어야 할 volume key가 남아 있음을 확인함
  • NixOS 통합 테스트와 cryptsetup 경고 패치가 제안됐으며, suspend 직전 키 삭제 같은 보안 기능은 정상처럼 보여도 실제 메모리 검증 없이는 실패를 놓치기 쉬움

Linux 6.9 이후 suspend 중 LUKS 키가 남은 문제

  • Linux 6.9, 즉 2024년 5월부터 노트북 suspend 시 드라이브를 잠그는 도구가 조용히 실패하고 있었음
  • LUKS 전체 디스크 암호화는 노트북 분실, 압수, 도난 시 데이터를 보호하기 위해 쓰이지만, 이번 경우 suspend 동안 암호화 키가 메모리에 남아 있었음
  • 전체 종료에서는 여전히 동작했지만, 노트북을 완전히 끄기보다 suspend-to-RAM으로 두는 경우가 많다는 점에서 영향이 커짐
  • 전원이 켜진 상태로 노트북을 확보한 사람이 있다면, 메모리에 남은 키가 노출될 수 있는 상태였음
  • Windows에서 같은 목적의 소프트웨어로 VeraCrypt가 언급됐으나, 이후 댓글에서 “canonical software”는 가장 널리 쓰이는 소프트웨어가 아니라 IT 보안 쪽의 대표 추천이라는 뜻으로 정정됨

원인과 한 줄짜리 패치

  • 원인은 Linux 커널 리팩터링 커밋인 md: port block device access to file였음
    • 변경 자체는 합리적이고 유용한 리팩터링이었지만, 암호화 코드와 장거리 상호작용을 일으킴
  • 제안된 수정은 한 줄짜리 패치
  • 패치 작성자는 형식 검증 없이는 이 패치가 올바르고 다른 장거리 상호작용이 없다고 말할 수 없다고 밝힘
  • 재발 방지를 위한 후속 작업도 함께 나옴

발견 과정

  • 시작점은 Debian cryptsetup-suspend의 NixOS 포트를 정리하는 작업이었음
  • Debian 원본과 NixOS 포트 모두 노트북이 가끔 잠들지 못하게 하는 불편하지만 해롭지는 않은 경쟁 조건을 갖고 있었음
  • 이를 해결하려고 Pali Rohár의 병합되지 않은 커널 패치인 dm-crypt suspend/hibernation 키 삭제 패치를 되살리려 했음
  • 그 과정에서 cryptsetup과 커널 소스 코드를 살펴보다가, 문서상 keyring이 호출 스레드에 붙고 스레드 종료 시 제거된다는 점을 확인함
  • 그러나 이전에 알지 못했던 /proc/keys에서 항목이 보였고, 이 부분이 의심을 키움
  • 결국 QEMU 가상머신을 띄워 메모리를 덤프했고, 지워졌어야 할 LUKS volume key가 그대로 남아 있음을 확인함

NixOS secure suspend-to-RAM 프로젝트

  • 별도로 공개된 secure-suspend 프로젝트는 NixOS에서 실험적 secure suspend-to-RAM을 제공함
  • 일반적인 전체 디스크 암호화는 노트북이 suspend 상태일 때 키가 메모리에 남아 있어, cold boot attack이나 RAM 유출 방식에 취약할 수 있음
  • 이 프로젝트는 Pali Rohár의 오래된 커널 패치를 되살려 suspend 시 LUKS 암호화 키를 지우는 방식임
  • Debian cryptsetup-suspend에서 영감을 받았지만, 커널 패치를 사용해 노트북이 가끔 잠들지 못하는 경쟁 조건을 피하고 추가 예방책을 더함
  • 암호화된 root filesystem을 완전히 지원하며, 통합 테스트도 제공됨
  • 커널 패치와 사용자 공간 도구는 다른 Linux 배포판에도 맞춰 적용할 수 있음
  • 프로젝트는 secure-suspend로 공개돼 있음

suspend 보안 검증이 어려운 이유

  • suspend 후 화면 잠금이 보인다고 해서 저장장치가 실제로 잠겼다고 볼 수는 없음
  • suspend에서 깨어난 직후 디스크에 바로 접근 가능하다면, 저장장치는 처음부터 잠기지 않은 상태였음을 알 수 있음
    • 예를 들어 잠금 화면 뒤에서 계속 실행되는 스크립트로 디스크 접근을 확인할 수 있음
    • suspend 후 SSH 공개키로 먼저 암호화 저장장치를 해제하지 않고 접속 가능하다면, 저장장치가 잠기지 않았음을 확인하기 쉬움
  • Ubuntu나 Debian 기본 설정은 이런 보호를 제공하려는 시도 자체가 없었다는 댓글도 있음
  • 저장장치를 잠그려는 시도가 실제로 올바르게 동작했는지는 별도로 검증해야 함
    • 로그 타임스탬프는 suspend 전 생성됐지만 wake 후 기록됐을 수 있음
    • 반대로 wake 직후 시스템 시간이 조정되기 전에 생성된 로그가 suspend 시점의 시간처럼 보일 수도 있음
  • 저장장치 잠금이 suspend 직전에 일어난 것인지, resume 직후 일어난 것인지는 사용자 경험상 같아 보여도 보안상으로는 결정적으로 다름
  • NixOS 통합 테스트는 가상머신으로 시스템을 부팅하고 메모리를 덤프해, 키가 suspend 시 실제로 지워졌는지 확인하는 방식임

댓글과 토론

Hacker News 의견들
  • 흥미로운 버그인 건 맞지만, 제목은 약간 클릭베이트처럼 느껴짐
    이해한 바로는 cryptsetup luksSuspend가 공식 지원 기능이라기보다 Debian에서 만든 확장에 가까워서, 이 회귀가 영향을 준 것도 Debian뿐 아닌가 싶음
    지원되거나 널리 테스트되는 것도 아닌 기능에 대해 커널을 탓할 수 있는지 잘 모르겠음
    그래도 인상적이고, 이 회귀가 다시 생기지 않도록 테스트가 생긴 건 좋음. NixOSTests는 정말 훌륭하다는 OP 의견에도 동의함
    다만 제목만 보면 특정 배포판 하나가 아니라 널리 퍼진 문제처럼 보임

    • 기술적으로 정확한 제목을 노렸고 클릭을 유도하려던 건 아니었음
      맞음. 기본 설정을 쓰는 사람들에게는 영향을 주지 않는데, 애초에 suspend 중에 볼륨 키가 안전하다고 기대하지 않을 것이기 때문임
      Debian의 해법은 여러, 아마 대부분의 다른 배포판으로 포팅됐고, 개인 포트를 유지하던 사람도 꽤 있었을 것 같음
      thread-keyring(7) 매뉴얼 페이지는 “thread keyring은 그것을 참조하는 스레드가 종료되면 파괴된다”고 약속함
      cryptsetup 프로젝트는 사용자 공간에서 커널 공간으로 키를 올리는 메커니즘에서 이 속성에 의존했는데, 커널 6.9가 이 속성을 깨는 회귀를 도입함
    • 이게 왜 Debian 전용이라고 하는지 혼란스러움. luksSuspend는 upstream 기능이고 2009년에 v1.1.0 릴리스에서 추가됐음
      예전에 Arch와 openSUSE에서도 가끔 써봤고, Debian이 아닌 배포판에도 분명히 존재함
      아마 system suspend와의 자동 통합을 떠올린 것 같은데, 그건 핵심에서 벗어남. luksSuspend는 시스템 메모리에서 키를 지운다고 문서화되어 있고, Linux 6.9의 해당 리팩터링 패치 때문에 그 동작이 멈췄음
      다만 실제로는 cryptsetup 쪽 버그라고도 볼 수 있음. 커널 keyring 키의 매우 구체적인 수명 동작에 의존했기 때문이고, 사용자 공간에서 더 명시적으로 지웠어야 했다는 주장도 가능함
      [1]: https://gitlab.com/cryptsetup/cryptsetup/-/commit/3cea5dcc7b...
      [2]: https://gitlab.com/cryptsetup/cryptsetup/-/blob/main/docs/v1...
      [3]: https://gitlab.com/cryptsetup/cryptsetup/-/merge_requests/93...
    • 하위 명령은 공식 cryptsetup 저장소에 있고 설명도 맞아 보임: https://gitlab.com/cryptsetup/cryptsetup/-/blob/main/man/cry...
    • 이 기능을 Arch에서 써봤고, 일반적인 LUKS에서도 사용할 수 있음. 다만 아는 한 suspend할 때 기본으로 쓰이진 않음
      아마 luksSuspend 이후 실제로 유용한 방식으로 RAM suspend를 실행하는 장치를 말하는 것 같은데, 그건 처음에 Debian 대상이었고 이후 Arch에도 생겼지만 둘 다 기본값은 아니었음
    • Debian 어느 버전에서 처음 6.9를 배포했는지 궁금함
  • 다른 방법이 잘 안 보임. 절전, 즉 RAM suspend를 하면 모든 것이 RAM에 저장되고 암호화돼 있지만, 마스터 키는 커널 메모리에 남아 있는 것으로 기억함
    반면 hibernate, 즉 디스크 suspend를 하면 RAM 전체 내용이 마스터 키까지 포함해 디스크에 기록되고 암호화되며 RAM은 지워짐
    다시 깨울 때는 마스터 키를 복호화해 디스크 내용을 메모리로 다시 올리기 위해 패스프레이즈를 다시 입력해야 함

    • 맞음. 대부분의 기본 Linux 배포판에서 그냥 노트북을 suspend하면 마스터 키를 포함한 모든 것이 메모리에 남아 있음
      하지만 Debian은 선택 기능인 cryptsetup-suspend 애드온을 먼저 만들었고, 이건 메모리에서 키를 지우도록 되어 있는 luksSuspend 명령을 실행한 뒤 resume 시 패스프레이즈를 다시 요구함
      커널 6.8까지는 설명대로 동작했지만, 커널 6.9부터는 조용히 동작하지 않게 됨
    • 최근 5년 정도의 Intel/AMD CPU는 둘 다 운영체제에는 투명한 전체 메모리 암호화를 지원함
      이 기능을 켜면 콜드 부트 공격은 과거 일이 됨. 보통 RAM 속도를 약 0.5% 낮춰서 기본으로 꺼져 있을 뿐임
  • Sleep 이후에 부팅 비밀번호를 다시 입력하지 않으니, 암호화 키가 아직 메모리에 남아 있는 게 분명함

    • 배포판이 cryptsetup-luksSuspend를 쓰지 않는 게 분명함
  • 이건 나에게 크게 신경 쓰이는 문제는 아님
    디스크 암호화를 하는 유일한 이유는 노트북을 팔 때 누군가 세금 문서나 신용카드 정보를 뒤지는 걸 걱정하지 않기 위해서임
    물론 노트북도 지우지만, 데이터가 드라이브 수준에서 암호화돼 있으면 포렌식 도구 같은 걸로 데이터를 복구할 위험이 아주 작다고 봄

    • 적당한 절충안으로 LUKS 헤더만 지워도 됨
      LUKS는 디스크를 열려면 볼륨 키 전체가 있어야 하는 안티 포렌식 알고리즘을 사용함. 키 블록들을 확산 알고리즘으로 결합하고 XOR해서 실제 마스터 키를 만드는 식이라, 이론상 볼륨 키의 한 섹터만 지워도 전체가 복구 불가능해야 함
      즉 키의 블록 하나라도 없으면 나머지를 쉽게 추측할 수 없다는 뜻임
    • 암호화 키가 강하다는 가정이면, 지우기는 이론적으로 중복 작업임
  • 보안 전문가는 전혀 아니지만, 요즘 “리팩터링 중 파일을 넘나드는 C 체크 한 줄을 놓쳐서 생긴” 치명적 보안 버그가 정기적으로 발견되는 걸 보면 거대한 안전한 오픈소스 C 코드베이스라는 전제 자체가 의심스러움
    C만의 문제는 아니지만, 특히 C에서는 불변식을 일관되게 강제하고 추적하기가 더 어렵고, 코드 변경 시에는 더 그렇다고 봄
    불변식을 타입에 인코딩하는 함수형 프로그래밍이 현실적으로 확장 가능한 해법인지도 모르겠음. 모델 검사? LLM 퍼징? 명확한 경계를 가진 더 적은 기본 요소? seLinux는 그런 식으로 “검사”된 건가?

    • C의 단점은 보이고 새 프로젝트에 일반적으로 추천하지도 않지만, 이 특정 버그가 Rust의 borrow checker나 다른 언어의 타입 시스템이 잡아낼 좋은 예라고 보진 않음. 정적 분석기도 못 잡을 것 같음
      본질적으로 이런 식임:
      original: DoTheThing()
      new: DoTheThingSlightlyDifferentButKeepMyCredentialsAlive()
      fix: DoTheThingSlightlyDifferentButDoInFactNOTKeepMyCredentialsAlive()
      경험상 까다로운 버그의 상당수는 상위 수준 시스템 불변식 위반에서 나오고, 이런 건 자동화할 수 있는 성격으로 보이지 않음
      Lean 같은 것으로도 프로그램이 특정 속성을 만족한다는 건 증명할 수 있지만, 그 속성을 먼저 떠올렸어야 함. 증명이 불변식을 대신 발견해주지는 않음
      관련 보안 속성을 떠올렸다면 회귀 테스트를 작성하는 건 어렵지 않았을 것임. 정말 어려운 부분은 구현을 안전하게 표현하는 게 아니라, 구현이 보존해야 하는 속성이 있다는 사실을 깨닫는 데 있다고 봄
    • 안전한 공개 코드베이스라는 전제 자체는 괜찮음
      문제는 감사 가능성이 더 높다고 해서 자동으로 더 많이 감사되는 건 아니라는 데 있음
      충분한 실력을 가진 사람들이 충분한 시간을 들여 작업해야 함
    • Rust로 번역해도 “Rust 체크 한 줄을 놓쳤다”가 됐을 것임
      이건 관심사가 교차하고 도메인 간 지식이 부족해서 생긴 버그임. Lisp나 어셈블리어에서도 아마 같았을 것임
    • 여기서 얻을 교훈은 어떤 기능에 최소한 관련 테스트 케이스가 없다면, 실제 기능이 아니라는 것임
    • “거대한 안전한 오픈소스 C 코드베이스”라는 전제가 의심스럽게 보이는 이유는, 코드 리뷰가 때로는 명세의 형식화된 버전에 접근할 수 있는 이상화된 정지 문제와 크게 다르지 않기 때문임
      다시 말해 무엇이 보안 이슈인지에 대한 엄격한 정의가 없음
  • 연방기관이 키를 얻을 방법이 절실히 필요했던 건가? 이건 버그도어인가? 커밋 추적은 됐나?
    최근 이런 패턴을 많이 봐서 조금 의심스러워지기 시작함. 사람들이 여기에 더 민감해져서 더 많이 올리기 때문일 수도 있음

    • 이건 회귀임. 사용자 공간 애플리케이션도 조용히 실패했을 것이고, 여러 부주의가 이어진 결과임
      암호화 키가 메모리에 있다는 게 곧 추출할 수 있다는 뜻은 아님. 원래 있으면 안 되는 곳에 불필요하게 무기한 남겨둔 것에 가까움
  • 이런 회귀는 모든 것이 계속 “동작”하기 때문에 놓치기 쉬움. 보안 버그는 자기를 드러내지 않는 경우가 많음

    • 맞음. 그래서 이런 기능에는 통합 테스트가 더 중요함
      작성하는 것도 재미있었고, 이 버그를 도입한 구체적인 커널 리팩터링을 찾기 위해 git-bisect를 돌릴 수 있게 해줬음: https://github.com/NixOS/nixpkgs/pull/532499
  • Fedora 노트북에서는 suspend 15분 뒤 디스크로 hibernate하도록 Linux를 설정해뒀음. 메모리 전원을 꺼버리면 이런 Debian 특정 버그는 문제가 되지 않음
    Debian의 Linux 도구 확장이 이론상으로는 좋지만, 실제로 콜드 부트 공격을 걱정한다면 LUKS 키뿐 아니라 모든 키와 중요한 문서가 메모리에서 지워져야 함
    그래서 콜드 부트를 막는 제대로 된 방법은 결국 hibernate뿐임

    • 동의함. 아니면 FridgeLock을 되살리는 방법도 있음: https://www.sec.in.tum.de/i20/publications/fridgelock-preven...
    • 그런데 resume할 때 메모리를 복호화할 키는 어디서 가져옴?
      아는 한 TPM을 쓰지 않으면 실용적이지 않음. 그리고 TPM을 쓰면 사실상 TPM에 운명을 맡기는 셈임
  • 이 취약점이 상용 운영체제에 있었다면 이 HN 스레드가 어떻게 보였을지 상상해보면 됨
    최상위 댓글은 분명 Applosoft가 더 이상 소프트웨어 품질에 신경 쓰지 않는다거나 “OS에 바이브 코딩 쓰레기를 허용하면 이렇게 된다”는 내용이었을 것임
    그 아래 댓글은 감시 산업 복합체와 NSA에 대한 음모론이었을 텐데, 다른 곳에서는 미친 소리겠지만 HN에서는 그렇지 않았을 것임

  • 이렇게 중요한 것이 왜 매 빌드마다 테스트되지 않는지 모르겠음