1P by GN⁺ | ★ favorite | 댓글 1개
  • 리눅스 커널 nf_tablesCVE-2024-1086은 Netfilter verdict 입력 검증 실패로 sk_buff 이중 해제를 만들며, 조건이 맞으면 로컬 권한 상승으로 이어질 수 있음
  • 공격 흐름은 NF_DROP 처리 중 해제된 skb가 NF_ACCEPT처럼 계속 처리되도록 만들어, 같은 객체가 이후 경로에서 다시 해제되는 구조임
  • PoC는 KernelCTF mitigation, Debian, Ubuntu 및 vanilla 커널에서 검증됐고, 최소 v5.14.21~v6.6.14 범위가 kconfig에 따라 영향권에 들어가며 2024년 2월 stable 브랜치에 수정이 배포됨
  • 핵심은 Dirty Pagedirectory로, PTE와 PMD 페이지를 같은 물리 페이지에 중복 할당해 사용자 공간 읽기/쓰기만으로 임의 물리 주소에 접근하는 데이터 전용 KSMA 방식임
  • 물리 KASLR 탐색, modprobe_path 우회, 파일 없는 실행, 네임스페이스 탈출용 파일 디스크립터 연결까지 결합돼 커널 메모리 관리와 네트워크 서브시스템을 함께 겨냥한 실전형 LPE 사례가 됨

취약점 조건과 영향 범위

  • nf_tables 버그는 CVE-2024-1086로 등록됐으며, 리눅스 커널 Netfilter의 verdict 처리에서 양수 drop error가 허용되는 입력 검증 실패가 핵심임
  • 익스플로잇에는 다음 조건이 필요함
    • nf_tables가 활성화돼 있어야 함
    • 비권한 사용자 네임스페이스가 활성화돼 있어야 함
    • Debian과 Ubuntu 같은 주요 배포판에서 이 설정이 기본 활성화된 경우가 공격 전제가 됨
  • 테스트 기준으로 stable 브랜치 linux-5.15.y, linux-6.1.y, linux-6.6.y가 영향권에 있으며, linux-6.7.1도 가능성이 있음
  • 2024년 2월 stable 브랜치에 버그 수정이 배포됨
  • PoC 소스 코드는 CVE-2024-1086 PoC repository에 공개됨

이중 해제가 만들어지는 코드 흐름

  • Netfilter verdict는 패킷을 드롭할지, 수락할지, 큐로 보낼지 등을 결정하는 값임
  • 취약한 흐름은 nft_verdict_init()가 사용자 입력 verdict 값을 충분히 제한하지 않아, NF_DROP로 보이면서도 drop error가 양수인 값을 설정할 수 있었던 점에서 시작함
  • nf_hook_slow()는 verdict의 하위 비트를 보고 NF_DROP로 판단하면 kfree_skb_reason()으로 skb를 먼저 해제함
  • 이후 NF_DROP_GETERR() 결과가 NF_ACCEPT에 해당하는 값으로 반환되면 호출자 쪽은 패킷이 수락된 것으로 보고 처리를 계속함
  • 결과적으로 이미 해제된 skb가 이후 경로에서 다시 해제돼 double-free primitive가 만들어짐

손상되는 객체와 패킷 처리 방식

  • 이중 해제는 skbuff_head_cachestruct sk_buffsk_buff->head 객체에 영향을 줌
  • sk_buff->head는 실제 패킷 내용을 담으며, IPv4 패킷 크기에 따라 kmalloc-256부터 buddy allocator의 order 4 페이지까지 할당될 수 있음
  • PoC는 큰 IP 패킷을 사용해 slab allocator가 아니라 buddy allocator 경로로 들어가도록 설계됨
  • IPv4 fragmentation queue는 skb의 두 번째 해제를 지연하거나 원하는 시점에 유도하는 데 활용됨
  • 패킷 경로에서 손상된 skb 필드가 사용되면 커널 패닉이 날 수 있어, TCP/UDP 스택을 피하고 특정 IP fragment 오류 경로를 이용함

