1. 개요
본 보고서는 Windows Telephony Service(TapiSrv)에서 발생하는 CVE-2024-26230 Use-After-Free 취약점에 대해 다룬다. Windows Telephony Service는 TAPI(Telephony Application Programming Interface)를 제공하는 시스템 서비스로, 일반 사용자 권한에서 접근 가능한 RPC 인터페이스를 노출한다.
본 취약점은 FreeDialogInstance 함수에서 GOLD 객체를 해제한 후에도 원본 컨텍스트 핸들이 해당 객체에 대한 포인터를 유지하여 Dangling Pointer가 발생하는 문제이다. 공격자는 이를 악용하여 Heap Feng Shui 기법으로 해제된 메모리 영역을 제어하고, TUISPIDLLCallback 함수에서 가상 함수 호출을 통해 임의 코드를 실행할 수 있다.
성공적인 익스플로잇 시 일반 사용자 권한에서 NT AUTHORITY\NETWORK SERVICE 권한으로 상승하며, 이후 SeImpersonatePrivilege 특권을 활용한 추가 공격으로 SYSTEM 권한까지 상승이 가능하다.
2. 취약점 근본 원인 분석
2.1. 취약점 기본 정보
항목 | 내용 |
취약점 번호 | CVE-2024-26230 |
취약점 이름 | Windows Telephony Service Elevation of Privilege Vulnerability |
취약점 종류 | CWE-416 (Use After Free) |
CVSS | 7.8 (High) - AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
영향받는 버전 | Windows 10/11, Windows Server 2016/2019/2022 (2024년 4월 패치 이전) |
패치 버전 | 2024년 4월 보안 업데이트 (KB5036892 등) |
CVE-2024-26230은 Windows Telephony Service의 GOLD 객체 수명 관리 결함에서 비롯된 Use-After-Free 취약점이다. 서로 다른 컨텍스트 핸들이 동일한 GOLD 객체를 참조할 수 있는 구조적 문제와, 객체 해제 시 모든 참조를 무효화하지 않는 설계 결함이 핵심 원인이다.
2.2. 취약점 발생 흐름 개요
[공격자 프로세스]
│
▼
┌─────────────────┐
│ RPC 바인딩 │ ──► ncalrpc:[tapsrvlpc]
│ (ClientAttach) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Main Context │ ──► GOLD 객체 6개 생성
│ 핸들 생성 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Worker Context │ ──► 별도 컨텍스트 생성
│ 핸들 생성 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ UAF 설정 │ ──► Main에서 GOLD 해제 → Worker에서 재할당 → Main에서 다시 해제
│ (Dangling Ptr) │ ═══► Dangling Pointer 생성
└────────┬────────┘
│
▼
┌─────────────────┐
│ Heap Feng Shui │ ──► 레지스트리 키로 Fake Object 배치
│ │
└────────┬────────┘
│
▼
┌─────────────────┐
│ UAF 트리거 │ ──► TUISPIDLLCallback에서 가상 함수 호출
│ (코드 실행) │ ═══► malloc → VirtualAlloc → memcpy_s → LoadLibraryW
└────────┬────────┘
│
▼
┌─────────────────┐
│ DLL 로드 │ ──► NETWORK SERVICE 권한으로 악성 DLL 실행
│ │
└─────────────────┘
Plain Text
복사
[그림 1] CVE-2024-26230 공격 흐름도
2.3. Windows Telephony Service RPC 인터페이스 구조
Windows Telephony Service는 RPC(Remote Procedure Call) 기반 서비스로 다음 세 가지 주요 인터페이스를 제공한다.
tapisrv.dll (Telephony Service)
│
├─ ClientAttach() ← RPC 연결 및 컨텍스트 핸들 생성
│ └─ PCONTEXT_HANDLE_TYPE 반환 ← 클라이언트별 컨텍스트 생성
│
├─ ClientRequest() ← RPC 요청 처리 (gaFuncs 테이블 디스패치)
│ │
│ ├─ gaFuncs[0x1] GetUIDllName() ← GOLD 객체 생성
│ │ └─ htDlgInst 반환 ← 객체 ID 반환
│ │
│ ├─ gaFuncs[0x2] TUISPIDLLCallback() ← UAF 트리거 코드 실행 지점
│ │ └─ Dangling Pointer의 가상 함수 호출
│ │
│ ├─ gaFuncs[0x3] FreeDialogInstance() ← GOLD 객체 해제 취약점 발생 지점
│ │ └─ 객체 해제 후 포인터 무효화 없음
│ │
│ └─ gaFuncs[0x79] TRequestMakeCall() ← 레지스트리 값을 힙에 할당
│ └─ HandoffPriorities\RequestMakeCall 키 읽기
│
└─ ClientDetach() ← RPC 연결 해제
HTML
복사
[그림 2] tapisrv.dll RPC 인터페이스 및 함수 호출 구조
2.3.1. ClientRequest 함수 - RPC 디스패처
ClientRequest 함수는 클라이언트의 요청을 gaFuncs 테이블의 적절한 함수로 라우팅하는 디스패처 역할을 한다.
__int64 __fastcall ClientRequest(__int64 a1, _DWORD *a2, unsigned int a3, _DWORD *a4)
{
_DWORD *v5 = 0i64;
// 최소 버퍼 크기 검증 (0x3C = 60 bytes)
if ( a3 < 0x3C || *a4 < 8u )
{
if ( a3 >= 4 )
{
*a2 = 0x80000032; // 에러 코드 반환
return result;
}
return TRACELogPrint(65538i64, "RPC buffer too small to even return error!!!");
}
// 첫 번째 DWORD는 gaFuncs 테이블의 인덱스
v9 = (unsigned int)*a2;
// 컨텍스트 객체 참조
result = ReferenceObject(a1, (unsigned int)a1, 0x544E4C43i64); // 'CLNT' 매직
v5 = (_DWORD *)result;
if ( !result )
{
*a2 = 0xF101;
return result;
}
// 함수 인덱스 유효성 검증 (0xA9 = 169개 함수)
if ( (unsigned int)v9 < 0xA9 )
{
if ( *v5 == 0x544E4C43 ) // 컨텍스트 매직 넘버 확인
{
*a2 = 0;
// ★ gaFuncs 테이블에서 함수 포인터 획득 및 호출
result = ((__int64 (__fastcall *)(_DWORD *, _DWORD *, _QWORD, _DWORD *, _DWORD *))
*(&gaFuncs + v9))(
v5, // 컨텍스트 핸들
a2, // 입력/출력 버퍼
a3 - 60, // 사용 가능한 버퍼 크기
a2 + 15, // 파라미터 시작 위치 (60바이트 오프셋)
a4); // 사용된 크기 포인터
}
else
{
*a2 = -2147483566; // 잘못된 컨텍스트
}
}
else
{
*a2 = -2147483575; // 잘못된 함수 인덱스
}
if ( v5 )
return DereferenceObject(v10, (unsigned int)v5[62], 1i64);
return result;
}
```
**ClientRequest 버퍼 구조:**
```
[그림 3] ClientRequest RPC 버퍼 구조
Offset Size Description
─────────────────────────────────────────────────────────
0x00 4 gaFuncs 테이블 인덱스 (함수 선택)
0x04 4 에러 코드 반환 영역
0x08 4 예약 영역
0x0C 48 내부 헤더 영역
0x3C ... 함수별 파라미터 시작 (a2 + 15)
```
#### 2.3.2. gaFuncs 함수 테이블
gaFuncs는 169개(0xA9)의 함수 포인터를 포함하는 전역 테이블이다.
```
[코드 2] gaFuncs 함수 테이블 정의 (.rdata 섹션)
.rdata:000000018003E010 gaFuncs dq offset GetAsyncEvents ; [0x00]
.rdata:000000018003E018 dq offset GetUIDllName ; [0x01] ← GOLD 생성
.rdata:000000018003E020 dq offset TUISPIDLLCallback ; [0x02] ← UAF 트리거
.rdata:000000018003E028 dq offset FreeDialogInstance ; [0x03] ← GOLD 해제
.rdata:000000018003E030 dq offset LAccept ; [0x04]
.rdata:000000018003E038 dq offset LAddToConference ; [0x05]
...
.rdata:000000018003E3D8 dq offset TRequestMakeCall ; [0x79] ← 레지스트리 힙 할당
...
.rdata:000000018003E558 align 20h
C
복사
[코드 1] ClientRequest 함수 - gaFuncs 테이블 디스패치 메커니즘 (tapisrv.dll)
주요 함수 인덱스:
인덱스 | 함수명 | 역할 |
0x01 | GetUIDllName | GOLD 객체 생성 및 ID 반환 |
0x02 | TUISPIDLLCallback | UAF 트리거 (가상 함수 호출) |
0x03 | FreeDialogInstance | GOLD 객체 해제 (취약점 발생) |
0x79 | TRequestMakeCall | 레지스트리 값을 힙에 할당 (Heap Feng Shui) |
2.4. 취약점 상세 분석
2.4.1. GetUIDllName - GOLD 객체 생성
GetUIDllName 함수는 TSP(Telephony Service Provider) UI 다이얼로그를 위한 GOLD 객체를 생성한다.
int __fastcall GetUIDllName(__int64 a1, int *a2, unsigned int a3, void *a4, _DWORD *a5)
{
// ...
// GOLD 객체 메모리 할당 (0x60 = 96 bytes)
LineLookupEntry = (__int64)HeapAlloc(ghTapisrvHeap, 8u, 0x60ui64);
v10 = LineLookupEntry;
if ( !LineLookupEntry )
{
v8 = -2147483580; // 메모리 할당 실패
goto LABEL_81;
}
// 새 객체 ID 생성 및 할당
v14 = NewObject(v13, LineLookupEntry, 0i64);
*(_DWORD *)(v10 + 88) = v14; // offset 0x58: 객체 ID 저장
if ( !v14 )
{
LODWORD(LineLookupEntry) = ServerFree((LPVOID)v10);
goto LABEL_9;
}
// ...
// GOLD 객체 매직 넘버 설정
if ( v10 )
{
*(_DWORD *)v10 = 1196379204; // 0x474F4C44 = 'GOLD'
// 컨텍스트의 다이얼로그 인스턴스 리스트에 추가
v36 = *(_QWORD *)(v6 + 184);
*(_QWORD *)(v10 + 80) = v36; // 다음 포인터
if ( v36 )
*(_QWORD *)(v36 + 72) = v10; // 이전 포인터
*(_QWORD *)(v6 + 184) = v10; // 리스트 헤드 업데이트
// 객체 ID를 출력 파라미터로 반환
LODWORD(LineLookupEntry) = *(_DWORD *)(v10 + 88);
a2[8] = LineLookupEntry;
}
// ...
}
C
복사
[코드 3] GetUIDllName - GOLD 객체 생성 및 초기화 (tapisrv.dll)
GOLD 객체 구조 (96 bytes):
Offset | Size | Description |
0x00 | 4 | 매직 넘버 (0x474F4C44 = 'GOLD') |
0x04~0x0D | 10 | 예약/미사용 |
0x0E | 4 | 비교용 매직 값 (TUISPIDLLCallback에서 사용) |
0x12~0x1D | 12 | 예약/미사용 |
0x1E | 8 | 함수 포인터 (가상 함수 테이블) |
0x26~0x47 | 34 | 예약/미사용 |
0x48 | 8 | 이전 객체 포인터 (링크드 리스트) |
0x50 | 8 | 다음 객체 포인터 (링크드 리스트) |
0x58 | 4 | 객체 ID (htDlgInst) |
0x5C~0x5F | 4 | 예약/미사용 |
2.4.2. FreeDialogInstance - 취약한 객체 해제
FreeDialogInstance 함수는 GOLD 객체를 해제하지만, 다른 컨텍스트에서 해당 객체를 참조하고 있는지 확인하지 않는다.
__int64 __fastcall FreeDialogInstance(unsigned __int64 a1, unsigned int *a2)
{
_DWORD *v4; // GOLD 객체 포인터
// 객체 참조 획득
v4 = (_DWORD *)ReferenceObject(a1, a2[2], 1196379204i64); // 'GOLD' 매직
v82 = v4;
result = TRACELogPrint(524290i64, "FreeDialogInstance: enter, pDlgInst=x%p", v4);
// ★ 매직 넘버 변경 (해제 표시)
if ( *v4 == 1196379204 ) // 0x474F4C44 = 'GOLD'
*v4 = 1280724553; // 0x4C495549 = 'UIIL'
else
*a2 = -2147483576;
// ... (중간 로직 생략)
LABEL_146:
FreeLibrary(*((HMODULE *)v4 + 3));
*a2 = a2[3];
LABEL_149:
// 배타적 접근 획득
if ( WaitForExclusiveClientAccess(a1) )
{
// 링크드 리스트에서 제거
v64 = *((_QWORD *)v4 + 10); // 다음 객체
if ( v64 )
{
*(_QWORD *)(v64 + 72) = *((_QWORD *)v4 + 9); // 다음->이전 = 현재->이전
v64 = *((_QWORD *)v4 + 10);
}
v65 = *((_QWORD *)v4 + 9); // 이전 객체
if ( v65 )
{
*(_QWORD *)(v65 + 80) = v64; // 이전->다음 = 현재->다음
}
else if ( *((_QWORD *)v4 + 3) )
{
*(_QWORD *)(a1 + 184) = v64; // 리스트 헤드 업데이트
}
else
{
*(_QWORD *)(a1 + 192) = v64;
}
LeaveCriticalSection((LPCRITICAL_SECTION)gLockTable +
((unsigned int)gdwPointerToLockTableIndexBits & (a1 >> 4)));
}
// ★ 취약점: DereferenceObject만 호출하고 실제 해제는 참조 카운트가 0이 될 때 발생
// 문제는 다른 컨텍스트가 이 객체를 참조하고 있어도 해제될 수 있다는 점!
return DereferenceObject(v63, a2[2], 2i64);
}
C
복사
[코드 4] FreeDialogInstance - 취약한 GOLD 객체 해제 로직 (tapisrv.dll)
취약점 핵심 원인:
1.
매직 넘버만 변경: 0x474F4C44 → 0x4C495549로 변경하지만, 메모리는 즉시 해제되지 않음
2.
참조 카운트 기반 해제: DereferenceObject 호출 시 참조 카운트가 0이 되면 힙에서 해제
3.
크로스 컨텍스트 검증 없음: Worker 컨텍스트가 해당 GOLD 객체를 참조하고 있어도 Main 컨텍스트에서 해제 가능
4.
Dangling Pointer 생성: 해제 후에도 Worker 컨텍스트의 포인터는 NULL로 설정되지 않음
2.4.3. TRequestMakeCall - Heap Feng Shui Primitive
TRequestMakeCall 함수는 사용자가 제어 가능한 레지스트리 키 값을 힙에 할당하여 Heap Feng Shui 공격의 핵심 프리미티브를 제공한다.
// TRequestMakeCall 함수는 다음 레지스트리 키를 읽어 힙에 할당한다:
// HKCU\Software\Microsoft\Windows\CurrentVersion\Telephony\HandoffPriorities
// Value: RequestMakeCall (REG_BINARY)
LONG TRequestMakeCall(...)
{
HKEY hKey;
DWORD dwSize;
LPVOID lpBuffer;
// 레지스트리 키 열기
if (RegOpenKeyExW(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Telephony\\HandoffPriorities",
0,
KEY_READ,
&hKey) == ERROR_SUCCESS)
{
// 값 크기 확인
RegQueryValueExW(hKey, L"RequestMakeCall", NULL, NULL, NULL, &dwSize);
// ★ 힙에 버퍼 할당 (크기와 내용을 사용자가 제어 가능!)
lpBuffer = HeapAlloc(ghTapisrvHeap, 0, dwSize);
// 레지스트리 값을 힙 버퍼로 복사
RegQueryValueExW(hKey, L"RequestMakeCall", NULL, NULL, lpBuffer, &dwSize);
// ... 이후 로직에서 lpBuffer 사용 ...
RegCloseKey(hKey);
}
return 0;
}
C
복사
[코드 5] TRequestMakeCall - 레지스트리 값 기반 힙 할당 (tapisrv.dll)
공격자 제어 가능 요소:
1.
할당 크기: 레지스트리 값의 크기를 조절하여 원하는 크기의 힙 블록 할당
2.
메모리 내용: 레지스트리 값의 내용을 조작하여 Fake Object 구조 생성
3.
타이밍: TRequestMakeCall 호출 시점을 제어하여 해제된 GOLD 객체 영역을 점유
2.4.4. TUISPIDLLCallback - UAF 트리거 및 코드 실행
TUISPIDLLCallback 함수는 GOLD 객체의 함수 포인터를 호출하여 Use-After-Free를 트리거한다.
LONG TUISPIDLLCallback(PTAPI_CONTEXT pContext, DWORD* pParams)
{
DWORD dwMagic = pParams[1]; // offset 0x04: 비교용 매직 값
DWORD dwCase = pParams[2]; // offset 0x08: case 선택자
// ★ Dangling Pointer 접근!
// Worker 컨텍스트의 GOLD 객체 포인터 (이미 해제되었을 수 있음)
PGOLD_OBJECT pGold = pContext->pGoldObject;
if (dwCase == 3)
{
// offset 0x0E의 매직 값 비교
if (pGold->dwMagic == dwMagic)
{
// ★ offset 0x1E의 함수 포인터 획득
PVOID pfnFunc = *(PVOID*)((BYTE*)pGold + 0x1E);
// ★ 공격자가 제어하는 함수 호출!
// CFG(Control Flow Guard)는 Win32 API 함수는 통과시킴
DWORD result = ((PFUNC)pfnFunc)(
pParams[3], // 첫 번째 인자
pParams[4], // 두 번째 인자
pParams[5], // 세 번째 인자
pParams[6], // 네 번째 인자
pParams[7] // 다섯 번째 인자
);
// 결과를 RPC 버퍼에 저장 (정보 유출)
*(DWORD*)pParams = result;
}
}
return 0;
}
```
**TUISPIDLLCallback 호출 흐름:**
```
[그림 4] TUISPIDLLCallback UAF 트리거 메커니즘
1. ClientRequest → gaFuncs[0x02] → TUISPIDLLCallback
2. pContext->pGoldObject 참조 (Dangling Pointer)
3. offset 0x0E: 매직 값 비교 (0x40000018)
4. offset 0x1E: 함수 포인터 획득
5. 함수 포인터 호출 → Win32 API (CFG 통과)
6. 반환값을 RPC 버퍼에 저장 → 정보 유출
C
복사
[코드 6] TUISPIDLLCallback - UAF 트리거 및 가상 함수 호출 (tapisrv.dll)
2.5. 취약점 근본 원인 요약
원인 | 설명 |
객체 수명 관리 결함 | GOLD 객체 해제 시 모든 컨텍스트의 참조 포인터를 무효화하지 않음 |
크로스 컨텍스트 권한 검증 부재 | Main 컨텍스트가 Worker 컨텍스트의 객체를 임의로 해제 가능 |
레지스트리 신뢰 문제 | 사용자 제어 가능한 레지스트리 값을 검증 없이 힙에 직접 할당 |
CFG 우회 가능성 | Win32 API 함수들은 CFG 검증을 통과하므로 함수 체이닝 가능 |
정보 유출 채널 | TUISPIDLLCallback이 함수 반환값을 RPC 버퍼에 저장 |
3. 시연 영상
[동영상 1] CVE-2024-26230 시연 영상
4. 공격 영향
4.1. 권한 상승 경로
┌─────────────────┐ CVE-2024-26230 ┌─────────────────────┐
│ 일반 사용자 │ ────────────────────► │ NETWORK SERVICE │
│ (Low IL) │ UAF + DLL Load │ (Medium IL) │
└─────────────────┘ └──────────┬──────────┘
│
SeImpersonatePrivilege
│
▼
┌─────────────────────┐
│ NT AUTHORITY\ │
│ SYSTEM │
│ (High IL) │
└─────────────────────┘
Plain Text
복사
4.2. SYSTEM 권한 획득 방법
NETWORK SERVICE 계정은 SeImpersonatePrivilege 특권을 보유하고 있어, Potato 계열 공격으로 SYSTEM 권한 획득이 가능하다.
도구 | 대상 서비스 | 동작 원리 |
PrintSpoofer | Print Spooler | Named Pipe Impersonation |
GodPotato | 여러 COM 서비스 | DCOM/RPCSS 악용 |
JuicyPotato | BITS, DCOM | COM 서버 악용 |
RoguePotato | DCOM | 원격 Potato |
# PrintSpoofer 사용 예시
C:\> whoami
nt authority\network service
C:\> PrintSpoofer.exe -i -c cmd
[+] Impersonating SYSTEM...
C:\> whoami
nt authority\system
Shell
복사
5. 완화 및 권고사항
5.1. 패치 정보
Product | 취약한 버전 | 패치 버전 |
Windows 10 | 모든 버전 (2024년 4월 이전) | KB5036892 이상 |
Windows 11 | 모든 버전 (2024년 4월 이전) | KB5036893 이상 |
Windows Server 2016 | 모든 버전 (2024년 4월 이전) | KB5036899 이상 |
Windows Server 2019 | 모든 버전 (2024년 4월 이전) | KB5036896 이상 |
Windows Server 2022 | 모든 버전 (2024년 4월 이전) | KB5036909 이상 |
5.2. 즉시 조치
우선순위 | 조치사항 |
최우선 | 2024년 4월 보안 업데이트 즉시 적용 |
긴급 | Telephony 서비스 비활성화 (사용하지 않는 경우) |
권장 | EDR/SIEM을 통한 svchost.exe (TapiSrv) 프로세스 모니터링 |
6. 결론
CVE-2024-26230 취약점은 Windows Telephony Service의 GOLD 객체 수명 관리 결함으로 인한 Use-After-Free 취약점이다. 공격자는 Heap Feng Shui 기법을 통해 해제된 메모리 영역을 제어하고, CFG(Control Flow Guard)를 우회하여 Win32 API 함수 체이닝으로 임의 DLL을 로드할 수 있다.
1.
ClientRequest의 gaFuncs 디스패치 구조: 169개 함수 테이블을 통한 RPC 요청 라우팅
2.
GetUIDllName의 GOLD 객체 생성: 96바이트 객체의 정확한 구조 및 링크드 리스트 관리
3.
FreeDialogInstance의 취약한 해제 로직: 크로스 컨텍스트 참조 무효화 누락
4.
TUISPIDLLCallback의 UAF 트리거: offset 0x0E 매직 검증, offset 0x1E 함수 포인터 호출
5.
TRequestMakeCall의 Heap Feng Shui: 사용자 제어 레지스트리를 통한 힙 조작



