다음을 통해 공유


사용자 모드 접근자

UMA(사용자 모드 접근자)는 커널 모드 코드에서 사용자 모드 메모리에 안전하게 액세스하고 조작하도록 설계된 DDI 집합입니다. 이러한 DDIS는 커널 모드 드라이버가 사용자 모드 메모리에 액세스할 때 발생할 수 있는 일반적인 보안 취약성 및 프로그래밍 오류를 해결합니다.

사용자 모드 메모리에 액세스/조작하는 커널 모드 코드는 곧 UMA를 사용해야 합니다.

커널 모드에서 사용자 모드 메모리에 액세스할 때 발생할 수 있는 문제

커널 모드 코드가 사용자 모드 메모리에 액세스해야 하는 경우 다음과 같은 몇 가지 문제가 발생합니다.

  • 사용자 모드 애플리케이션은 커널 모드 코드에 악의적이거나 잘못된 포인터를 전달할 수 있습니다. 적절한 유효성 검사가 부족하면 메모리 손상, 충돌 또는 보안 취약성이 발생할 수 있습니다.

  • 사용자 모드 코드는 다중 스레드입니다. 따라서 서로 다른 스레드가 별도의 커널 모드 액세스 간에 동일한 사용자 모드 메모리를 수정하여 커널 메모리가 손상될 수 있습니다.

  • 커널 모드 개발자는 액세스하기 전에 사용자 모드 메모리를 검색하는 것을 잊어버리는 경우가 많습니다. 이는 보안 문제입니다.

  • 컴파일러는 단일 스레드 실행을 가정하고 중복 메모리 액세스로 보이는 것을 최적화할 수 있습니다. 이러한 최적화를 인식하지 못하는 프로그래머는 안전하지 않은 코드를 작성할 수 있습니다.

다음 코드 조각에서는 이러한 문제를 보여 줍니다.

예제 1: 사용자 모드에서 다중 스레딩으로 인한 메모리 손상 가능성

사용자 모드 메모리에 액세스해야 하는 커널 모드 코드는 메모리가 유효한지 확인하기 위해 블록 내에서 __try/__except 수행해야 합니다. 다음 코드 조각은 사용자 모드 메모리에 액세스하기 위한 일반적인 패턴을 보여 줍니다.

// User-mode structure definition
typedef struct _StructWithData {
    ULONG Size;
    CHAR* Data[1];
} StructWithData;

// Kernel-mode call that accesses user-mode memory
void MySysCall(StructWithData* Ptr) {
    __try {
        // Probe user-mode memory to ensure it's valid
        ProbeForRead(Ptr, sizeof(StructWithData), 1);

        // Allocate memory in the kernel
        PVOID LocalData = ExAllocatePool2(POOL_FLAG_PAGED, Ptr->Size);
        
        // Copy user-mode data into the heap allocation
        RtlCopyMemory(LocalData, Ptr->Data, Ptr->Size);
    } __except (…) {
        // Handle exceptions
    }
}

이 코드 조각은 먼저 메모리를 검색합니다. 이 단계는 중요하지만 자주 간과되는 단계입니다.

그러나 이 코드에서 발생할 수 있는 한 가지 문제는 사용자 모드의 멀티스레딩 때문입니다. 특히 Ptr->SizeExAllocatePool2 를 호출한 후 RtlCopyMemory를 호출하기 전에 변경되어 커널의 메모리가 손상될 수 있습니다.

예제 2: 컴파일러 최적화로 인한 가능한 문제

예제 1에서 다중 스레딩 문제를 해결하기 위한 방법은 할당 및 복사 전에 Ptr->Size을 지역 변수로 복사하는 것일 수 있습니다.

void MySysCall(StructWithData* Ptr) {
    __try {
        // Probe user-mode memory to ensure it's valid
        ProbeForRead(Ptr, sizeof(StructWithData), 1);
        
        // Read Ptr->Size once to avoid possible memory change in user mode
        ULONG LocalSize = Ptr->Size;
        
        // Allocate memory in the kernel
        PVOID LocalData = ExAllocatePool2(POOL_FLAG_PAGED, LocalSize);
        
        //Copy user-mode data into the heap allocation
        RtlCopyMemory(LocalData, Ptr, LocalSize);
    } __except (…) {}
}

이 방법은 다중 스레딩으로 인한 문제를 완화하지만 컴파일러가 여러 스레드를 인식하지 못하므로 단일 실행 스레드를 가정하므로 안전하지 않습니다. 컴파일러는 최적화로 Ptr->Size가 가리키는 값의 복사본이 스택에 이미 있으므로 LocalSize로 복사를 수행하지 않을 수 있습니다.

사용자 모드 액세서 솔루션