테스트 범위와 성공률

  • vanilla 커널, KernelCTF, Debian, Ubuntu 환경에서 여러 커널 버전과 설정이 테스트됨
  • 성공 사례에는 다음 환경이 포함됨
    • Linux v5.14.21, v5.15.148, v5.16.20, v5.17.15, v5.18.19, v5.19.17, v6.0.19
    • KernelCTF Mitigation v3의 Linux v6.1.55
    • Debian Bookworm 6.1.0-17의 Linux v6.1.69
    • KernelCTF LTS의 Linux v6.1.72
    • Ubuntu Jammy v6.2.0-37
    • Linux v6.2.16, v6.3.13
  • 실패 사례에는 v5.4.270, v5.10.209, v6.4.16, Ubuntu Jammy v6.5.0-15, v6.5.13, v6.6.14, v6.7.1 등이 포함됨
  • v6.4.0 이후 일부 실패는 CONFIG_INIT_ON_ALLOC_DEFAULT_ON=y로 인한 bad_page() 감지와 연결됨
  • v6.4.16 환경에서는 성공률이 99.4%였고, 경우에 따라 93.0%까지 내려갔으며, 표본 수는 각각 n=1000임

수정 방식

  • 처음 제안된 수정은 Netfilter stack 중간에 breaking change를 만들 수 있었음
  • Netfilter maintainer의 수정은 사용자 입력에서 들어오는 verdict를 API 단계에서 더 엄격하게 제한함
  • 패치는 userland 입력에 대해 DROP/QUEUE verdict parameter를 거부하는 방식임
  • CVE 설명상 nft_verdict_init()은 hook verdict 안의 drop error로 양수 값을 허용했고, nf_hook_slow()NF_DROPNF_ACCEPT가 겹치는 상황에서 double free를 만들 수 있었음
  • 수정 패치는 [PATCH nf] netfilter: nf_tables: reject QUEUE/DROP verdict parameters.에서 확인할 수 있음

Dirty Pagedirectory와 임의 물리 메모리 접근

  • PoC의 중심 기법은 Dirty Pagedirectory이며, 기존 Dirty Pagetable 기법의 변형임
  • 핵심 아이디어는 PTE 페이지와 PMD 페이지를 같은 물리 페이지에 중복 할당하는 것임
  • 한 가상 주소 영역에 PTE 값처럼 보이는 값을 쓰면, 다른 가상 주소 영역이 이를 페이지 테이블 항목으로 해석해 지정한 물리 페이지를 매핑함
  • 이 방식은 사용자 공간 주소 읽기/쓰기만으로 임의 물리 주소와 권한 플래그를 설정해 접근하는 kernel-space mirroring attack으로 동작함
  • virtual KASLR, KPTI, SMAP, SMEP, CONFIG_STATIC_USERMODEHELPER 같은 완화책 우회에 사용됨

페이지 할당자 조작

  • 커널 페이지 할당에는 slab allocator, buddy allocator, PCP allocator가 관여함
  • skb head는 큰 크기에서 buddy allocator의 order 4 페이지를 쓰지만, PTE/PMD 페이지는 order 0 페이지라 직접 맞물리지 않음
  • 이를 해결하기 위해 두 가지 page conversion 방법이 사용됨
    • PCP list draining: order 4 페이지를 buddy freelist에 넣고, PCP order 0 freelist를 비워 buddy allocator에서 order 0 페이지로 다시 채우게 함
    • race condition: 두 번째 free 중 경합을 이용해 order 4 페이지를 order 0 freelist로 넣는 방식
  • PCP list draining은 더 단순하고 안정적이며 빠른 방법임
  • race condition 방식은 KernelCTF 초기 exploit에 사용됐지만, QEMU VM처럼 serial TTY 지연이 큰 환경에 의존해 obsolete로 처리됨

KernelCTF mitigation 우회

  • KernelCTF mitigation 환경에서 적극적으로 우회해야 했던 완화책은 sk_buff freelist corruption check였음
  • skbuff_head_cache->offset == 0x70이라 freelist next pointer가 skb->len과 겹침
  • 첫 번째 skb 해제 뒤 skb->len이 freelist pointer 일부로 덮이고, 이후 패킷 파싱 중 이 값이 바뀌면서 corruption check에 걸릴 수 있음
  • 손상된 skb 위에 정상 skb를 추가로 해제해 freelist head를 덮는 방식으로 감지를 우회함
  • KernelCTF 개발자는 free 시점에도 freelist head next pointer를 검사하면 이 우회를 완화할 수 있다고 봄

