제로데이 익스플로잇에 매우 근접한 Microsoft 커널 스트리밍 서비스

마감일을 끝내기 위해 사무실에서 늦게까지 일하는 수염 난 남성과 그 뒤에 앉아 있는 두 명의 동료가 찍힌 장면

지난 달 Microsoft는 카메라 디바이스의 가상화 및 공유에 사용되는 Windows 커널 구성 요소인 Microsoft Kernel Streaming Server의 취약점을 패치했습니다. CVE-2023-36802 취약점은 로컬 공격자가 시스템으로 권한을 상승시킬 수 있게 합니다.

이 블로그 게시물에서는 Windows 커널의 새로운 공격 표면을 탐색하고, 제로데이 취약점을 찾고, 흥미로운 버그 클래스를 탐색하고, 안정적인 익스플로잇을 구축하는 프로세스를 자세히 설명합니다. 이 게시물을 따라하기 위해 전문적인 Windows 커널 지식이 필요하지는 않지만 메모리 손상 및 운영 체제 개념에 대한 기본적인 이해가 있으면 도움이 됩니다. 또한 익숙하지 않은 커널 드라이버에서 초기 분석을 수행하는 기본을 다루고, 새로운 타겟을 보는 과정을 단순화할 것입니다.

공격 표면

Microsoft Kernel Streaming Server(mskssrv.sys)는 Windows Multimedia Framework 서비스인 Frame Server의 구성 요소입니다. 이 서비스는 카메라 디바이스를 가상화하고 여러 애플리케이션 간에 디바이스를 공유할 수 있도록 합니다.

저는 처음에 TPM 드라이버 취약점으로 나열된 CVE-2023-29360에 주목한 후 이 공격 표면을 탐색하기 시작했습니다. 버그는 실제로 Microsoft Kernel Streaming Server에 있습니다. 당시에는 MS KS Server에 익숙하지 않았지만 이 드라이버의 이름만으로도 관심을 끌기에 충분했습니다. 그 목적이나 기능에 대해서는 아무것도 몰랐지만 커널의 스트리밍 서버가 취약점을 찾을 수 있는 유용한 장소가 될 수 있다고 생각했습니다. 무작정 들어가서 다음과 같은 질문에 답하려고 노력했습니다.

  • 권한이 없는 애플리케이션이 이 커널 모듈과 어느 정도 상호 작용할 수 있나요?
  • 모듈이 직접 처리하는 애플리케이션의 데이터 유형은 무엇인가요?

첫 번째 질문에 답하기 위해 먼저 디스어셈블러에서 바이너리를 분석했습니다. 앞서 언급한 취약점, 즉, 간단하고 우아한 논리 버그를 빠르게 파악했습니다. 이 문제는 트리거하고 완전히 익스플로잇하는 것이 간단해 보였기 때문에 저는 mskssrv.sys 드라이버의 내부 작동을 더 잘 이해하기 위해 빠른 개념 증명을 개발하고자 했습니다.

전문가의 인사이트를 바탕으로 한 최신 기술 뉴스

Think 뉴스레터를 통해 AI, 자동화, 데이터 등 가장 중요하고 흥미로운 업계 동향에 대한 최신 소식을 받아보세요. IBM 개인정보 보호정책을 참조하세요.

감사합니다! 구독이 완료되었습니다.

구독한 뉴스레터는 영어로 제공됩니다. 모든 뉴스레터에는 구독 취소 링크가 있습니다. 여기에서 구독을 관리하거나 취소할 수 있습니다. 자세한 정보는 IBM 개인정보 보호정책을 참조하세요.

초기 분석

MS KS 서버 내에서 실행 트리거하기

먼저, 사용자 공간 애플리케이션에서 드라이버에 접근할 수 있어야 합니다. 이 취약한 함수는 드라이버의 DispatchDeviceControl 루틴에서 접근할 수 있으며, 이는 드라이버에 IOCTL을 발행하여 접근할 수 있음을 의미합니다. 이렇게 하려면 디바이스의 경로를 사용하여 CreateFile을 호출하여 드라이버의 디바이스에 대한 핸들을 가져와야 합니다. 일반적으로 디바이스 이름/경로는 드라이버에서 IoCreateDevice 호출을 찾아 디바이스 이름이 포함된 세 번째 파라미터를 확인하면 쉽게 찾을 수 있습니다.

디바이스 이름에 대한 NULL 포인터를 사용하여 IoCreateDevice를 호출하는 mskssrv.sys 내부 함수

