infiltr8 Lab
Draft · 검수 전 · 심층판
Home/ CVE/ 커널·OS/ Linux netfilter nf_tables verdict 더블프리
CVE · 커널·OS · 권한상승 · CVE-2024-1086
심층 분석 연구 분석

버렸다는 신호를 두 번 읽자, 메모리도 두 번 사라졌다

리눅스 커널 넷필터(netfilter)의 nf_tables 컴포넌트에서, 패킷을 '버리라(drop)'는 판정값 하나가 동시에 '받아들이라(accept)'는 판정으로도 읽히는 CVSS 7.8의 더블프리(double-free) 취약점입니다(CVE-2024-1086). 2014년에 들어간 코드 한 줄이 10년 뒤인 2024년에야 되돌려졌고, 2024년 5월 CISA 긴급취약점 목록(KEV)에 등재된 데 이어 2025년 10월에는 실제 랜섬웨어 공격에 쓰인 사실이 확인됐습니다. 이 글은 원 발견자(Notselwyn)의 분석을 재구성하고, 실제 리눅스 커널 오픈소스를 고정 커밋 기준으로 직접 열어 근본원인·호출 흐름·패치를 분석합니다.

이 글의 코드 발췌(nf_tables_api.c의 nft_verdict_init()·nft_data_init(), core.c의 nf_hook_slow(), include/linux/netfilter.h의 NF_DROP_GETERR()·NF_HOOK())는 torvalds/linux 오픈소스를 패치 커밋(f342de4e2f33e0e39165d8639387aa6c19dff660)의 부모 커밋(b462579b2b86a8f5230543cadd3a4836be27baf7, 패치 전 상태) 및 패치 커밋 자체(패치 후 상태) 기준으로 raw.githubusercontent.com에서 직접 받아 파일·라인 단위로 byte-level 대조했습니다. 패치 diff는 GitHub의 .patch 원문을 그대로 받아 확인했습니다. 검증값이 '더블프리를 유발하는 구체적 원인'(NF_DROP_GETERR가 verdict 상위 16비트를 부호 있는 정수로 해석해 우연히 1(NF_ACCEPT와 동일한 정수)이 될 수 있고, 그 값을 NF_HOOK() 매크로가 '훅이 패킷을 통과시켰다'는 신호로 오판한다는 연쇄)은 이 세 파일의 실제 코드를 직접 대조해 저희가 계산·재구성한 것이며, 이 인과 서술 자체는 1차 소스(코드)로 확인됩니다. 다만 이 더블프리를 임의 물리 메모리 읽기/쓰기 프리미티브로 확장하는 후반부 익스플로잇 기법(더러운 페이지테이블 기법 등)은 원 발견자의 공개 기술 블로그(pwning.tech)와 NVD·벤더 권고문·CISA 보도자료를 교차 확인한 2차 분석이며, 저희가 익스플로잇 코드 자체를 직접 실행·재현하지는 않았습니다 — 그 한계는 §패치 완전성 평가·본문 배지에 명시했습니다.

infiltr8 team 직접 분석 #nf_tables#netfilter#double-free#LPE
무엇리눅스 커널 넷필터 nf_tables의 nft_verdict_init()이 사용자가 넘긴 판정(verdict) 값의 상위 16비트를 검증 없이 그대로 받아들여, 이미 해제한 네트워크 버퍼(skb)를 한 번 더 해제하게 만드는 더블프리 취약점입니다.
원리판정값의 하위 1바이트는 '버려라/받아들여라'를, 상위 16비트는 원래 '에러코드' 용도로 쓰이는데, 이 상위 비트를 검증하지 않고 그대로 통과시키면 NF_DROP_GETERR()가 그 값을 우연히 NF_ACCEPT와 같은 정수로 해석할 수 있습니다.
핵심패치는 2014년에 상위 비트 검증을 느슨하게 만든 커밋을 통째로 되돌렸을 뿐입니다 — 코드 한 줄의 실수가 10년간 남아 있었고, 2024년 5월 CISA KEV 등재·2025년 10월 랜섬웨어 실사용까지 확인됐습니다. CVSS 7.8(High), CWE-416.

01개요