TLB flush와 물리 KASLR 탐색

  • Dirty Pagedirectory로 페이지 테이블을 예상 밖 방식으로 바꾸면 CPU의 TLB 캐시에 오래된 변환 정보가 남을 수 있음
  • 사용자 공간에서 fork() 후 자식이 munmap()을 수행하고 잠드는 방식으로 TLB를 flush함
  • 이 방법은 AMD CPU와 QEMU VM에서 100% 동작한 것으로 확인됨
  • 물리 KASLR은 커널 물리 base 주소가 CONFIG_PHYSICAL_START 또는 CONFIG_PHYSICAL_ALIGN에 정렬된다는 점을 이용해 탐색 범위를 줄임
  • 8GiB 물리 메모리와 16MiB 정렬을 가정하면 후보는 512개이며, get-sig 스크립트로 커널 base signature를 생성해 식별함

modprobe_path와 root shell 획득

  • 임의 물리 메모리 읽기/쓰기를 얻은 뒤, PoC는 커널 base 이후 약 80MiB 범위를 스캔해 modprobe_path를 찾음
  • 일반 설정에서는 "/sbin/modprobe"와 null padding 패턴을 찾고, /proc/sys/kernel/modprobe 반영 여부로 실제 변수를 검증함
  • CONFIG_STATIC_USERMODEHELPER가 활성화된 경우에는 "/sbin/usermode-helper" 문자열을 대상으로 삼음
  • root shell을 얻기 위해 modprobe_path 또는 static usermode helper 문자열을 /proc/<pid>/fd/<fd> 형태의 memfd 경로로 덮음
  • privilege escalation 스크립트는 exploit의 파일 디스크립터를 shell의 stdin/stdout에 연결해 로컬 터미널과 reverse shell 모두에서 동작하도록 구성됨

파일 없는 실행과 PoC 구성

  • PoC는 디스크에 파일을 쓰지 않는 fileless execution을 지원함
  • 대상에 Perl이 있으면 memfd_create()를 이용해 exploit 바이너리를 메모리에 올리고 /proc/$$/fd/<fd>로 실행할 수 있음
  • 컴파일 의존성은 libnftnl-devlibmnl-dev
  • KernelCTF용 정적 빌드는 musl-gcc를 사용했으며, glibc 정적 링크와 QEMU AVX512 opcode 문제를 피하기 위한 선택임
  • exploit 소스는 여러 파일로 나뉘어 있고, 독립 실행 바이너리 목적이라 오류 발생 시 error code 반환보다 crash/exit 방식을 택함

안정성과 한계

  • exploit 프로세스의 pagetable 상태가 불안정해질 수 있어, 성공 또는 실패 뒤 자식 프로세스를 종료하지 않고 sleep 상태로 남겨 커널 불안정을 줄임
  • 네트워크 활동이 있으면 skb freelist에 노이즈가 생겨 안정성에 영향을 줄 수 있음
  • SSH나 reverse shell 환경에서는 double-free 시점 주변의 stdout 출력을 줄여 네트워크로 인한 skb 할당/해제를 최소화함
  • 일부 하드웨어 테스트에서는 몇 초 뒤 시스템이 crash했고, WiFi frame도 skb를 사용한다는 점 때문에 WiFi 활동이 영향을 줬을 가능성이 있음
  • WiFi 어댑터를 BIOS에서 비활성화하면 해당 환경에서 exploit이 정상 동작함

연구 과정에서 얻은 결론

  • PoC는 넓은 호환성, 높은 안정성, 은닉 실행을 목표로 다듬어짐
  • 개발 기간 2개월에 더해 안정성과 호환성 개선에 2개월을 더 사용함
  • exploit 자체는 slab allocator 동작에 크게 의존하지 않고 IPv4 subsystem과 virtual memory처럼 널리 활성화된 기능을 중심으로 구성됨
  • 초기 버그는 unprivileged user namespace와 nftables가 필요하지만, Dirty Pagedirectory와 PCP draining 같은 기법은 다른 실제 exploit에도 활용될 수 있음
  • 이 작업은 Linux 커널의 networking subsystem과 memory management subsystem을 함께 파고든 사례로 남음

