돌고 도는 레이싱: 발생할 수 있는 작은 버그

밤에 컴퓨터로 작업하면서 사색에 잠긴 젊은 여성

이 작은 버그는 CVE-2024-30089는 완전히 업데이트된 Windows 11 머신(모든 가상화 기반 보안 및 하드웨어 보안 완화가 활성화된 상태)을 익스플로잇하는 데 사용한 미묘한 커널 취약점으로, 올해 Pwn2Own에서 첫 우승을 차지한 바 있습니다.

이 글에서는 시작점을 정하고 무언가가 눈에 띌 때까지 직관적으로 경로를 따라가는 버그 헌팅에 대한 저의 간단한 접근 방식을 간략하게 설명합니다. 이 버그는 논리 오류로 인해 안정적으로 트리거될 수 있다는 점에서 흥미롭습니다. 프로세스 간 통신 시스템 내의 특정 상태에서 오류가 발생하면 해제 후 사용이 불가능해집니다. 버그를 찾으려면 다양한 가능한 상태에서 프로그램의 코드 경로를 비교해야 하는데, 이 프로세스에 대해 자세히 설명합니다. 마찬가지로 흥미로운 것은 버그의 출처와 패치에 대한 Microsoft의 접근 방식입니다. 이러한 주제는 이 게시물에서도 다루고 있습니다.

제로데이 헌팅: 어디서부터 시작해야 할까요?

취약성 연구에 관해서 가장 많이 받는 질문은 바로 시작하는 방법입니다. 사실, 목표를 정하고 그것을 고수하는 것은 연구 과정에서 가장 어려운 단계 중 하나일 수 있습니다. 여기에서 설명하는 취약점은 Microsoft Kernel Streaming Service(mskssrv.sys)에 있습니다. 이 블로그 게시물에서 하위 시스템에 대한 일반적인 개요를 확인하세요. 이 글에서 저는 MSKSSRV 서브시스템의 몇 가지 특징, 특히 프로세스 간 통신(IPC) 메커니즘이 좋은 공격 표면이 될 수 있다고 지적했습니다.

MSKSSRV의 코드 기반은 매우 작으며, 제가 발견한 이 하위 시스템의 마지막 취약점도 제로데이로 야생에서 독립적으로 익스플로잇되었습니다. 또한 이 드라이버를 감사하기 위해 다른 연구자와 회사에서 추가적인 노력을 기울이고 있다는 소식도 들었습니다. 이 때문에 처음에는 이 공격 표면에서 더 이상 발견할 버그가 없다고 가정하는 일반적인 함정에 빠졌습니다. 하지만 이전 블로그 게시물에서 제안한 적이 있기 때문에 저는 제 직감을 믿고 계속 찾아보기로 했습니다.

잠금 장치: 누가 있나요?

새로운 연구 아이디어를 얻는 가장 좋은 방법 중 하나는 최신 연구 동향을 지속적으로 파악하는 것입니다. k0shl이 작성한 훌륭한 블로그 게시물을 읽고 특정 유형의 취약점을 찾아보고자 하는 영감을 얻었습니다. k0shl이 발견한 취약점에서는 객체의 참조 카운트가 적절한 잠금 없이 초기화되고 증가되어 해제 후 사용(use-after-free) 발생 가능 구간이 형성되었습니다. 해당 취약점은 커널이 아닌 사용자 영역 취약점이었지만, 취약한 라이브러리의 코드 스타일이 과거 MSKSSRV 드라이버를 감사했을 때와 유사하다는 점이 떠올랐습니다.

MS KS Server(MSKSSRV)는 FSStreamRegFSContextReg 객체를 통해 사용자 영역 프로세스와 상호작용합니다. FSStreamRegFSContextReg는 모두 기본 클래스 FSRegObject에서 파생됩니다. FSContextReg가 동기화용 vtable 함수를 구현하지 않았으며, 기본 클래스(FSRegObject)의 구현은 단순한 nop 명령에 불과하다는 점을 확인했습니다. 이는 FSContextReg 객체에 대해 실제로는 어떠한 락킹 메커니즘도 구현되어 있지 않음을 의미합니다. 반대로 FSStreamReg는 뮤텍스를 사용하는 락킹 함수를 구현하고 있습니다. 이 락킹 메커니즘은 FSRendezvousServer::Close 함수에서 객체를 정리(cleanup)할 때 사용됩니다.