UMA 인터페이스는 커널 모드에서 사용자 모드 메모리에 액세스할 때 발생하는 문제를 해결합니다. UMA는 다음을 제공합니다.

  • 자동 검색: 모든 UMA 함수가 주소 안전을 보장하기 때문에 명시적 검색(ProbeForRead/ProbeForWrite)이 더 이상 필요하지 않습니다.

  • 휘발성 액세스: 모든 UMA DDI는 컴파일러 최적화를 방지하기 위해 휘발성 의미 체계를 사용합니다.

  • 이식성 용이성: 포괄적인 UMA DDI 집합을 통해 고객은 기존 코드를 쉽게 포팅하여 UMA DDIs를 사용하여 사용자 모드 메모리에 안전하고 올바르게 액세스할 수 있습니다.

UMA DDI 사용 예제

다음 코드 조각에서는 이전에 정의된 사용자 모드 구조를 사용하여 UMA를 사용하여 사용자 모드 메모리에 안전하게 액세스하는 방법을 보여 줍니다.

void MySysCall(StructWithData* Ptr) {
    __try {

        // This UMA call probes the passed user-mode memory and does a
        // volatile read of Ptr->Size to ensure it isn't optimized away by the compiler.
        ULONG LocalSize = ReadULongFromUser(&Ptr->Size);
        
        // Allocate memory in the kernel.
        PVOID LocalData = ExAllocatePool2(POOL_FLAG_PAGED, LocalSize);
        
        //This UMA call safely copies UM data into the KM heap allocation.
        CopyFromUser(&LocalData, Ptr, LocalSize);
        
        // To be safe, set LocalData->Size to be LocalSize, which was the value used
        // to make the pool allocation just in case LocalData->Size was changed.
        ((StructWithData*)LocalData)->Size = LocalSize;

    } __except (…) {}
}

UMA 구현 및 사용

UMA 인터페이스는 WDK(Windows 드라이버 키트)의 일부로 제공됩니다.

  • 함수 선언은 usermode_accessors.h 헤더 파일에 있습니다.
  • 함수 구현은 umaccess.lib라는 정적 라이브러리에 있습니다.

UMA는 최신 버전뿐만 아니라 모든 버전의 Windows에서 작동합니다. usermode_accessors.humaccess.lib에서 각각 함수 선언 및 구현을 얻으려면 최신 WDK를 사용해야 합니다. 결과 드라이버는 이전 버전의 Windows에서 잘 실행됩니다.

Umaccess.lib 는 모든 DPI에 대해 안전하고 하위 수준 구현을 제공합니다. UMA 인식 버전의 Windows 커널에서 드라이버는 모든 함수가 ntoskrnl.exe구현된 안전한 버전으로 리디렉션됩니다.

사용자 모드 메모리에 액세스할 때 잠재적인 예외로 인해 모든 사용자 모드 접근자 함수는 SEH(구조적 예외 처리기) 내에서 실행되어야 합니다.

사용자 모드 접근자 DDI 유형

UMA는 다양한 유형의 사용자 모드 메모리 액세스를 위한 다양한 DDI를 제공합니다. 대부분의 이러한 DDI는 부울, ULONG 및 포인터와 같은 기본 데이터 형식을 위한 것입니다. 또한 UMA는 대량 메모리 액세스, 문자열 길이 검색 및 연동 작업을 위한 DDI를 제공합니다.

기본 데이터 형식에 대한 제네릭 DDI

UMA는 간단한 데이터 형식을 읽고 쓰기 위한 6가지 함수 변형을 제공합니다. 예를 들어 다음 함수는 BOOLEAN 값에 사용할 수 있습니다.

함수 이름 Description
ReadBooleanFromUser 사용자 모드 메모리에서 값을 읽습니다.
ReadBooleanFromUserAcquire 메모리 순서 지정에 대한 의미 체계를 획득하여 사용자 모드 메모리에서 값을 읽습니다.
ReadBooleanFromMode 모드 매개 변수를 기반으로 사용자 모드 또는 커널 모드 메모리에서 읽습니다.
WriteBooleanToUser 사용자 모드 메모리에 값을 씁니다.
WriteBooleanToUserRelease 메모리 순서 지정에 대한 릴리스 의미 체계를 사용하여 사용자 모드 메모리에 값을 씁니다.
WriteBooleanToMode 모드 매개 변수를 기반으로 사용자 모드 또는 커널 모드 메모리에 씁니다.

ReadXxxFromUser 함수의 경우 Source 매개변수는 사용자 모드 가상 주소 공간(VAS)을 가리켜야 합니다. ReadXxxFromMode 버전에서도 Mode == UserMode 같은 방식이 적용됩니다.

XxxFromMode 읽기Mode == KernelMode경우, Source 매개 변수가 반드시 커널 모드 VAS를 가리켜야 합니다. 전처리기 정의 DBG가 정의되면 FAST_FAIL_KERNEL_POINTER_EXPECTED 코드로 작업이 빠르게 중단됩니다.

WriteXxxToUser 함수에서 대상 매개 변수는 사용자 모드 VAS를 가리킵니다. 쓰기 XxxToMode 버전Mode == UserMode에서도 마찬가지입니다.

복사 및 메모리 조작 DDI

