Skip to content

Latest commit

 

History

History
168 lines (134 loc) · 8.76 KB

File metadata and controls

168 lines (134 loc) · 8.76 KB

Netfilter nf_tables 익명 집합 UAF (CVE-2023-32233)

메타데이터

항목 내용
CVE ID CVE-2023-32233
영향 버전 Linux 6.3.1 이하 (익명 집합 기능이 있는 상당 기간의 커널)
CVSS v3.1 7.8 (High) — AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
공개일 2023-05-08
비고 배치(batch) 트랜잭션 처리 로직의 상태 관리 오류

개요

nftables는 여러 개의 변경 작업(규칙 추가/삭제 등)을 하나의 배치(batch) 로 묶어 한 번에 커밋할 수 있다. CVE-2023-32233은 이 배치 안에서 "규칙을 지우면서 그 규칙에 딸린 익명 집합(anonymous set)도 암묵적으로 지워지는 상황"과 "같은 배치 안에서 그 집합을 다시 건드리는 작업"이 함께 오면, 커널이 이미 해제 예정인 집합을 여전히 살아있는 것처럼 다뤄서 use-after-free 가 발생하는 취약점이다. 권한 없는 로컬 사용자가 커널 메모리를 임의로 읽고 쓸 수 있는 상태까지 이어질 수 있어 root 권한 획득이 가능하다.

사전 지식

익명 집합(anonymous set)

nftables의 "집합(set)"은 IP 주소 목록처럼 여러 값을 묶어 규칙에서 참조할 수 있게 해주는 자료구조다. 익명 집합은 이름을 붙이지 않고 규칙 안에 인라인으로 즉석에서 만들어지는 집합이다 — 그 집합을 참조하는 규칙(정확히는 lookup 표현식)이 사라지면, 익명 집합도 같이 사라지는 게 정상이다. 즉 익명 집합의 수명은 그것을 참조하는 규칙의 수명에 종속된다.

nftables 트랜잭션의 단계(phase)

nftables는 배치 하나를 커밋할 때 여러 단계를 거친다. 이 문서에서 중요한 두 단계는:

  • PREPARE: "이 변경들을 실제로 적용해도 되는지" 미리 준비하는 단계. 아직 최종 확정은 아니다.
  • COMMIT / 실제 반영: PREPARE 를 통과한 변경들이 실제 커널 상태에 반영되는 단계.

집합이나 규칙에는 use(참조 카운트) 라는 값이 있어서, 몇 곳에서 이 집합을 쓰고 있는지를 추적한다. 그리고 "활성 상태인지" 를 나타내는 별도의 세대(generation) 비트마스크도 있다 — 이 두 가지가 서로 다른 시점에 갱신되면 문제가 생긴다는 것이 이 취약점의 핵심이다.

취약점 분석

근본 원인: PREPARE 단계에서 "비활성화 표시"가 빠져 있었음

패치 전 nf_tables_deactivate_set() 은 PREPARE 단계에서 참조 카운트만 줄일 뿐, 익명 집합을 "다음 세대에서는 비활성 상태" 로 표시하지는 않았다.

// net/netfilter/nf_tables_api.c — nf_tables_deactivate_set() (패치 전)
switch (phase) {
case NFT_TRANS_PREPARE:
	set->use--;              // 참조 카운트만 감소, "비활성 표시"는 없음
	return;
case NFT_TRANS_ABORT:
	...
}

즉 같은 배치 안에서 "규칙을 지워서 익명 집합의 use 가 0이 됨" 과 "그 집합의 원소를 지우는 또 다른 작업" 이 순서대로 들어오면, 두 번째 작업이 실행되는 시점에도 커널은 "이 집합이 아직 유효하다" 고 착각한다 — use 카운트는 줄었지만 "비활성" 이라는 상태 자체가 별도로 기록되지 않았기 때문이다.

배치 안의 작업 순서 (패치 전)

  [1] NFT_MSG_DELRULE   ─▶ 규칙 삭제 ─▶ 딸려 있던 익명 집합도 암묵적으로 삭제 대상이 됨
                              └─ nf_tables_deactivate_set(PREPARE)
                                   set->use--  (비활성 "표시"는 안 됨)

  [2] NFT_MSG_DELSETELEM ─▶ "그 집합의 원소를 지워라"
                              └─ 커널: 이 집합, 아직 유효한 걸로 보임 → 그대로 진행
                                   (하지만 실제 메모리 해제는 이미 예약된 상태)

  [실제 커밋 단계] nf_tables_trans_destroy_work()
                              └─ [1]에서 예약된 집합 메모리 해제 실행
                                   └─ [2]가 참조했던 포인터는 이미 해제된 메모리를 가리킴
                                        → Use-After-Free