디바이스 이름에 대한 NULL 포인터를 사용하여 IoCreateDevice를 호출하는 mskssrv.sys 내부 함수

이 경우 디바이스 이름에 대한 매개변수는 NULL입니다. 호출 함수 이름을 보면 mskssrv가 PnP 드라이버임을 알 수 있으며, IoAttachDeviceToDeviceStack에 대한 호출은 생성된 디바이스 객체가 디바이스 스택의 일부임을 나타냅니다. 사실상 이는 디바이스에 I/O 요청이 전송될 때 여러 드라이버가 호출된다는 의미입니다. PnP 디바이스의 경우 디바이스에 액세스하려면 디바이스 인터페이스 경로가 필요합니다.

WinDbg 커널 디버거를 사용하여 mskssrv 드라이버와 디바이스 스택에 속한 디바이스를 확인할 수 있습니다.

상위 및 하위 디바이스를 표시하는 !drvobj 및 !devobj 명령의 아웃풋

상위 및 하위 디바이스를 표시하는 !drvobj 및 !devobj 명령의 아웃풋

위에서는 mskssrv의 디바이스가 swenum.sys 드라이버에 속하는 하위 디바이스 객체에 연결되어 있고, 상위 디바이스가 ksthunk.sys에 속하는 상위 디바이스에 연결되어 있는 것을 볼 수 있습니다.

디바이스 관리자에서 대상 디바이스 인스턴스 ID를 찾을 수 있습니다.

디바이스 인스턴스 ID 및 인터페이스 GUID를 보여주는 디바이스 관리자

디바이스 인스턴스 ID 및 인터페이스 GUID를 보여주는 디바이스 관리자

이제 구성 관리자 또는 SetupApi 함수를 사용하여 디바이스 인터페이스 경로를 가져올 수 있는 충분한 정보를 확보했습니다. 검색된 디바이스 인터페이스 경로를 사용하여 디바이스에 대한 핸들을 열 수 있습니다.

마지막으로, 이제 mskssrv.sys 내에서 코드 실행을 트리거할 수 있습니다. 디바이스가 생성되면 드라이버의 PnP 디스패치 생성 함수가 호출됩니다. 추가 코드 실행을 트리거하기 위해 IOCTL을 보내 드라이버의 디스패치 디바이스 제어 기능에서 실행할 디바이스와 통신할 수 있습니다.

고스트 드라이버 디버깅

이진 분석을 수행할 때는 정적(디스어셈블러, 디컴파일러) 및 동적(디버거) 도구를 조합하여 사용하는 것이 가장 좋습니다. WinDbg를 사용하여 대상 드라이버를 커널 디버깅할 수 있습니다. 특정 지점에 브레이크포인트를 설정함으로써 코드 실행이 이루어질 것으로 기대됩니다(디스패치, 생성, 디스패치, 디바이스 제어).

처음에는 드라이버 내부에 설정한 중단점이 하나도 맞지 않아서 어려움을 겪었습니다. 제대로 기기를 열었는지, 아니면 다른 잘못을 하고 있는 건 아닌지 의심이 들었습니다. 나중에 드라이버가 언로드되고 있었기 때문에 중단점이 설정되지 않았다는 사실을 깨달았습니다. 인터넷에서 답변을 검색했지만 Windows에서 기본적으로 로드되고 액세스할 수 있음에도 불구하고 mskssrv를 검색하면 결과가 많지 않습니다. 제가 찾은 몇 안 되는 결과 중에는 다른 사람이 비슷한 문제를 겪었다는 OSR의 스레드가 있었습니다.

포럼에 댓글 달기

PnP 필터 드라이버는 한동안 사용하지 않은 경우 언로드했다가 필요할 때 다시 로드할 수 있습니다.

디바이스에 대한 핸들을 연 후 DeviceIoControl을 호출하기 전에 중단점을 설정하여 드라이버가 최근에 로드되었는지 확인하여 문제를 해결했습니다.

드라이버 기능에 대한 간단한 설문조사

mskssrv 드라이버는 72KB 크기의 바이너리이며 다음 함수를 호출하는 디바이스 IO 제어 코드를 지원합니다.

  • FSRendezvousServer::InitializeContext
  • FSRendezvousServer::InitializeStream
  • FSRendezvousServer::RegisterContext
  • FSRendezvousServer::RegisterStream
  • FSRendezvousServer::DrainTx
  • FSRendezvousServer::NotifyContext
  • FSRendezvousServer::PublishTx
  • FSRendezvousServer::PublishRx
  • FSRendezvousServer::ConsumeTx
  • FSRendezvousServer::ConsumeRx

