소학회/기술스터디

[IGLOO]Linux Kernel 내 Use-After-Free 취약점 (CVE-2024-1086) 분석 및 대응방안

haerim9.9 2025. 2. 10. 23:35

Use-After-Free 취약점 개요

Use-After-Free(UAF) : 메모리 할당 후 해제된 메모리를 사용하여 발생하는 메모리 손상(Memory Corruption) 취약점, 힙 메모리(Heap Memory)에서 주로 발생한다.

*힙(Heap) : 컴퓨터의 메모리 구조 중 사용자가 임의로 사용할 수 있는 공간, 동적으로 할당하고 해제할 수 있다.

 

힙 영역 중, 사용하지 않는 공간(Unused Heap)은 이중 연결리스트로 관리한다. 메모리 해제가 발생하면 해당 영역이 연결 리스트에 연결되고, 새로운 메모리 할당이 발생하면 연결 리스트의 첫 번째 매칭(First-Fit)영역을 할당한다.

 

문제는 할당했던 메모리를 해제한 후 재할당해서 사용할 때 동일한 크기로 재할당 하는 경우, 이전에 사용했던 메모리 공간을 재사용하게 되면서 악의적으로 사용할 수 있는 것이다.

+운영체제에서는 재할당을 막기 위해, 주소 공간 배치 난수화(ASLR, Address Space Layout Randomization)/세그멘테이션 및 힙 보호 등을 이용한다.

 

리눅스 커널에서, 동적 메모리 할당 시 메모리 블록을 재활용하여 성능을 높이는 것을 목표로 SLUB 할당자(SLUB Allocator)를 사용한다. 힙에 다수의 NOP코드와 쉘 코드를 스프레이처럼 뿌리는 공격 기법인 힙 스프레이(Heap Spray)와 같은 UAF 발현이 가능한 공격에 노출될 수 있다. 


CVE-2024-1086 취약점 분

CVE-2024-1086 : 리눅스 커널 내 리눅스 패킷 필터링 및 네트워크 주소 변환(NAT) 프레임워크인 netfilter의 nf_tables 구성 요소에서 UAF가 발생하는 취약점

익스플로잇에 성공하면 로컬 권한 상승(Local Privilege Escalation)이 가능하다. 해당 취약점은 nf_hook_slow() 함수에서 이중으로 메모리를 해제시켜 발생한다.

 

iptables 프레임워크를 대체한 netfilter를 사용하는 커널의 대부분이 취약 버전에 해당되므로, 공격 대상 범위가 넓다.

 

 

1. CVE-2024-1086: 취약점 원인

netfilter에서 발생하는 패킷 처리 과정에서 해제된 메모리를 사용하는 것과 동시에 이중 해제(double-free) 형식을 이룰 수 있다.

nftables 패킷 처리 과정에서 문제가 발생한 함수 : nf_hook_slow(), nft_verdict_init()

 

CVE-2024-1086 발생 원인 코드(code) : nf_hook_slow() / nft_verdict_init()

nf_hook_slow() 함수에서 590번 라인을 살펴보면, 규칙에 대한 verdict 값을 얻고 verdict 값과 NF_VERDICT_MASK 매크로 값을 통해 패킷 처리를 결정하는 것을 알 수 있다.

verdict 값은 패킷 처리 결과값으로, 사용자가 직접 설정할 수 있다. 이에 대한 처리는 nft_verdict_init() 함수에서 이루어진다.

 

nft_verdict_init() 함수에서 data->verdict.code는 규칙 설정에 대한 리턴 값으로 설정할 수 있다. 해당 값에 공격자가 "0xFFFF0000"이라는 값을 설정하면 취약점이 발현된다. netfilter에서 정의된 NF_VERDICT_MASK 매크로를 통해 verdict.code의 하위 16비트를 추출한다.이 경우, "data->verdict.code & NF_VERDICT_MASK"는 "0xFFFF0000 & 0x00000000"으로 결과값이 도출된다. 그리고 해당 값은 netfilter에서 NF_DROP을 의미하며, 따라서 패킷을 버리는 것으로 해석된다.

 

