- 리눅스 커널
nf_tables의 CVE-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_cache의struct sk_buff와sk_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_DROP과NF_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_bufffreelist 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-dev와libmnl-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을 함께 파고든 사례로 남음