infiltr8 Lab
Draft · 검수 전 · 심층판
Home/ CVE/ 네이티브·바이너리/ PwnKit — polkit pkexec 인자 파서 권한상승
CVE · 네이티브·바이너리 · 권한상승 · CVE-2021-4034
심층 분석 연구 분석

인자를 세지 않자, 그 다음 자리엔 이미 환경변수가 앉아 있었다

CVE-2021-4034(PwnKit)는 리눅스 polkit의 SUID-root 프로그램 pkexec가 인자 개수(argc)가 0인 호출을 검증하지 않아, 존재하지 않는 인자 자리를 읽고 쓰다 환경변수 배열을 침범하는 로컬 권한상승 취약점입니다. 2009년부터 있었던 이 결함은 2022년 공개 직후 실제 악용이 확인돼 CISA 긴급 대응 목록에 올랐습니다. 이 글은 원 분석을 재구성하고, polkit의 실제 소스와 패치 커밋을 고정 커밋 SHA 기준으로 직접 열어 근본원인·흐름·영향을 우리 관점에서 분석합니다.

이 글의 코드 인용(pkexec.c·pkcheck.c 패치 전/후)은 polkit 공식 저장소(gitlab.freedesktop.org/polkit/polkit)의 실제 소스를 고정 커밋 SHA 기준으로 직접 열어 대조했으며, 패치 diff(커밋 a2bf5c9c83b6ae46cbd5c779d3055bff81ded683, 부모 4ff1abe4a4c1f8c8378b9eaddb0346ac6448abd8)도 GitLab에서 받은 실제 .diff 원문과 패치 전/후 파일을 각각 직접 비교해 확인했습니다. CVSS·CWE·발행일·CISA KEV 등재일은 NVD REST API(JSON)에서 직접 받았습니다. 취약 메커니즘 서술(§7 미흡점, 실제 소스 줄번호 534·610·629·632·639)은 Qualys 원 어드바이저리(qualys.com의 pwnkit.txt 원문)를 직접 fetch해 우리가 확보한 polkit 소스의 같은 줄번호와 대조 확인한 것입니다 — 두 출처가 정확히 일치함을 이 세션에서 직접 검증했습니다. 대응방안 §10의 Red Hat 권고(SUID 비트 제거를 권장하지 않는다는 입장, SystemTap 완화)도 Red Hat 공식 문서(RHSB-2022-001) 원문을 직접 fetch해 확인했습니다. Linux 커널의 후속 하드닝(§11)은 torvalds/linux 저장소의 실제 커밋을 GitHub API로 직접 조회해 확인했습니다. 즉 이 글의 핵심 팩트(소스 위치·패치 내용·취약점 메커니즘)는 모두 1차출처 직접 대조이며, 2차 분석에 의존한 부분은 없습니다.

infiltr8 team 직접 분석 #PwnKit#pkexec#SUID#off-by-one#argc
무엇polkit의 SUID-root 프로그램 pkexec가 인자 없이(argc=0) 실행되면, 존재하지 않는 인자 자리를 읽고 쓰다가 환경변수 배열을 침범해 root 권한으로 임의 코드 실행에 이를 수 있는 취약점입니다.
원리커널이 새 프로세스의 메모리에 argv 배열과 envp 배열을 이어 붙여 배치한다는 사실 때문에, argv 경계를 벗어난 접근이 실제로는 바로 뒤에 이어지는 envp[0]을 가리킵니다.
핵심이 값을 읽어 프로그램 경로로 오인하는 것에서 그치지 않고, 그 결과를 다시 같은 자리에 되씀(out-of-bounds write)으로써 SUID 프로그램이 원래 걸러냈어야 할 환경변수를 재도입할 길을 엽니다. CVSS 7.8(High), CWE-787/CWE-125.

01개요

polkit(옛 PolicyKit)은 리눅스에서 권한 없는 프로세스가 권한 있는 작업을 정책에 따라 요청할 수 있게 해주는 컴포넌트입니다. 그중 pkexecsudo와 비슷하게, 인가된 사용자가 다른 사용자(기본값 root) 권한으로 명령을 실행하게 해주는 SUID-root 바이너리입니다 — 실행 파일 소유자(root) 권한으로 항상 동작한다는 뜻입니다.