nf_hook_slow() 함수에서 패킷을 NF_DROP 처리하라는 요청을 받아 kfree_skb() 함수를 통해 패킷을 해제한다.

 

패킷 처리의 값은 return을 1로 반환, 이는 NF_ACCEPT 처리로 해석된다. nf_hook_slow() 함수에서는 해당 패킷에 대해 긍정적인 드롭 에러가 발생해, NF_ACCEPT 처리하는 것으로 혼동한다.

 

nf_hook_slow() 함수 내 메모리 해제 후 NF_DROP_GETERR() 처리

kfree_skb()에 의해 해제됐던 패킷이 NF_ACCEPT 반환 처리로 다시 처리되어 이전에 해제됐던 소켓 메모리 참조(skb)가 남아있게 되고(UAF 발생), 해당 패킷이 NF_ACCEPT 처리를 모두 마치게 되어 해당 소켓 메모리(skb)를 해제하면 이중 해제(double-free)가 발생할 수 있게 된다.

 

 

2. CVE-2024-1086: PoC 환경 구성 및 시연

네임스페이스(Namespace) : 시스템 리소스를 격리하여 프로세스가 독립적인 실행 환경에서 작동할 수 있는 리눅스 커널의 기능 중 하나

비특권 사용자 네임스페이스(unprivileged-user namespace) : 일반 사용자가 네임스페이스(User namespace)를 생성할 수 있도록 허용하는 설정

 

일반 사용자의 사용자 네임스페이스 생성을 허용한 이후 로컬 사용자는 사용자 네임스페이스를 생성해 nftables에 대한 권한을 얻을 수 있다. nftables의 경우 높은 액세스 권한을 요구하는데, 일반 사용자가 사용자 네임스페이스를 통해 nftables를 사용 가능한 환경을 만들어 주기 위한 조건으로 파악된다.

 

3번과 4번은 접근 권한을 얻은 로컬 일반 사용자가 취약점을 트리거하기 위한 악의적인 nftables 체인/룰을 생성하는 것과 background 내 메모리 노이즈(memory noise)가 많을 경우 취약점 발현의 악영향을 줄 수 있기에, 메모리를 미리 할당하여 취약점 발현이 예측 가능한 메모리 레이아웃을 만들기 위함으로 분석된다.

 

 

3. CVE-2024-1086: PoC 분석

CVE-2024-1086 PoC main() flow

521번 : 메모리 맵핑, 익스플로잇 코드 세팅

526번 : 자식 프로세스 생성 및 설정

542번 : privesc_flh_bypass_no_time() 함수를 포함한 상-하에 위치한 코드로 초기 환경 구축 및 실제 익스플로잇

549번 : 부모 프로세스의 설정 마무리 및 PoC의 익스플로잇 과정 종료

 

이중 해제(double-free) : 일반적인 "할당-해제-할당-해제"의 형식과 달리 "할당-해제-해제"와 같은 형식으로 진행되어 메모리 오염(Memory corruption)이 발생하는 취약점

refcount : 리눅스의 메모리 해제를 나타내는 변수, 값이 1에서 0으로 감소해야 메모리 해제가 이루어진다. 이중 해제 시, refcount 값이 "1 -> 0 -> -1"로 변경되어 커널 패닉(Kernel Panic)이 발생할 수 있다.

 