UMA는 비템포럴 및 정렬된 복사본에 대한 변형을 포함하여 사용자 모드와 커널 모드 간에 메모리를 복사하고 이동하기 위한 함수를 제공합니다. 이러한 함수는 잠재적인 SEH 예외 및 IRQL 요구 사항(최대 APC_LEVEL)을 나타내는 주석으로 표시됩니다.

예를 들어 CopyFromUser, CopyToModeCopyFromUserToMode가 있습니다.

CopyFromModeAlignedCopyFromUserAligned와 같은 매크로에는 복사 작업을 수행하기 전에 안전을 위한 맞춤 검색이 포함됩니다.

CopyFromUserNonTemporalCopyToModeNonTemporal과 같은 매크로는 캐시 오염을 방지하는 비템포럴 복사본을 제공합니다.

읽기/쓰기 매크로의 구조

모드 간 구조체를 읽고 쓰는 매크로는 형식 호환성과 맞춤을 보장하며 크기 및 모드 매개 변수를 사용하여 도우미 함수를 호출합니다. 예를 들어 WriteStructToMode, ReadStructFromUser 및 정렬된 변형이 있습니다.

채우기 및 0 메모리 함수

DDI는 대상, 길이, 채우기 값 및 모드를 지정하는 매개 변수를 사용하여 사용자 또는 모드 주소 공간에서 메모리를 채우거나 0으로 지정하기 위해 제공됩니다. 이러한 함수는 SEH 및 IRQL 주석도 전달합니다.

예를 들어 FillUserMemoryZeroModeMemory가 있습니다.

연동 작업

UMA에는 동시 환경에서 스레드로부터 안전한 메모리 조작에 필수적인 원자성 메모리 액세스를 위한 연동 작업이 포함됩니다. DDI는 사용자 또는 모드 메모리를 대상으로 하는 버전이 있는 32비트 및 64비트 값 모두에 대해 제공됩니다.

예를 들어 InterlockedCompareExchangeToUser, InterlockedOr64ToModeInterlockedAndToUser가 있습니다.

문자열 길이 DDI

사용자 또는 모드 메모리에서 안전하게 문자열 길이를 결정하는 함수가 포함되어 ANSI 및 와이드 문자열을 모두 지원합니다. 이러한 함수는 안전하지 않은 메모리 액세스에 대한 예외를 발생하도록 설계되었으며 IRQL이 제한됩니다.

StringLengthFromUserWideStringLengthFromMode를 예로 들 수 있습니다.

큰 정수 및 유니코드 문자열 접근자

UMA는 사용자 및 커널 모드 메모리 간에 LARGE_INTEGER, ULARGE_INTEGER 및 UNICODE_STRING 형식을 읽고 쓸 수 있는 DDI를 제공합니다. 변형은 모드 매개변수를 사용하여 획득 및 해제 시맨틱스를 제공함으로써 안전성과 정확성을 보장합니다.

예를 들어 ReadLargeIntegerFromUser, WriteUnicodeStringToModeWriteULargeIntegerToUser가 있습니다.

의미 체계 획득 및 해제

ARM과 같은 일부 아키텍처에서 CPU는 메모리 액세스의 순서를 변경할 수 있습니다. 메모리 액세스가 사용자 모드 액세스에 대해 다시 정렬되지 않도록 보장해야 하는 경우 제네릭 DDI에는 모두 Acquire/Release 구현이 있습니다.

  • 의미 체계를 획득하면 다른 메모리 작업에 비해 부하가 다시 정렬되지 않습니다.
  • 릴리스 의미 체계는 다른 메모리 작업을 기준으로 저장소의 순서를 다시 지정하지 못하게 합니다.

UMA의 획득 및 릴리스 의미 체계의 예로는 ReadULongFromUserAcquireWriteULongToUserRelease입니다.

자세한 내용은 획득 및 릴리스 의미 체계를 참조하세요.

모범 사례

  • 커널 코드에서 사용자 모드 메모리에 액세스할 때는 항상 UMA DDI를 사용합니다.
  • 적절한 블록을 __try/__except.
  • 코드가 사용자 모드 및 커널 모드 메모리를 모두 처리할 수 있는 경우 모드 기반 DDI를 사용합니다.
  • 사용 사례에 메모리 순서 지정이 중요한 경우 획득/해제 의미 체계를 고려합니다.
  • 복사한 데이터를 커널 메모리에 복사한 후 유효성을 검사하여 일관성을 보장합니다.

향후 하드웨어 지원

사용자 모드 접근자는 다음과 같은 향후 하드웨어 보안 기능을 지원하도록 설계되었습니다.

  • SMAP(감독자 모드 액세스 방지): 커널 코드가 UMA DDIs와 같은 지정된 함수를 제외하고 사용자 모드 메모리에 액세스하지 못하도록 합니다.
  • ARM PAN(특권 접근 거부): ARM 아키텍처에서의 유사한 보호.

UMA DDI를 일관되게 사용하면 드라이버가 향후 Windows 버전에서 사용하도록 설정되면 이러한 보안 향상 기능과 호환됩니다.