FSRendezvousServer::Close 함수 스크린샷

코드 블록 1: FSRendezvousServer::Close — FSRegObject에 대한 잠금 및 해제 경로

FSRegObject  기본 클래스를 상속한 두 객체가 존재하지만, 한 객체는 적절한 잠금 메커니즘을 구현하고 있고 다른 객체는 그렇지 않습니다. 이 점이 의심스러웠습니다. 이 가설을 검증하기 위해 잠금 보호가 없는 FSContextReg 객체에 대한 참조를 이용해 정의되지 않은 동작을 유발하려 했습니다. 그러나 FSRegObjects 목록에 대한 포인터를 보관하는 전역  FSRendezvousServer  객체에 잠금 보호가 적용되어 있어, FSContextReg  객체에 잠금이 없다는 점에도 불구하고 의미 있는 동작을 유발하지는 못했습니다. 그럼에도 불구하고 FSRegObjects의 참조 카운팅 시스템에는 뭔가 수상한 점이 있다는 느낌이 들었지만, 정확히 무엇인지는 파악하지 못했습니다.

MS KS 서버의 IPC

이전 블로그 글에서 언급했듯이, MSKSSRV의 프로세스 간 객체 공유 메커니즘은 취약점으로 이어질 수 있는 흥미로운 지점이므로, 이를 더 깊이 살펴보기로 했습니다. 이 서브시스템의 IPC 메커니즘은 아래 다이어그램에 설명되어 있습니다.

서브시스템의 IPC 메커니즘 다이어그램

다이어그램 1: MS KS Server의 프로세스 간 통신

CreateFile을 통해 MSKSSRV 장치에 대한 파일 핸들을 열면, 해당 핸들에 대응하는 FILE_OBJECT가 생성됩니다. 해당 핸들을 사용하여 프로세스는 DeviceIoControl 함수를 통해 장치에 IOCTL을 전송함으로써 새로운 스트림 또는 컨텍스트 객체를 초기화할 수 있습니다. 초기화 프로세스는lpInBuffer 인자를 통해 프로세스 ID를 지정하여 어떤 원격 프로세스가 해당 객체를 등록할 수 있는지 설정합니다. 원격 프로세스는 MSKSSRV 장치에 대한 새로운 파일 핸들을 사용하여 장치 IOCTL을 통해 해당 객체를 등록할 수 있습니다. FSRegObject에 대한 포인터는 다음 위치에 저장됩니다.

Irp->CurrentStackLocation->FileObject->FsContext2.

동일한 FSRegObject 객체에 대한 포인터가 FsContext2에 두 번 저장되며, 하나는 초기화 프로세스가 사용하는 FILE_OBJECT 에, 다른 하나는 등록 프로세스가 사용하는 FILE_OBJECT 에 저장됩니다. 이 방식으로 FSRegObject 객체에 대한 참조가 프로세스 간에 공유될 수 있습니다. 예를 들어 FSStreamReg 객체를 사용하면 다이어그램 1에 나타난 것처럼 여러 프로세스가 스트림 프레임 버퍼에 접근할 수 있습니다. FSRegObject 의 참조 카운트는 1로 초기화되며, 초기화가 완료된 후 한 번 더 증가합니다. FSRegObject 를 등록하면 참조 카운트가 다시 증가하여 객체당 총 세 개의 참조를 갖게 됩니다.

FSContextReg 객체 초기화 및 등록 다이어그램

다이어그램 2: FSContextReg 객체의 초기화 및 등록

버그 발견

이전에 락킹 취약점을 찾기 위해 감사했던 부분을 다시 검토해 보기로 했습니다.FSRendezvousServer::Close 을 호출하는 함수FSRendezvousClose, 이 드라이버의 디스패치 정리디스패치 닫기 루틴에서 호출된다는 점을 확인했습니다.

MS KS Server 드라이버의 DispatchCleanup 및 DipatchCleanup 루틴이 포함된 코드 블록 스크린샷

코드 블록 2: MS KS Server 드라이버의 DispatchCleanup 및 DipatchCleanup 루틴

디스패치 루틴은 패키징된 I/O 요청인 IRP의 하나 이상의 유형을 처리합니다. Windows에서는 파일에 대한 모든 핸들 참조가 닫히면 해당 파일 시스템 드라이버가IRP_MJ_CLEANUP IRP_MJ_CLOSE  요청을 수신하며, 이는 드라이버의 DispatchCleanupDispatchClose 루틴에서 처리됩니다.