이러한 심볼 이름을 살펴보면 드라이버의 일부 기능, 즉 스트림 전송 및 수신과 관련된 기능을 추론할 수 있습니다. 이 시점에서 저는 드라이버의 의도된 기능을 더 자세히 살펴보았습니다. Windows의 멀티미디어 프레임워크에 대한 Michael Maltsev프레젠테이션에서 이 드라이버가 카메라 스트림을 공유하는 프로세스 간 메커니즘의 일부라는 사실을 알게 되었습니다.

드라이버가 그다지 크지 않고 IOCTL이 많지 않기 때문에 각 함수를 살펴보고 드라이버의 내부를 파악할 수 있었습니다. 각 IOCTL 함수는 컨텍스트 등록 객체 또는 스트림 등록 객체에서 작동하며, 해당 '초기화' IOCTL을 통해 할당되고 초기화됩니다. 객체에 대한 포인터는 Irp->CurrentStackLocation->FileObject->FsContext2에 저장됩니다. FileObject는 열린 파일마다 생성되는 장치 파일 객체를 가리키고, FsContext2는 파일 객체별 메타데이터를 저장하는 필드입니다.

취약점

먼저 사용자 모드 구성 요소인 fsclient.dll과 frameserver.dll을 분석하기 전에 드라이버와 직접 통신하는 방법을 이해하려고 노력하던 중 이 버그를 발견했습니다. 개발자가 간과한 간단한 검사를 인스턴스화했다고 생각했기 때문에 버그를 놓칠 뻔했습니다. PublishRX IOCTL 함수를 살펴보겠습니다.

FSRendezvousServer::PublishRx 디컴파일 스니펫

FSRendezvousServer::PublishRx 디컴파일 스니펫

FsContext2에서 스트림 객체를 가져온 후, FSRendezvousServer::FindObject 함수가 호출되어 전역 FSRendezvousServer에 의해 저장된 두 개의 리스트에 해당 포인터가 존재하는 객체와 일치하는지를 검증합니다. 처음에는 이 함수가 요청된 객체 유형을 검증하는 어떤 방식이 있을 것이라 생각했습니다. 그러나 이 함수는 포인터가 컨텍스트 객체 리스트 또는 스트림 객체 리스트 중 어느 하나에라도 존재하면TRUE를 반환합니다. 주의할 점은, 해당 객체가 어떤 유형이어야 하는지에 대한 정보가 FindObject에 전달되지 않는다는 것입니다. 즉, 컨텍스트 객체를 스트림 객체로 전달할 수 있습니다. 이는 객체 유형 혼동 취약점입니다! 이 취약점은 스트림 객체를 처리하는 모든 IOCTL 함수에서 발생합니다. 이 취약점을 패치하기 위해 Microsoft는 FSRendezvousServer::FindObjectFSRendezvousServer::FindStreamObject로 교체했으며, 이 함수는 유형 필드를 확인하여 해당 객체가 실제로 스트림 객체인지 먼저 검증합니다.

악용

프리미티드

컨텍스트 등록 객체는 스트림 등록 객체(0x1D8 바이트)보다 작기 때문에 범위를 벗어난 메모리에서 스트림 객체 작업을 수행할 수 있습니다.

객체 유형 혼동 취약점 설명

객체 유형 혼동 취약점 설명

풀 스프레이

취약성 프리미티브를 활용하려면 범위를 벗어난 메모리를 제어할 수 있는 기능이 필요합니다. 이는 취약한 객체의 동일한 메모리 영역에 있는 많은 객체의 할당을 트리거하여 수행할 수 있습니다. 이 기술을 힙 또는 풀 스프레이라고 합니다. 취약한 객체는 비페이징된 낮은 조각화 힙 풀에 할당됩니다. Alex Ionescu고전적인 기법을 사용해 0x30 바이트 DATA_QUEUE_ENTRY 헤더 아래에 메모리 내용을 완전히 제어할 수 있는 버퍼를 스프레이할 수 있습니다. 이 기술을 사용하여 스프레이하면 다이어그램에 표시된 메모리 레이아웃을 얻을 수 있습니다.

비페이징 풀 스프레이 일러스트

