2024년에는 네트워크 리디렉터가 Windows 코드 무결성 설계의 잘못된 가정을 위반하여 한 쌍의 커널 익스플로잇을 초래하는 데 어떻게 활용될 수 있는지를 보여주는 새로운 Windows 취약성 클래스인 거짓 파일불변성 (FFI)을 공개했습니다. 이러한 익스플로잇은 Windows 네트워크 드라이브에 의존하여 복잡성을 더하고 킬 체인에 초크 포인트를 생성하여 탐지 및 완화를 쉽게 할 수 있었습니다.
이 연구는 보다 간소화되고 독립적인 착취 방법을 도입하여 발전된 방법을 제시합니다. 이 새로운 접근 방식은 Windows에 내장된 기능을 활용하여 복잡한 SMB 설정 없이도 동일한 파일 수정 우회를 달성합니다. 이 기능의 커널 드라이버가 파일 데이터를 처리하는 방식을 분석하여 공격자가 Windows가 불변이라고 잘못 가정한 파일을 수정하여 개념 증명 커널 익스플로잇으로 이어질 수 있는 보안 우회 방법을 발견했습니다.
핵심 사항:
- 네트워크 리디렉터가 필요하지 않습니다: 이전 익스플로잇과 달리, 새로운 익스플로잇 방법은 Windows 파일 공유를 사용하지 않고도 거짓 파일 불변성을 익스플로잇합니다.
- 기본 제공 기능 익스플로잇: 이 익스플로잇은 클라우드 파일 동기화를 처리하는 Windows 기본 제공 기능 내의 보안 우회 기능을 활용합니다.
- 불변성 위반: Windows 커널과 메모리 관리자가 변경 불가능하다고 잘못 가정한 파일을 수정할 수 있어 커널 익스플로잇으로 이어질 수 있습니다.
- 완화 우회: 공격자가 이전 FFI 익스플로잇을 위해 Microsoft가 특별히 만든 완화 조치를 우회할 수 있도록 합니다.
- 포에버데이: Microsoft는 일부 Windows 버전에서만 이 익스플로잇을 패치하기로 결정했기 때문에 2026년 2월 현재 메인스트림 지원에서 완전히 패치된 일부 Windows 버전에서 계속 작동합니다.
잘못된 파일 불변성
최근 글과 BlueHat IL 2024 강연에서 잘못된 파일 불변성을 기억하실 수도 있지만, 기억이 나지 않는다면 이 섹션이 기억을 되살리는 데 도움이 될 것입니다. 이미 익숙하다면 다음 섹션으로 건너뛰셔도 됩니다.
애플리케이션이 Windows에서 파일을 열 때 일반적으로 어떤 형태의 Win32 CreateFile API를 사용합니다.
HANDLE CreateFileW(
[in] LPCWSTR lpFileName,
[in] DWORD dwDesiredAccess,
[in] DWORD dwShareMode,
[in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
[in] DWORD dwCreationDisposition,
[in] DWORD dwFlagsAndAttributes,
[in, optional] HANDLE hTemplateFile
);
CreateFile 의 호출자는 dwDesiredAccess 에서 원하는 액세스를 지정합니다. 예를 들어, 호출자는 데이터를 읽으려면 FILE_READ_DATA, 데이터를 쓰려면 FILE_WRITE_DATA 을 전달합니다. 전체 액세스 권한 집합은 Microsoft Learn 웹 사이트에 문서화되어 있습니다.
발신자는 dwDesiredAccess 을 통과하는 것 외에도 dwShareMode 에서 FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_SHARE_DELETE 중 0 개 이상으로 구성된 "공유 모드"를 통과해야 합니다 . 공유 모드는 호출자가 "내가 이 파일을 사용하는 동안 다른 사람이 이 파일에 X를 해도 괜찮습니다"라고 선언하는 것으로 생각할 수 있으며, 여기서 X는 읽기, 쓰기, 이름 바꾸기 등이 될 수 있습니다. 예를 들어 FILE_SHARE_WRITE 을 전달하는 호출자는 다른 사람들이 파일을 작업하는 동안 파일을 쓸 수 있도록 허용합니다.
파일이 열리면 호출자의 dwDesiredAccess 이 기존 모든 파일 핸들의 dwShareMode 과 비교하여 테스트됩니다. 동시에 호출자의 dwShareMode 은 해당 파일에 대한 모든 기존 핸들의 이전에 부여된 dwDesiredAccess 과 비교하여 테스트됩니다. 이 테스트 중 하나라도 실패하면 CreateFile은 공유 위반으로 실패합니다.
공유는 필수는 아닙니다. 발신자는 공유 모드를 0으로 전달하여 독점 액세스 권한을 얻을 수 있습니다. Microsoft 문서에 따릅니다:
공유되지 않은 열린 파일(
dwShareMode0으로 설정)은 해당 핸들이 닫힐 때까지 파일을 연 애플리케이션이나 다른 애플리케이션에서 다시 열 수 없습니다. 이를 독점 액세스 권한이라고도 합니다.
공유는 파일 시스템(일반적으로 NTFS)에 의해 적용되지만, Windows는 FAT32와 같은 다른 파일 시스템도 지원합니다. Windows 자체에서 특정 유형의 파일을 열 때 FILE_SHARE_WRITE 을 생략하여 사용 중에 파일을 수정하지 못하도록 합니다. 이렇게 수정할 수 없는 파일은 불변 파일로 간주할 수 있습니다.
어떤 상황에서는 메모리 관리자가 이 불변성에 의존하기도 합니다. 변경 불가능한 메모리 매핑 파일 내에서 페이지 오류가 발생했는데 해당 페이지가 수정되지 않은 경우 메모리 관리자는 해당 페이지의 내용을 원본 백업 파일에서 직접 읽을 수 있습니다. 불변성은 디스크의 파일을 변경할 수 없도록 보장하므로 파일 콘텐츠의 두 번째 복사본을 페이지 파일에 저장할 필요가 없습니다. EXE 및 DLL과 같이 메모리에서 실행되는 실행 파일은 변경할 수 없으므로 메모리 관리자가 이 최적화를 적용할 수 있습니다.
네트워크 리디렉터를 사용하면 파일 경로를 허용하는 모든 API에서 네트워크 경로를 사용할 수 있습니다. 이는 매우 편리하여 사용자와 애플리케이션이 네트워크 드라이브에서 파일을 쉽게 작업하고 프로그램을 실행할 수 있습니다. 커널은 모든 I/O를 원격 머신으로 투명하게 리디렉션합니다. 네트워크 드라이브에서 프로그램이 시작되면 필요에 따라 모든 EXE와 해당 DLL이 네트워크에서 투명하게 가져옵니다.
네트워크 리디렉터를 사용 중인 경우, 파이프의 다른 쪽 끝에 있는 서버가 Windows 컴퓨터일 필요는 없습니다. Samba를 실행하는 Linux 머신일 수도 있고,SMB 네트워크 프로토콜을"말하는" Python Impacket 스크립트일 수도 있습니다. 즉, 서버는 Windows 파일 시스템 공유 의미를 따를 필요가 없습니다. 공격자는 네트워크 리디렉터를 사용하여 서버 측에서 '변경 불가능한' 파일을 수정하여 공유 제한을 우회할 수 있습니다. 즉, 이러한 파일은 변경할 수 없는 것으로 잘못 가정됩니다. 이를 거짓 파일 불변성 (FFI)이라고 부르는 취약점 클래스입니다.
클라우드 파일
하루를 시작하기 위해 집을 나서는데 발걸음 앞에 소포가 놓여 있다고 상상해 보세요. 지난주에 주문하신 그 멋진 Surface Book입니다. 설레지만 시간이 부족해 가방에 넣고 체육관으로 향합니다. Zune에서 신나는 비트에 맞춰 운동한 후, Xbox Live에서 만난 친구를 만나기 위해 동네 레드몬드 커피숍으로 향합니다. 안타깝게도 시간이 늦어져 새 Surface Book을 열고 Windows에 로그인하여 Recall을 설정하려고 합니다. 평범한 커피숍 Wi-Fi를 사용해도 어떻게 된 일인지 1TB의 OneDrive 전체가 바로 눈앞에 나타납니다. 1TB를 그렇게 빨리 다운로드할 수 있는 방법은 없으니 뭔가 마법 같은 일이 벌어지고 있는 게 분명합니다. 그 마법은 바로 클라우드 파일입니다.
Windows 10 버전 1709에 도입된 클라우드 파일을 사용하면 OneDrive와 같은 사용자 모드 애플리케이션이 클라우드 동기화 공급자로 등록하고 시스템에 빈 '플레이스홀더' 파일을 만들 수 있습니다. 처음에는 이러한 자리 표시자가 비어 있습니다(비어 있음). 액세스하면 CloudFiles 커널 드라이버(cldflt.sys)가 I/O를 가로채서 제공업체의 프로세스를 호출합니다. 그런 다음 공급자는 클라우드에서 파일의 콘텐츠를 검색할 수 있습니다. 전체 파일을 한 번에 다운로드할 필요는 없습니다. 1MB만 필요한 경우 해당 1MB만 검색할 수 있습니다. 더 많은 파일을 요청하면 필요에 따라 파일 내용을 계속 재수화(채우기)할 수 있습니다.
드라이버가 파일을 재수화해야 하는 경우 공급자의 프로세스에서 재수화 콜백을 호출합니다(예 OneDrive.exe). 이 콜백은 파일의 콘텐츠를 검색하고(아마도 클라우드에서) CfExecute 을 호출하여 해당 콘텐츠를 드라이버에 전달하면 드라이버가 파일에 씁니다. CloudFiles는 현재 수화되지 않은 파일 영역의 재수화만 요청하지만, 현재 시스템의 여유 공간을 확보하기 위해 파일 수화를 해제 할 수도 있습니다.
익스플로잇 개발
기본적으로 Windows에서는 SMB(서버 메시지 블록) 프로토콜을 사용하여 네트워크를 통해 파일과 폴더를 공유할 수 있습니다. 회사 네트워크에서 공유 네트워크 드라이브에 연결한 적이 있다면 SMB를 사용했을 가능성이 높습니다. Windows에는 기본적으로 SMB 클라이언트와 서버가 모두 포함되어 있습니다. 클라이언트 컴포넌트는 위에서 설명한 대로 네트워크 리디렉터를 제공하여 파일 경로를 허용하는 모든 API를 통해 파일에 대한 투명한 SMB 액세스를 가능하게 합니다. 예를 들어, 지금 바로 \\live.sysinternals.com\Procmon.exe 을 실행하여 인터넷을 통해 프로세스 모니터를 실행할 수 있습니다.
저희는 5월에 블랙햇 아시아 강연과 함께 PPLFault 익스플로잇을 2023 공개했습니다(). PPLFault는 네트워크 리디렉터를 활용하여 보호된 프로세스 라이트(PPL) 프로세스에 로드된 DLL의 FFI를 악용합니다. 초기 프로토타입에는 악성 SMB 서버를 실행하는 공격자가 제어하는 두 번째 머신이 필요했습니다. Windows의 기본 제공 SMB 서버를 비활성화함으로써 악성 SMB 서버를 로컬 머신으로 이동하여 두 번째 머신(프로토타입)에 대한 필요성을 제거할 수 있었습니다.
그러나 당시에는 Windows 기본 제공 SMB 서버를 중지하려면 재부팅해야 한다고 잘못 알고 있었기 때문에 이 문제는 여전히 생각보다 더 복잡했습니다. 다행히도 CloudFiles 공급자와 루프백(로컬 호스트) SMB 어댑터를 결합하는 James Forshaw의 기술을 발견하여 재부팅이 필요 없는 최종 익스플로잇을 만들 수 있었습니다. 간소화된 것 외에도, CloudFiles/SMB 페어링은 파일 공유를 존중해야 하는 일반 Windows SMB 서버를 사용한다는 점에서 이전 두 익스플로잇 버전과 구별됩니다(즉, 다음과 같습니다). FILE_SHARE_*) 의미론. 예를 들어, SMB 클라이언트가 서버에 FILE_SHARE_WRITE 이 없는 파일을 열어두는 동안 서버는 다른 클라이언트가 해당 파일을 열어 쓰기 액세스하는 것을 허용해서는 안 됩니다. 마찬가지로 서버에서 로컬로 실행 중인 실행 파일에 대한 쓰기 권한도 허용하지 않아야 합니다.
우리에게는 모순이 있는 것 같습니다. PPLFault가 파일 공유 제한을 준수해야 한다면 실행 중인 DLL에 어떻게 코드를 삽입할까요? 프로세스 모니터가 무엇을 알려줄 수 있는지 살펴봅시다. 프로세스 모니터에서 PPLFault를 실행하면 다음 세 가지 작업이 표시됩니다(예시용으로 필터링됨). 이 분석은 Windows 11 22631.2861에서 cldflt.sys 10.0.22621.2861 버전으로 수행되었습니다.
작업 순서는 다음과 같습니다:
- 피해 프로세스인
services.exe은 실행 이미지로 DLL을 로드합니다. - 로드된 후
PPLFault.exe을 엽니다. - 열면
PPLFault.exe에 기록됩니다.
여기서 주목해야 할 몇 가지 중요한 사항이 있습니다:
불변성 위반
실행 가능한 이미지로 로드되는 동안 파일에 대한 쓰기 작업이 성공적으로 수행되는 것을 볼 수 있습니다. 이전 FFI 연구에서 바로 이러한 상황을 방지하기 위해 설계된 파일 시스템의 MmFlushImageSection 검사에 대해 설명했습니다. 어떻게 이 검사를 우회할 수 있었나요?
파일 액세스 모델 위반
PPLFault가 파일을 성공적으로 덮어쓴 것을 확인할 수 있습니다. WriteFile에 대한 Microsoft 설명서에는 파일이 쓰기 권한으로 열렸어야 한다고 명시되어 있습니다. FILE_WRITE_DATA로 열어야 하지만 출력에는 "읽기 속성, 쓰기 속성, 동기화"( FILE_READ_ATTRIBUTES, FILE_WRITE_ATTRIBUTES, SYNCHRONIZE)를 위해 열렸음을 보여줍니다. FILE_WRITE_DATA 이 없으면 어떻게 이 파일을 덮어썼나요?
다음 섹션에서 이 두 가지 질문에 답해 보겠습니다.
📘 괴짜 보너스 -
프로세스 모니터는 파일시스템 미니필터 드라이버를 설치하여 시스템의 I/O 활동을 가로채서 기록합니다. Windows는 I/O 작업을 IRP( I/O 요청 패킷 )라는 구조로 캡슐화합니다. 각 미니필터에는 건물의 층수로 생각하면 되는 '고도'가 할당되어 있습니다. 대부분의 IRP는 맨 위층에서 시작하여 아래층으로 내려갑니다. 미니필터가 자체 I/O를 발행하는 경우 해당 IRP는 해당 고도에서 시작하여 거기서부터 아래쪽으로 이동합니다. 즉, 6층에 있는 미니필터는 5층의 I/O를 절대 볼 수 없습니다. 프로세스 모니터의 미니필터 드라이버는 고도
385200에서 실행됩니다. 일반적으로 고도180451에서 실행되는cldflt.sys의 활동은 표시되지 않습니다. 다행히도 /altitude 스위치를 사용하여 프로세스 모니터의 고도를 조정할 수 있으며, CloudFiles 아래에 고도180450로 배치할 수 있습니다.
너를 위한 규칙이지만 나를 위한 규칙은 아닙니다.
앞서 설명한 것처럼 애플리케이션에는 파일 공유 제한이 적용되지만 커널 자체는 항상 같은 방식으로 제한되는 것은 아닙니다. 예를 들어 커널 드라이버는 IoCreateFileEx를 사용하여 파일을 열거나 만들 수 있습니다.
NTSTATUS IoCreateFileEx(
[out] PHANDLE FileHandle,
[in] ACCESS_MASK DesiredAccess,
[in] POBJECT_ATTRIBUTES ObjectAttributes,
[out] PIO_STATUS_BLOCK IoStatusBlock,
[in, optional] PLARGE_INTEGER AllocationSize,
[in] ULONG FileAttributes,
[in] ULONG ShareAccess,
[in] ULONG Disposition,
[in] ULONG CreateOptions,
[in, optional] PVOID EaBuffer,
[in] ULONG EaLength,
[in] CREATE_FILE_TYPE CreateFileType,
[in, optional] PVOID InternalParameters,
[in] ULONG Options,
[in, optional] PIO_DRIVER_CREATE_CONTEXT DriverContext
);
IoCreateFileEx 는 사용자 대면 함수 NtCreateFile 와 매우 유사해 보이지만, 해당 문서에서는 플래그를 지원하는 Options 매개변수 등 몇 가지 중요한 추가 기능을 설명합니다:
IO_IGNORE_SHARE_ACCESS_CHECK
I/O 관리자는 파일 객체가 생성된 후 공유 액세스 검사를 수행하지 않아야 합니다. 그러나 파일 시스템에서 여전히 이러한 검사를 수행할 수 있습니다.
그렇게 간단할까요? 커널 드라이버가 IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) 를 사용하여 사용 중인 DLL을 열어 쓰기 액세스를 할 수 있나요? 커널 드라이버를 작성하여 시험해 보겠습니다. 이 글의 코드는 GitHub에서 Visual Studio 프로젝트로 사용할 수 있습니다.
/*
* This experiment shows that a file opened without FILE_SHARE_WRITE
* can't be modified unless IO_IGNORE_SHARE_ACCESS_CHECK is used.
*/
VOID ExperimentOne()
{
DECLARE_CONST_UNICODE_STRING(filePath, L"\\??\\C:\\TestFile.bin");
NTSTATUS ntStatus = STATUS_SUCCESS;
HANDLE hFile = NULL;
HANDLE hFile2 = NULL;
OBJECT_ATTRIBUTES objAttr{};
IO_STATUS_BLOCK iosb{};
BOOLEAN bSuccessful = FALSE;
BOOLEAN bReportResults = FALSE;
InitializeObjectAttributes(&objAttr, (PUNICODE_STRING)&filePath,
OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
// Create a file without FILE_SHARE_WRITE
// This mimics ntdll!LdrpMapDllNtFileName
ntStatus = ZwCreateFile(
&hFile,
FILE_READ_DATA | FILE_EXECUTE | SYNCHRONIZE,
&objAttr, &iosb, NULL, FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ | FILE_SHARE_DELETE,
FILE_OPEN_IF,
FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE,
NULL, 0);
if (!NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentOne: ZwCreateFile %wZ failed with NTSTATUS 0x%08x\n",
&filePath, ntStatus);
goto Cleanup;
}
bReportResults = TRUE;
// IoCreateFileEx without IO_IGNORE_SHARE_ACCESS_CHECK should not be able to open the file
ntStatus = IoCreateFileEx(
&hFile2,
FILE_WRITE_DATA | SYNCHRONIZE,
&objAttr, &iosb, NULL, FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_OPEN,
FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE,
NULL, 0, CreateFileTypeNone, NULL,
0,
NULL);
if (NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentOne: IoCreateFileEx(FILE_WRITE_DATA) unexpectedly "
"succeeded on a write-sharing-denied file\n");
ntStatus = STATUS_UNSUCCESSFUL;
goto Cleanup;
}
// Can IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) open a
// write-sharing-denied file for write access?
ntStatus = IoCreateFileEx(
&hFile2,
FILE_WRITE_DATA | SYNCHRONIZE,
&objAttr, &iosb, NULL, FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_OPEN,
FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE,
NULL, 0, CreateFileTypeNone, NULL,
IO_IGNORE_SHARE_ACCESS_CHECK,
NULL);
bSuccessful = NT_SUCCESS(ntStatus);
Cleanup:
if (bReportResults)
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentOne complete. "
"IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) %s open a "
"write-sharing-denied file for FILE_WRITE_DATA. "
"Status: 0x%08x\n",
bSuccessful ? "CAN" : "CANNOT",
ntStatus);
}
HandleDelete(hFile);
HandleDelete(hFile2);
}
테스트 서명을 활성화한 상태에서 VM에 로드하면 다음과 같은 결과가 나타납니다:
ExperimentOne complete. IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) CAN open a write-sharing-denied file for FILE_WRITE_DATA. Status: 0x00000000
방금 PPLFault가 '불변' 파일을 수정하는 방법에 대한 그럴듯한 설명이 나왔나요? 그렇지 않습니다. 이 실험은 다소 지나치게 단순화되었지만, 커널 API가 사용자 모드에 비해 더 많은 자유를 제공할 수 있다는 것을 증명하는 IO_IGNORE_SHARE_ACCESS_CHECK 을 실제로 보여줍니다.
PPLFault에서 CloudFiles는 쓰기 공유가 거부된 핸들로 파일을 수정하는 것만이 아닙니다. 오히려 실행 가능한 이미지로 메모리에 매핑된 상태에서 DLL을 수정하는 것입니다. PPLFault 시나리오에 조금 더 가까운 다른 실험을 해보겠습니다. 실험 2에서는 에뮬레이션 LoadLibrary 를 에뮬레이션하고 SEC_IMAGE 섹션을 만든 다음 해당 섹션의 보기를 메모리에 매핑합니다. 뷰가 매핑되면 핸들을 닫고 IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) 쓰기 가능한 핸들을 가져올 수 있는지 테스트합니다.
LoadLibrary 과 유사하게 PE를 이미지 섹션으로 매핑하는 헬퍼 함수부터 시작해 보겠습니다. 실험을 단일 드라이버로 유지하기 위해 커널에서 이 작업을 수행하겠지만, 기능적으로는 LoadLibrary 와 동일하다는 점에 유의하세요.
// Emulate a portion of LoadLibrary
NTSTATUS MapFileAsImageSection(
PCUNICODE_STRING pPath,
HANDLE* phFile,
HANDLE* phSection,
PVOID* ppMappedBase
)
{
NTSTATUS ntStatus = STATUS_SUCCESS;
HANDLE hFile = NULL;
HANDLE hSection = NULL;
PVOID pMappedBase = NULL;
SIZE_T viewSize = 0;
OBJECT_ATTRIBUTES objAttr{};
IO_STATUS_BLOCK iosb{};
InitializeObjectAttributes(&objAttr, (PUNICODE_STRING)pPath,
OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
// From ntdll!LdrpMapDllNtFileName
// NtOpenFile(&FileHandle, 0x100021u, &ObjectAttributes, &IoStatusBlock, 5u, 0x60u);
ntStatus = ZwOpenFile(
&hFile,
FILE_READ_DATA | FILE_EXECUTE | SYNCHRONIZE,
&objAttr, &iosb,
FILE_SHARE_READ | FILE_SHARE_DELETE,
FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE);
if (!NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"MapFileAsImageSection: ZwCreateFile %wZ failed with NTSTATUS 0x%08x\n",
pPath, ntStatus);
goto Cleanup;
}
InitializeObjectAttributes(&objAttr, NULL, OBJ_KERNEL_HANDLE, NULL, NULL);
// From ntdll!LdrpMapDllNtFileName
// NtCreateSection(&Handle, 0xDu, 0LL, 0LL, 0x10u, v18, FileHandle);
ntStatus = ZwCreateSection(&hSection,
SECTION_QUERY | SECTION_MAP_READ | SECTION_MAP_EXECUTE,
&objAttr, NULL, PAGE_EXECUTE, SEC_IMAGE, hFile
);
if (!NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"MapFileAsImageSection: ZwCreateSection %wZ failed with NTSTATUS 0x%08x\n",
pPath, ntStatus);
goto Cleanup;
}
// From ntdll!LdrpMinimalMapModule
// Map a view of this SEC_IMAGE section into lower half of the the System process address space
ntStatus = ZwMapViewOfSection(
hSection, ZwCurrentProcess(), &pMappedBase, 0, 0, NULL,
&viewSize, ViewShare, 0, PAGE_EXECUTE_WRITECOPY);
if (!NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"MapFileAsImageSection: ZwMapViewOfSection %wZ failed with NTSTATUS 0x%08x\n",
pPath, ntStatus);
goto Cleanup;
}
// Move ownership to output parameters and prevent cleanup
*ppMappedBase = pMappedBase;
pMappedBase = NULL;
*phFile = hFile;
hFile = NULL;
*phSection = hSection;
hSection = NULL;
Cleanup:
HandleDelete(hFile);
HandleDelete(hSection);
if (pMappedBase)
{
NTSTATUS unmapStatus = ZwUnmapViewOfSection(ZwCurrentProcess(), pMappedBase);
NT_ASSERT(NT_SUCCESS(unmapStatus));
}
return ntStatus;
}
이제 이 헬퍼를 사용하여 DLL을 매핑한 다음 IO_IGNORE_SHARE_ACCESS_CHECK 로 쓸 수 있는지 확인해 보겠습니다:
/*
* This experiment shows that a file opened without FILE_SHARE_WRITE can't be
* modified even if IO_IGNORE_SHARE_ACCESS_CHECK is used because the file has
* an associated active SEC_IMAGE section.
*/
VOID ExperimentTwo()
{
DECLARE_CONST_UNICODE_STRING(filePath, L"\\SystemRoot\\System32\\TestDll.dll");
NTSTATUS ntStatus = STATUS_SUCCESS;
HANDLE hFile = NULL;
HANDLE hSection = NULL;
HANDLE hFile2 = NULL;
OBJECT_ATTRIBUTES fileObjAttr{};
OBJECT_ATTRIBUTES sectionObjAttr{};
IO_STATUS_BLOCK iosb{};
BOOLEAN bSuccessful = FALSE;
BOOLEAN bReportResults = FALSE;
PVOID pMappedBase = NULL;
PFILE_OBJECT pFileObject = NULL;
ntStatus = MapFileAsImageSection(
&filePath, &hFile, &hSection, &pMappedBase);
if (!NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentThree: MapFileAsImageSection %wZ failed with NTSTATUS 0x%08x\n",
&filePath, ntStatus);
goto Cleanup;
}
// MmFlushImageSection should return FALSE. This is what fails the FILE_WRITE_DATA request below.
// MmFlushImageSection requires SECTION_OBJECT_POINTERS, which we can get from the FILE_OBJECT.
ntStatus = ObReferenceObjectByHandle(hFile, 0, *IoFileObjectType, KernelMode, (PVOID*)&pFileObject, NULL);
if (!NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentThree: ObReferenceObjectByHandle %wZ failed with NTSTATUS 0x%08x\n",
&filePath, ntStatus);
goto Cleanup;
}
if (MmFlushImageSection(pFileObject->SectionObjectPointer, MmFlushForWrite))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentThree: MmFlushImageSection unexpectedly succeeded %wZ\n",
&filePath);
goto Cleanup;
}
// Now that a view of the SEC_IMAGE mapping exists, close the file and section handles to remove them from the equation
// We're trying to test whether IO_IGNORE_SHARE_ACCESS_CHECK can bypass the MmFlushImageSection check here:
// https://github.com/Microsoft/Windows-driver-samples/blob/622212c3fff587f23f6490a9da939fb85968f651/filesys/fastfat/create.c#L3572-L3593
ReferenceDelete(pFileObject);
HandleDelete(hFile);
HandleDelete(hSection);
InitializeObjectAttributes(&fileObjAttr, (PUNICODE_STRING)&filePath, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
// Can IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) open a file mapped as SEC_IMAGE for write access?
ntStatus = IoCreateFileEx(
&hFile2,
FILE_WRITE_DATA | SYNCHRONIZE,
&fileObjAttr, &iosb, NULL, FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_OPEN,
FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE,
NULL, 0, CreateFileTypeNone, NULL,
IO_IGNORE_SHARE_ACCESS_CHECK,
NULL);
bSuccessful = NT_SUCCESS(ntStatus);
bReportResults = TRUE;
Cleanup:
if (bReportResults)
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentTwo complete. "
"IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) %s open a "
"file backing a local SEC_IMAGE section for FILE_WRITE_DATA. "
"Status: 0x%08x\n",
bSuccessful ? "CAN" : "CANNOT",
ntStatus);
}
HandleDelete(hFile);
HandleDelete(hSection);
HandleDelete(hFile2);
ReferenceDelete(pFileObject);
if (pMappedBase)
{
NTSTATUS unmapStatus = ZwUnmapViewOfSection(ZwCurrentProcess(), pMappedBase);
NT_ASSERT(NT_SUCCESS(unmapStatus));
}
}
이 실험을 실행하면 다음과 같은 결과를 얻을 수 있습니다:
ExperimentTwo complete. IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) CANNOT open a file backing a local SEC_IMAGE section for FILE_WRITE_DATA. Status: 0xc0000043
IoCreateFileEx 이 경우 실행 가능한 이미지로 매핑된 파일은 핸들이 열려 있지 않아도 변경할 수 없도록 추가 보호 기능이 있기 때문에 0xC0000043 (STATUS_SHARING_VIOLATION)이 실패했습니다. 이 검사는 Microsoft FastFat 샘플 드라이버 코드의 MmFlushImageSection API를 사용하여 확인할 수 있지만, NTFS를 비롯한 다른 파일 시스템에도 존재합니다:
//
// If the user wants write access access to the file make sure there
// is not a process mapping this file as an image. [ ... ]
//
if (FlagOn(*DesiredAccess, FILE_WRITE_DATA) || DeleteOnClose) {
[ ... ]
if (!MmFlushImageSection( &Fcb->NonPaged->SectionObjectPointers,
MmFlushForWrite )) {
Iosb.Status = DeleteOnClose ? STATUS_CANNOT_DELETE :
STATUS_SHARING_VIOLATION;
try_return( Iosb );
}
}
IO_IGNORE_SHARE_ACCESS_CHECK 플래그는 I/O 관리자 검사는 우회하지만 파일 시스템의 MmFlushImageSection 검사는 우회하지 않습니다. IO_IGNORE_SHARE_ACCESS_CHECK 의 설명을 다시 읽어보니 지금 생각해보면 분명합니다:
IO_IGNORE_SHARE_ACCESS_CHECK
I/O 관리자는 파일 객체가 생성된 후 공유 액세스 검사를 수행하지 않아야 합니다. 그러나 파일 시스템에서 여전히 이러한 검사를 수행할 수 있습니다.
실험 2는 네트워크 드라이브에서 DLL을 로드하는 PPLFault를 공정하게 표현하지 못합니다. 네트워크 클라이언트가 서버에서 파일을 열면 SMB 클라이언트 드라이버는 해당 논리 파일을 나타내는FCB( 파일 제어 블록) 구조를 할당합니다. 이에 따라 서버는 요청된 공유 모드로 파일을 열고 자체 FCB를 할당합니다. 즉, 서로 다른 의미를 가진 두 개의 별개의 FCB가 사용된다는 의미입니다. 클라이언트가 DLL을 실행 파일로 메모리에 매핑하면 결과물인 SEC_IMAGE 파일 매핑(일명 섹션)이 해당 FCB와 연결되므로 MmFlushImageSection 의 보호를 받게 됩니다. 서버는 이에 따라 이미지 섹션을 생성하지 않으므로 FCB는 이러한 보호 기능을 얻지 못합니다. PPLFault는 MmFlushImageSection 확인을 우회하여 서버의 FCB에 쓰기를 수행함으로써 이 차이를 악용합니다.
실험 3에서 이를 시도해 보겠습니다:
/*
* This experiment shows that a file loaded as a DLL by an SMB client can't be modified
* server-side unless IO_IGNORE_SHARE_ACCESS_CHECK is used.
*/
VOID ExperimentThree()
{
DECLARE_CONST_UNICODE_STRING(filePathLocal,
L"\\SystemRoot\\System32\\TestDll.dll");
DECLARE_CONST_UNICODE_STRING(filePathSMB,
L"\\Device\\Mup\\127.0.0.1\\c$\\Windows\\System32\\TestDll.dll");
NTSTATUS ntStatus = STATUS_SUCCESS;
HANDLE hFile = NULL;
HANDLE hSection = NULL;
HANDLE hFile2 = NULL;
OBJECT_ATTRIBUTES fileObjAttr{};
OBJECT_ATTRIBUTES sectionObjAttr{};
IO_STATUS_BLOCK iosb{};
BOOLEAN bSuccessful = FALSE;
BOOLEAN bReportResults = FALSE;
PVOID pMappedBase = NULL;
ntStatus = MapFileAsImageSection(
&filePathSMB, &hFile, &hSection, &pMappedBase);
if (!NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentThree: MapFileAsImageSection %wZ failed with NTSTATUS 0x%08x\n",
&filePathSMB, ntStatus);
goto Cleanup;
}
// Now that a view of the SEC_IMAGE mapping exists,
// close the file and section handles to remove them from the equation.
// We're trying to test whether IO_IGNORE_SHARE_ACCESS_CHECK can bypass the
// MmFlushImageSection check here:
// https://github.com/Microsoft/Windows-driver-samples/blob/622212c3fff587f23f6490a9da939fb85968f651/filesys/fastfat/create.c#L3572-L3593
HandleDelete(hFile);
HandleDelete(hSection);
InitializeObjectAttributes(&fileObjAttr,
(PUNICODE_STRING)&filePathLocal, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
bReportResults = TRUE;
// Can IoCreateFileEx() open a file mapped as SEC_IMAGE for write access?
ntStatus = IoCreateFileEx(
&hFile2,
FILE_WRITE_DATA | SYNCHRONIZE,
&fileObjAttr, &iosb, NULL, FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_OPEN,
FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE,
NULL, 0, CreateFileTypeNone, NULL,
0,
NULL);
if (NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentThree: IoCreateFileEx(FILE_WRITE_DATA) unexpectedly succeeded "
"on a file mapped as SEC_IMAGE remotely by an SMB client\n");
ntStatus = STATUS_UNSUCCESSFUL;
goto Cleanup;
}
// Can IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) open
// a file mapped as SEC_IMAGE for write access?
ntStatus = IoCreateFileEx(
&hFile2,
FILE_WRITE_DATA | SYNCHRONIZE,
&fileObjAttr, &iosb, NULL, FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_OPEN,
FILE_SYNCHRONOUS_IO_NONALERT | FILE_NON_DIRECTORY_FILE,
NULL, 0, CreateFileTypeNone, NULL,
IO_IGNORE_SHARE_ACCESS_CHECK,
NULL);
bSuccessful = NT_SUCCESS(ntStatus);
bReportResults = TRUE;
Cleanup:
if (bReportResults)
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentThree complete. "
"IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) %s open a "
"file backing a remote SEC_IMAGE view for FILE_WRITE_DATA. "
"Status: 0x%08x\n",
bSuccessful ? "CAN" : "CANNOT",
ntStatus);
}
HandleDelete(hFile);
HandleDelete(hSection);
HandleDelete(hFile2);
if (pMappedBase)
{
NTSTATUS unmapStatus = ZwUnmapViewOfSection(ZwCurrentProcess(), pMappedBase);
NT_ASSERT(NT_SUCCESS(unmapStatus));
}
}
ExperimentThree는 다음과 같은 출력을 생성합니다:
ExperimentThree complete. IoCreateFileEx(IO_IGNORE_SHARE_ACCESS_CHECK) CAN open a file backing a remote SEC_IMAGE view for FILE_WRITE_DATA. Status: 0x00000000
위의 실험 3은 커널 드라이버가 해당 파일의 서버 버전에서 IO_IGNORE_SHARE_ACCESS_CHECK 플래그를 사용하여 SMB 클라이언트가 매핑한 DLL을 수정할 수 있는 방법을 보여줍니다.
소매를 걷어붙이고
방금 어떤 것이 가능한지 보여드렸지만, 클라우드 파일이 실제로 어떤 기능을 하는지는 아직 알 수 없습니다. 앞서 제기된 질문에 대한 답을 찾기 위해 프로세스 모니터 출력에 대해 자세히 살펴보겠습니다.
앞서 두 가지 질문을 드렸습니다:
불변성 위반
실행 가능한 이미지로 로드되는 동안 파일에 대한 쓰기 작업이 성공적으로 수행되는 것을 볼 수 있습니다. 이전 FFI 연구에서 바로 이러한 상황을 방지하기 위해 설계된 파일 시스템의MmFlushImageSection검사에 대해 설명했습니다. 어떻게 이 검사를 우회할 수 있었나요?파일 액세스 모델 위반
PPLFault가 파일을 성공적으로 덮어쓴 것을 확인할 수 있습니다. WriteFile에 대한 Microsoft 설명서에는 파일이 쓰기 권한으로 열렸어야 한다고 명시되어 있습니다.FILE_WRITE_DATA로 열어야 하지만 출력에는 "읽기 속성, 쓰기 속성, 동기화"(FILE_READ_ATTRIBUTES,FILE_WRITE_ATTRIBUTES,SYNCHRONIZE)를 위해 열렸음을 보여줍니다.FILE_WRITE_DATA이 없으면 어떻게 이 파일을 덮어썼나요?
MmFlushImageSection 바이패스는 쉽게 설명할 수 있습니다. 이 수표는 여기서는 사용되지 않은 FILE_WRITE_DATA 을 찾습니다. 파일은 "속성 읽기, 속성 쓰기, 동기화"에 대해서만 열렸습니다. 그러나 파일 액세스 모델 위반에 대해서는 설명할 수 없습니다. 쓰기 불가능한 파일을 어떻게 덮어썼나요? WriteFile 작업의 통화 스택을 확대하여 확인해 보겠습니다.
호출 스택에서 호출 의 176 줄을 PPLFault.cpp cldapi.dll!CfExecute 볼 수 있습니다. (행 24-25)를 호출하는 것을 볼 수 있습니다. 결국 cldflt.sys!HsmiRecallWriteFileNoLock 호출 FltWriteFileEx. FltWriteFileEx 가 쓰기 권한을 위해 열리지 않은 파일에 어떻게든 쓸 수 있습니다. 커널 디버거를 연결하여 자세히 살펴보겠습니다.
FltWriteFileEx 에 중단점을 설정하고 익스플로잇을 다시 실행하면 HsmiRecallWriteFileNoLock 으로의 호출에서 중단할 수 있습니다:
2: kd> bp fltmgr!FltWriteFileEx
2: kd> g
Breakpoint 0 hit
FLTMGR!FltWriteFileEx:
fffff800`425aad40 4055 push rbp
0: kd> k
# Child-SP RetAddr Call Site
00 ffffb90e`faa968e8 fffff800`5c2878d3 FLTMGR!FltWriteFileEx
01 ffffb90e`faa968f0 fffff800`5c2b2ccc cldflt!HsmiRecallWriteFileNoLock+0x2df
02 ffffb90e`faa969f0 fffff800`5c2b25f8 cldflt!HsmRecallTransferData+0x25c
03 ffffb90e`faa96aa0 fffff800`5c2b35d7 cldflt!CldStreamTransferData+0x65c
04 ffffb90e`faa96bd0 fffff800`5c27196c cldflt!CldiSyncTransferOrAckDataByObject+0x4c7
05 ffffb90e`faa96cb0 fffff800`5c2bb568 cldflt!CldiSyncTransferOrAckData+0xdc
06 ffffb90e`faa96d10 fffff800`5c2bafe1 cldflt!CldiPortProcessTransferData+0x46c
07 ffffb90e`faa96db0 fffff800`5c27895a cldflt!CldiPortProcessTransfer+0x291
08 ffffb90e`faa96e50 fffff800`4259530a cldflt!CldiPortNotifyMessage+0xd9a
09 ffffb90e`faa96f70 fffff800`425cf299 FLTMGR!FltpFilterMessage+0xda
0a ffffb90e`faa96fd0 fffff800`42597e60 FLTMGR!FltpMsgDispatch+0x179
0b ffffb90e`faa97040 fffff800`3eaebef5 FLTMGR!FltpDispatch+0xe0
0c ffffb90e`faa970a0 fffff800`3ef40060 nt!IofCallDriver+0x55
0d ffffb90e`faa970e0 fffff800`3ef41a90 nt!IopSynchronousServiceTail+0x1d0
0e ffffb90e`faa97190 fffff800`3ef41376 nt!IopXxxControlFile+0x700
0f ffffb90e`faa97380 fffff800`3ec2bbe8 nt!NtDeviceIoControlFile+0x56
10 ffffb90e`faa973f0 00007ffe`b074f454 nt!KiSystemServiceCopyEnd+0x28
11 000000dc`e7bff448 00007ffe`99383ca2 ntdll!NtDeviceIoControlFile+0x14
12 000000dc`e7bff450 00007ffe`99383251 FLTLIB!FilterpDeviceIoControl+0x136
13 000000dc`e7bff4c0 00007ffe`94f3b12b FLTLIB!FilterSendMessage+0x31
14 000000dc`e7bff510 00007ffe`94f36059 cldapi!CfpExecuteTransferData+0x103
15 000000dc`e7bff690 00007ff7`ac9216e0 cldapi!CfExecute+0x349
16 000000dc`e7bff730 00000029`8969cee4 PPLFault!FetchDataCallback+0x4b0 [C:\git\PPLFault\PPLFault\PPLFault.cpp @ 176]
FltWriteFileEx 에 대한 두 번째 파라미터에 있는 핸들(~= FILE_OBJECT)에 어떤 종류의 액세스 권한이 부여되었는지 살펴봅시다. x64의 경우 rdx 입니다.
0: kd> dt _FILE_OBJECT @rdx ReadAccess WriteAccess DeleteAccess SharedRead SharedWrite SharedDelete Flags
ntdll!_FILE_OBJECT
+0x04a ReadAccess : 0 ''
+0x04b WriteAccess : 0 ''
+0x04c DeleteAccess : 0 ''
+0x04d SharedRead : 0 ''
+0x04e SharedWrite : 0 ''
+0x04f SharedDelete : 0x1 ''
+0x050 Flags : 0x4000a
0: kd> !fileobj @rdx
Device Object: 0xffffa909953848f0 \Driver\volmgr
Vpb: 0xffffa90995352ee0
Event signalled
Access: SharedDelete
Flags: 0x4000a
Synchronous IO
No Intermediate Buffering
Handle Created
FsContext: 0xffffcf04ac4c6170 FsContext2: 0xffffcf04a7d1cad0
CurrentByteOffset: 0
Cache Data:
Section Object Pointers: ffffa90999f44378
Shared Cache Map: 00000000
File object extension is at ffffa9099a4c5f40:
Flags: 00000001
Ignore share access checks.
쓰기 액세스를 위해 파일이 열리지 않았음을 알 수 있으며 "공유 액세스 확인 무시"는 IO_IGNORE_SHARE_ACCESS_CHECK 와 매우 유사하게 들립니다. FltWriteFileEx 의 세 번째 및 네 번째 파라미터인 ByteOffset 및 Length 파라미터를 각각 r8 및 r9 에 저장되어 있는지 확인해 보겠습니다.
0: kd> dx ((PLARGE_INTEGER)@r8)->QuadPart
((PLARGE_INTEGER)@r8)->QuadPart : 0 [Type: __int64]
0: kd> dx (int)@r9
(int)@r9 : 90112 [Type: int]
오프셋 0 에 90,112 바이트 쓰기 - ProcMon 출력과 일치합니다. 여섯 번째 매개변수인 Flags 은 어떻게 되나요?
0: kd> dx *(PULONG)(@rsp+(8*6))
*(PULONG)(@rsp+(8*6)) : 0xa [Type: unsigned long]
0xA 는 0x2 | 0x8, 즉 FLTFL_IO_OPERATION_PAGING | FLTFL_IO_OPERATION_SYNCHRONOUS_PAGING 입니다. 이는 ProcMon에서 보았던 "페이징 I/O, 동기식 페이징 I/O"와 일치합니다.
드라이버에서 이를 재현할 수 있는지 살펴봅시다. 실험 2에서 했던 것처럼 로컬로 매핑된 DLL을 열되, FILE_WRITE_DATA 대신 CloudFiles와 동일한 권한을 요청하겠습니다: SYNCHRONIZE | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES 입니다. 이렇게 하면 FILE_WRITE_DATA 을 찾는 MmFlushImageSection 검사가 트립되지는 않지만 CloudFiles의 동작을 더 가깝게 복제하기 위해 IO_IGNORE_SHARE_ACCESS_CHECK 을 넣을 것입니다. 다음으로 FltWriteFileEx 을 사용하여 쓰기 불가능한 파일 오브젝트에 동기식 페이징 쓰기를 수행합니다.
간결성을 위해 일부 헬퍼 코드는 생략합니다. 이 글의 모든 예제 코드는 GitHub에서 확인할 수 있습니다.
VOID ExperimentFour()
{
DECLARE_CONST_UNICODE_STRING(filePath,
L"\\SystemRoot\\System32\\TestDll.dll");
NTSTATUS ntStatus = STATUS_SUCCESS;
HANDLE hFile = NULL;
HANDLE hSection = NULL;
HANDLE hFile2 = NULL;
OBJECT_ATTRIBUTES fileObjAttr{};
IO_STATUS_BLOCK iosb{};
BOOLEAN bSuccessful = FALSE;
BOOLEAN bReportResults = FALSE;
PVOID pMappedBase = NULL;
PFILE_OBJECT pFileObject = NULL;
PFLT_INSTANCE pInstance = NULL;
PFLT_VOLUME pVolume = NULL;
LARGE_INTEGER byteOffset{};
ULONG bytesWritten = 0;
ntStatus = MapFileAsImageSection(
&filePath, &hFile, &hSection, &pMappedBase);
if (!NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentFour: MapFileAsImageSection %wZ failed with NTSTATUS 0x%08x\n",
&filePath, ntStatus);
goto Cleanup;
}
// Find our own minifilter instance for the volume containing this file
// We'll need it later
ntStatus = GetMyInstanceForFile(hFile, &pVolume, &pInstance);
if (!NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentFour: GetMyInstanceForFile failed with NTSTATUS 0x%08x\n",
ntStatus);
goto Cleanup;
}
// Now that a view of the SEC_IMAGE mapping exists,
// close the file and section handles because that's what ntdll does
// https://github.com/Microsoft/Windows-driver-samples/blob/622212c3fff587f23f6490a9da939fb85968f651/filesys/fastfat/create.c#L3572-L3593
HandleDelete(hFile);
HandleDelete(hSection);
InitializeObjectAttributes(&fileObjAttr,
(PUNICODE_STRING)&filePath, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
// Open the file without FILE_WRITE_DATA
// cldflt.sys!HsmiOpenFile uses this instead of IoCreateFileEx
ntStatus = FltCreateFileEx2(
gpFilter,
NULL,
&hFile2,
&pFileObject,
SYNCHRONIZE | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES,
&fileObjAttr, &iosb, NULL, 0,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
FILE_OPEN,
FILE_NO_INTERMEDIATE_BUFFERING | FILE_SYNCHRONOUS_IO_NONALERT |
FILE_NON_DIRECTORY_FILE | FILE_OPEN_REPARSE_POINT,
NULL, 0,
IO_IGNORE_SHARE_ACCESS_CHECK,
NULL);
if (!NT_SUCCESS(ntStatus))
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentFour: IoCreateFileEx failed with NTSTATUS 0x%08x\n",
ntStatus);
goto Cleanup;
}
// cldflt.sys is using FltWriteFileEx with synchronous paging I/O
ntStatus = FltWriteFileEx(
pInstance, pFileObject, &byteOffset,
sizeof(gZeroBuf), gZeroBuf,
FLTFL_IO_OPERATION_PAGING | FLTFL_IO_OPERATION_SYNCHRONOUS_PAGING,
&bytesWritten, NULL, NULL, NULL, NULL);
// If FltWriteFileEx returns success without us passing FILE_WRITE_DATA,
// then we have succeeded
bSuccessful = NT_SUCCESS(ntStatus) && (sizeof(gZeroBuf) == bytesWritten);
bReportResults = TRUE;
Cleanup:
if (bReportResults)
{
DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL,
"ExperimentFour complete. "
"FltWriteFileEx %s be used to write to a non-writable FILE_OBJECT "
"Status: 0x%08x\n",
bSuccessful ? "CAN" : "CANNOT",
ntStatus);
}
HandleDelete(hFile);
HandleDelete(hSection);
HandleDelete(hFile2);
if (pMappedBase)
{
NTSTATUS unmapStatus = ZwUnmapViewOfSection(ZwCurrentProcess(), pMappedBase);
NT_ASSERT(NT_SUCCESS(unmapStatus));
}
ReferenceDelete(pFileObject);
if (pInstance) FltObjectDereference(pInstance);
if (pVolume) FltObjectDereference(pVolume);
}
이 실험의 결과는 다음과 같습니다:
ExperimentFour complete. FltWriteFileEx CAN be used to write to a non-writable FILE_OBJECT Status: 0x00000000
이는 FltWriteFileEx 이 여러 규칙을 위반하는 데 사용될 수 있음을 증명합니다. PPLFault와 이 실험 사이에는 중요한 차이점이 있습니다: 이 실험은 네트워크 리디렉터 없이도 성공했으며, 로컬로 매핑되든 SMB를 통해 매핑되든 상관없이 CloudFiles만으로도 사용 중인 실행 파일을 수정할 수 있음을 증명했습니다. 좀 더 추상적으로 말하자면, 네트워크 리디렉터 없이도 CloudFiles를 통한 FFI 악용이 가능하다는 것을 증명합니다.
새로운 익스플로잇
Microsoft의 PPLFault 완화는 특히 네트워크 리디렉터를 통해 로드되는 실행 파일을 대상으로 합니다. 네트워크 리디렉터 없이도 동일한 효과를 얻기 위해 여기서 발견한 내용을 적용할 수 있을까요?
CI가 서명 확인을 위해 DLL을 요청하면 PPLFault는 CfExecute 를 사용하여 데이터 가져오기 콜백에서 플레이스홀더에 쓰기(재수화)합니다. 서명 확인을 위해 원본 파일이 제공되면 페이로드로 전환하여 동일한 콜백 중에 CfExecute를 두 번 호출하여 파일의 일부를 페이로드로 덮어씁니다. 피해자가 루프백 SMB가 아닌 로컬에서 DLL을 로드하도록 PPLFault를 조정하면 CfExecute 로의 두 번째 호출이 "사용자가 클라우드 작업을 취소했습니다."라는 메시지와 함께 실패합니다. 다른 접근 방식이 필요했습니다.
C:\Users\user\Desktop>PPLFault.exe 760 services.dmp
[+] Ready. Spawning WinTcb.
[+] SpawnPPL: Waiting for child process to finish.
[!] CfExecute #2 failed with HR 0x8007018e: The cloud operation was canceled by user.
[!] Did not find expected dump file: services.dmp
몇 가지 리버스 엔지니어링을 통해 이 오류는 I/O 관리자나 파일 시스템과의 상호 작용이 아닌 CloudFilter 자체의 검사로 인한 것임을 알게 되었습니다. 전화를 걸고 CfDehydratePlaceholder 를 호출한 다음 CfHydratePlaceholder 를 호출하면 다른 스레드(재수화 콜백 외부)에서 CloudFilter 드라이버 내부의 파일 상태가 재설정되어 재수화 콜백이 다시 호출되는 것을 발견했습니다. 이를 통해 사용 중인 DLL을 페이로드로 덮어쓰고 WinTcb-Light로 임의의 코드를 실행할 수 있었습니다. 이 작은 코드 변경으로 PPLFault가 부활했기 때문에 이 변형을 Redux라고 명명했습니다.
마찬가지로 고도의 권한이 있는 PPL 액세스 권한을 활용하여 커널 메모리를 손상시키고 Windows Defender의 프로세스 보호를 우회하여 정상적으로 실행할 수 없는 프로세스를 종료하는 GodFault를 부활시켰습니다.
Redux 및 GodFault-Redux에 대한 PoC는 GitHub에서 확인할 수 있습니다.
아래 동영상은 완전히 업데이트된 Windows Server 2022 (2월 2026 버전 20348.4773)에서 다음을 보여줍니다.
- PPLFault 덤프 실패
lsass - Redux 덤핑 성공
lsass - 관리자가 PPL이라는 이유로
MsMpEng.exe을 종료하지 못하는 경우 - GodFault-Redux 종료 성공
MsMpEng.exe
완화
MSRC에 대한 보고서에서 다음 기준을 모두 충족하는 IRP_MJ_ACQUIRE_FOR_SECTION_SYNCHRONIZATION 작업을 차단하여 Redux를 완화하는 파일 시스템 미니필터를 제공했습니다:
- 요청자가 PPL입니다.
- 요청자의
PreviousMode은UserMode입니다. - 페이지 보호가 실행 가능합니다(예
PAGE_EXECUTE_READ) 또는 할당 속성에SEC_IMAGE이 포함되어 있습니다. - 파일에는
IO_REPARSE_TAG_CLOUD과 같은 클라우드 필터 재분석 태그가 있습니다.
완화 기능은 Elastic Defend 버전 8.14 이상에 내장되어 있습니다. 차량에서 영향을 받는 운영 체제를 실행하는 경우 고급 정책 방어에서 다음을 설정하여 이를 활성화할 수 있습니다.
windows.advanced.flags: e931849d52535955fcaa3847dd17947b
이 완화 조치가 적용되면 익스플로잇이 차단됩니다:
C:\Users\user\Desktop>Redux 624 services.dmp
[+] Ready. Spawning WinTcb.
[+] SpawnPPL: Waiting for child process to finish.
[!] SpawnPPL: WaitForSingleObject returned 258. Expected WAIT_OBJECT_0. GLE: 2
[!] Did not find expected dump file: services.dmp
동시에 Windows에 STATUS_ACCESS_DENIED (0xC0000022) 상태 코드가 포함된 팝업이 표시됩니다.
완화 기능에 대한 PoC는 GitHub에서 확인할 수 있습니다.
공개 및 수정
공개 일정은 다음과 같습니다:
- 2024-02-14 MSRC에 Redux를 보고했습니다.
- 2024-02-29 윈도우 디펜더 팀에서 공개를 조정하기 위해 연락을 취했습니다.
- 2024-10-01 Windows 11 24H2는 완화 조치로 GA에 도달했습니다.
MSRC에 Redux를 공개했을 당시에는 정식 패치된 Windows 11 버전에서는 작동했지만, 실험적인 Insider Canary 빌드 25936에서는 작동하지 않았습니다. Windows Defender 팀과 이 문제를 논의하던 중, (전) Microsoft 수석 보안 연구원이었던 필립 츠커만이 PPLFault의 변종을 찾던 중 이 문제를 발견했으며, 수정 사항은 아직 출시 전 테스트 중이라는 사실을 알게 되었습니다.
아래 표는 발행일 기준으로 영향을 받는 Windows 버전과 수정된 버전을 보여줍니다.
| 운영 체제 | 라이프사이클 | 수정 상태 |
|---|---|---|
| Windows 11 24H2 | 메인스트림 지원 | 수정됨 |
| Windows 10 엔터프라이즈 LTSC 2021 | 메인스트림 지원 | 2월 현재 여전히 작동 중 2026 (19044.6937) |
| Windows Server 2025 | 메인스트림 지원 | 수정됨 |
| Windows Server 2022 | 메인스트림 지원 | 2월 현재 여전히 작동 중 2026 (20348.4773) |
| Windows Server 2019 | 확장 지원 | 2월 현재 여전히 작동 중 2026 (17763.8389) |
결론
2024년에 저희는 새로운 Windows 취약점 클래스인 거짓 파일 불변성(FFI)을 공개하고, 두 가지 커널 익스플로잇을 공개하여 이를 시연했습니다: PPLFault와 ItsNotASecurityBoundary. 두 익스플로잇 모두 네트워크 리디렉터를 활용하여 Windows 코드 무결성의 설계 결함을 악용합니다. 이번 연구에서는 네트워크 리디렉터 없이 FFI를 익스플로잇하는 방법을 보여주는 또 다른 익스플로잇을 선보이고 공개했습니다. 2024년 2월에 보고된 이 익스플로잇은 세 번째 FFI 익스플로잇이었으며, 이후 최소 두 차례 더 발생한 것으로 추정됩니다.
리덕스는 FFI의 끝이 아니며 악용 가능한 FFI 취약점은 더 많습니다.