CVE-2021-4034는 이 pkexec가 명령줄 인자 개수(argc)가 0인 극단적인 호출을 전혀 예상하지 못한 채 짜여 있었다는 사실에서 시작합니다. argc=0으로 실행되면 pkexec는 '다음 인자'라고 믿는 자리를 읽는데, 그 자리는 실제로는 인자 배열 밖 — 프로세스의 환경변수 배열의 첫 항목입니다. 이 하나의 검증 누락이, 아래에서 다루는 것처럼 읽기와 쓰기 양쪽 모두에서 경계를 넘게 만듭니다.

pkexec는 2009년 5월 최초 커밋(c8c3d83, "Add a pkexec(1) command")부터 이 결함을 그대로 지니고 있었습니다 확인. 2022년 1월 25일 조율된 공개(coordinated disclosure)와 동시에 패치가 나왔지만, 결함 자체가 워낙 오래되고 트리거가 간단해 공개 직후 실제 악용이 빠르게 확인됐고, CISA는 2022년 6월 27일 이 CVE를 알려진 악용 취약점(KEV) 목록에 올렸습니다(대응 기한 2022년 7월 18일) 확인.

CVSS: 7.8 (CVSS 3.1, High, AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) · CWE-787(Out-of-bounds Write, 신고 벤더 Red Hat 분류) + CWE-125(Out-of-bounds Read, NVD 자체 추가 분류)

같은 이름으로 묶여 보도되는 별개 취약점은 확인되지 않았습니다 — CVE-2021-4034 하나가 이 사건 전체를 가리킵니다. CISA KEV 등록명은 "Red Hat Polkit Out-of-Bounds Read and Write Vulnerability"로, 읽기·쓰기 두 결함이 함께 있다는 점을 이름에 그대로 담고 있습니다. 확인

02영향받는 버전

대상취약 버전안전 버전
polkit (업스트림 git, pkexec)2009-05 최초 커밋(c8c3d83)부터 2022-01-25 패치 커밋 이전까지 전 버전 — 구조적으로 처음부터 존재패치 커밋 a2bf5c9c 이후(마스터, 2022-01-25) · 정식 태그 릴리스로는 0.121(2022-07-11)부터 포함 분석 — 업스트림이 패치를 반영한 정식 릴리스를 낸 건 커밋 5개월 반 뒤였습니다(아래 참고)
주요 배포판 패키지(Debian·Ubuntu·RHEL·Fedora·SUSE 등)배포판이 패키징한 기존 polkit 버전(대부분 0.1xx대, 배포판·릴리스마다 상이)2022-01-25~26 각 배포판이 발표한 보안 업데이트로 같은 패치 커밋을 자기 버전에 백포트 — 정확한 안전 버전은 사용 중인 배포판의 공식 보안 공지를 확인해야 합니다 확인

03배경 — SUID 프로그램과, 커널이 argv·envp를 배치하는 방식

리눅스에서 실행 파일에 SUID 비트가 설정돼 있으면, 그 프로그램은 누가 실행하든 파일 소유자(여기선 root)의 권한으로 동작합니다. pkexec가 SUID-root인 이유는 명확합니다 — 권한 없는 사용자의 요청을 받아 권한 있는 작업을 대신 해줘야 하니, 그 자체가 이미 높은 권한으로 시작해야 합니다. 이런 프로그램은 태생적으로 '내가 받은 입력을 얼마나 의심해야 하는가'가 일반 프로그램보다 훨씬 중요합니다.

새 프로세스가 시작될 때, 리눅스 커널은 execve()에 넘겨진 인자 문자열들(argv)과 환경변수 문자열들(envp)의 포인터 배열을 새 프로세스의 스택 끝부분에 서로 이어 붙여 배치합니다. argv[0]부터 argv[argc-1]까지, 그 뒤에 배열의 끝을 알리는 NULL 하나, 그리고 바로 이어서 envp[0]부터 시작하는 환경변수 포인터들이 옵니다. 이 배치 자체는 정상적인 리눅스 프로세스 동작이며 문제가 아닙니다 — 문제는 이 배치를 전제로, argv 배열의 경계를 넘어서 읽거나 쓰는 코드가 있을 때 생깁니다.