MSKSSRV에서는Irp->CurrentStackLocation->FileObject->FsContext2 에 저장된 포인터가 NULL이 아닌 경우, 드라이버의 DispatchCleanupDispatchClose 루틴 양쪽에서FSRendezvousClose 이 호출됩니다. 이전에 분석했던FSRendezvousServer::Close 에서 특히 눈에 띄었던 부분은 호출자의 프로세스 ID에 대한 다양한 검사였습니다. 프로세스 컨텍스트는 중요합니다. 커널 모드 코드는 단일 커널 주소 공간에서 실행되며, 이는 사용자 모드 주소 공간과 분리되어 있기 때문입니다. 각 프로세스는 자체 사용자 모드 메모리 컨텍스트를 가지며, 커널 스레드가 어떤 프로세스 컨텍스트에서 실행되는지는 해당 스레드가 사용자 주소에 접근할 때 어느 프로세스의 사용자 주소 공간에 접근하게 되는지를 결정합니다. 아래 코드는 호출 프로세스가 초기화 프로세스인지 또는 등록 프로세스인지 확인합니다.

FSRendezvousServer::Close 함수 스크린샷

코드 블록 3: FSRendezvousServer::Close, FSRegObject에 대한 프로세스 검사

프로세스별 정보는FSRegObject 에 저장됩니다.

 Irp->CurrentStackLocation->FileObject->FsContext2

 
초기화 또는 등록 시점에. 드라이버는 호출자의 프로세스 ID를 확인하여

EPROCESS

 어떤 프로세스별 리소스(객체, 이벤트 객체 등)를 해제해야 하는지를 결정합니다. 해당 프로세스가 초기화 또는 등록을 수행한 프로세스인 경우, 해당 프로세스 전용 리소스에 대해 추가 정리 작업이 수행됩니다.

이는 일부 예외를 제외하면 일반적으로 모든 디스패치 루틴이 임의의 프로세스 컨텍스트에서 실행되기 때문에 특히 눈에 띄었습니다. 즉, 시스템은 디스패치 작업을 수행할 스레드를 선택하며, 어떤 스레드가 선택될지는 임의적입니다. 저는DispatchCleanup 가 마지막 핸들을 닫은 프로세스의 컨텍스트에서 호출된다는 사실을 확인했습니다. 따라서 이 경우에는 프로세스 ID 검사가 타당합니다. 그러나DispatchClose임의의 프로세스 컨텍스트에서 호출됩니다. 즉,FSRendezvousServer::CloseIRP_MJ_CLOSE 요청의 결과로 호출된다면, 이는 임의의 프로세스 컨텍스트에서 실행되므로 프로세스 ID 검사의 목적이 무의미해집니다. 이는 이 설계에 문제가 있음을 시사하는 중요한 단서였습니다.

또한 Windows OS의 특성상, 핸들은 자식 프로세스 상속 또는 DuplicateHandle API를 통해 다른 프로세스와 공유될 수 있습니다. 공유된 파일 핸들을 통해 다른 프로세스도 동일한FILE_OBJECT 와 상호작용할 수 있습니다.

MS KS Server 디바이스 핸들 공유 다이어그램

 다이어그램 3: MS KS Server 디바이스 핸들 공유

이로 인해 DispatchCleanup  실행 시의 프로세스 컨텍스트가 초기화 프로세스도 등록 프로세스도 아닐 가능성도 존재합니다. 해당 핸들 중 하나가 복제되어 다른 프로세스와 공유되고, 그 프로세스가 마지막으로 핸들을 닫는다면, DispatchCleanup은 초기화 또는 등록 프로세스가 아닌 외부 프로세스의 컨텍스트에서 호출됩니다.

외부 프로세스가 FILE_OBJECT의 마지막 핸들을 닫는 경우 다이어그램

다이어그램 4: 외부 프로세스가 FILE_OBJECT의 마지막 핸들을 닫는 경우

