이 작은 버그는 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는 모두 기본 클래스 FSRegObject가 동기화용 vtable 함수를 구현하지 않았으며, 기본 클래스()의 구현은 단순한 nop 명령에 불과하다는 점을 확인했습니다. 이는 객체에 대해 실제로는 어떠한 락킹 메커니즘도 구현되어 있지 않음을 의미합니다. 반대로 FSStreamReg
코드 블록 1: FSRendezvousServer::Close — FSRegObject에 대한 잠금 및 해제 경로
이전 블로그 글에서 언급했듯이, MSKSSRV의 프로세스 간 객체 공유 메커니즘은 취약점으로 이어질 수 있는 흥미로운 지점이므로, 이를 더 깊이 살펴보기로 했습니다. 이 서브시스템의 IPC 메커니즘은 아래 다이어그램에 설명되어 있습니다.
다이어그램 1: MS KS Server의 프로세스 간 통신
CreateFile을 통해 MSKSSRV 장치에 대한 파일 핸들을 열면, 해당 핸들에 대응하는 FILE_OBJECT가 생성됩니다. 해당 핸들을 사용하여 프로세스는 DeviceIoControl 함수를 통해 장치에 IOCTL을 전송함으로써 새로운 스트림 또는 컨텍스트 객체를 초기화할 수 있습니다. 초기화 프로세스는lpInBufferFSRegObject에 대한 포인터는 다음 위치에 저장됩니다.
Irp->CurrentStackLocation->FileObject->FsContext2.
동일한 FSRegObject 객체에 대한 포인터가 FsContext2에 두 번 저장되며, 하나는 초기화 프로세스가 사용하는 에, 다른 하나는 등록 프로세스가 사용하는 에 저장됩니다. 이 방식으로 객체에 대한 참조가 프로세스 간에 공유될 수 있습니다. 예를 들어 FSStreamReg 객체를 사용하면 다이어그램 1에 나타난 것처럼 여러 프로세스가 스트림 프레임 버퍼에 접근할 수 있습니다. 의 참조 카운트는 1로 초기화되며, 초기화가 완료된 후 한 번 더 증가합니다. 를 등록하면 참조 카운트가 다시 증가하여 객체당 총 세 개의 참조를 갖게 됩니다.
다이어그램 2: FSContextReg 객체의 초기화 및 등록
코드 블록 2: MS KS Server 드라이버의 DispatchCleanup 및 DipatchCleanup 루틴
디스패치 루틴은 패키징된 I/O 요청인 IRP의 하나 이상의 유형을 처리합니다. Windows에서는 파일에 대한 모든 핸들 참조가 닫히면 해당 파일 시스템 드라이버가
MSKSSRV에서는
코드 블록 3: FSRendezvousServer::Close, FSRegObject에 대한 프로세스 검사
프로세스별 정보는
Irp->CurrentStackLocation->FileObject->FsContext2
초기화 또는 등록 시점에. 드라이버는 호출자의 프로세스 ID를 확인하여
어떤 프로세스별 리소스(객체, 이벤트 객체 등)를 해제해야 하는지를 결정합니다. 해당 프로세스가 초기화 또는 등록을 수행한 프로세스인 경우, 해당 프로세스 전용 리소스에 대해 추가 정리 작업이 수행됩니다.
이는 일부 예외를 제외하면 일반적으로 모든 디스패치 루틴이 임의의 프로세스 컨텍스트에서 실행되기 때문에 특히 눈에 띄었습니다. 즉, 시스템은 디스패치 작업을 수행할 스레드를 선택하며, 어떤 스레드가 선택될지는 임의적입니다. 저는
또한 Windows OS의 특성상, 핸들은 자식 프로세스 상속 또는 DuplicateHandle API를 통해 다른 프로세스와 공유될 수 있습니다. 공유된 파일 핸들을 통해 다른 프로세스도 동일한
다이어그램 3: MS KS Server 디바이스 핸들 공유
이로 인해
다이어그램 4: 외부 프로세스가 FILE_OBJECT의 마지막 핸들을 닫는 경우
파일 핸들이 닫힐 때
코드 블록 4: FSRendezvousServer::Close에서 FSRegObject::Release가 두 번 호출될 수 있음
일반적으로 객체의 참조 수보다 많은 역참조가 발생하는 것만이 해제 후 사용을 유발하는 유일한 방식은 아닙니다. 그러나 이 경우에는 이 해제되었다면 참조 카운트가 0으로 감소했음을 확실히 알 수 있습니다. IRP 요청 과정에서 유효한 이 마지막으로 접근되는 시점은 호출 시점입니다. 따라서 해제 후 사용이 가능하다면 호출은 항상 객체가 이미 해제된 이후에 발생하게 됩니다. 해당 호출 과정에서 객체는 다시 한 번 역참조됩니다. 이러한 이유로, 이 사례에서는 역참조 횟수를 계산하는 것이 해제 후 사용 취약점을 식별하는 데 유효한 휴리스틱이 됩니다.
남은 작업은 객체가 언제 해제되고 언제 접근되는지를 추적하며 프로그램의 가능한 상태 전이를 분석하는 것이었습니다. 이를 위해 각 가능한 경우에 대해 요청 처리 로직을 머릿속으로 시뮬레이션했으며, 각 흐름은 해당 디스패치 함수(코드 블록 2)에서 시작합니다.
아래에는 어떤 프로세스가 마지막 참조를 닫는지에 따른 상태가 정리되어 있습니다. 각 항목은 해당 프로세스가 마지막 핸들을 닫을 경우 FSContextReg 객체에 대해 발생하는 역참조 횟수를 나타냅니다. 참고: HANDLE #1(초기화 핸들)과 HANDLE #2(등록 핸들) 사이에는 기능적 차이가 없습니다. 두 핸들 모두 필드가 동일한 메모리를 가리키는 구조를 공유하기 때문입니다.
각 MSKSSRV IPC 상태에서의 FSContextReg 역참조 횟수
성공! 최종 상태에서는 외부 프로세스가 두 번, 초기화(및 등록) 프로세스가 두 번, 총 네 번의 역참조가 발생합니다. 그러나 초기 참조 수는 세 개뿐이므로 해제 후 사용이 발생할 수 있습니다.
위에서 설명한 가상 머신 시뮬레이션 방식으로 로직을 따라가다 보니 문제를 발견했습니다. 프로세스가 초기화 또는 등록 프로세스인 경우 적절한 정리 작업이 수행되며,
코드 블록 5: FSRegObject::CloseInitProcess가 FSContext 포인터를 NULL로 설정
이는
그러나 호출 프로세스가 외부 프로세스인 경우 정리 작업이 수행되지 않으며, 함수 종료 직전에
이제 두 번째 핸들을 FSContextReg 객체를 초기화하고 등록한 동일한 프로세스가 닫는 경우, 객체는 자신이 보유한 모든 프로세스 관련 리소스를 정리하여 비어 있는 상태가 됩니다. 이로 인해 FSRendezvousServer::Close(코드 블록 4) 내부에서
결과적으로 단일
다이어그램 5: CVE-2024-30089 개념도
여기까지 따라온 독자는 외부 프로세스가 두 핸들을 모두 마지막으로 닫을 수 없는 이유를 궁금해할 수 있습니다. 그렇게 되면 겉보기에는 네 번의 역참조가 발생할 것처럼 보이기 때문입니다. 객체가 소멸되어 해제되기 전에, 전역 FSRendezvousServer 의 시작 부분에서 에 저장된 포인터가 해당 리스트의 유효한 구성원인지 확인합니다. 이 경우 객체는 함수의 마지막 부분에서 두 번째 호출 시 해제됩니다. 네 번째 호출 시점에는 객체가 이미 리스트에서 제거된 상태이므로 유효하지 않은 객체가 되어 더 이상 사용할 수 없습니다. 해제 후 사용을 트리거하려면 객체가 에서 조회되고 유효성 검사를 통과한 이후에 발생해야 합니다. 아래 코드 스니펫은 해당 취약점을 통해 획득할 수 있는 해제 후 사용 원시 동작을 보여줍니다.
코드 블록 6: use-after-free 프리미티브 경로
이 취약점에 대한 보안 업데이트 가이드에서 CVSS 점수는 "악용 가능성 높음"이며 이 취약점에 대한 공격 복잡성은 "낮음"으로 표시됩니다. Microsoft는 점수에 대한 자세한 설명을 제공하지 않지만, 다른 취약점을 패치하는 동안 몇 가지 패턴에 주목했습니다. 이 취약점은 논리 오류에서 비롯되어 안정적으로 트리거할 수 있기 때문에 이 점수를 받은 것으로 보입니다. 다이어그램 5에 설명된 단계를 따르면 공격자는 코드 블록 6에 설명된 무료 사용 후 시나리오를 일관되게 트리거할 수 있습니다. 그러나 이것이 실제로 이를 악용하는 것이 간단하다는 의미는 아닙니다. 이 시리즈의 다음 부분에서는 악용 단계에 대한 자세한 설명을 다루겠습니다.
취약점이 어떻게 발생했는지 이해하는 것은 보안 중심 개발 문화를 정착시키는 데 중요합니다. 취약점이 도입된 시점을 특정하기 위해 Winbindex에서 이전 버전의 드라이버를 확보하여
취약점 섹션에서 저는 이 버그의 근본 원인은 호출 프로세스가 외부 프로세스일 경우
Irp->CurrentStackLocation->FileObject->FsContext2
해당 포인터를
NULL로 설정하지 않은 데 있다고 언급했습니다. 놀랍게도, mskssrv.sys의 초기 버전에서 바로 이 코드 한 줄을 확인할 수 있었습니다.
코드 블록 7: FSRendezvousServer::Close의 초기 버전, FsContext2를 NULL로 설정
위 코드 블록에서는 앞선 프로세스 ID 검사 결과와 관계없이
코드 블록 8: 기능 플래그 검사 내부에서 FsContext를 NULL로 설정
위 코드 블록에는 다음 기능 플래그에 대한 검사 로직이 포함되어 있습니다.
Feature_Servicing_TeamsUsingMediaFoundationCrashes
.
기능 플래그는 다양한 기능 및 실험적 기능을 전환하는 Windows 구성 요소이지만, 이에 대한 공개 정보는 많지 않습니다. 이전 블로그 게시물에서는 취약점 패치에 기능 플래그가 어떻게 사용되었는지 설명했습니다. 기능 플래그는 특정 기능이 공식적으로 채택되기 전에 시험적으로 적용하기 위해 사용되기도 합니다. 이 경우, 만약
Feature_Servicing_TeamsUsingMediaFoundationCrashes
해당 기능이 활성화된 경우 이 NULL로 설정되지 않아 취약점이 발생합니다. 이 기능은 Windows 10 설치 환경에서 기본적으로 활성화되어 있는 것으로 확인되었습니다. Windows 11에서는 코드 블록 1에 나타난 것처럼 해당 기능 플래그 조건문이 존재하지 않으며, 포인터가 로 설정되지 않아 마찬가지로 취약합니다.
이 기능 이름을 보고 화상 회의 소프트웨어인 Microsoft Teams의 동작을 조사했습니다. 해당 애플리케이션이 MSKSSRV 기능을 사용해 프로세스 간 미디어 스트림을 공유할 수 있다는 점을 확인했습니다. 스트림 핸들 공유가 Teams의 크래시를 유발했을 가능성이 있습니다. 향후 연구 과제로는 Teams가 MSKSSRV 디바이스 핸들을 프로세스 간에 어떻게 공유하는지, 그리고 왜 올바른 포인터 정리를 수행하면 애플리케이션이 충돌할 수 있는지 분석해 보는 것이 흥미로울 것입니다.
이 시리즈의 이 부분은 취약점 자체와 해당 패치에 초점을 맞춥니다. 제가 제안한 수정 방식이 Microsoft Teams에서 충돌을 유발하는 것으로 보였기 때문에, 이 버그의 패치를 특히 면밀히 분석하고자 했습니다. 이 시점에서는 실제 애플리케이션이 MSKSSRV 드라이버를 어떻게 사용하는지에 대해 아직 충분히 분석하지 못했다는 점을 밝힙니다. 이러한 맥락이 부족하면 시스템이 현재와 같이 설계된 이유를 이해하는 데 사각지점이 생길 수 있습니다. 이 버그에 대한 완전한 패치는 기본 코드 구조의 일부 재설계를 필요로 하며, IPC 시스템이 의도된 방식으로 어떻게 동작하는지에 대한 추가 정보를 드러낼 수 있습니다. 또한 Microsoft 개발자들의 안전한 코딩 관행에 대한 통찰을 얻고자 했습니다.
실망스럽게도, 2024년 6월 보안 업데이트에서 패치된 해당 취약점의 근본적인 로직 오류는 직접적으로 수정되지 않았습니다. 대신 취약한 코드 경로에 진입하기 전에 액세스 토큰 검사를 추가하는 방식으로 대응했습니다. 아래는
코드 블록 9: IOCTL 함수는 기능 플래그 검사로 시작하며, 호출 프로세스가 프레임 서버인지 확인
위 함수는 기능이 활성화되어 있는지 확인하는 것으로 시작합니다. 이는 해당 패치와 연관된 기능 플래그일 가능성이 높습니다. 기능이 활성화된 경우
이제
코드 블록 10: KsIsCurrentProcessFrameServer가 호출 스레드의 액세스 토큰에 대해 SID 검사를 수행
이 함수는 호출 스레드의 토큰을 두 개의 특정 보안 식별자(SID)와 비교합니다. 해당 SID는 그룹에 속한 토큰에 해당합니다. 호출 스레드의 액세스 토큰에서 이 두 SID 중 하나라도 활성화되어 있다면 취약한 함수 코드가 실행될 수 있습니다.
이를 확인한 후, 관리자 권한에서 커널 권한으로 상승 가능한 취약점이 여전히 존재한다고 의심했습니다. 결국 메모리 손상 문제는 전혀 해결되지 않았습니다. 기존 익스플로잇을 약간 수정하여 이를 확인했습니다. 관리자 사용자는 서비스를 시작하고, 해당 서비스에 대한 핸들을 연 뒤, 그 핸들을 사용하여 익스플로잇 프로세스를 생성할 수 있습니다. 완전히 패치된 시스템에서도 커널 읽기/쓰기(R/W) 원시 동작을 확보할 수 있었습니다.
Microsoft는 Administrator에서 Kernel로의 권한 상승을 보안 경계로 간주하지 않지만, 유사한 버그는 위협 행위자에 의해 커널 읽기/쓰기(R/W) 프리미티브를 획득하고 이를 EDR 우회 및 루트킷 동작에 활용하는 데 사용된 사례가 있습니다. 이 프리미티브로 어떤 작업이 가능한지 궁금하다면, FuzzySec과 함께 진행한 제 BlackHat 발표를 참고하시기 바랍니다.
이 게시물은 권한 상승에 악용될 수 있는 0일 커널 취약점을 찾는 것으로 구성된 Pwn2Own 노력의 취약성 연구 부분에 초점을 맞췄습니다. 이 게시물에서는 연구에서 영감을 얻고, 버그를 찾지 못하고, 새로운 관점을 선택하고, 의심스러운 점을 발견하고, 마지막으로 취약점이 어디에 있는지 정확히 찾아내는 과정을 간략하게 설명합니다. 이제 버그가 식별되고 무료 후 사용 프리미티브가 있으므로 나머지는 간단해야 합니다. Microsoft는 그렇게 생각하는 것으로 보이며, 이 버그는 공격 복잡성이 "낮음"으로 평가되었으며 "악용 가능성이 더 높음"으로 평가되었습니다. 그들의 말이 맞을까요? 다음 편에서는 이 악용 전략과 시리즈 제목의 의미에 대해 알아보도록 하겠습니다!
Think 뉴스레터를 통해 AI, 자동화, 데이터 등 가장 중요하고 흥미로운 업계 동향에 대한 최신 소식을 받아보세요. IBM 개인정보 보호정책을 참조하세요.
훌륭한 다이어그램을 제공해 준 Andréa Piazza께 감사드립니다.
Windows 보안 모델을 인내심 있게 설명해 준 Emma Kirkpatrick께 감사드립니다.