넷필터(netfilter)는 리눅스 커널이 패킷을 처리하는 각 지점(훅)마다 등록된 함수들을 호출해 '이 패킷을 어떻게 할지' 판정(verdict)을 받는 프레임워크입니다. nf_tables는 이 넷필터 위에서 동작하는 최신 방화벽/패킷 필터링 서브시스템으로, nft 명령이나 netlink API로 사용자가 직접 규칙을 등록합니다.

CVE-2024-1086은 이 규칙에 포함되는 '즉시 판정(immediate verdict)' 값을 커널이 받아들이는 코드(nft_verdict_init())가, 판정값의 일부(상위 16비트)를 전혀 검증하지 않는 데서 시작합니다. 이 값이 넷필터 훅 처리 루프(nf_hook_slow())까지 흘러가면, '패킷을 버렸다(drop)'는 판정과 '패킷을 받아들였다(accept)'는 판정이 같은 정수로 겹쳐 읽히는 순간이 생기고, 그 결과 이미 해제된 네트워크 버퍼가 한 번 더 해제됩니다(use-after-free/double-free, CWE-416).

이 취약점은 로컬 권한상승(local privilege escalation)에 쓰입니다 — 원격에서 직접 트리거되지는 않지만, 비특권 사용자 네임스페이스를 통해 CAP_NET_ADMIN을 얻을 수 있는 환경(다수의 리눅스 배포판 기본 설정, 컨테이너 런타임 포함)에서는 일반 사용자가 이 경로로 root 권한까지 올라갈 수 있습니다. 2024년 5월 CISA 긴급취약점목록(KEV)에 등재됐고, 2025년 10월에는 실제 랜섬웨어 공격 체인에서 이 취약점이 쓰였다는 사실이 확인됐습니다.

CVSS: 7.8 (CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H, NVD) · CWE-416(Use After Free)

이 근본원인은 2014년 2월 커밋 e0abdadcc6e1("netfilter: nf_tables: accept QUEUE/DROP verdict parameters")에서 들어갔고, 2024년 1월 20일 커밋 f342de4e2f33e0e39165d8639387aa6c19dff660으로 그 커밋 전체가 되돌려지며 고쳐졌습니다(발견·신고자: Notselwyn, pwning.tech). NVD 발행일은 2024-01-31입니다. 별개 CVE로 쪼개진 변형(variant)은 확인되지 않았습니다 — 이 CVE 하나가 이 근본원인 전체를 대표합니다.

02영향받는 버전

대상취약 버전안전 버전
Linux 커널(mainline)v3.15 ~ v6.8-rc1 (커밋 e0abdadcc6e1이 들어간 이후 전체)커밋 f342de4e2f33e0e39165d8639387aa6c19dff660 포함 버전
stable/longterm 백포트5.15.148 이하 / 6.1.75 이하 / 6.6.14 이하 / 6.7.2 이하5.15.149 이상 / 6.1.76 이상 / 6.6.15 이상 / 6.7.3 이상
Ubuntu(대표 예시, 배포판마다 백포트 버전 상이)22.04 LTS: 5.15.0-101.111 미만 등각 배포판 보안공지의 패치 버전 이상(벤더별 개별 확인 필요) 분석 — 배포판별 백포트 시점은 벤더 공지 기준, 전수 확인은 하지 않음

03배경 — 판정값 하나에 두 가지 정보를 욱여넣은 인코딩

넷필터 훅 함수는 패킷마다 unsigned int 하나로 판정을 반환합니다. 이 정수는 include/uapi/linux/netfilter.h에서 두 부분으로 겹쳐 인코딩됩니다 — 하위 1바이트(NF_VERDICT_MASK = 0x000000ff)는 NF_ACCEPT(1)·NF_DROP(0)·NF_QUEUE(3) 같은 '무엇을 할지'를, 상위 16비트(NF_VERDICT_QMASK = 0xffff0000)는 NF_QUEUE일 땐 큐 번호를, NF_DROP일 땐 (선택적으로) 에러코드를 담습니다. 헤더 주석 자체가 이걸 "nice하지 않지만, 추가 함수 인자를 두는 것보다는 낫다"고 표현할 만큼 의도된 절충입니다.