파일 핸들이 닫힐 때 FSRendezvousServer::Close 함수가 두 번 호출될 수 있다는 점을 확인했습니다(한 번은 IRP_MJ_CLEANUP 요청에서, 한 번은 IRP_MJ_CLOSE  요청에서. 코드 블록 2 참조). 해당 함수 내부에는 FSRegObject::Release에 대한 두 번의 호출 가능성이 있으며(코드 블록 4), 이 함수는 객체를 역참조하고 참조 카운트가 0이 되면 메모리를 해제합니다. 따라서 하나의 핸들에 대해 FSRegObject::Release가 최대 네 번 호출될 수 있습니다. 또한 두 개의 파일 핸들에서 FSContext2 포인터를 통해 동일한 FSRegObject를 가리키는 FILE_OBJECT 가 존재합니다. 이는 동일한 객체에 대해 FSRegObject::Release 이 핸들당 네 번, 총 최대 여덟 번 호출될 가능성이 있음을 의미합니다. 객체의 초기 참조 카운트가 세 개뿐이라면, 과도한 역참조로 인해 해제 후 사용이 발생할 가능성이 있습니다. 당시 제 추론은 그러했습니다. 프로그램 구조상 이론적인 최대 역참조 횟수까지 도달하는 것은 방지될 가능성이 높다고 보았지만, 완전히 차단되지는 않을 수도 있다고 판단했습니다. 이 시점에서 의미 있는 단서를 찾았다고 판단했고, 추가 조사를 진행하기로 했습니다.

FSRendezvousServer::Close에서 FSRegObject::Release가 두 번 호출되는 장면 스크린샷

코드 블록 4: FSRendezvousServer::Close에서 FSRegObject::Release가 두 번 호출될 수 있음

일반적으로 객체의 참조 수보다 많은 역참조가 발생하는 것만이 해제 후 사용을 유발하는 유일한 방식은 아닙니다. 그러나 이 경우에는 FSRegObject이 해제되었다면 참조 카운트가 0으로 감소했음을 확실히 알 수 있습니다. IRP_MJ_CLEANUP/CLOSE IRP 요청 과정에서 유효한 FSRegObject이 마지막으로 접근되는 시점은 FSRegObject::Release 호출 시점입니다. 따라서 해제 후 사용이 가능하다면 FSRegObject::Release 호출은 항상 객체가 이미 해제된 이후에 발생하게 됩니다. 해당 호출 과정에서 객체는 다시 한 번 역참조됩니다. 이러한 이유로, 이 사례에서는 역참조 횟수를 계산하는 것이 해제 후 사용 취약점을 식별하는 데 유효한 휴리스틱이 됩니다.

남은 작업은 객체가 언제 해제되고 언제 접근되는지를 추적하며 프로그램의 가능한 상태 전이를 분석하는 것이었습니다. 이를 위해 각 가능한 경우에 대해 IRP_MJ_CLEANUP/CLOSE 요청 처리 로직을 머릿속으로 시뮬레이션했으며, 각 흐름은 해당 디스패치 함수(코드 블록 2)에서 시작합니다.

아래에는 어떤 프로세스가 마지막 참조를 닫는지에 따른 상태가 정리되어 있습니다. 각 항목은 해당 프로세스가 마지막 핸들을 닫을 경우 FSContextReg 객체에 대해 발생하는 역참조 횟수를 나타냅니다. 참고: HANDLE #1(초기화 핸들)과 HANDLE #2(등록 핸들) 사이에는 기능적 차이가 없습니다. 두 핸들 모두 FileObject->FSContext2 필드가 동일한 메모리를 가리키는 FILE_OBJECT 구조를 공유하기 때문입니다.

각 MSKSSRV IPC 상태에서의 FSContextReg 역참조 횟수 스크린샷

각 MSKSSRV IPC 상태에서의 FSContextReg 역참조 횟수

성공! 최종 상태에서는 외부 프로세스가 두 번, 초기화(및 등록) 프로세스가 두 번, 총 네 번의 역참조가 발생합니다. 그러나 초기 참조 수는 세 개뿐이므로 해제 후 사용이 발생할 수 있습니다. FSStreamReg 객체에 대해서도 동일한 분석을 반복했지만, 코드에 존재하는 메모리 누수 버그로 인해 FSStreamReg 객체는 등록된 이후에는 실제로 해제될 수 없습니다.

취약점