일반적으로 argc는 최소 1입니다(argv[0]에 프로그램 자신의 이름이 들어가는 관례 때문입니다). 하지만 execve()는 시스템 호출 차원에서 이 관례를 강제하지 않습니다 — argv 배열을 아예 빈 상태({NULL})로 넘기며 execve()를 직접 호출하면 argc=0인 프로세스가 실제로 시작됩니다. pkexec는 자신이 항상 argc≥1로 불릴 것이라 가정했고, 이 가정이 다음 절의 단서입니다.

04취약점 발생 원인

이 취약점은 하나의 검증 누락(§W1)에서 출발해, 그 결과가 '경로 문자열로 오인'(§W2)되고, 다시 '경계 밖에 되쓰임'(§W3)으로 이어지는 3단 연쇄입니다. 아래 코드는 모두 polkit 공식 저장소의 실제 소스이며, 줄번호는 패치 직전 커밋(4ff1abe4, 패치 커밋 a2bf5c9c의 부모) 기준입니다.

1"인자 처리 루프는 argc=0을 그냥 건너뛴다"

핵심

인자를 하나씩 훑는 루프가 argc를 부호 없는 정수로 비교하는 탓에, argc=0이면 루프 자체가 실행되지 않고 넘어가면서 '다음 인자 자리'를 가리키는 인덱스 n이 1로 고정된 채 남습니다.

pkexecmain()--user·--help 같은 옵션을 처리하기 위해 for (n = 1; n < (guint) argc; n++) 루프로 argv를 훑습니다. 여기서 argcguint(부호 없는 정수)로 캐스팅되는데, argc=0이면 이 비교는 1 < 0이 되어(부호 없는 산술이라 항상 거짓) 루프 몸체가 단 한 번도 돌지 않습니다.

for (n = 1; n < (guint) argc; n++) { /* --help, --version, --user, --disable-internal-agent 처리 (생략) */ } // argc=0이면 이 루프는 한 번도 안 돌고, n은 초기값 1 그대로 다음 코드로 넘어감

polkit 오픈소스 실제 소스 — pkexec.c · main() (커밋 4ff1abe4a4c1f8c8378b9eaddb0346ac6448abd8, 패치 커밋 a2bf5c9c의 부모=패치 직전 상태).

루프가 안 도는 것 자체는 안전해 보입니다. 문제는 이 루프를 빠져나온 다음, 코드가 n을 '실제 명령 인자가 시작하는 위치'로 그대로 믿고 계속 쓴다는 점입니다 — n은 여전히 초기값 1을 가진 채로요. argc=0이라 argv에는 인덱스 0(그 값은 NULL)밖에 없는데, 이후 코드는 argv[1]이 존재하는 인자라고 가정하고 접근합니다.

왜 이렇게 됐나이 루프는 '정상적인 호출이라면 argc는 항상 1 이상'이라는 암묵적 전제 위에서 짜였습니다. execve() 시스템 호출 자체는 이 전제를 강제하지 않는다는 사실이, 애플리케이션 코드 레벨의 관례와 커널이 실제로 허용하는 범위 사이의 간극입니다. 분석

2"경계 밖 값을, 검증 없이 프로그램 경로로 취급한다"

핵심

n=1argv[n]을 읽는 코드가, 이 값이 정말 사용자가 넘긴 인자인지 확인하지 않고 곧바로 '실행할 프로그램 경로'로 취급해 PATH 탐색까지 실행합니다.

610행 path = g_strdup (argv[n])은 W1에서 고정된 n=1argv[1]을 읽습니다. argc=0인 이 상황에서 argv[1]은 배열 밖입니다 — 배경 절에서 설명한 메모리 배치 때문에, 이 자리는 실제로는 이 프로세스의 첫 환경변수(envp[0])를 가리키는 포인터입니다. 즉 path에는 공격자가 완전히 통제하는 문자열이 담깁니다.

path = g_strdup (argv[n]); /* ... (경로가 NULL이면 기본 셸로 대체하는 분기, 생략) ... */ if (path[0] != '/') { s = g_find_program_in_path (path); /* ... (탐색 실패 시 오류 처리, 생략) ... */ }

polkit 오픈소스 실제 소스 — pkexec.c · main() (같은 커밋). 610행이 경계 밖 읽기, 629·632행이 그 값을 프로그램 경로로 취급하는 지점입니다.