왜 배치라는 특징이 필요한가

이 문제는 "규칙 하나 지우고 끝" 이었다면 일어나지 않는다. 하나의 배치 안에 여러 작업을 순서대로 넣을 수 있다는 nftables의 기능 자체가 공격 표면이 된다 — 공격자는 "지운 것을 다시 건드리는" 순서를 의도적으로 만들어 이 틈을 노릴 수 있다.

공격이 성립하는 경로

  1. 로컬 사용자가 (권한 없는 network namespace 안에서도 가능) lookup 표현식으로 익명 집합을 참조하는 규칙을 하나 만든다.
  2. 하나의 batch netlink 메시지 안에 다음을 순서대로 담는다: 그 규칙을 지우는 NFT_MSG_DELRULE, 그리고 곧바로 이어서 그 규칙이 참조하던 익명 집합의 원소를 지우는 NFT_MSG_DELSETELEM.
  3. 커널이 이 배치를 처리하면서 nf_tables_deactivate_set() 이 PREPARE 단계에서 집합을 "비활성"으로 표시하지 않기 때문에, 두 번째 작업이 "아직 유효한 집합"이라고 믿고 그대로 진행된다.
  4. 실제 커밋 단계에서 집합의 메모리가 해제되면, 이미 만들어진 댕글링 참조를 통해 해제된 메모리에 접근(읽기/쓰기)할 수 있는 상태가 된다.
  5. 커널 힙 할당자를 원하는 대로 유도(heap grooming)해 그 해제된 메모리 자리에 공격자가 제어하는 다른 객체를 채워 넣으면, 임의 커널 메모리 읽기/쓰기 프리미티브로 발전시킬 수 있고, 이를 이용해 프로세스 자격 증명(credential) 구조체를 덮어써 root 권한을 얻을 수 있다.

수정 방법

수정 커밋(c1592a89942e9678f7d9c8030efa777c0d57edab, "netfilter: nf_tables: deactivate anonymous set from preparation phase")은 PREPARE 단계에서 익명 집합을 실제로 비활성 상태로 표시하도록 고쳤고, 그 반대 동작(활성화)을 위한 새 함수도 추가했다.

--- a/net/netfilter/nf_tables_api.c
+++ b/net/netfilter/nf_tables_api.c
@@
+void nf_tables_activate_set(const struct nft_ctx *ctx, struct nft_set *set)
+{
+	if (nft_set_is_anonymous(set))
+		nft_clear(ctx->net, set);
+
+	set->use++;
+}
+
 void nf_tables_deactivate_set(const struct nft_ctx *ctx, struct nft_set *set,
 			      struct nft_set_binding *binding,
 			      enum nft_trans_phase phase)
 {
 	switch (phase) {
 	case NFT_TRANS_PREPARE:
+		if (nft_set_is_anonymous(set))
+			nft_deactivate_next(ctx->net, set);
+
 		set->use--;
 		return;

그리고 집합을 사용하는 각 표현식(nft_dynset, nft_lookup, nft_objref 등)의 활성화 경로도 단순히 set->use++ 만 하던 것에서 새로 만든 nf_tables_activate_set() 을 호출하도록 통일했다. 이제 배치 안에서 집합이 삭제 대상이 되는 순간 "비활성" 세대 표시가 즉시 반영되므로, 같은 배치의 이후 작업이 그 집합을 참조하려 하면 "이미 비활성"임을 정확히 인식한다.

배치 안의 작업 순서 (패치 후)

  [1] NFT_MSG_DELRULE ─▶ 규칙 삭제 ─▶ 익명 집합도 삭제 대상
                              └─ nf_tables_deactivate_set(PREPARE)
                                   nft_deactivate_next() 로 "다음 세대엔 비활성" 즉시 표시
                                   set->use--

  [2] NFT_MSG_DELSETELEM ─▶ "그 집합의 원소를 지워라"
                              └─ 커널: 이 집합은 이미 비활성 상태로 표시됨 → 요청 거부/무효화
                                   → 해제 예정 메모리에 접근하는 경로 자체가 차단됨

참고 자료

미해결/불확실 지점

  • "해제된 집합 메모리를 힙 그루밍으로 다른 객체와 겹치게 만드는" 구체적인 익스플로잇 기법 (어떤 커널 객체로 재점유하는지 등)은 이번에는 다루지 않았다. Google의 security-research 저장소에 CVE-2023-32233 관련 exploit 문서가 있는 것으로 검색되었으나 이번에는 직접 열어 대조하지 못했다.
  • nft_tables_trans_destroy_work() 가 정확히 어느 시점에 실행되는지(워크큐 스케줄링 디테일)는 커밋 메시지 수준까지만 확인했다.