위에서 설명한 가상 머신 시뮬레이션 방식으로 로직을 따라가다 보니 문제를 발견했습니다. 프로세스가 초기화 또는 등록 프로세스인 경우 적절한 정리 작업이 수행되며, Irp->CurrentStackLocation->FileObject->FsContext2에 저장된 포인터는 NULL로 설정됩니다. 아래 코드 블록은 호출자가 초기화 프로세스인 경우 해당 동작이 발생하는 위치를 보여줍니다.

FSRegObject::CloseInitProcess가 FSContext 포인터를 NULL로 설정하는 코드 스크린샷

코드 블록 5: FSRegObject::CloseInitProcess가 FSContext 포인터를 NULL로 설정

이는 DispatchCloseIRP_MJ_CLOSE 요청 처리 완료 과정(SrvDispatchClose, 코드 블록 2)에서 FSRendezvousClose이 다시 호출되지 않음을 의미합니다.

그러나 호출 프로세스가 외부 프로세스인 경우 정리 작업이 수행되지 않으며, 함수 종료 직전에 FSRegObject::Release이 한 번 호출됩니다. FileObject->FsContext2이 NULL이 아니기 때문에 이후  IRP_MJ_CLOSE 요청 처리 중 FSRendezvousServer::Close가 다시 호출되며, 그 결과 FSRegObject::Release가 한 번 더 호출됩니다.

이제 두 번째 핸들을 FSContextReg 객체를 초기화하고 등록한 동일한 프로세스가 닫는 경우, 객체는 자신이 보유한 모든 프로세스 관련 리소스를 정리하여 비어 있는 상태가 됩니다. 이로 인해 FSRendezvousServer::Close(코드 블록 4) 내부에서 FSRegObject::Release 이 두 번 호출됩니다. 이 추가 역참조는 객체가 비어 있는 상태에서 초기화 과정에서 증가했던 추가 참조를 정산하기 위한 것입니다.

결과적으로 단일 FSContextReg 객체에 대해 총 네 번의 역참조가 발생하며, 이는 해제 후 사용 취약점이 발생함을 의미합니다.

CVE-2024-30089 개념도

다이어그램 5: CVE-2024-30089 개념도

여기까지 따라온 독자는 외부 프로세스가 두 핸들을 모두 마지막으로 닫을 수 없는 이유를 궁금해할 수 있습니다. 그렇게 되면 겉보기에는 네 번의 역참조가 발생할 것처럼 보이기 때문입니다. 객체가 소멸되어 해제되기 전에, 전역 FSRendezvousServer 객체에 저장된 리스트에서 먼저 연결이 해제됩니다. FSRendezvousServer::Close의 시작 부분에서 FSContext2에 저장된 포인터가 해당 리스트의 유효한 구성원인지 확인합니다. 이 경우 객체는 함수의 마지막 부분에서 두 번째 FSRegObject::Release 호출 시 해제됩니다. 네 번째 FSRendezvousServer::Close 호출 시점에는 객체가 이미 리스트에서 제거된 상태이므로 유효하지 않은 객체가 되어 더 이상 사용할 수 없습니다. 해제 후 사용을 트리거하려면 객체가 FSRendezvousServer::Close에서 조회되고 유효성 검사를 통과한 이후에 발생해야 합니다. 아래 코드 스니펫은 해당 취약점을 통해 획득할 수 있는 해제 후 사용 원시 동작을 보여줍니다.

use-after-free 프리미티브 경로 코드 스크린샷

코드 블록 6: use-after-free 프리미티브 경로

공격 복잡도

이 취약점에 대한 보안 업데이트 가이드에서 CVSS 점수는 "악용 가능성 높음"이며 이 취약점에 대한 공격 복잡성은 "낮음"으로 표시됩니다. Microsoft는 점수에 대한 자세한 설명을 제공하지 않지만, 다른 취약점을 패치하는 동안 몇 가지 패턴에 주목했습니다. 이 취약점은 논리 오류에서 비롯되어 안정적으로 트리거할 수 있기 때문에 이 점수를 받은 것으로 보입니다. 다이어그램 5에 설명된 단계를 따르면 공격자는 코드 블록 6에 설명된 무료 사용 후 시나리오를 일관되게 트리거할 수 있습니다. 그러나 이것이 실제로 이를 악용하는 것이 간단하다는 의미는 아닙니다. 이 시리즈의 다음 부분에서는 악용 단계에 대한 자세한 설명을 다루겠습니다.

회고