이어서 629행 if (path[0] != '/')가 이 값이 절대 경로가 아니면, 632행 g_find_program_in_path(path)PATH 환경변수가 가리키는 디렉터리들에서 이 이름의 실행 파일을 찾습니다. path가 사실은 사용자가 넘긴 명령 인자가 아니라 공격자가 직접 설정한 첫 환경변수 값이라는 걸 이 코드는 전혀 모릅니다.

왜 이렇게 됐나g_find_program_in_path() 자체는 안전한 함수입니다(환경변수를 신뢰하지 않고 파일시스템만 확인). 위험한 건 이 함수에 '사용자 인자'라는 이름으로 위장한, 사실은 공격자가 쓴 환경변수가 입력으로 들어간다는 점입니다. 분석

3"탐색 결과를 같은 경계 밖 자리에 도로 쓴다"

핵심

PATH 탐색 결과를 argv[n](=envp[0])에 그대로 되씁니다 — 이 out-of-bounds 쓰기가, pkexec가 나중에 참조할 환경변수 자리 하나를 공격자가 원하는 값으로 바꿔치기할 길을 엽니다.

639행 argv[n] = path = s;는 632행에서 찾은 실행 파일의 전체 경로(s)를, W1에서 고정된 인덱스 n=1 자리에 그대로 씁니다. argv[1]이 이 상황에서 실제로는 envp[0]이므로, 이 쓰기는 이 프로세스의 첫 환경변수 자리를 덮어씁니다.

g_free (path); argv[n] = path = s;

polkit 오픈소스 실제 소스 — pkexec.c · main() (같은 커밋). 639행이 PATH 탐색 결과를 envp[0] 자리에 되쓰는 지점입니다.

Qualys 원 어드바이저리는 이 지점을 정확히 이렇게 정리합니다 — PATH를 PATH=name처럼 설정하고 현재 디렉터리에 name이라는 디렉터리를 만들어 그 안에 특정 이름의 실행 파일을 두면, g_find_program_in_path()가 돌려주는 문자열(예: name/value)이 envp[0] 자리에 그대로 써진다는 것입니다. 즉 공격자는 이 out-of-bounds 쓰기의 결과 문자열 내용까지 원하는 대로 설계할 수 있습니다 확인 — Qualys 원문 직접 대조.

이 쓰기 자체가 곧바로 코드 실행은 아닙니다. 의미는 'pkexec가 이후 자신의 환경을 정리(sanitize)할 때 참조할 자리 하나가, 원래 있어야 할 값이 아니라 공격자가 심은 값으로 바뀌어 있다'는 것입니다 — ld.so가 SUID 프로세스 실행 전에 이미 제거했어야 할 민감한 환경변수를 이 경로를 통해 재도입할 여지가 여기서 열립니다.

왜 이렇게 됐나쓰기 지점 역시 읽기 지점(W2)과 마찬가지로 n이 유효한 argv 인덱스인지 재확인하지 않습니다. 하나의 검증 누락(W1)이 읽기(W2)와 쓰기(W3) 두 번의 경계 침범을 동시에 정당화해 버린 셈입니다. 분석

분석 결론

무게 중심은 W1(argc<1에 대한 사전 가드 부재)입니다. 이 한 줄(정확히는 한 번의 조건 검사)이 없었다는 사실 하나가 W2·W3 두 경계 침범을 모두 성립시킵니다 — 실제 패치도 정확히 이 지점만 막았습니다(§9).

다만 실질적 위험은 W3(out-of-bounds WRITE)에서 옵니다. W2(읽기)만 있었다면 공격자가 통제한 문자열이 PATH 탐색의 입력이 되는 정도로 끝났겠지만, W3이 그 결과를 다시 envp 자리에 되쓰기 때문에 '환경변수 재도입'이라는, SUID 프로그램에게 훨씬 위험한 프리미티브로 이어집니다. CISA KEV 등재명이 '읽기·쓰기'를 둘 다 명시한 이유이기도 합니다. 분석

05공격 과정

5.1 공격 과정 요약