nf_tables에서는 이 판정값이 규칙의 '즉시(immediate)' 문(예: accept, drop, jump target)을 통해 사용자 공간(userspace)에서 netlink로 직접 전달됩니다. nft_immediate_init()이 이 값을 받아 nft_data_init()을 거쳐 nft_verdict_init()으로 넘기고, 이 함수가 그 값을 struct nft_dataverdict.code 필드에 채워 넣습니다 — 이 필드가 나중에 훅이 실제로 반환하는 값이 됩니다.

여기서 핵심은 이 값이 커널이 계산한 값이 아니라 사용자가 netlink로 직접 써 보낸 32비트 정수라는 점입니다. 이 값을 커널의 다른 계층(nf_hook_slow(), NF_HOOK())이 얼마나 엄격하게 검증하고 받아들이는지가 다음 절의 단서입니다.

04취약점 발생 원인

이 취약점은 서로 다른 두 계층이 같은 32비트 정수를 서로 다른 전제로 읽으면서 성립합니다. 상위 16비트를 검증하지 않고 저장한 것(W1)과, 그 상위 비트를 무조건 유효한 값으로 믿고 계산에 쓴 것(W2)이 만나야 완성됩니다.

1"하위 1바이트만 확인하고, 상위 16비트는 그대로 저장했다"

핵심

nft_verdict_init()의 스위치문이 data->verdict.code & NF_VERDICT_MASK(하위 1바이트)만 NF_ACCEPT/NF_DROP/NF_QUEUE인지 확인하고, 원본 32비트 값 전체를 그대로 verdict.code에 저장합니다.

nft_verdict_init()은 netlink 속성에서 data->verdict.code = ntohl(nla_get_be32(...))로 32비트 값을 그대로 읽어들인 뒤, 이 값이 유효한지 스위치문으로 검사합니다. 문제는 검사 대상이 원본 32비트 전체가 아니라 data->verdict.code & NF_VERDICT_MASK — 즉 하위 1바이트만이라는 것입니다.

