목차
PurpleHound팀 Jin 연구원
1. 취약점 개요
2025년 3월 11일, Microsoft는 자사 제품군의 보안 취약점을 해결하기 위한 정기 보안 업데이트를 발표했다. 다양한 제품에서 신규 취약점이 보고되었으며 이 글에서는 그중 CVE-2025-24985에 대해 집중적으로 다룬다.
CVE-2025-24985는 실제 악용 사례가 확인된 취약점으로 Windows FastFAT 파일 시스템 드라이버에서 발생하는 원격 코드 실행(RCE) 취약점이다. CVSS 기준 심각도는 7.8점으로 높은 수준에 해당한다.
2. 보안 패치 분석
CVE-2025-24985는 Windows의 FastFAT 파일 시스템 드라이버(fastfat.sys)에서 발생한 취약점이다.
취약점의 근본 원인을 파악하기 위해 BinDiff 도구를 사용해 보안 업데이트 적용 전후의 드라이버 파일을 비교 분석했다.
그 결과, FatExamineFatEntries와 FatSetupAllocationSupport 두 함수에서 코드 변경이 발생한 것을 확인했다. 이 글에서는 해당 함수들의 수정된 코드 영역을 비교하고 이를 바탕으로 취약점의 원인을 분석한다.
3. 함수 단위 비교 분석을 통한 취약점 원인 도출
fastfat.sys에 적용된 보안 패치는 FatExamineFatEntries와 FatSetupAllocationSupport 함수의 코드 흐름을 수정하고 메모리 크기 계산 과정에 대한 유효성 검증을 강화하는 방향으로 이루어졌다.
3.1 NumberOfClusters 계산 흐름에 대한 유효성 검증 로직 추가
FatSetupAllocationSupport 함수는 마운트된 VHD의 Bpb(BIOS Parameter Block) 필드 값을 기반으로 볼륨의 클러스터 수(NumberOfClusters)를 계산한 뒤 내부 구조체의 해당 필드에 저장한다.
그러나 보안 패치 이전에는 이 계산에 사용되는 값들에 대한 유효성 검증이 전혀 이루어지지 않았다.
ReservedSectors, Fats, SectorsPerFat, RootEntries 등 외부 입력으로 조작 가능한 필드들이 연산에 그대로 사용되었으며 이로 인해 계산 결과가 비정상적인 값이 되더라도 그대로 저장되는 구조였다.
아래는 패치 적용 이전의 해당 코드 일부이다.
ULONG __fastcall FatSetupAllocationSupport(__int64 a1, __int64 a2)
{
...
v3 = *(unsigned __int16 *)(a2 + 302); // SectorsPerFat
v4 = *(unsigned __int16 *)(a2 + 288); // BytesPerSector
v5 = (int *)(a2 + 316); // LargeSectorsPerFat
v7 = *(unsigned __int16 *)(a2 + 292); // ReservedSectors
v8 = *(unsigned __int8 *)(a2 + 294); // Fats
v9 = *(unsigned __int16 *)(a2 + 296); // RootEntries
...
v12 = *(unsigned __int16 *)(a2 + 298);// Sectors
if ( (_WORD)v3 )
{
v17 = *(unsigned __int16 *)(a2 + 298);
if ( !(_WORD)v12 )
v17 = *(_DWORD *)(a2 + 312); // LargeSectors
v16 = v3 * v8;
v14 = *(_BYTE *)(a2 + 290); // SectorsPerCluster
v15 = (v17 - 32 * v9 / v4 - (unsigned int)(v7 + v16)) / v14;
}
else
{
v13 = *(unsigned __int16 *)(a2 + 298);
if ( !(_WORD)v12 )
v13 = *(_DWORD *)(a2 + 312);
v14 = *(_BYTE *)(a2 + 290);
LODWORD(v15) = (v13 - *v5 * v8 - v7) / (unsigned int)v14;
v16 = v3 * v8;
}
*(_DWORD *)(a2 + 364) = v15;
...
}
C
복사
3월 보안 업데이트 이전의 FatSetupAllocationSupport 함수 일부 코드
해당 함수는 클러스터 수를 계산할 때 ReservedSectors, Fats, RootEntries, SectorsPerFat 등의 Bpb 필드를 직접 참조한다. 이 필드들은 모두 외부에서 조작 가능한 입력값이다.
이러한 값들이 곱셈 및 나눗셈 연산에 그대로 사용됨에 따라 다음과 같은 계산 오류가 발생하는 구조였다:
•
곱셈 결과가 32비트 정수 범위를 초과하는 정수 오버플로우
•
연산 순서에 따라 값이 역전되는 언더플로우
그 결과, 내부 구조체의 NumberOfClusters 필드에 공격자가 의도한 비정상적인 값이 저장될 수 있다.
이 문제는 이후 보안 패치를 통해 해결되었으며, 아래 코드 비교를 통해 관련 변경 내용을 확인할 수 있다.
ULONG __fastcall FatSetupAllocationSupport(__int64 a1, __int64 a2)
{
...
v6 = (unsigned int *)(a2 + 316); // LargeSectorsPerFat
...
if ((unsigned int)Feature_371473723__private_IsEnabledDeviceUsageNoInline()) {
v11 = *(unsigned __int16 *)(a2 + 302); // SectorsPerFat
v12 = *(unsigned __int16 *)(a2 + 298); // Sectors
if (!(_WORD)v11) {
v13 = *(unsigned __int16 *)(a2 + 298);
if (!v12)
v13 = *(unsigned __int32 *)(a2 + 312); // LargeSectors
// FAT 영역 크기 오버플로우 방지
if (*v6 * (unsigned __int64)*(unsigned __int8 *)(a2 + 294) <= 0xFFFFFFFF) {
v14 = *(unsigned __int16 *)(a2 + 292); // ReservedSectors
v15 = v14 + *v6 * *(unsigned __int8 *)(a2 + 294);
// Reserved + FAT 합이 비정상적일 경우 차단
if (v15 >= v14 && v13 >= v15)
goto LABEL_20;
}
LABEL_19:
*(_DWORD *)(a1 + 72) = -1073741566;
ExRaiseStatus(-1073741566);
}
v16 = *(unsigned __int16 *)(a2 + 298);
if (!v12)
v16 = *(unsigned __int32 *)(a2 + 312);
v17 = *(unsigned __int8 *)(a2 + 294) * v11;
v18 = *(unsigned __int16 *)(a2 + 292);
v19 = v18 + v17;
// Reserved + FAT 계산 값 역전 방지
if (v19 < v18)
goto LABEL_19;
v20 = 32 * *(unsigned __int16 *)(a2 + 296) / *(unsigned __int16 *)(a2 + 288) + v19;
// 루트 디렉터리 포함 후 전체 섹터 수 초과 여부 확인
if (v20 < v19 || v16 < v20)
goto LABEL_19;
}
...
}
C
복사
3월 보안 업데이트 이후의 FatSetupAllocationSupport 함수 일부 코드
보안 패치 이후 FatSetupAllocationSupport 함수에는 입력값에 대한 유효성 검증 로직이 추가되었다.
이 조건들은 Bpb 필드를 기반으로 클러스터 수를 계산하기 전에 적용되며 연산 중 비정상적인 값이 감지될 경우 함수 실행을 중단하고 예외를 발생시킨다.
3.2 FatIndexBitSize 결정 및 제한값 산정 로직 개선
FatSetupAllocationSupport 함수는 내부적으로 계산된 클러스터 수(NumberOfClusters)가 파일 시스템이 처리할 수 있는 범위를 초과하지 않도록 제한 값을 산출하고 이를 초과할 경우 값을 강제로 조정하는 로직을 포함하고 있다.
ULONG __fastcall FatSetupAllocationSupport(__int64 a1, __int64 a2)
{
...
v5 = (int *)(a2 + 316); // LargeSectorsPerFat
...
if ( *(_BYTE *)(a2 + 376) == 32 )
v20 = *v5;
else
v20 = *(unsigned __int16 *)(a2 + 302); // SectorsPerFat
v21 = *(unsigned __int16 *)(a2 + 302);
if ( (_WORD)v21 )
{
if ( *(_WORD *)(a2 + 298) )
v23 = *(unsigned __int16 *)(a2 + 298);
else
v23 = *(unsigned int *)(a2 + 312);
v22 = (v23
- 32 * (unsigned __int64)*(unsigned __int16 *)(a2 + 296) / *(unsigned __int16 *)(a2 + 288)
- *(unsigned __int8 *)(a2 + 294) * v21
- *(unsigned __int16 *)(a2 + 292))
/ *(unsigned __int8 *)(a2 + 290) < 0xFF7
? 12
: 16; // FatIndexBitSize 추정
}
else
{
v22 = 32;
}
v24 = *(unsigned __int16 *)(a2 + 288);
v25 = 8 * v20 * v24 / v22 - 2;
if ( *(_DWORD *)(a2 + 364) > v25 )
*(_DWORD *)(a2 + 364) = v25;
...
}
C
복사
3월 보안 업데이트 이전의 FatSetupAllocationSupport 함수 일부 코드
FAT 유형(FatIndexBitSize)을 결정하기 위한 로직이 비교적 단순한 조건 분기와 값 비교에 기반해 구성되어 있었고 FAT12 또는 FAT16을 구분한 후 해당 값을 기반으로 제한 클러스터 수를 계산하는 방식이었다.
이 과정에서 사용되는 모든 값은 Bpb 필드에서 직접 참조되며 외부 입력으로 조작이 가능하다.
ULONG __fastcall FatSetupAllocationSupport(__int64 a1, __int64 a2)
{
...
v6 = (unsigned int *)(a2 + 316); // LargeSectorsPerFat
...
IsEnabledDeviceUsageNoInline = Feature_371473723__private_IsEnabledDeviceUsageNoInline();
v33 = *(_BYTE *)(a2 + 376);
if ( IsEnabledDeviceUsageNoInline )
{
if ( v33 == 32 )
v34 = *v6;
else
v34 = *(unsigned __int16 *)(a2 + 302); // SectorsPerFat
v35 = *(unsigned __int16 *)(a2 + 302);
if ( (_WORD)v35 )
{
if ( *(_WORD *)(a2 + 298) )
v37 = *(unsigned __int16 *)(a2 + 298);
else
v37 = *(unsigned int *)(a2 + 312);
v36 = (-(__int64)((v37
- 32 * (unsigned __int64)*(unsigned __int16 *)(a2 + 296) / *(unsigned __int16 *)(a2 + 288)
- v35 * *(unsigned __int8 *)(a2 + 294)
- *(unsigned __int16 *)(a2 + 292))
/ *(unsigned __int8 *)(a2 + 290) < 0xFF7) & 0xFFFFFFFFFFFFFFFCuLL)
+ 16; // FatIndexBitSize 추정
}
else
{
v36 = 32LL;
}
v38 = *(unsigned __int16 *)(a2 + 288);
v39 = 8 * v34 * (unsigned __int64)*(unsigned __int16 *)(a2 + 288) / v36 - 2;
v40 = *(unsigned int *)(a2 + 364) <= v39;
}
...
if ( !v40 )
*(_DWORD *)(a2 + 364) = v39;
...
}
C
복사
3월 보안 업데이트 이후의 FatSetupAllocationSupport 함수 일부 코드
보안 패치 이후에는 FAT 유형을 결정하는 방식과 클러스터 수 제한값 계산 방식 모두에서 구조적인 보완이 이루어졌다.
기존에는 FAT Bit Size가 단순한 조건 분기 결과에 따라 12, 16, 32 중 하나로 선택되었지만 패치 이후에는 비교 연산 결과에 기반해 비트 정렬 연산이 적용되는 방식으로 변경되었다. 이를 통해 FAT 유형 결정 기준이 보다 고정적이고 예측 가능한 형태로 강화됐다.
또한 제한값 계산에 사용되는 분모인 FatIndexBitSize의 신뢰도를 높이기 위해 오버플로우가 발생하지 않도록 변경됐다.
3.3 NumberOfClusters 기반 메모리 할당 크기 계산의 오버플로우 방어
FatSetupAllocationSupport 함수는 내부적으로 계산된 클러스터 수를 기반으로 FAT32 포맷 조건일 경우 메모리 할당 단위를 산정하고 해당 크기만큼 힙 메모리를 할당하는 로직을 포함하고 있다.
ULONG __fastcall FatSetupAllocationSupport(__int64 a1, __int64 a2)
{
...
if ( *(_BYTE *)(a2 + 376) == 32 && (v28 = *(_DWORD *)(a2 + 364), v28 > 0x10000) )
{
v29 = (v28 + 0xFFFF) >> 16;
*(_DWORD *)(a2 + 216) = v29;
}
else
{
*(_DWORD *)(a2 + 216) = 1;
v29 = 1LL;
}
*(_QWORD *)(a2 + 224) = ExAllocatePoolWithTag((POOL_TYPE)1041, 12 * v29, 'WtaF');
...
}
C
복사
3월 보안 업데이트 이전의 FatSetupAllocationSupport 함수 일부 코드
클러스터 수가 0x10000을 초과하는 경우 (NumberOfClusters + 0xFFFF) >> 16 방식으로 정렬된 단위를 계산한 뒤 여기에 12를 곱해 최종 힙 크기를 산정했다.
그러나 이 계산 과정에는 정수 오버플로우에 대한 방어가 전혀 없었다.
공격자가 클러스터 수 값을 조작할 경우 + 0xFFFF 연산에서 wrap-around가 발생할 수 있고 이로 인해 최종 계산 결과가 매우 작은 수로 축소되어 힙 메모리 할당 크기가 비정상적으로 작아질 수 있다.
결과적으로 ExAllocatePoolWithTag()로 전달되는 버퍼 크기가 의도보다 작아지며 이후 단계에서 버퍼 오버플로우를 유발할 수 있는 위험한 구조였다.
ULONG __fastcall FatSetupAllocationSupport(__int64 a1, __int64 a2)
{
...
if ( *(_BYTE *)(a2 + 376) == 32 && *(_DWORD *)(a2 + 364) > 0x10000u )
{
v46 = Feature_371473723__private_IsEnabledDeviceUsageNoInline();
v47 = *(_DWORD *)(a2 + 364);
if ( v46 )
{
if ( v47 + 0xFFFF < v47 )
{
*(_DWORD *)(a1 + 72) = -1073741566;
ExRaiseStatus(-1073741566);
}
v48 = v47 + 0xFFFF;
}
else
{
v48 = v47 + 0xFFFF;
}
v49 = HIWORD(v48);
*(_DWORD *)(a2 + 216) = v49;
}
else
{
*(_DWORD *)(a2 + 216) = 1;
v49 = 1;
}
*(_QWORD *)(a2 + 224) = ExAllocatePoolWithTag((POOL_TYPE)1041, 12LL * v49, 0x57746146u);
...
}
C
복사
3월 보안 업데이트 이후의 FatSetupAllocationSupport 함수 일부 코드
패치 이후에는 동일한 연산 흐름에 대해 보안 검증 로직이 추가되었다.
클러스터 수가 0x10000을 초과하는 경우 기존과 같은 정렬 계산을 수행하되 사전에 클러스터 수 + 0xFFFF 덧셈 연산 결과가 원래 값보다 작아지는지 확인하여 정수 오버플로우 발생 여부를 검사한다.
이 조건이 만족될 경우 함수는 STATUS_INVALID_PARAMETER를 반환하며 강제로 종료되고 해당 상황이 무시된 채 연산이 계속되는 것을 방지한다.
3.4 FatExamineFatEntries 함수의 비트맵 크기 계산 로직 보완
FatExamineFatEntries 함수는 클러스터 수를 기준으로 비트맵 버퍼 크기를 계산하고 해당 크기만큼 힙 메모리를 할당하는 로직을 포함하고 있다.
void __fastcall FatExamineFatEntries(__int64 a1, __int64 a2, unsigned int a3, unsigned int a4, char a5, unsigned int *a6, PULONG BitMapBuffer)
{
...
v13 = 0x10000;
if ( *(_DWORD *)(a2 + 216) <= 1u )
v13 = *(_DWORD *)(a2 + 364);
...
p_BitMapHeader = &BitMapHeader;
v16 = (v13 - (((_BYTE)v13 - 1) & 7u) + 7) >> 3;
PoolWithTag = (PULONG)ExAllocatePoolWithTag((POOL_TYPE)1041, v16 - (((_BYTE)v16 - 1) & 3) + 3, 'BtaF');
RtlInitializeBitMap(&BitMapHeader, PoolWithTag, v15 - v7 + 1);
...
}
C
복사
3월 보안 업데이트 이전의 FatExamineFatEntries 함수 일부 코드
클러스터 수가 0x10000 이하일 경우 해당 값을 그대로 사용하고 초과할 경우에는 고정값 0x10000을 사용하여 비트맵 크기를 계산했다.
이때 (value - ((value - 1) & 7) + 7) >> 3 방식으로 바이트 단위 정렬이 수행되며 계산된 크기에 따라 ExAllocatePoolWithTag()를 통해 힙 메모리가 할당된다.
하지만 이 연산 과정에서는 계산 결과가 value보다 작아지는 경우 즉 wrap-around 또는 언더플로우가 발생할 가능성에 대한 방어 로직이 존재하지 않았다.
void __fastcall FatExamineFatEntries(__int64 a1, __int64 a2, unsigned int a3, unsigned int a4, char a5, unsigned int *a6, PULONG BitMapBuffer)
{
...
if ( *(_DWORD *)(a2 + 216) <= 1u )
v46 = *(_DWORD *)(a2 + 364);
else
v46 = 0x10000;
...
p_BitMapHeader = &BitMapHeader;
if ( (unsigned int)Feature_371473723__private_IsEnabledDeviceUsageNoInline()
&& v46 - (((_BYTE)v46 - 1) & 7) + 7 < v46 )
{
*(_DWORD *)(a1 + 72) = -1073741566;
ExRaiseStatus(-1073741566);
}
v15 = (v46 - (((_BYTE)v46 - 1) & 7) + 7) >> 3;
PoolWithTag = (PULONG)ExAllocatePoolWithTag((POOL_TYPE)1041, v15 - (((_BYTE)v15 - 1) & 3) + 3, 0x42746146u);
RtlInitializeBitMap(&BitMapHeader, PoolWithTag, v14 - v7 + 1);
...
}
C
복사
3월 보안 업데이트 이후의 FatExamineFatEntries 함수 일부 코드
보안 패치 이후에는 이러한 계산 흐름에 유효성 검사 로직이 추가되었다.
정렬 연산 결과가 원래 클러스터 수보다 작아지는 경우를 감지하여 위험한 계산 흐름이 감지되면 예외를 발생시키고 함수 실행을 중단한다.
이러한 방어 로직은 할당 크기가 왜곡되는 상황을 사전에 차단하고 이후 힙 메모리 초기화 과정에서 발생할 수 있는 메모리 손상 위험을 근본적으로 제거하는 역할을 한다.
4. 취약점 재현 결과
PoC 제작 및 실행 방식에 대한 세부 내용은 공개하지 않지만 아래 영상에서는 취약점이 실제 환경에서 어떤 결과로 이어지는지 확인할 수 있다.
이번 취약점은 특정 입력 조작만으로 커널 메모리와 시스템 자원에 영향을 줄 수 있는 구조였다.
실제 환경에서 이와 같은 취약점이 발생했을 때 현재 보안 인프라가 이를 어디까지 탐지하고 차단할 수 있는지 확인할 방법은 많지 않다.
당사의 솔루션인 PurpleHound는 이러한 실전 기반 시나리오를 재구성하여 기업이 보유한 보안 장비와 시스템이 실제 공격에 대해 어느 수준까지 대응 가능한지 검증할 수 있도록 지원한다.
취약점 공격, 악성코드 활동, 파일 시스템 조작 등 다양한 위협 시나리오를 직접 실행해보며 보안 인프라의 실효성을 객관적으로 확인할 수 있다.