선택한 풀 스프레이 메서드를 사용하여 0xC0-0x10F0x150-0x19F 범위 내의 객체 오프셋 필드를 제어할 수 있습니다. 익스플로잇 프리미티브를 찾기 위해 스트림 객체에 대한 IOCTL 함수를 다시 한 번 살펴보았습니다. 제어 가능한 객체 필드에 액세스하고 조작할 수 있는 위치를 검색했습니다.

상시 쓰기

PublishRx IOCTL에서 좋은 상수 쓰기 위치 프리미티브를 찾았습니다. 이 기본형은 임의의 메모리 주소에 상수 값을 쓰는 데 사용할 수 있습니다. FSStreamReg: :PublishRx 함수의 일부를 살펴보겠습니다.

FSStreamReg::PublishRx 디컴파일 스니펫

FSStreamReg::PublishRx 디컴파일 스니펫

스트림 객체는 오프셋 0x188에 FSFrameMdl 객체 목록을 설명하는 목록 헤드를 포함합니다. 위의 디컴파일 스니펫에서 이 목록은 반복되고, FSFrameMdl 객체의 태그 값이 애플리케이션에서 전달된 시스템 버퍼의 태그와 일치하면 FSFrameMdl::UnmapPages 함수가 호출됩니다.

앞서 언급한 익스플로잇 프리미티브를 사용하면 FSFrameMdlList와 pFrameMdl이 가리키는 FsFrameMdl 객체를 완전히 제어할 수 있습니다. 이제 UnmapPages를 살펴보겠습니다.

FSFrameMdl:UnmapPages 디컴파일

FSFrameMdl:UnmapPages 디컴파일

위의 디컴파일된 함수의 마지막 줄에서 상수 값 2는 제어 가능한 (FSFrameMdl 객체)의 오프셋 값에 기록되고 있습니다. 이러한 상수 쓰기는 I/O 링 기술과 함께 사용하여 임의의 커널 읽기 쓰기 및 권한 확대를 얻을 수 있습니다. 이 기법의 작동 방식에 대한 자세히 보기 여기여기에서 확인할 수 있습니다.

상수 쓰기 원시 함수를 사용하기로 선택했지만, 이 함수에는 또 다른 유용한 착취 원시 함수도 등장합니다. MmUnmapLockedPages 호출에 대한 인수 BaseAddressMemoryDescriptorList는 모두 제어할 수 있습니다. 이는 임의의 가상 주소에서 매핑을 언매핑하고 해제 후 사용과 유사한 프리미티브를 구성하는 데 사용할 수 있습니다.

요금 문제

이 시점에서 임의의 커널 읽기-쓰기를 제공하는 몇 가지 적합한 익스플로잇 프리미티브가 확인되었습니다. 스트림 객체의 내용을 여러 번 검사해야 원하는 코드 경로를 트리거할 수 있다는 것을 눈치채셨을 겁니다. 대부분의 경우 풀 분사를 통해 물체의 적절한 상태를 얻을 수 있습니다. 하지만 문제가 발생하여 어려움을 겪었습니다. 다음은 FSFrameMdlList를 반복하는 작업이 완료된 후의 FSStreamReg::PublishRx 코드 스니펫입니다.

FSStreamReg::PublishRx 디컴파일 스니펫

FSStreamReg::PublishRx 디컴파일 스니펫

위의 디컴파일에서bPagesUnmappedFSFrameMdl::UnmapPages가 호출되면 설정되는 부울 변수입니다.그렇다면 스트림 객체의 오프셋 0x1a8 을 검색하고, null이 아니면 이에 대해 KeSetEvent가 호출됩니다.

이 오프셋은 풀에서 버퍼 할당을 분리하는 데이터 구조인 POOL_HEADER 내의 경계를 벗어난 메모리 및 포인트에 해당합니다. 특히 할당이 "청구된" 프로세스에 대한 _EPROCESS 객체에 대한 포인터를 저장하는 데 사용되는 ProcessBilled 필드를 가리킵니다. 이는 특정 프로세스가 가질 수 있는 풀 할당 수를 설명하는 데 사용됩니다. 모든 풀 할당이 프로세스에 대해 "청구"되는 것은 아니며, POOL_HEADERProcessBilled 필드가 NULL로 설정되어 있지 않은 풀 할당은 청구되지 않습니다. 또한 ProcessBilled에 저장된 EPROCESS 포인터는 실제로 임의 쿠키로 XOR 처리되므로 ProcessBilled에 유효한 포인터가 포함되어 있지 않습니다.