data->verdict.code = ntohl(nla_get_be32(tb[NFTA_VERDICT_CODE])); switch (data->verdict.code) { default: switch (data->verdict.code & NF_VERDICT_MASK) { case NF_ACCEPT: case NF_DROP: case NF_QUEUE: break; default: return -EINVAL; } fallthrough; case NFT_CONTINUE:

Linux 커널 오픈소스 실제 소스 — net/netfilter/nf_tables_api.c · nft_verdict_init() (패치 전, 커밋 b462579b2b86a8f5230543cadd3a4836be27baf7 = 패치 커밋 f342de4e2f33e0e39165d8639387aa6c19dff660의 부모). 10996행의 마스킹이 하위 1바이트만 검증하는 지점입니다.

이 마스킹된 값이 NF_ACCEPT·NF_DROP·NF_QUEUE 중 하나이기만 하면, 검증되지 않은 상위 16비트를 포함한 원본 32비트 전체가 그대로 verdict.code에 저장된 채 통과합니다. 즉 하위 1바이트는 화이트리스트로 걸러지지만, 상위 16비트는 사실상 사용자가 원하는 어떤 값이든 넣을 수 있습니다.

왜 이렇게 됐나이 코드는 '판정 코드가 커널이 아는 값인가'라는 질문에만 답하도록 설계됐습니다. 판정값이 상위 비트에 부가 정보(에러코드·큐 번호)를 함께 실어 나른다는 사실, 그리고 그 부가 정보를 사용자가 임의로 채울 수 있다는 사실은 이 함수가 검증할 질문으로 다뤄지지 않았습니다.

2"상위 16비트는, 유효한 에러코드일 거라고 믿었다"

핵심

nf_hook_slow()NF_DROP 판정을 처리할 때, 이미 버퍼를 해제한 뒤 상위 16비트를 NF_DROP_GETERR()로 부호 있는 정수로 변환해 그대로 반환하는데, 이 반환값을 상위 호출자(NF_HOOK())가 '1이면 훅이 패킷을 통과시켰다'는 별개의 계약으로 재해석합니다.

nf_hook_slow()는 하위 1바이트로 분기한 뒤 NF_DROP이면 kfree_skb_reason()으로 패킷 버퍼(skb)를 즉시 해제합니다. 그다음 NF_DROP_GETERR(verdict)를 호출해 상위 16비트를 -(verdict >> 16)로 계산한 값을 반환값으로 씁니다 — 이 값은 원래 'drop한 이유(에러코드)'를 호출자에게 알려주기 위한 것입니다.

switch (verdict & NF_VERDICT_MASK) { case NF_ACCEPT: break; case NF_DROP: kfree_skb_reason(skb, SKB_DROP_REASON_NETFILTER_DROP); ret = NF_DROP_GETERR(verdict); if (ret == 0) ret = -EPERM; return ret;

Linux 커널 오픈소스 실제 소스 — net/netfilter/core.c · nf_hook_slow() (커밋 b462579b2b86a8f5230543cadd3a4836be27baf7, 패치 대상이 아니라 이 취약점의 두 번째 축 — 이 함수 자체는 이 CVE로 수정되지 않았습니다). 631~633행이 '해제 후 상위비트를 에러코드로 신뢰'하는 지점입니다.

그런데 W1 때문에 상위 16비트가 검증되지 않은 사용자 값이므로, 이 계산 결과가 우연히 1 — 즉 NF_ACCEPT와 같은 정수 — 이 될 수 있습니다(예: 상위 16비트를 0xffff로 채우면, 부호 있는 정수 연산에서 -(-1) = 1이 됩니다). nf_hook_slow()를 호출하는 NF_HOOK() 매크로는 반환값이 정확히 1일 때 '이 훅이 패킷을 받아들였으니 다음 처리 함수(okfn)를 마저 실행하라'는 완전히 다른 계약으로 이 숫자를 읽습니다. 그 결과 이미 kfree_skb_reason()으로 해제된 skb가 okfn() 경로에서 정상 패킷인 것처럼 다시 처리됩니다 — 사용후해제이자, 그 경로가 다시 skb를 해제하면 더블프리입니다.

왜 이렇게 됐나nf_hook_slow()·NF_DROP_GETERR()·NF_HOOK() 세 조각은 각자 합리적인 계약 위에서 짜였습니다. 문제는 '상위 16비트=에러코드'라는 계약과 '반환값 1=훅이 통과시킴'이라는 계약이 같은 정수 공간을 공유한다는 점입니다. 이 두 계약이 우연히 충돌하는 값(에러코드로 해석하면 1이 되는 상위 비트)이 존재한다는 것 자체가 이 인코딩 방식의 구조적 위험이고, W1이 그 값을 사용자가 고를 수 있게 열어준 것입니다.

분석 결론

무게 중심은 W1(nft_verdict_init()이 상위 16비트를 검증하지 않은 것)입니다.

W2(nf_hook_slow()가 상위 비트를 에러코드로 신뢰하고, NF_HOOK()이 반환값 1을 '통과'로 해석하는 것)는 그 자체로는 결함이 아닙니다 — 판정값 인코딩 규약이 원래 그렇게 설계됐고, 이 함수들은 2024년 패치에서도 바뀌지 않았습니다. W1이 사용자에게 이 상위 비트에 대한 통제권을 열어주지만 않았다면 두 계약은 절대 충돌하지 않았습니다.

한 문장으로 묶으면 이렇습니다. 한 정수 안에 두 가지 다른 의미(무엇을 할지 / 부가 정보)를 겹쳐 담는 인코딩에서, 그중 절반(상위 비트)의 입력 검증을 빠뜨렸습니다. 그 틈에서 '버림'을 뜻하는 값과 '받아들임'을 뜻하는 값이 같은 숫자로 겹쳤습니다.

05공격 과정

5.1 공격 과정 요약

공격자 · netlink NFT_MSG_NEWRULE 비특권 user namespace로 CAP_NET_ADMIN 확보 후 규칙 등록 ① 원시 32비트 판정값이 netlink로 그대로 전달 핵심 지점 nft_verdict_init() · 하위 1바이트만 검증 상위 16비트는 검증 없이 verdict.code에 그대로 저장 (W1) ② 검증 안 된 상위 비트를 포함한 채 verdict.code 확정 nf_hook_slow() · NF_DROP 분기 kfree_skb_reason()으로 skb 해제 — 1차 해제 ③ 해제 직후 상위 비트를 에러코드로 계산 핵심 지점 NF_DROP_GETERR(verdict) 계산 검증 안 된 상위 비트를 에러코드로 해석 → 우연히 1 반환 (W2) ④ 계산 결과가 NF_ACCEPT(1)와 동일한 정수로 충돌 NF_HOOK() · ret==1을 '통과'로 오판 okfn()이 이미 해제된 skb를 다시 처리 ⑤ 이미 해제된 skb가 정상 경로로 재사용/재해제 use-after-free / double-free → 힙 그루밍 → 권한상승 Local Privilege Escalation (root)
공격 흐름도 (직접 작성)

5.2 상세 공격 과정

예를 들어 공격자가 판정값의 하위 1바이트를 NF_DROP(0)으로, 상위 16비트를 0xffff로 채운 32비트 값(플레이스홀더 — 실제로는 0xffff0000 형태)을 규칙에 넣는다고 가정합니다. nft_verdict_init()의 검증(& NF_VERDICT_MASK)은 하위 1바이트만 보므로 이 값을 그대로 통과시킵니다.

이 규칙에 매치되는 패킷이 지나가면 nf_hook_slow()NF_DROP 분기로 들어가 kfree_skb_reason()을 호출합니다 — 이 시점에 skb는 이미 해제됩니다. 그다음 줄의 NF_DROP_GETERR(verdict)-(verdict >> 16)을 계산하는데, 상위 16비트가 0xffff(부호 있는 정수로는 -1)이므로 -(-1) = 1이 되어 nf_hook_slow()1을 반환합니다.

이 '1'이라는 숫자는 NF_DROP_GETERR() 입장에서는 그냥 계산된 에러코드지만, 이 함수를 호출한 NF_HOOK() 매크로 입장에서는 완전히 다른 뜻입니다 — NF_HOOK()의 코드는 if (ret == 1) ret = okfn(net, sk, skb);로, 반환값이 정확히 1이면 '이 훅이 패킷을 받아들였으니(NF_ACCEPT) 다음 정상 처리 함수를 실행하라'고 해석합니다. 그 결과 이미 해제된 skb가 okfn() 경로로 다시 흘러 들어가 사용후해제 상태에서 계속 처리되고, 그 경로가 skb를 다시 해제하는 지점에서 더블프리가 성립합니다.

여기서부터 안정적인 root 권한 획득까지 이어지는 구체적 힙 그루밍·주소 유출·페이지테이블 조작 절차는 원 발견자의 공개 기술 블로그가 상세히 다루고 있으나, 이 글은 그 절차를 재현하거나 요약하지 않습니다(무해화). 실제 동작하는 익스플로잇 코드나 트리거 값 조합은 이 글에 싣지 않습니다.

5.3 미흡점 ↔ 공격 흐름 매핑

미흡점(§4)발화 지점(§5.2)여는 프리미티브→ 다음으로 잇는 것
W1 — 상위 16비트 미검증nft_verdict_init() 저장 시점(§8.2 2~3단계)검증 안 된 원시 32비트 값이 verdict.code로 확정nf_hook_slow()가 이 값을 그대로 신뢰
W2 — 상위 비트=에러코드 계약과 ret==1=통과 계약의 충돌nf_hook_slow()의 NF_DROP_GETERR() 계산(§8.2 4~5단계)해제 후 계산값이 NF_ACCEPT(1)와 정수적으로 충돌NF_HOOK()이 '통과'로 오판, okfn() 재진입
(귀결) 사용후해제/더블프리okfn() 경로의 skb 재사용(§8.2 6단계)이미 해제된 메모리가 정상 객체로 재해석·재해제힙 그루밍을 거친 임의 R/W 프리미티브 → 로컬 권한상승

06취약점 패치

net/netfilter/nf_tables_api.c·nft_verdict_init()·커밋 f342de4e2f33e0e39165d8639387aa6c19dff660 · 부모 b462579b2b86a8f5230543cadd3a4836be27baf7 (Florian Westphal, 2024-01-20 작성 · stable 백포트 5.15.149/6.1.76/6.6.15/6.7.3)
switch (data->verdict.code) { default: switch (data->verdict.code & NF_VERDICT_MASK) { case NF_ACCEPT: case NF_DROP: case NF_QUEUE: break; default: return -EINVAL; } fallthrough; case NF_ACCEPT: case NF_DROP: case NF_QUEUE: break; case NFT_CONTINUE: / NFT_BREAK: / NFT_RETURN: / NFT_JUMP: / NFT_GOTO: (변경 없음) default: return -EINVAL; }

§7 W1에서 지목한 자리 — 하위 1바이트만 검증하고 상위 16비트를 포함한 원본 값을 그대로 저장하는 로직 — 을 직접 겨냥한 수정입니다. 커밋 메시지는 이 취약한 로직을 만든 2014년 커밋(e0abdadcc6e1)을 명시적으로 지목하며, core.c:nf_hook_slow()가 'NF_DROP 판정의 상위 16비트에 유효한 에러코드가 들어 있다고 가정한다'고 정확히 짚습니다.

어떻게패치는 새 검증 로직을 추가한 게 아니라, 2014년 이전의 엄격한 switch (data->verdict.code) 구조를 그대로 복원했습니다 — NF_ACCEPT/NF_DROP/NF_QUEUE는 하위 비트 마스킹 없이 정확히 그 값(상위 16비트가 0인 경우)일 때만 통과하고, 스위치문 끝에 default: return -EINVAL;을 추가해 그 외의 모든 값(상위 비트가 채워진 값 포함)을 거부합니다. 즉 '상위 16비트가 0이 아닌 판정값은 애초에 nft_tables에 들어올 수 없다'로 계약을 되돌린 것입니다. 커밋 메시지는 "NF_QUEUE는 nftables가 쓰지 않는다(queue 규칙은 별도의 nft_queue 표현식을 쓴다)"고 덧붙이며, 훗날 정말 에러코드를 사용자에게 열어줘야 한다면 NF_DROP_GETERR()err <= 0을 확인하는 별도 검증이 필요하다고 명시합니다.

비고패치가 하는 일은 새 방어선을 세우는 게 아니라 '2014년에 왜 이 검증을 느슨하게 했는지 모르겠다(Its not clear to me why this commit was made)'는 커밋 메시지 그대로, 원래 있던 엄격한 검증으로 되돌아가는 것뿐입니다 — 10년 전 최적화 혹은 편의를 위한 변경이 검증되지 않은 채 남아 있었다는 뜻입니다.

07대응방안

근본 해결은 커널 업그레이드입니다. mainline 기준 커밋 f342de4e2f33e0e39165d8639387aa6c19dff660을 포함한 버전(사실상 v6.8 이상), stable/longterm 라인은 §영향받는 버전 표의 5.15.149/6.1.76/6.6.15/6.7.3 이상, 그 외 배포판(Ubuntu·Debian·RHEL 등)은 각 벤더 보안공지가 지목하는 패치 버전 이상으로 올립니다.

08왜 이게 반복되는가

이 취약점이 흥미로운 이유는 코드가 복잡해서가 아니라, 정확히 반대이기 때문입니다. 검증 로직 자체는 몇 줄짜리 스위치문이고, 패치도 '10년 전 상태로 되돌리기'가 전부입니다. 위험한 건 한 정수 안에 서로 다른 두 가지 의미(무엇을 할지 + 부가 정보)를 겹쳐 담는 인코딩에서, 그 절반만 검증하면 나머지 절반이 다른 계약과 충돌할 수 있다는 사실이었습니다. 저희가 Jenkins CLI 임의 파일 읽기(CVE-2024-23897)에서 본 것과 결이 비슷합니다 — 그때는 '로컬 신뢰 전제로 만들어진 라이브러리 기본값'이 네트워크 노출 컨텍스트로 옮겨지며 다시 검토되지 않았고, 여기서는 '두 계층이 같은 정수 공간을 공유한다'는 사실이 입력 검증 설계에 반영되지 않았습니다. V8 Maglev 힙 손상(CVE-2024-0517)에서도 컴파일러가 자신이 통제하지 못하는 다른 서브시스템(GC)의 규칙을 몰랐다는 같은 계열의 이야기를 다뤘습니다 — 세 사건 모두 개별 코드는 각자 정상인데, 그 코드들이 공유하는 경계에서 전제가 어긋났습니다.

패치완전성 관점에서 이 사건은 비교적 깔끔한 사례입니다. 패치는 새 검증을 추가한 게 아니라, 검증을 느슨하게 만든 2014년 커밋 전체를 되돌렸습니다 — 원인 지점 하나를 정확히 겨냥한 수정이고, 커밋 메시지도 '왜 이 커밋이 만들어졌는지 모르겠다'며 원래 검증이 불필요하게 약화됐었다는 점을 스스로 인정합니다. 다만 이 CVE가 남기는 진짜 질문은 코드가 아니라 시간입니다 — 검증을 느슨하게 만든 변경이 10년간 아무도 문제 삼지 않았고, 2024년 1월에 조용히 패치된 뒤 2024년 5월 CISA KEV, 2025년 10월 랜섬웨어 실사용까지 갔습니다. '오래된 코드라서 안전하다'는 가정 자체가 이 취약점의 수명을 늘린 셈입니다.

이 한 틈은 고쳐졌지만, '한 값에 두 가지 의미를 겹쳐 담을 때 그중 절반만 검증하지 않았는가'라는 질문은 다른 서브시스템, 다른 프로토콜에서 또 나옵니다 — 그래서 저희는 이 커밋 하나가 아니라 이 질문을 남깁니다.

09결론

리눅스 커널 nf_tables의 nft_verdict_init()이 판정값의 상위 16비트를 검증하지 않은 채 저장한 것이 근본원인입니다. 이 검증되지 않은 값이 nf_hook_slow()의 에러코드 계산과 NF_HOOK()의 '통과' 판정 계약을 우연히 충돌시켜, 이미 해제된 네트워크 버퍼를 다시 처리하는 더블프리가 성립합니다. CVSS 7.8(High), CWE-416이며 2014년에 들어간 코드가 2024년 1월에야 되돌려졌습니다. 패치는 원인 지점을 정확히 되돌린 것으로 확인되지만, 10년간 방치됐다가 CISA KEV 등재·랜섬웨어 실사용까지 이어진 시간 자체가 이 사건의 핵심 교훈입니다.

이 CVE의 표준판(요약본)은 아직 별도로 작성되지 않았습니다.

참고 자료

이 글의 코드 발췌(nf_tables_api.c의 nft_verdict_init()·nft_data_init(), core.c의 nf_hook_slow(), include/linux/netfilter.h의 NF_DROP_GETERR()·NF_HOOK())는 torvalds/linux 오픈소스를 패치 커밋(f342de4e2f33e0e39165d8639387aa6c19dff660)의 부모 커밋(b462579b2b86a8f5230543cadd3a4836be27baf7, 패치 전 상태) 및 패치 커밋 자체(패치 후 상태) 기준으로 raw.githubusercontent.com에서 직접 받아 파일·라인 단위로 byte-level 대조했습니다. 패치 diff는 GitHub의 .patch 원문을 그대로 받아 확인했습니다. 검증값이 '더블프리를 유발하는 구체적 원인'(NF_DROP_GETERR가 verdict 상위 16비트를 부호 있는 정수로 해석해 우연히 1(NF_ACCEPT와 동일한 정수)이 될 수 있고, 그 값을 NF_HOOK() 매크로가 '훅이 패킷을 통과시켰다'는 신호로 오판한다는 연쇄)은 이 세 파일의 실제 코드를 직접 대조해 저희가 계산·재구성한 것이며, 이 인과 서술 자체는 1차 소스(코드)로 확인됩니다. 다만 이 더블프리를 임의 물리 메모리 읽기/쓰기 프리미티브로 확장하는 후반부 익스플로잇 기법(더러운 페이지테이블 기법 등)은 원 발견자의 공개 기술 블로그(pwning.tech)와 NVD·벤더 권고문·CISA 보도자료를 교차 확인한 2차 분석이며, 저희가 익스플로잇 코드 자체를 직접 실행·재현하지는 않았습니다 — 그 한계는 §패치 완전성 평가·본문 배지에 명시했습니다.