이중 해제 유발 과정

  1. UDP 패킷 생성 -> 다량의 skb(sockets buffers) 생성 : 이중 해제 탐지와 안전성을 확보하기 위한 masking용
    -alloc_ipv4_udp() 함수 : UDP 패킷 생성 후 전송, 버퍼(skb)가 메모리에 할당됨
    -memset() 함수 : intermed_buf라는 버퍼를 \x00값으로 초기화, send_ipv4_udp() 함수를 호출하여 초기화된 버퍼와 크기를 사용해 UDP 패킷 생성 및 전송
    -send_ipv4_udp() 함수 : sockaddr_in dst_addr로 목적지 주소 설정, 이후 sendto_noconn() 함수로 지정된 소켓을 통해 데이터그램 전송

  2. nftables 규칙 트리거
    -alloc_rule() 함수 : 프로토콜과 패킷 내 처음 4바이트 검사, add_payload() 함수는 IPv4 헤더에서 프로토콜 필드 추출해 NFT_REG_1레지스터에 저장한 후 add_cmp 함수로 NFT_REG_1에 저장된 값이 proto 값과 동일한지 비교
    그 다음 패킷의 4바이트 검사, 역시 add_paylload() 함수를 통해 헤더 끝에서 4바이트 추출한 뒤 NFT_REG_1레지스터에 저장, 이후 동일하게 add_cmp로 저장된 값이 "\x41\x41\x41\x41"와 동일한지 비교

    위 규칙에 해당되는 패킷이 전송될 경우 결과를 "0xFFFF0000"으로 설정, 이는 NF_ACCEPT로 해석되어 긍정적인 드롭 에러 유발

  3. skb Free(첫 번째 해제)
    privesc_flh_bypass_no_time() 함수 : IP헤더 오프셋 필드에 IP_MF(0x2000) 플래그(IP패킷 조각화) 설정, IP패킷 생성 위해 trigger_double_free_hdr() 함수 호출, 조각화된 패킷 전송 시 할당된 skb는 nftable 규칙에 의해 NF_DROP 케이스로 해제

    -조각화된 패킷은 IP fragment queue에서 관리, 즉 조각화된 패킷이 queue 안에 머무를 수 있음 -> double-free 상태 유발 가능
    -UDP 패킷 모두 해제 -> 이중 해제 감지 방지

  4. PTE spray : 첫 번째 해제된 위치를 안정적으로 할당하기 위해 진행
    VMA에 등록된 가상 메모리 페이지에 접근, CONFIG_PTE_SPRAY_AMOUNT만큼 PTE 스프레이(2MB 간격으로 할당)
    PMD와 PTE가 오버랩되는 구간을 찾기 위해 PTE에 0x41 할당, 메모리 위치 계산을 위해 PTI_TO_VIRT 매크로 사용

    -PIT_TO_VIRT 매크로 : 페이지 테이블 인덱스를 가상 메모리 주소로 변환해 정확한 메모리 위치에 접근하고, 해당 메모리 위치에 PTE 할당하기 위해 사용

  5. 동일 skb Free(두 번째 해제)
    IP fragment queue 대기열에 위치 중인 IP 조각화 패킷 전송을 위해 ip_id를 이전에 할당한 ip_id와 동일한 값으로 설정, 해당 패킷 전송하면 조각화 대기열 완성 -> ip_fragement_queue가 패킷 재조립 시도, 대기열에 있는 패킷이 두 번째 해제됨

  6. 두 번째 해제 이후
    이중 해제가 완료된 시점에서 freelist에 반환된 페이지를 Overlapping PMD(Page Middle Directory)로 재할당, 오버랩된 PMD와 PTE가 존재하는 상황에서 할당된 PTE 중 어떤 것이 오버랩된 것인지 찾아야 함
    따라서 PTE 영역에서 PMD 영역에 속하는 PTE 항목을 확인하는 작업 수행, 발견하면 pte_area 변수에 오버랩된 PTE 항목의 주소 저장
    오버랩된 PTE 항목에 PTE 물리 주소와 플래그를 포함한 새로운 값으로 설정, 이후 flush_tlb() 함수로 TLB 플러시하여 변경된 PTE 값이 반영되도록 설정

 

물리적 메모리 스캔(Physical Memory Scan)