댓글과 토론

Hacker News 의견들
  • 오늘 CVE-2024-1086의 개념증명 익스플로잇을 공개했고, Debian과 Ubuntu 등에서 동작함
    영향받는 버전은 Linux 커널 v5.14~v6.6이며, v6.4~v6.6 지원 여부는 CONFIG_INIT_ON_ALLOC_DEFAULT_ON 커널 설정에 따라 달라짐
    이 버그는 2024년 2월에 패치됐으니 Linux 장비를 업데이트해야 함
    • 소스도 좋고 작업도 훌륭함. 더 큰 반응이 어떻게 나올지 궁금함
      https://github.com/Notselwyn/CVE-2024-1086/blob/main/src/mai...
    • 익스플로잇 이후 단계에서 KSMA 비슷한 원시 연산을 얻었다면, 왜 여전히 modprobe_path 방식을 쓰고 파일 없는 방식 때문에 pid 무차별 대입까지 넣었는지 궁금함
      예를 들어 커널 .text를 짧은 셸코드로 패치해서 root가 되고 네임스페이스를 탈출하는 방식은 왜 선택하지 않았는지 묻고 싶음
    • 이 취약점의 가능한 공격 경로와 영향이 무엇인지 궁금함
    • 본문에는 영향받는 익스플로잇 버전이 Linux 커널 v5.14부터 v6.4까지라고 되어 있는데, 링크된 페이지에는 v5.14부터 v6.6까지라고 되어 있음
      단, 패치된 브랜치 v5.15.149>, v6.1.76>, v6.6.15>는 제외된다고 적혀 있음
  • 패치에 이런 문장이 있음: “This reverts commit e0abdadcc6e1. [...] Its not clear to me why this commit was made.”
    이 커밋의 배경 이력을 찾아본 사람이 있는지 궁금함
    [1] https://lore.kernel.org/all/20240120215012.129529-1-fw@strle...
    • 원래 커밋이 이제 10년 넘은 커밋이라 시간 속에 묻혔을 가능성이 큼
      예전 netdev 목록을 찾아봤지만 아무것도 못 찾았고, 커미터인 Pablo Neira Ayuso에게 직접 보낸 패치였을 수도 있음
      원 작성자는 지금은 기피 인물이 된 Patrick McHardy였고, Pablo가 기억하거나 메일에서 찾아내지 않는 한 정확한 사용 사례는 기본적인 조사만으로는 밝혀지기 어려워 보임
    • 관련 커밋은 다음과 같음: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/lin...
      netfilter: nf_tables: accept QUEUE/DROP verdict parameters로, 사용자 공간이 QUEUEDROP 판정에 큐 번호나 errno 코드를 지정할 수 있게 하는 내용임
  • 이 글은 보안 글쓰기 관점에서도 매우 인상적임
    보안 블로그를 쓸 때 “얼마나 많은 배경지식과 선행지식을 가정할 것인가”가 늘 고민인데, 접근 가능하면서도 작성 가능한 균형을 잡는 일은 어렵다
    대상 독자를 먼저 정하고 충분한 배경 설명을 제공한 점이 훌륭해서, 매년 만나는 예비 연구자들에게 안내 자료로 주려고 북마크함
  • 이 익스플로잇은 비권한 사용자 네임스페이스 접근에 의존함: sysctl kernel.unprivileged_userns_clone = 1
    Debian/Ubuntu와 Arch Linux 커널의 기본값이며, sudo 없이 Docker 명령을 실행하는 식의 필요가 없다면 꺼두는 편이 좋음
    • 이 설정은 Docker나 Pacman만 쓰는 게 아님
      Electron 앱이나 1Password 같은 곳에서 쓰는 Chrome 샌드박스도 관련되고, 샌드박스 보조 바이너리는 setuid 프로그램으로도 동작할 수 있음
      Proton도 앞으로 사용자 네임스페이스를 쓰기 시작해도 놀랍지 않아서, 일반 데스크톱 Linux에서는 꺼두지 않는 편이 나을 수 있음
      서버나 특수하게 강화한 Linux에서는 꺼두는 것이 나쁘지 않은 경우가 많음
    • 이 설정은 사람들이 root 권한 없이 컨테이너 같은 것을 실행하게 해준다는 점에서는 좋지만, 결국 root 권한 상승 취약점으로 이어진 전력이 있다는 점이 정말 아쉬움
    • 진짜 문제는 그 설정 자체가 아니라, 네임스페이스는 원래 권한을 떨어뜨리는 수단이라는 데 있음
      문제는 컨테이너가 “그냥 동작”하려면 네트워크 쪽에 수많은 해킹이 필요하고, Docker의 핵심 판매 포인트도 그런 위험한 해킹을 대신 해주는 데 가까움
      결국 어떤 컨테이너 해법도 그런 해킹을 받아들이게 되고, 정상적인 네임스페이스 기능이 접근을 줄여도 커널이 네트워크 해킹 때문에 문을 열어버리는 구조가 됨
    • 6.1.65에서는 그 옵션이 없는데, 이름이 바뀐 건지 궁금함
  • 비권한 사용자 네임스페이스가 기본으로 켜져 있는지 모르겠음
    “비권한” 네임스페이스 안에서 실행된다 해도, 기본적으로 사용자에게 iptables나 mount 같은 것을 실행할 능력을 주는 이유가 무엇인지 의문임
    • 비권한 사용자 네임스페이스는 권한 없는 프로그램이 샌드박스를 구성할 수 있게 해줌
      예를 들어 Chrome은 프로세스 샌드박스를 구현하기 위해 네임스페이스를 쓰지만, 비권한 네임스페이스가 없어도 되도록 setuid-root 바이너리를 설치함
      setuid-root 바이너리 자체가 보안 위험이기 때문에, 장기적으로 Chrome이 그런 바이너리를 설치하지 않아도 되는 쪽이 바람직함
      다만 그러려면 비권한 사용자 네임스페이스가 널리 제공되어야 하고, 이런 버그가 그 미래를 늦추는 셈임
      또한 Chrome처럼 네임스페이스 기반 샌드박스를 구성하는 프로그램은 seccomp도 함께 써서, 샌드박스 안의 코드가 네임스페이스 같은 특이한 커널 기능을 쓰지 못하게 막는 경우가 많음
      단일 사용자 데스크톱에서는 사용자와 root 사이의 분리를 강하게 적용하는 이점이 크지 않고, 흥미로운 것은 대부분 사용자 계정에서도 접근 가능함
      반면 Chrome 같은 샌드박스는 데스크톱 보안에 필수이므로, 단일 사용자 데스크톱에서는 비권한 사용자 네임스페이스를 켜는 편이 전체 보안을 높인다고 보는 편임
      다중 사용자 시스템은 당연히 다른 이야기임
    • 이런 버그만 계속 없었다면 비권한 사용자 네임스페이스는 훌륭한 보안 기능이었을 것임
      예를 들어 Flatpak 실행이 앱 격리를 위해 호스트의 setuid 바이너리를 필요로 하지 않는다면 좋을 것임
    • 문제는 “그냥 동작한다” 철학
      대부분의 기본값은 안전하지 않고, 기술 지식과 강화 작업이 필요함
  • 문제를 도입한 커밋: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/lin...
    중첩된 switch/case에서 기본 경로에 return이 있는데 이후 fall-through를 기대하는 구조가 이상해 보임
  • ASLR 같은 현대적 완화 기법이 있는데도 이런 익스플로잇이 어떻게 가능한지 궁금함
    대학 수업에서 특정 Ubuntu 버전에서 실행할 바이너리 여러 개를 받고, 사용 후 해제나 버퍼 오버플로 같은 버그를 찾아 익스플로잇하는 과제를 했는데 정말 어려웠음
    결함을 찾는 것도 어렵고, 그 결함으로 유용한 일을 하는 정확한 셸코드를 작성하는 것은 더 어려웠음
    더 어려운 단계에서는 ASLR, 스택 카나리 같은 완화 기법도 켜져 있었고, 통제된 학생 환경에서도 거의 불가능하게 느껴졌음
    결국 1) 익스플로잇 가능한 결함을 발견하고 2) 프로그램을 그냥 크래시시키지 않으면서 유용한 일을 하는 정확한 바이너리 페이로드를 찾아야 하는데, 현실 세계에서는 더 어려울 텐데 어떻게 가능한지 의문임
    [0] https://web.stanford.edu/class/archive/cs/cs107/cs107.1194/a...
    • 수업을 듣던 사람은 경험이 적은 초보였고, 10~15주짜리 과정에서 여러 버그를 찾아 익스플로잇해야 했음
      다른 수업과 병행하면서 버그 하나에 쓸 수 있는 시간은 몇 시간에서 며칠 정도였을 것임
      Zerodium에서 일반적인 Linux 로컬 권한 상승은 5만 달러를 지급하고, 숙련된 익스플로잇 개발자 비용 기준으로 대략 200 인시를 살 수 있음
      즉 전문가들이 초보 학생보다 10~100배 많은 시간을 쓰는 셈임
      목공소에 처음 간 사람과 목수, 도예 초보와 장인, 새내기 화가와 전문 예술가의 차이를 떠올리면 됨
      거기에 시간까지 10~100배 더 들어가는 격임
      [1] https://zerodium.com/program.html
    • 현대적 완화 기법이 익스플로잇을 훨씬 어렵게 만들었지만, 연구자들은 계속 그 완화를 우회하는 방법을 찾아냄
      대부분의 최신 보호 기법을 우회하는 “고전적” 기법들이 있고, 없으면 새로운 공격이나 우회 방법이 나오기도 함
      예를 들어 힙 보호 우회는 how2heap을 볼 수 있고[0], KASLR 우회 익스플로잇 사례도 있었으며[1], 이번 익스플로잇은 dirty pagetable 기법을 쓰는 것으로 보임[2]
      완화 기법이 추가되고 연구자들이 이를 우회하는 고양이와 쥐 게임이 계속되는 구조임
      [0] https://github.com/shellphish/how2heap
      [1] https://www.willsroot.io/2022/12/entrybleed.html
      [2] https://pwning.tech/nftables/#452-the-technique
    • 짧게 말하면 잘하는 사람도 있고, 운 좋은 사람도 있고, 잘하면서 운도 좋은 사람도 있음
      이런 것을 찾는 데는 마지막 부류 한 명이면 충분함
      요즘은 정말 어렵고, 완화 기법을 꺼도 취약점을 찾고 익스플로잇을 작성하는 일은 여전히 쉽지 않음
      다만 이런 취약점을 찾는 많은 사람은 팀으로 일하면서 퍼징을 병렬화하고, 지식과 다른 익스플로잇을 결합하거나 체이닝할 수 있음
      일부 연구자의 전문성과 재능은 놀라울 정도이고, 이런 분야에서는 수년에서 수십 년의 경험이 엄청난 가치가 있음
    • 비슷한 수업에서 바이너리를 받아 버그를 찾는 과제를 해봤는데 역시 어려웠음
      하지만 지금 발견되는 주요 버그가 자금이 충분한 팀이나 국가 행위자에 의해 찾아진다고 생각하면, 인력과 자원을 대량으로 투입해 기존 완화 기법을 우회할 수 있다는 점이 더 이해됨
    • 저장소에 링크된 블로그 글에는 KASLR에 관한 별도 섹션이 있음
  • Ubuntu에 따르면 모든 LTS 릴리스가 영향을 받으며, 현재 패치된 커널에서는 수정됨: https://ubuntu.com/security/CVE-2024-1086
    Focal은 5.4.0-174.193, Jammy는 5.15.0-101.111, Mantic은 6.5.0-26.26에서 수정됨
    확장 지원을 쓰는 경우 Xenial과 Bionic도 포함됨
  • 취약한 Debian 시스템에서 실행해 봤는데 권한 상승은 되지 않았지만, 두 번째 실행에서 전체 시스템이 멈췄음
    첫 번째 실행은 그냥 실패했으니, 그래도 시간을 들여 패치할 가치는 충분함
  • 현재 커널 설정은 /boot/config/proc/config.gz 같은 파일에서 확인할 수 있음