[하루한줄] CVE-2024-26809 : Linux Kernel Netfilter의 Use-After-Free로 인한 LPE 취약점
URL
Target
- 커널 버전 6.8 이상 ~ 6.8.2 미만
- 커널 버전 6.7 이상 ~ 6.7.11 미만
- 커널 버전 6.2 이상 ~ 6.6.23 미만
- 커널 버전 5.18.11 이상 ~ 6.1.83 미만
- 커널 버전 5.15.54 이상 ~ 5.15.153 미만
- 커널 버전 5.10.130 이상 ~ 5.10.214 미만
배경지식
CVE-2024-26809 취약점을 이해하기 위해서는 리눅스 커널의 Netfilter 프레임워크와 nftables 서브시스템, 그리고 nft_set_pipapo
집합 유형에 대한 기본적인 이해가 필요합니다.
- Netfilter 및 nftables: Netfilter는 리눅스 커널 내에서 네트워크 패킷을 가로채고 조작하기 위한 핵심 프레임워크입니다. 방화벽 구축, NAT(Network Address Translation), 패킷 로깅 등 다양한 네트워킹 기능을 제공합니다. nftables는 기존의
iptables
,ip6tables
,arptables
,ebtables
를 대체하기 위해 개발된 차세대 패킷 분류 프레임워크로, 더욱 유연하고 효율적인 규칙 관리를 목표로 합니다. nftables는 테이블(tables), 체인(chains), 규칙(rules), 집합(sets), 객체(objects) 등의 추상화된 개념을 사용하여 복잡한 패킷 처리 로직을 구성합니다. - nft_set_pipapo: nftables의 집합(set)은 특정 데이터 요소들의 컬렉션을 저장하고 빠르게 조회하는 데 사용됩니다.
nft_set_pipapo
는 ‘Possible Intersection of Prefix Aggregation Prefix Overlap’의 약자로, 특히 IP 주소 범위나 포트 번호 범위와 같은 간격(interval) 기반의 데이터를 효율적으로 관리하기 위한 집합 유형 중 하나입니다. 이 집합 유형은 내부적으로 복잡한 자료구조를 사용하여 요소의 삽입, 삭제, 검색 연산을 최적화합니다.nft_set_pipapo.c
파일 내의 함수들은 이 PIPAPO 집합의 생성, 수정, 삭제, 요소 관리 등과 관련된 로직을 구현합니다. - 트랜잭션 및 클론: nftables의 설정 변경은 원자성(atomicity)을 보장하기 위해 트랜잭션 기반으로 처리됩니다. 여러 개의 규칙 추가, 집합 요소 변경 등의 작업이 하나의 배치(batch)로 묶여 처리되며, 모든 작업이 성공하면 커밋(commit)되고 하나라도 실패하면 전체 작업이 중단(abort)되어 롤백됩니다. 이 과정에서 일관성을 유지하고 동시성 문제를 관리하기 위해, 변경 대상이 되는 집합과 같은 객체는 내부적으로 복제본, 즉 클론(clone)을 생성하여 작업을 수행할 수 있습니다. 클론은 일반적으로 특정 시점의 상태를 반영하는 읽기 전용 뷰로 사용될 수 있습니다.
취약점 메커니즘 상세 설명
CVE-2024-26809 취약점은 nft_set_pipapo
집합의 클론 처리 로직과 트랜잭션 관리 메커니즘 사이의 상호작용에서 발생하는 메모리 관리 오류입니다.
핵심 문제는 클론된 집합의 요소를 해제하는 방식에 있었습니다. 취약한 버전의 코드에서는 특정 조건, 특히 트랜잭션이 중단(abort)되거나 집합이 삭제(destroy)되는 경우, 클론된 집합에 속한 요소들을 해제하기 위해 정상적인 객체 파괴 경로(destroy path)를 따르지 않는 별도의 로직을 사용하였습니다.
넷필터 커밋 프로토콜의 설계 원칙상, 클론은 항상 조회 테이블의 ‘현재 뷰(current view)’를 제공해야 하며, 객체의 실제 파괴는 일관된 파괴 경로를 통해 이루어져야 합니다. 그러나 취약한 코드는 이러한 원칙을 위반하여, 어보트 경로 등에서 클론의 뷰를 사용하지 않고 요소를 직접 해제하려 시도했습니다. 이 과정에서 다음과 같은 문제가 발생할 수 있습니다:
- Double-Free: 특정 조건(아래 근본 원인 분석 참조) 하에서
nft_pipapo_destroy
함수 내에서nft_set_pipapo_match_destroy
함수가 두 번 호출될 수 있습니다. 한 번은 원본priv->match
객체에 대해, 다른 한 번은 클론priv->clone
객체에 대해 호출됩니다. 이미 커밋된 요소들은 두 객체 모두에 뷰(view)를 가지므로, 이 요소들에 대해nf_tables_set_elem_destroy
함수가 두 번 호출되어 동일한 메모리(elem_priv
)를 두 번 해제(kfree)하는 Double-Free가 발생합니다. - Use-After-Free (UAF): Double-Free가 발생하면, 해제된 메모리 영역(
elem_priv
)을 가리키는 포인터(댕글링 포인터)가 커널 내 다른 곳(예: 다른 넷필터 구조체, 진행 중인 작업)에 남아 있을 수 있습니다. 만약 이 포인터가 이후 코드 경로에서 참조되면, 이미 해제되어 다른 용도로 재할당되었을 수 있는 메모리 영역에 접근하게 되어 예측 불가능한 동작이나 시스템 충돌을 일으킵니다.
이러한 문제는 특히 넷필터 커밋 프로토콜과 nft_set_pipapo
기능이 통합된 이후(commit 212ed75dc5fb
) 더욱 복잡해졌을 가능성이 있습니다. 이전 버전에서 어보트 경로에서의 요소 해제를 처리하기 위해 추가된 로직(commit 9827a0e6e23b
)이 새로운 커밋 프로토콜과의 상호작용 속에서 의도치 않은 부작용을 일으켰을 수 있습니다. 즉, 복잡한 트랜잭션 상태 관리와 객체 생명주기 관리 로직 사이의 미묘한 동기화 문제 또는 설계상의 결함이 Double-Free/UAF로 이어진 것입니다.
취약한 코드 분석
--- a/net/netfilter/nft_set_pipapo.c
+++ b/net/netfilter/nft_set_pipapo.c
@@ -2329,8 +2329,6 @@ static void nft_pipapo_destroy(const struct nft_ctx *ctx,
m = rcu_dereference_protected(priv->match, true);
if (m) {
rcu_barrier();
-
- nft_set_pipapo_match_destroy(ctx, set, m); // [취약점 관련 1] priv->match에 대해 호출
for_each_possible_cpu(cpu)
pipapo_free_scratch(m, cpu);
@@ -2342,8 +2340,7 @@ static void nft_pipapo_destroy(const struct nft_ctx *ctx,
if (priv->clone) {
m = priv->clone;
- if (priv->dirty) // [취약점 관련 2] priv->dirty 플래그 확인
- nft_set_pipapo_match_destroy(ctx, set, m); // [취약점 관련 3] priv->clone에 대해 호출
+ nft_set_pipapo_match_destroy(ctx, set, m); // 패치된 버전에서는 이 라인만 남음
for_each_possible_cpu(cpu)
pipapo_free_scratch(priv->clone, cpu);
이 코드에서 priv->clone
이 존재하고 priv->dirty
플래그가 설정되어 있으면, nft_set_pipapo_match_destroy
함수가 priv->match
에 대해 한 번 (제거된 라인), 그리고 priv->clone
에 대해 또 한 번 호출됩니다. 이것이 Double-Free의 직접적인 원인이 됩니다.
static void nft_set_pipapo_match_destroy(const struct nft_ctx *ctx,
const struct nft_set *set,
struct nft_pipapo_match *m)
{
struct nft_pipapo_field *f;
int i, r;
// 마지막 필드(field_count - 1)를 찾음
for (i = 0, f = m->f; i < m->field_count - 1; i++, f++)
;
// 마지막 필드의 모든 규칙(rules)을 순회
for (r = 0; r < f->rules; r++) {
struct nft_pipapo_elem *e;
// 동일한 요소를 가리키는 연속된 규칙은 건너뜀 (중복 처리 방지)
if (r < f->rules - 1 && f->mt[r + 1].e == f->mt[r].e)
continue;
e = f->mt[r].e; // 요소(element) 포인터 가져오기
// 요소를 파괴하는 함수 호출
nf_tables_set_elem_destroy(ctx, set, &e->priv);
}
}
이 함수는 주어진 nft_pipapo_match
객체(m
) 내의 요소(nft_pipapo_elem
)들을 순회하며, 각 요소에 대해 nf_tables_set_elem_destroy
를 호출하여 메모리를 free합니다.
void nf_tables_set_elem_destroy(const struct nft_ctx *ctx,
const struct nft_set *set,
const struct nft_elem_priv *elem_priv)
{
struct nft_set_ext *ext = nft_set_elem_ext(set, elem_priv);
// 요소에 연결된 표현식(expressions)이 있으면 해제
if (nft_set_ext_exists(ext, NFT_SET_EXT_EXPRESSIONS))
nft_set_elem_expr_destroy(ctx, nft_set_ext_expr(ext));
// 요소의 private 데이터(elem_priv) 메모리 해제
kfree(elem_priv); // *** Double-Free 발생 지점 ***
}
이 함수는 최종적으로 kfree(elem_priv)
를 호출하여 요소의 메모리를 free합니다. nft_pipapo_destroy
에서 nft_set_pipapo_match_destroy
가 두 번 호출되면, 동일한 elem_priv
에 대해 이 kfree
가 두 번 실행되어 Double-Free가 발생합니다.
관련 주요 자료구조 및 변수:
struct nft_set
: 넷필터 집합의 공통 헤더 구조체입니다.struct nft_pipapo_set
: PIPAPO 집합의 내부 상태 관리 구조체입니다.match
(현재 커밋된 뷰),clone
(트랜잭션 중 변경 사항이 반영되는 뷰),dirty
(변경 여부 플래그) 포인터/멤버를 포함합니다.struct nft_pipapo_match
: PIPAPO 집합 요소들의 뷰(view)를 관리하는 객체입니다.priv->match
와priv->clone
은 이 타입의 포인터입니다.clone
은 커밋되지 않은 변경 사항을 포함한 뷰를 가지고 있습니다.struct nft_pipapo_elem
: PIPAPO 집합에 저장되는 개별 요소입니다.priv
멤버를 통해nft_elem_priv
구조체에 접근합니다.struct nft_elem_priv
: 집합 요소의 private 데이터입니다.kfree
대상이 되는 메모리 영역입니다.priv->dirty
:nft_pipapo_set
의 멤버 변수로, 현재 트랜잭션에서 해당 집합에 변경 사항이 있었는지 나타내는 불리언(boolean) 플래그입니다.nft_pipapo_insert
등에서true
로 설정되고,nft_pipapo_commit
에서false
로 초기화됩니다.set->dead
:nft_set
의 멤버 변수로, 해당 집합이 삭제 대상으로 표시되었는지 나타내는 플래그입니다.NFT_MSG_DELSET
또는NFT_MSG_DESTROYSET
메시지 처리 시 설정됩니다.
트리거
트리고 조건은 다음과 같은 커밋 경로(commit path)의 상호작용을 통해 만족됩니다.
priv->dirty
플래그 설정: 사용자가 트랜잭션 내에서nft_pipapo_insert
함수를 호출하여 집합에 요소를 추가하면, 해당 집합의priv->dirty
플래그가true
로 설정됩니다. 이는 커밋 경로에서 이 집합의 변경 사항을 반영해야 함을 알립니다.static int nft_pipapo_insert(const struct net *net, const struct nft_set *set, const struct nft_set_elem *elem, struct nft_elem_priv **elem_priv) { [...] priv->dirty = true; [...] }
set->dead
플래그 설정: 동일한 트랜잭션 내에서 사용자가 해당 집합을 삭제하도록 요청(NFT_MSG_DELSET
또는NFT_MSG_DESTROYSET
메시지 사용[1])하면,nf_tables_commit
함수 내에서 해당 집합의set->dead
플래그가true
로 설정[2]됩니다.static int nf_tables_commit(struct net *net, struct sk_buff *skb) { [...] case NFT_MSG_DELSET: case NFT_MSG_DESTROYSET: // [1] nft_trans_set(trans)->dead = 1; // [2] list_del_rcu(&nft_trans_set(trans)->list); nf_tables_set_notify(&trans->ctx, nft_trans_set(trans), trans->msg_type, GFP_KERNEL); break; case NFT_MSG_NEWSETELEM: // [3] [...] if (te->set->ops->commit && list_empty(&te->set->pending_update)) { list_add_tail(&te->set->pending_update, &set_update_list); } [...] } nft_set_commit_update(&set_update_list); [...] nf_tables_commit_release(net); return 0; }
->commit()
메소드 건너뛰기:nf_tables_commit
함수는 이후nft_set_commit_update
함수를 호출하여 변경된 집합들의->commit()
메소드를 실행합니다. 그러나nft_set_commit_update
함수는set->dead
플래그가 설정된 집합[4]에 대해서는->commit()
메소드 호출을 건너뜁니다[5]. 이는 삭제될 집합에 대해 불필요한 커밋 작업을 방지하기 위함입니다.static void nft_set_commit_update(struct list_head *set_update_list) { struct nft_set *set, *next; list_for_each_entry_safe(set, next, set_update_list, pending_update) { list_del_init(&set->pending_update); if (!set->ops->commit || set->dead) // [4] continue; set->ops->commit(set); // [5] } }
priv->dirty
플래그 유지:nft_set_pipapo
의->commit()
메소드(nft_pipapo_commit
)는priv->dirty
플래그를false
로 초기화하는 역할을 합니다. 하지만 3단계에서->commit()
메소드가 건너뛰어졌기 때문에,priv->dirty
플래그는true
상태로 유지됩니다.static void nft_pipapo_commit(struct nft_set *set) { [...] if (!priv->dirty) return; [...] priv->dirty = false; [...] }
->destroy()
메소드 호출: 트랜잭션 처리 마지막 단계에서nf_tables_commit_release
함수는 삭제 대상으로 표시된 객체들을 해제하기 위해nft_commit_release
를 호출하고, 이는 최종적으로nft_set_destroy
함수를 통해 해당 집합의->destroy()
메소드(nft_pipapo_destroy
)를 호출합니다.static void nf_tables_commit_release(struct net *net) { [...] schedule_work(&trans_destroy_work); [...] } [...] static void nf_tables_trans_destroy_work(struct work_struct *w) { [...] list_for_each_entry_safe(trans, next, &head, list) { nft_trans_list_del(trans); nft_commit_release(trans); } } [...] static void nft_commit_release(struct nft_trans *trans) { switch (trans->msg_type) { [...] case NFT_MSG_DELSET: case NFT_MSG_DESTROYSET: nft_set_destroy(&trans->ctx, nft_trans_set(trans)); [...] } [...] static void nft_set_destroy(const struct nft_ctx *ctx, struct nft_set *set) { [...] set->ops->destroy(ctx, set); [...] }
Double-Free 발생:
nft_pipapo_destroy
함수가 호출될 때,priv->clone
은 존재하고(dirty
플래그가 설정될 정도의 변경이 있었으므로),priv->dirty
플래그는 여전히true
입니다. 따라서 취약한 버전의nft_pipapo_destroy
함수 내의if (priv->clone)
블록 안에서if (priv->dirty)
조건이 참이 되어,nft_set_pipapo_match_destroy(ctx, set, priv->clone)
가 호출됩니다. 이전에 (제거된 라인에서)nft_set_pipapo_match_destroy(ctx, set, priv->match)
도 호출되었으므로, 결과적으로 이미 커밋된 요소들에 대해nf_tables_set_elem_destroy
가 두 번 호출되어 Double-Free가 발생합니다.
이 취약점은 두 개의 커밋이 상호작용한 결과로 발생했습니다. 첫 번째 커밋(9827a0e6e23b
)은 어보트(abort) 경로에서 클론 내의 커밋되지 않은 요소들을 해제하기 위해 nft_pipapo_destroy
내에 if (priv->dirty)
조건과 함께 nft_set_pipapo_match_destroy(ctx, set, m)
호출을 추가했습니다. 당시에는 이 코드가 주로 어보트 경로에서만 도달 가능했고, 이 경로에서는 커밋된 요소가 없어 중복 해제 문제가 발생하지 않았습니다. 그러나 약 1년 후 두 번째 커밋(212ed75dc5fb
)이 set->dead
플래그와 관련 로직을 도입하면서, 커밋 경로에서도 priv->dirty
플래그가 초기화되지 않은 채 nft_pipapo_destroy
에 도달할 수 있는 새로운 경로가 생겼습니다. 이 새로운 경로에서는 이미 커밋된 요소들이 priv->match
와 priv->clone
양쪽에 존재하므로, 첫 번째 커밋에서 추가된 코드가 의도치 않게 Double-Free를 유발하게 된 것입니다.
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.