취약점이 어떻게 발생했는지 이해하는 것은 보안 중심 개발 문화를 정착시키는 데 중요합니다. 취약점이 도입된 시점을 특정하기 위해 Winbindex에서 이전 버전의 드라이버를 확보하여 FSRendezvousServer::Close 함수의 로직 차이를 분석했습니다.

취약점 섹션에서 저는 이 버그의 근본 원인은 호출 프로세스가 외부 프로세스일 경우

Irp->CurrentStackLocation->FileObject->FsContext2

해당 포인터를

NULL

NULL로 설정하지 않은 데 있다고 언급했습니다. 놀랍게도, mskssrv.sys의 초기 버전에서 바로 이 코드 한 줄을 확인할 수 있었습니다.

FSRendezvousServer::Close 초기 버전 코드 스크린샷, FsContext2가 NULL로 설정됨

코드 블록 7: FSRendezvousServer::Close의 초기 버전, FsContext2를 NULL로 설정

위 코드 블록에서는 앞선 프로세스 ID 검사 결과와 관계없이 FileObject->FsContext2이 명시적으로 NULL로 설정됩니다. 이로 인해 이후 IRP_MJ_CLOSE 요청에서 FSRendezvousServer::Close가 다시 호출되지 않으며, 추가적인 역참조는 발생하지 않습니다. 이상하게도 이 코드 줄은 왜 삭제된 걸까요? 버그가 처음 도입된 이후 버전의 함수를 살펴보겠습니다.

기능 플래그 검사 내부에서 FsContext를 NULL로 설정하는 코드 스크린샷

코드 블록 8: 기능 플래그 검사 내부에서 FsContext를 NULL로 설정

위 코드 블록에는 다음 기능 플래그에 대한 검사 로직이 포함되어 있습니다.

Feature_Servicing_TeamsUsingMediaFoundationCrashes

.
기능 플래그는 다양한 기능 및 실험적 기능을 전환하는 Windows 구성 요소이지만, 이에 대한 공개 정보는 많지 않습니다. 이전 블로그 게시물에서는 취약점 패치에 기능 플래그가 어떻게 사용되었는지 설명했습니다. 기능 플래그는 특정 기능이 공식적으로 채택되기 전에 시험적으로 적용하기 위해 사용되기도 합니다. 이 경우, 만약

Feature_Servicing_TeamsUsingMediaFoundationCrashes
해당 기능이 활성화된 경우 FileObject->FsContext2NULL로 설정되지 않아 취약점이 발생합니다. 이 기능은 Windows 10 설치 환경에서 기본적으로 활성화되어 있는 것으로 확인되었습니다. Windows 11에서는 코드 블록 1에 나타난 것처럼 해당 기능 플래그 조건문이 존재하지 않으며, 포인터가 NULL로 설정되지 않아 마찬가지로 취약합니다.

 

이 기능 이름을 보고 화상 회의 소프트웨어인 Microsoft Teams의 동작을 조사했습니다. 해당 애플리케이션이 MSKSSRV 기능을 사용해 프로세스 간 미디어 스트림을 공유할 수 있다는 점을 확인했습니다. 스트림 핸들 공유가 Teams의 크래시를 유발했을 가능성이 있습니다. 향후 연구 과제로는 Teams가 MSKSSRV 디바이스 핸들을 프로세스 간에 어떻게 공유하는지, 그리고 왜 올바른 포인터 정리를 수행하면 애플리케이션이 충돌할 수 있는지 분석해 보는 것이 흥미로울 것입니다.

패치

이 시리즈의 이 부분은 취약점 자체와 해당 패치에 초점을 맞춥니다. 제가 제안한 수정 방식이 Microsoft Teams에서 충돌을 유발하는 것으로 보였기 때문에, 이 버그의 패치를 특히 면밀히 분석하고자 했습니다. 이 시점에서는 실제 애플리케이션이 MSKSSRV 드라이버를 어떻게 사용하는지에 대해 아직 충분히 분석하지 못했다는 점을 밝힙니다. 이러한 맥락이 부족하면 시스템이 현재와 같이 설계된 이유를 이해하는 데 사각지점이 생길 수 있습니다. 이 버그에 대한 완전한 패치는 기본 코드 구조의 일부 재설계를 필요로 하며, IPC 시스템이 의도된 방식으로 어떻게 동작하는지에 대한 추가 정보를 드러낼 수 있습니다. 또한 Microsoft 개발자들의 안전한 코딩 관행에 대한 통찰을 얻고자 했습니다.

