[하루한줄] CVE-2024-30088 : Windows 커널 토큰 보안 특성 Copyout의 TOCTOU Race Condition으로 인한 권한 상승
URL
Target
- 10.0.22621.0 ≤ Windows 11 22H2 < 10.0.22621.3737
- 10.0.22631.0 ≤ Windows 11 23H2 < 10.0.22631.3737
- 10.0.19045.0 ≤ Windows 10 22H2 < 10.0.19045.4529
- 그 외 Windows 10 / Server 2016 ~ 2022 다수 (상세 버전은 NVD 참고)
Explain
Overview
CVE-2024-30088은 Windows 커널이 NtQueryInformationToken의 TokenAccessInformation(정보 클래스 22) 요청을 처리하는 과정에서 발생하는 TOCTOU(Time-of-check Time-of-use) Race Condition 취약점입니다.
커널은 토큰의 보안 특성(security attribute) 정보를 사용자 출력 버퍼로 내보낼 때, 특성 이름을 담을 목적지 UNICODE_STRING 설명자를 사용자 버퍼 안에 구성한 뒤, 같은 설명자를 다시 읽어 문자열 복사에 사용했습니다. 설명자가 사용자 메모리에 있었기 때문에, 커널이 설명자를 쓴 시점과 다시 읽어 복사에 사용하는 시점 사이에 공격자 스레드가 MaximumLength와 Buffer 필드를 바꿔치기할 수 있었습니다.
그 결과 공격자는 고정된 특성 이름 문자열을 자신이 지정한 커널 주소에 겹쳐 쓰는 constrained write primitive를 얻습니다. 공개 PoC는 이 write를 현재 토큰 객체의 privilege mask 영역에 정렬하여, 저권한(Medium integrity) 사용자가 SYSTEM 권한으로 권한 상승하는 체인을 구성했습니다.
이 취약점은 CVSSv3.1 기준 7.0(High, Microsoft CNA)으로 평가되었으며 CWE-367(TOCTOU Race Condition)로 분류되었습니다. 2024년 6월 12일 패치가 공개되었고, 이후 CISA의 Known Exploited Vulnerabilities(KEV) 카탈로그에 등재되어 실제 악용이 확인된 취약점입니다.
Background
NtQueryInformationToken / TokenAccessInformation (class 22)
NtQueryInformationToken은 토큰 핸들과 정보 클래스를 받아 토큰의 특정 정보를 사용자 출력 버퍼로 돌려주는 시스템 호출입니다. PoC는 정보 클래스22(TokenAccessInformation)를 사용합니다.공개 SDK의
TOKEN_ACCESS_INFORMATION구조체에서 마지막SecurityAttributes필드는 문서상 예약된 포인터로만 보이지만, 실제 하위 경로는 private 보안 특성 레코드를 사용자 출력 버퍼에 구성합니다. 이 호출 자체가 권한을 올려 주는 것은 아니며, 취약 동작은 class 22 처리 중 보안 특성을 내보내는 하위 copyout 경로에서 발생합니다.NtQueryInformationToken(TokenAccessInformation = 22) -> TOKEN_ACCESS_INFORMATION.SecurityAttributes -> AuthzBasepCopyoutInternalSecurityAttributes -> RtlCopyUnicodeStringUNICODE_STRING 과 RtlCopyUnicodeString
UNICODE_STRING은 문자열 자체가 아니라 UTF-16 바이트열을 가리키는 설명자입니다.Length는 현재 데이터 길이,MaximumLength는 목적지에 담을 수 있는 최대 길이,Buffer는 실제 바이트가 있는 주소이며, 두 길이는 문자 수가 아니라 바이트 수로 다룹니다.RtlCopyUnicodeString은 원본Length와 목적지MaximumLength중 작은 길이만큼 목적지Buffer로 복사합니다. 호출자가 목적지 세 필드를 먼저 올바르게 채워야 하며, 함수가Buffer나MaximumLength를 새 값으로 바꿔 주지는 않습니다.copy length = min(source.Length, destination.MaximumLength) copy target = destination.Buffer따라서 이 설명자가 어느 메모리에 놓였는지가 핵심입니다. 커널 스택에 있으면 같은 호출 동안 커널이 소유하지만, 사용자 버퍼에 있으면 다른 사용자 스레드가 중간에 필드를 바꿀 수 있습니다.
Root Cause
취약점은 두 가지 조건이 결합되어 발생합니다.
- 보안 특성 이름의 목적지
UNICODE_STRING설명자가 사용자 출력 버퍼에 구성됨 커널이 그 설명자를 쓴 뒤 다시 읽어
RtlCopyUnicodeString의 목적지로 사용 (write-then-read-back)
write-then-read-back double fetch
정상이라면 커널이 복사 목적지로 사용할 설명자는 실제 복사가 끝날 때까지 신뢰할 수 있는 메모리에 있어야 합니다. 그러나 취약 경로는
Length,MaximumLength,Buffer가 든 목적지UNICODE_STRING을 사용자 버퍼에 쓴 뒤, 그 주소를 그대로RtlCopyUnicodeString에 넘겼습니다.AuthzBasepCopyoutInternalSecurityAttributes가 설명자를 준비(write)한 뒤 name copy와 attribute-value copy가 이어지는데, 이 사이에 설명자를 한 번 더 읽어(read-back) 사용하므로 두 시점 사이에 공격자 스레드가 개입할 수 있는 창이 생깁니다.원하는 값 전체를 쓰는 원시 동작은 아님
복사되는 원본은 16글자 UTF-16LE 특성 이름(공개 PoC 기준
TSA://ProcUnique)으로 고정됩니다. 공격자가 제어하는 것은 목적지 주소와 race에 의존하는 길이이지, 복사되는 바이트 내용이 아닙니다. 즉 임의 값 쓰기(Arbitrary Write)보다 약한 constrained write primitive입니다.HEVD Arbitrary Overwrite : WHAT 과 WHERE 모두 제어 CVE-2024-30088 : WHERE 와 길이 timing 제어, WHAT 은 고정공개 short chain은 이 고정 바이트열을 토큰 객체의 privilege mask가 위치한 영역(
_TOKEN.Privileges, offset+0x040)에 겹치도록 정렬합니다. 임의 커널 write나 RIP 제어로 과장하지 않고, 알려진 바이트를 유용한 필드 정렬에 맞추는 constrained write로 보는 편이 정확합니다.
PoC
공개 PoC(commit bffcbbccac2f024024938a805ab32c693b260b91)를 사용해 Windows 11 Pro 22H2 x64 빌드 22621.963 환경에서 재현했습니다. 패치 경계인 KB5039212 / 빌드 22621.3737 이전 환경이며 해당 KB는 설치되지 않았습니다.
토큰 객체 주소 획득
먼저
NtQuerySystemInformation(SystemExtendedHandleInformation = 64)결과에서 현재 토큰 핸들과 같은 항목을 찾아 토큰 커널 객체 주소를 얻습니다. 주소를 알아내는 호출과 취약점을 트리거하는 토큰 query는 서로 다른 역할임에 유의합니다.
레이싱 스레드 구성
반환된 private list에서 첫 보안 특성 이름의
UNICODE_STRING주소를Flink + 0x20으로 계산합니다. 높은 priority의 레이싱 스레드가 이 사용자UNICODE_STRING에 대해MaximumLength = 2,Buffer = token address + 0x40 - 4를 반복해서 덮어씁니다. 트리거 스레드는 같은 출력 버퍼로 class 22 토큰 query를 attempt마다 수천 번 반복 호출합니다.
Medium integrity 에서 class 22 token query 반복 -> 사용자 출력 버퍼의 UNICODE_STRING 을 레이싱 스레드가 변경 -> 고정된 특성 이름이 현재 token privilege 영역에 기록 -> winlogon.exe full-access handle 획득 -> winlogon 을 parent 로 둔 SYSTEM child 생성성공 판정
성공 판정은 query 반환값이 아니라, 이전에는
ERROR_ACCESS_DENIED로 거부되던winlogon.exe의OpenProcess(PROCESS_ALL_ACCESS)가 성공하는지로 확인했습니다. 이번 실행에서는 32번째 시도에서 레이스를 이겼고 유효한 핸들이 처음 기록되었습니다.
Attempt 30: winlogon handle still denied (error 5) Race won on attempt 32 Got Winlogon handle: 0xb4 Created PID: 2484권한 상승 결과
획득한
winlogon.exe핸들을PROC_THREAD_ATTRIBUTE_PARENT_PROCESS의 부모로 지정해 child process를 생성하면, 해당 child가S-1-5-18(SYSTEM)과 System integrityS-1-16-16384를 획득합니다. 이 경로는_EPROCESS.Token을 SYSTEM 토큰으로 교체하는 방식이 아니라, 현재 토큰의 privilege mask를 조작해 privileged handle을 얻는 토큰 privilege chain입니다.
Patch
수정 경로는 사용자 모드 요청일 때 특성 이름을 먼저 커널 스택의 로컬 UNICODE_STRING으로 capture한 뒤 사용자 버퍼로 내보냅니다. 핵심은 probe를 한 번 더 하는 것이 아니라, 사용 전에 설명자를 커널 소유 메모리로 옮겨 사용자 스레드가 중간에 바꿀 수 없게 만드는 것입니다. 진입 API(NtQueryInformationToken)를 제거하지 않고 하위 copyout 경로의 메모리 소유권만 변경했으므로, 다른 정보 클래스까지 같은 결함이 있다고 확장 해석하면 안 됩니다.
Reference
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.