공격자 · execve()를 argc=0으로 직접 호출 argv={NULL}, envp는 공격자가 미리 구성 ① SUID 프로세스가 argc=0으로 시작 main() 인자 처리 루프 통과 for(n=1;n<(guint)argc;n++) — argc=0이라 통과, n=1 고정 ② argc<1 가드 부재 — 인덱스가 1로 고정된 채 다음 단계 진입 핵심 지점 argv[1] 읽기 = 실제로는 envp[0] out-of-bounds READ — 프로그램 경로로 오인 ③ 경계 밖 값을 검증 없이 경로로 취급 PATH 탐색 → 실행 파일 경로 확보 g_find_program_in_path(path) ④ 탐색 결과를 같은 경계 밖 자리에 재기입 핵심 지점 결과를 argv[1] 자리에 되씀 = envp[0] 덮어씀 out-of-bounds WRITE — 환경변수 재도입 여지 ⑤ 환경 정리 단계가 재기입된 값을 다시 참조할 여지 root 권한 임의 코드 실행 경로 성립 구체적 트리거 조건은 무해화 원칙상 미서술
공격 흐름도 (직접 작성)

5.2 상세 공격 과정

이 공격의 첫 조건은 셸에서 pkexec를 그냥 실행하는 것과 다릅니다 — 표준 C 라이브러리의 execve()를 직접 호출하면서 인자 배열 자체를 비워({NULL}) 넘겨야, argc=0pkexec 프로세스가 실제로 시작됩니다. 이 조건 하나가 §7의 W1~W3 전체 연쇄의 전제입니다.

공격자는 이 실행 전에 자신의 환경변수, 특히 envp[0]이 될 첫 번째 변수를 원하는 값으로 미리 구성해 둡니다. Qualys 원 어드바이저리는 PATH 환경변수를 특정 문자열로 설정하고 현재 작업 디렉터리에 그 이름의 디렉터리·실행 파일을 함께 준비하는 조합을 구체적으로 제시합니다 — 이 글은 그 조합의 정확한 값·트리거 절차는 다루지 않고, 원리(경계 밖 읽기·쓰기가 왜 위험한 프리미티브로 이어지는가)까지만 남깁니다.

④~⑤에서 되쓰인 값이 실제로 코드 실행까지 이어지려면, pkexec가 이후 자신의 환경을 clearenv()로 완전히 비우기 전에 이 재도입된 자리를 다시 참조하는 경로가 있어야 합니다. Qualys는 이 마지막 연결 고리로 GLib의 오류 메시지 출력 함수(문자 인코딩 변환을 위해 내부적으로 공유 라이브러리를 로드하는 경로)를 지목했습니다 — 이 부분 역시 구체적 트리거 조건은 무해화 원칙에 따라 이 글에서 재현하지 않습니다.

흥미로운 점은, Qualys 원문이 이 취약점이 로그에 흔적을 남기는 경로와 남기지 않는 경로가 둘 다 있다고 명시한다는 것입니다 확인 — 탐지 관점에서 중요한 단서라 §10에서 다시 다룹니다.

5.3 미흡점 ↔ 공격 흐름 매핑

미흡점(§4)발화 지점(§5.2)여는 프리미티브→ 다음으로 잇는 것
W1 — argc<1 가드 부재§8.2의 인자 처리 루프(② 지점)n이 실제 인자 개수와 무관하게 1로 고정argv[1] 읽기가 배열 밖으로 나가는 조건 성립(③)
W2 — 경계 밖 값을 경로로 오인§8.2의 argv[1] 읽기·PATH 탐색(③~④ 지점)공격자가 설정한 envp[0]이 신뢰된 경로 탐색의 입력이 됨탐색 결과 문자열 확보(④)
W3 — 탐색 결과를 경계 밖에 재기입§8.2의 되쓰기(⑤ 지점)envp[0] 자리가 공격자가 설계한 값으로 재기입됨환경 정리 단계가 재도입된 값을 참조할 여지(⑥) → 코드 실행 경로(⑦)

06취약점 패치

src/programs/pkexec.c (+ src/programs/pkcheck.c)·main() 진입부 argc 가드·커밋 a2bf5c9c83b6ae46cbd5c779d3055bff81ded683 · 부모 4ff1abe4a4c1f8c8378b9eaddb0346ac6448abd8 (2022-01-25, Jan Rybar/Red Hat) 확인
pid_t pid_of_caller; gpointer local_agent_handle; /* * If 'pkexec' is called THIS wrong, someone's probably evil-doing. Don't be nice, just bail out. */ if (argc<1) { exit(127); } ret = 127;

