ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Windows Minifilter 드라이버에서 Fast I/O와 FLT_PREOP_PENDING 충돌 문제 해결
    윈도우/커널 덤프분석 2025. 12. 14. 11:50
    반응형

    개요

    Windows 커널 모드 minifilter 드라이버를 개발하면서 Driver Verifier 환경에서 BSOD가 발생하는 문제를 겪었습니다. 이 글에서는 Fast I/O 경로에서 FLT_PREOP_PENDING을 반환할 때 발생하는 문제와 그 해결 방법을 공유합니다.


    문제 상황

    증상

    • BSOD 코드: SYSTEM_SERVICE_EXCEPTION (0x3B)
    • Exception 코드: 0x80000003 (Breakpoint)
    • 발생 조건: Driver Verifier 활성화 상태에서 파일 쓰기 작업 수행 시

    크래시 덤프 분석

    STACK_TEXT:
    nt!DebugPrompt+0x17
    nt!DbgPrompt+0x44
    FLTMGR!FltpvPrintErrors+0x188
    FLTMGR!FltpvVerifyPreOperationStatus+0x27f    ← 여기서 검증 실패
    FLTMGR!FltvPreOperation+0x290
    FLTMGR!FltpPerformPreCallbacksWorker+0x36c
    FLTMGR!FltpPassThroughFastIo+0xc3             ← Fast I/O 경로
    FLTMGR!FltpFastIoWrite+0x165
    nt!IopWriteFile+0x137
    nt!NtWriteFile+0xd0

    핵심은 FltpvVerifyPreOperationStatus - Filter Manager의 Verifier가 PreOperation 콜백의 반환값을 검증하다가 실패한 것입니다.


    원인 분석

    Fast I/O란?

    Windows 파일 시스템에는 두 가지 I/O 경로가 있습니다:

    구분 IRP-based I/O Fast I/O
    처리 방식 IRP 패킷 생성 후 처리 직접 함수 호출
    동기성 비동기 가능 항상 동기
    용도 일반적인 I/O 캐시된 데이터 빠른 접근
    PENDING 지원 ✅ 가능 불가능

    문제의 코드

    저희 드라이버는 파일 변조 전 백업을 위해 비동기 큐 시스템을 사용했습니다:

    // PreWrite 콜백에서...
    if (NT_SUCCESS(RTQueueBackupRequest(...))) {
        // 백업 큐에 추가 성공 → PENDING 반환
        return FLT_PREOP_PENDING;  // ← Fast I/O에서는 이게 문제!
    }

    IRP-based I/O에서는 이 방식이 정상 동작합니다. 하지만 Fast I/O는 동기적 작업이므로 FLT_PREOP_PENDING을 반환하면 안 됩니다.

    Driver Verifier는 이 규칙 위반을 감지하고 시스템을 중단시킨 것입니다.


    해결 방법

    FLT_IS_FASTIO_OPERATION 매크로 활용

    Filter Manager는 현재 작업이 Fast I/O인지 확인하는 매크로를 제공합니다:

    NTSTATUS
    RTQueueBackupRequest(
        _In_ PFLT_CALLBACK_DATA Data,
        _In_ PCFLT_RELATED_OBJECTS FltObjects,
        ...
    )
    {
        // Paging I/O 체크
        if (IsPagingIo) {
            return STATUS_NOT_SUPPORTED;
        }
    
        //
        // [핵심] Fast I/O는 PENDING 불가!
        // Fast I/O 경로에서 FLT_PREOP_PENDING 반환 시 Driver Verifier가 BSOD 발생
        // Fast I/O는 동기적이어야 하므로 백업을 건너뛰고 I/O 진행
        //
        if (FLT_IS_FASTIO_OPERATION(Data)) {
            DbgPrint("BackupQueue: Fast I/O - cannot PENDING, skipping backup\n");
            return STATUS_NOT_SUPPORTED;
        }
    
        // IRP-based I/O만 큐에 추가하여 PENDING 처리
        ...
    }

    호출부 수정

    // PreWrite 콜백
    NTSTATUS statusQ = RTQueueBackupRequest(Data, FltObjects, ...);
    
    if (NT_SUCCESS(statusQ)) {
        // IRP-based I/O만 여기 도달
        return FLT_PREOP_PENDING;
    }
    // Fast I/O는 STATUS_NOT_SUPPORTED로 실패 → 백업 없이 진행

    설계 고려사항

    Fast I/O에서 백업을 건너뛰어도 괜찮을까?

    결론: 대부분의 경우 괜찮습니다.

    1. 랜섬웨어 특성: 대부분의 랜섬웨어는 대용량 파일을 암호화하므로 IRP-based I/O를 사용합니다. Fast I/O는 주로 작은 캐시된 데이터 접근에 사용됩니다.
    2. 캐시 일관성: Fast I/O로 수정된 데이터도 결국 디스크에 쓰일 때는 IRP-based I/O를 거칩니다. 이때 백업이 수행됩니다.
    3. 성능 최적화: Fast I/O는 성능이 중요한 경로이므로 동기적 백업이 오히려 시스템 성능을 저하시킬 수 있습니다.

    대안: 동기 백업

    만약 Fast I/O에서도 반드시 백업이 필요하다면:

    if (FLT_IS_FASTIO_OPERATION(Data)) {
        // 동기적으로 즉시 백업 수행 (PENDING 없이)
        RTBackupFileSync(FltObjects, OriginalPath);
        return FLT_PREOP_SUCCESS_WITH_CALLBACK;
    }

    단, 이 방식은 성능 저하를 유발할 수 있습니다.


    추가 발견: IRQL 문제

    같은 프로젝트에서 발견한 또 다른 BSOD 원인도 공유합니다.

    문제

    DRIVER_IRQL_NOT_LESS_OR_EQUAL (0xD1) - SpinLock 내에서 Paged Pool 메모리 접근

    원인

    Fast I/O 경로에서 minifilter 콜백이 호출될 때, 파일 경로 정보(UNICODE_STRING)가 Paged Pool에 있을 수 있습니다. SpinLock을 획득하면 IRQL이 DISPATCH_LEVEL로 상승하는데, 이 상태에서 Paged Pool에 접근하면 BSOD가 발생합니다.

    해결

    SpinLock 획득 전에 데이터를 스택 버퍼(NonPaged)로 복사:

    BOOLEAN RTIsSelfProtectedPath(PCUNICODE_STRING FilePath)
    {
        // [IRQL 안전] FilePath를 스택 버퍼에 복사
        WCHAR LocalPathBuffer[520];
        USHORT CopyLength;
    
        CopyLength = min(FilePath->Length, sizeof(LocalPathBuffer) - sizeof(WCHAR));
    
        __try {
            RtlCopyMemory(LocalPathBuffer, FilePath->Buffer, CopyLength);
        } __except (EXCEPTION_EXECUTE_HANDLER) {
            return FALSE;
        }
    
        // 이제 SpinLock 획득 후에도 안전하게 접근 가능
        KeAcquireSpinLock(&gProtectedPathLock, &OldIrql);
        // LocalPathBuffer 사용 (스택 = NonPaged)
        ...
        KeReleaseSpinLock(&gProtectedPathLock, OldIrql);
    }

    정리

    Minifilter PreOperation 콜백에서 주의할 점

    상황 FLT_PREOP_PENDING 권장 처리
    IRP-based I/O ✅ 가능 비동기 큐 사용 가능
    Fast I/O ❌ 불가 동기 처리 또는 건너뛰기
    Paging I/O ❌ 불가 항상 건너뛰기

    IRQL 관련 주의사항

    IRQL Paged Pool 접근 권장 처리
    PASSIVE_LEVEL ✅ 가능 일반적인 처리
    APC_LEVEL ✅ 가능 일반적인 처리
    DISPATCH_LEVEL ❌ 불가 스택/NonPaged 버퍼 사용

    디버깅 팁

    1. Driver Verifier 활용: 개발 초기부터 Driver Verifier를 켜고 테스트하면 잠재적 문제를 조기에 발견할 수 있습니다.
    2. FLT_IS_FASTIO_OPERATION 체크: PENDING을 반환하기 전에 항상 확인하세요.
    3. IRQL 체크: SpinLock 사용 전에 외부에서 전달된 포인터가 가리키는 데이터를 로컬 버퍼로 복사하세요.

    참고 자료

    반응형

    댓글

Designed by Tistory.