이는 NpFr 버퍼가 호출 프로세스에 청구되므로 ProcessBilled가 설정되기 때문에 어려움이 있습니다. 필요한 익스플로잇 프리미티브를 트리거할 때 bPagesUnmappedTRUE로 설정됩니다. 잘못된 포인터가 KeSetEvent에 전달되면 시스템이 충돌합니다. 따라서 POOL_HEADER가 무료 할당용인지 확인해야 합니다. 이 시점에서 컨텍스트 등록(Creg) 객체 자체에는 요금이 부과되지 않는다는 사실을 확인했습니다. 그러나 이 객체는 FSFrameMdl 오프셋에서 메모리 내용을 제어할 수 없습니다. 따라서 NpFrCreg 객체 모두 스프레이해야 하며 시퀀스도 올바르게 지정되어야 합니다.

풀 누출 — 스프레이 후 기도하지 마세요!

대규모 풀 할당과 달리 LFH 풀 할당의 주소는 NtQuerySystemInformation를 통해 누출할 수 없습니다. 또한 할당 순서는 무작위입니다. 따라서 취약한 객체에 인접한 버퍼들이 익스플로잇 원시 요소를 트리거하고 시스템 충돌을 방지할 수 있는 올바른 순서인지 알 방법이 없습니다. 다행히도 이 취약점을 이용하면 인접 버퍼의 풀 누출을 유발할 수 있습니다. ConsumeTx의 IOCTL 함수를 살펴보겠습니다.

FSRendezvousServer::ConsumeTx 디컴파일 스니펫

FSRendezvousServer::ConsumeTx 디컴파일 스니펫

위에서 FSStreamReg::GetStats 함수가 호출됩니다.

FSStreamReg::GetStats 디컴파일

FSStreamReg::GetStats 디컴파일

여기에서 취약한 스트림 객체의 범위를 벗어난 메모리 내용은 호출 사용자 공간 애플리케이션으로 다시 반환되는 SystemBuffer로 복사됩니다. 이 풀 정보 누출 원시는 취약한 객체에 인접한 버퍼에 대한 서명 검사를 수행하는 데 사용할 수 있습니다. 원하는 메모리 레이아웃 내의 객체를 찾을 때까지 많은 취약한 객체를 스캔할 수 있습니다. 원하는 객체를 찾으면 메모리 배치는 다음과 같습니다.

CVE-2023-36802 낮은 조각화 힙 풀 그룸 레이아웃

CVE-2023-36802 낮은 조각화 힙 풀 그룸 레이아웃

이제 메모리에서 올바른 위치에 취약한 대상 객체를 찾았으므로, 앞서 언급한 대상 객체의 악용 기본 요소를 시스템을 충돌시키지 않고 실행할 수 있습니다.

악용 사례

MSRC에 이 문제를 보고한 후, 취약점을 악용한 사례가 발견되었습니다.

이 블로그 게시물에 소개된 익스플로잇 방법은 여러 가지 접근 방법 중 일부에 불과합니다. 현재로선 공격자가 이 취약점을 어떻게 악용했는지에 대한 공개 정보는 없습니다. 익스플로잇 코드는 여기에서 찾을 수 있습니다.

결론

소급 패치 분석에 따르면 Windows 10의 1809 빌드에서는 새로운 코드의 상당 부분이 mskssrv.sys에 추가된 것으로 나타났습니다. 새로운 코드 추가를 모니터링하면 취약점을 찾는 데 유용한 경우가 많습니다.

이 분석을 통해 얻을 수 있는 또 다른 진부하지만 고전적인 교훈은 수행된 검사에 대해 가정하지 말라는 것입니다. 친구와 동료는 FsContext2를 사용하는 유형 혼동이 "흔하지만 연구되지 않은 버그 클래스"가 될 수 있다고 제안했습니다. 특히 프로세스 간 통신을 다루는 드라이버의 경우 이 버그 클래스에 대해 더 많은 변형 분석이 필요하다고 생각합니다.

이 취약점은 단순히 낯선 공격 표면과 인터페이스를 시도하는 과정에서 발견되었습니다. 시스템에 대해 '거의 제로에 가까운 지식'을 가지고 있다는 것은 시스템을 깨뜨릴 수 있는 새로운 사고방식을 가지고 있다는 의미이기도 합니다.

Mixture of Experts | 12월 12일, 에피소드 85

AI 디코딩: 주간 뉴스 요약

세계적인 수준의 엔지니어, 연구원, 제품 리더 등으로 구성된 패널과 함께 불필요한 AI 잡음을 차단하고 실질적인 AI 최신 소식과 인사이트를 확인해 보세요.