취약점의 근본 원인이 정확히 'argc가 1 미만인 경우를 아무도 검사하지 않았다'는 것이었기 때문입니다(§7 W1). 이 검사 하나가 §7의 W1~W3 연쇄 전체의 전제 조건을 원천 차단합니다.

어떻게지역 변수 선언 직후, 나머지 로직이 시작되기 전에 argc < 1 검사를 추가해 이 조건이 성립하면 즉시 exit()하도록 했습니다. 완전히 같은 인자 처리 루프 구조(for (n = 1; n < (guint) argc; n++))를 가진 형제 프로그램 pkcheck.c에도 동일한 가드가 같은 커밋에 함께 추가됐습니다 — pkcheck은 SUID가 아니라 이 경로로 직접적인 권한상승에 쓰이진 않지만, 같은 코드 패턴이 polkit 안에 반복되고 있었다는 뜻입니다. 이 커밋은 이 두 가드 외에도, 639행(§7 W3)에서 탐색 결과를 argv[n]에 되쓰는 지점에 if (argv[n] != NULL) 방어 검사를 추가로 얹었습니다 — n이 실제로 유효한 인덱스를 가리킬 때만 쓰도록 이중으로 막은 것입니다 확인 — 실제 diff 원문 대조.

비고이 패치는 조율된 공개 절차를 그대로 따라, 2022-01-25 같은 날 여러 배포판이 각자 패키징한 구버전에 동일한 커밋을 백포트해 배포했습니다. 업스트림 polkit 저장소 자체가 이 커밋을 포함한 정식 릴리스(0.121)를 낸 건 그보다 5개월 반 뒤인 2022-07-11입니다 — 실사용 환경에서는 '업스트림 릴리스를 기다린다'가 아니라 '배포판 보안 업데이트를 즉시 적용한다'가 실제 대응 경로였다는 뜻입니다. 확인

07대응방안

가장 확실한 조치는 사용 중인 배포판이 배포한 polkit 보안 업데이트를 적용하는 것입니다(§5 표). polkit을 직접 빌드해 쓰는 경우에는 업스트림 커밋 a2bf5c9c 이후 소스, 또는 정식 릴리스 0.121 이상을 씁니다. '패키지 버전 번호가 0.121 이상인가'만으로 안전 여부를 판단하지 말 것 — 대부분의 배포판은 이보다 훨씬 낮은 자기 버전 번호에 이 패치만 백포트했습니다.

08왜 이게 반복되는가

이 사건이 남기는 질문은 '왜 argc<1을 안 봤나'보다 한 단계 더 큽니다. C 언어에서 배열 인덱스의 유효성은 언어가 보장해주지 않고, 그 배열이 어떤 조건에서 만들어졌는지(여기선 execve() 시스템 호출)까지 알아야 판단할 수 있습니다. pkexec는 자신을 부르는 쪽이 항상 셸의 관례(argc≥1)를 지킬 거라 가정했지만, 커널은 그 관례를 강제하지 않았습니다 — 애플리케이션 레벨의 '으레 그럴 것'과 시스템 콜 레벨의 '실제로 허용되는 것' 사이의 간극이 13년 동안 조용히 남아 있었던 셈입니다.

패치완전성 관점에서 이 CVE를 세 질문으로 정리하면 이렇습니다. ① 근본원인을 막았나. 예 — argc < 1 가드는 §7 W1이 성립하는 정확한 조건을 원천 차단해, 증상(경계 밖 읽기·쓰기)이 아니라 그 전제(검증 누락)를 겨냥합니다. 같은 코드 패턴을 가진 pkcheck.c에도 같은 커밋에서 동일 가드가 함께 들어갔다는 점도, 이 패치가 '한 파일의 증상 봉합'이 아니라 '패턴 단위의 수정'이었음을 보여줍니다. 확인

② variant가 남았나. polkit 코드 자체에서 같은 취약점의 재발 사례는 확인하지 못했습니다. 흥미로운 건 그 대신 더 아래 계층(리눅스 커널)이 이 취약점 클래스 전체를 겨냥한 일반화된 방어를 추가했다는 점입니다 — 커밋 dcd46d8argc=0 호출을 커널 단에서 봉쇄해, 앞으로 pkexec와 똑같은 실수를 하는 다른 어떤 SUID 프로그램이 나오더라도 이 특정 경로로는 뚫리지 않게 만들었습니다. 개별 프로그램 패치(polkit)와 시스템 차원의 클래스 봉쇄(커널)가 같은 CVE에서 함께 나온, 방어 계층화의 좋은 사례입니다. 확인