실망스럽게도, 2024년 6월 보안 업데이트에서 패치된 해당 취약점의 근본적인 로직 오류는 직접적으로 수정되지 않았습니다. 대신 취약한 코드 경로에 진입하기 전에 액세스 토큰 검사를 추가하는 방식으로 대응했습니다. 아래는 FSRendezvousServer::InitializeContext 함수에서 처리되는 initialize context IOCTL 관련 코드입니다.

기능 플래그 검사로 시작하고 호출 프로세스가 프레임 서버인지 확인하는 IOCTL 함수 코드 스크린샷

코드 블록 9: IOCTL 함수는 기능 플래그 검사로 시작하며, 호출 프로세스가 프레임 서버인지 확인

위 함수는 기능이 활성화되어 있는지 확인하는 것으로 시작합니다. 이는 해당 패치와 연관된 기능 플래그일 가능성이 높습니다. 기능이 활성화된 경우 KsIsCurrentProcessFrameServer은 TRUE를 반환해야 하며, 그렇지 않으면 NTSTATUS STATUS_ACCESS_DENIED가 반환됩니다.

이제 KsIsCurrentProcessFrameServer을 살펴보겠습니다.

KsIsCurrentProcessFrameServer가 호출 스레드의 액세스 토큰에 대해 SID 검사를 수행하는 코드 스크린샷

코드 블록 10: KsIsCurrentProcessFrameServer가 호출 스레드의 액세스 토큰에 대해 SID 검사를 수행

이 함수는 호출 스레드의 토큰을 두 개의 특정 보안 식별자(SID)와 비교합니다. 해당 SID는 NT SERVICE\FrameServer 그룹에 속한 토큰에 해당합니다. 호출 스레드의 액세스 토큰에서 이 두 SID 중 하나라도 활성화되어 있다면 취약한 함수 코드가 실행될 수 있습니다.

이를 확인한 후, 관리자 권한에서 커널 권한으로 상승 가능한 취약점이 여전히 존재한다고 의심했습니다. 결국 메모리 손상 문제는 전혀 해결되지 않았습니다. 기존 익스플로잇을 약간 수정하여 이를 확인했습니다. 관리자 사용자는 FrameServer 서비스를 시작하고, 해당 서비스에 대한 핸들을 연 뒤, 그 핸들을 사용하여 익스플로잇 프로세스를 생성할 수 있습니다. 완전히 패치된 시스템에서도 커널 읽기/쓰기(R/W) 원시 동작을 확보할 수 있었습니다.

Microsoft는 Administrator에서 Kernel로의 권한 상승을 보안 경계로 간주하지 않지만, 유사한 버그는 위협 행위자에 의해 커널 읽기/쓰기(R/W) 프리미티브를 획득하고 이를 EDR 우회 및 루트킷 동작에 활용하는 데 사용된 사례가 있습니다. 이 프리미티브로 어떤 작업이 가능한지 궁금하다면, FuzzySec과 함께 진행한 제 BlackHat 발표를 참고하시기 바랍니다.

결론 및 다음 단계

이 게시물은 권한 상승에 악용될 수 있는 0일 커널 취약점을 찾는 것으로 구성된 Pwn2Own 노력의 취약성 연구 부분에 초점을 맞췄습니다. 이 게시물에서는 연구에서 영감을 얻고, 버그를 찾지 못하고, 새로운 관점을 선택하고, 의심스러운 점을 발견하고, 마지막으로 취약점이 어디에 있는지 정확히 찾아내는 과정을 간략하게 설명합니다. 이제 버그가 식별되고 무료 후 사용 프리미티브가 있으므로 나머지는 간단해야 합니다. Microsoft는 그렇게 생각하는 것으로 보이며, 이 버그는 공격 복잡성이 "낮음"으로 평가되었으며 "악용 가능성이 더 높음"으로 평가되었습니다. 그들의 말이 맞을까요? 다음 편에서는 이 악용 전략과 시리즈 제목의 의미에 대해 알아보도록 하겠습니다!

감사의 말

훌륭한 다이어그램을 제공해 준 Andréa Piazza께 감사드립니다.

Windows 보안 모델을 인내심 있게 설명해 준 Emma Kirkpatrick께 감사드립니다.

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

AI 디코딩: 주간 뉴스 요약

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