이중 할당을 설정했기 때문에 userland에서 커널 공간 미러링 공격(KSMA, Kernal Space Mirroring Attack) 수행이 가능하다. PTE 영역 내의 특정 주소에 물리적 주소를 쓰고, PTD 영역에서 일반 메모리 페이지로 역참조(dereference)가 가능한 것이다.

 

  1. 물리적 메모리 스캔한 페이지가 커널 기본 주소를 참고하고 있는지 확인하기 위해 is_kernal_base() 함수 호출, return이 1일 때 커널 기본 주소를 참조하는 것이 확정적으로 판단된다.
  2. modprobe_path 식별한다. 이를 위해 커널 기본 주소부터 80MiB 바이트를 스캔하여 modprobe_path를 찾는다. 스캔하기 전 modprobe_iteration_base를 설정해 현재 스캔할 물리적 주소의 범위를 설정한다.
    memmem() 함수로 CONFIG_STATIC_USERMODEHELPER_PATH 또는 modprobe_path를 검색한다.
  3. 익스플로잇의 실제 PID를 얻는다. 해당 PoC는 brute force 방식을 차용했다. 반복문으로 PID 값을 일일이 대입한다.
  4. modprobe_path 커널 변수 성공적 탐지 시, lseek() 함수를 통해 modprobe_script_fd 파일 디스크럽터(File Descriptor)의 파일 오프셋을 시작점으로 한다. 이후 dprintf() 함수로 원하는 스크립트를 작성한다.
  5. 해당 코드가 실행됐다면 루트 쉘(root shell)을 획득했음을 의미한다.

 

안정적으로 루트 쉘을 활용할 수 있는 환경 조성

자식 프로세스 및 부모 프로세스 관련 코드(code)

1번 과정

  • fork() 함수 선언, return 값 비교 -> 자식 프로세스 생성 및 코드 블록 실행
  • 자식 프로세스가 SIGINT 시그널 처리를 위해 signal_handler_sleep 핸들러 등록(SIGINT 시그널 발생 시 기본 동작인 프로세스 종료를 하지 않고 백그라운드에서 계속 실행되도록 설정)

2번 과정 

  • exploit_status 값을 EXPLOIT_STAT_FINISHED로 변경, 루트 쉘 획득 상태를 오래 유지하기 위해 sleep 상태로 유지

3번 과정 

  • SPINLOCK 매크로 사용 -> exploit status가 EXPLOIT_STAT_RUNNING 상태에서 벗어날 때까지 대기, 익스플로잇 완료 후 수행 완료되면 부모 프로세스도 종료
  • 그러나 1번 2번 과정으로 자식 프로세스가 종료되지 않으므로 부모 프로세스도 종료되지 않도록 유지 가능

 

대응 방안

CVE-2024-1086의 원인 : 리눅스 커널의 내장 모듈 중 하나인 nefilter의 nft_verdict_init(), nf_hook_slow() 함수에서 긍정적인 드롭 에러가 발생하는 것

따라서 커널의 최신 버전 및 보안 업데이트를 적용하는 것이 중요하며, 업데이트 불가능 시 CVE-2024-1086을 트리거 할 수 없도록 설정 값을 변경해야 한다.

 

  • 일반 사용자의 네임스페이스 생성 권한 제거 : 일반 사용자가 네임스페이스 생성할 수 없도록 권한 제한, 취약점이 발생한 netfilter 모듈 접근 제한
  • nftables 접근 권한 통제 : 규칙 생성 및 적용 권한 제한

본 게시글은 아래의 링크 속 콘텐츠를 기반으로 작성.

https://www.igloo.co.kr/security-information/linux-kernel-%eb%82%b4-use-after-free-%ec%b7%a8%ec%95%bd%ec%a0%90cve-2024-1086-%eb%b6%84%ec%84%9d-%eb%b0%8f-%eb%8c%80%ec%9d%91%eb%b0%a9%ec%95%88/

 

Linux Kernel 내 Use-After-Free 취약점 (CVE-2024-1086) 분석 및 대응방안

01. Use-After-Free 취약점 개요 Use-After-Free(UAF)는 메모리 할당 후 해제된 메모리를 사용하여 발생하는 메모리 손상(Memory Corruption)취약점으로 주로 힙 메모리(Heap Memory)에서 발생한다. 힙(Heap)은 컴퓨터

www.igloo.co.kr