③ silent patch인가. 전혀 아닙니다. 2021-11-18 Red Hat 보안팀 신고 → 2022-01-11 distros@openwall 조율 공개 → 2022-01-25 협정된 공개일에 Qualys 상세 어드바이저리·다수 배포판 동시 패치·패치 커밋이 함께 나왔고(NVD 공식 등재는 사흘 뒤인 2022-01-28), 이후 CISA KEV 등재(2022-06-27)까지 이어진, 책임 있는 공개 절차의 정석에 가까운 사례입니다. 확인

이 패턴은 저희가 다룬 다른 사건들과도 결이 닿아 있습니다 — Jenkins CLI 인자 파서 취약점(CVE-2024-23897)에서는 '로컬 신뢰를 전제로 한 라이브러리 기본값을 원격 컨텍스트로 그대로 가져온 것'이 문제였고, 여기서는 '항상 성립한다고 믿은 관례(argc≥1)를 시스템 콜이 강제하지 않는다는 것'이 문제였습니다. 둘 다 코드 자체는 각자 정상인데, 그 코드가 전제하는 경계 조건이 실제 실행 환경과 어긋난 지점에서 사고가 났습니다.

이 틈은 patch 한 줄과 커널 하드닝 한 커밋으로 닫혔지만, 'C로 짠 권한 있는 프로그램이 자신의 입력이 만들어진 방식(시스템 콜의 실제 허용 범위)까지 검증하고 있는가'라는 질문은 다른 SUID·SGID 바이너리에서 또 나올 수 있습니다 — 그래서 저희는 이 CVE 번호가 아니라 이 검증 습관을 기록으로 남깁니다.

09결론

CVE-2021-4034(PwnKit)는 polkit의 SUID-root 프로그램 pkexec가 argc=0인 호출을 검증하지 않아, argv 배열 경계를 벗어나 envp[0]을 읽고(out-of-bounds read) 다시 그 자리에 되쓰는(out-of-bounds write) 취약점입니다. CVSS 7.8(High), CWE-787/CWE-125로 등록됐고, 커밋 a2bf5c9c의 argc<1 가드로 패치됐습니다. 2009년부터 존재했던 이 결함은 2022년 공개 직후 실제 악용이 확인돼 CISA KEV에 올랐으며, 이후 리눅스 커널 자체가 argc=0 실행을 봉쇄하는 시스템 차원의 방어를 추가했습니다.

참고 자료

이 글의 코드 인용(pkexec.c·pkcheck.c 패치 전/후)은 polkit 공식 저장소(gitlab.freedesktop.org/polkit/polkit)의 실제 소스를 고정 커밋 SHA 기준으로 직접 열어 대조했으며, 패치 diff(커밋 a2bf5c9c83b6ae46cbd5c779d3055bff81ded683, 부모 4ff1abe4a4c1f8c8378b9eaddb0346ac6448abd8)도 GitLab에서 받은 실제 .diff 원문과 패치 전/후 파일을 각각 직접 비교해 확인했습니다. CVSS·CWE·발행일·CISA KEV 등재일은 NVD REST API(JSON)에서 직접 받았습니다. 취약 메커니즘 서술(§7 미흡점, 실제 소스 줄번호 534·610·629·632·639)은 Qualys 원 어드바이저리(qualys.com의 pwnkit.txt 원문)를 직접 fetch해 우리가 확보한 polkit 소스의 같은 줄번호와 대조 확인한 것입니다 — 두 출처가 정확히 일치함을 이 세션에서 직접 검증했습니다. 대응방안 §10의 Red Hat 권고(SUID 비트 제거를 권장하지 않는다는 입장, SystemTap 완화)도 Red Hat 공식 문서(RHSB-2022-001) 원문을 직접 fetch해 확인했습니다. Linux 커널의 후속 하드닝(§11)은 torvalds/linux 저장소의 실제 커밋을 GitHub API로 직접 조회해 확인했습니다. 즉 이 글의 핵심 팩트(소스 위치·패치 내용·취약점 메커니즘)는 모두 1차출처 직접 대조이며, 2차 분석에 의존한 부분은 없습니다.