클라우드 컴퓨팅이 필요하신가요? 지금 시작해보세요

RPC 런타임의 세 가지 원격 코드 실행 취약점 탐색

Akamai Wave Blue

에 의해 작성

Ben Barnea

May 26, 2023

Akamai Wave Blue

에 의해 작성

Ben Barnea

벤 바니아는 Akamai의 보안 연구원으로, Windows, Linux, 사물 인터넷, 모바일 등 다양한 아키텍처에서 저수준 보안 리서치와 취약점 리서치 수행에 관한 관심과 경험을 보유하고 있습니다. 그는 복잡한 메커니즘의 작동 원리, 특히 복잡한 메커니즘의 실패 방식을 배우는 것을 좋아합니다.

악용하기 어려운 취약점이라도 유능한(그리고 인내심 있는) 공격자에게는 기회가 된다는 점을 기억해야 합니다.

핵심 요약

  • Akamai 연구원 벤 바르니아(Ben Barnea)가 Microsoft Windows RPC 런타임에서 세 가지 중요한 취약점을 발견했습니다. 이들 취약점은 CVE-2023-24869, CVE-2023-24908, CVE-2023-23405이며 기본 점수는 모두 8.1입니다.

  • 이러한 취약점은 원격 코드 실행으로 이어질 수 있습니다. RPC 런타임 라이브러리는 모든 RPC 서버에 로드되고 RPC 서버는 Windows 서비스에서 일반적으로 사용되므로 모든 Windows 버전(데스크톱 및 서버)이 영향을 받습니다.

  • 이 취약점은 RPC 런타임에서 사용하는 세 가지 데이터 구조체의 정수 오버플로입니다.

  • 이는 Microsoft에 보고되어 2023년 3월 패치 화요일에서 처리됐습니다.

서론

MS-RPC는 Windows 네트워크에서 많이 사용되는 프로토콜로, 여러 서비스와 애플리케이션이 이에 의존하고 있습니다. 따라서 MS-RPC의 취약점은 심각한 결과로 이어질 수 있습니다. Akamai Security Intelligence Group은 지난 1년간 MS-RPC 리서치를 수행해왔습니다. Akamai는 취약점을 발견하고 악용하여 리서치 툴을 구축하고, 프로토콜의 문서화되지 않은 일부 내부 구조를 작성했습니다. 

이전 블로그 게시물에서는 서비스의 취약점을 중점적으로 다루었지만 이번에는 MS-RPC "엔진"인 RPC 런타임의 취약점을 검토해보겠습니다. 이 취약점은 Akamai가 2022년 5월에 발견한 취약점과 유사합니다.

정수 오버플로 패턴

세 가지 새로운 취약점은 공통적인 테마를 가지고 있습니다. 즉 세 가지 데이터 구조체에 삽입되는 정수 오버플로로 인해 존재한다는 것입니다.

  1. SIMPLE_DICT(값만 저장하는 사전)

  2. SIMPLE_DICT2(키와 값을 모두 저장하는 사전)

  3. 대기열 

이러한 모든 데이터 구조체는 배열이 가득 찰 때마다 증가하는 동적 배열을 사용해 구축됩니다. 이는 현재 배열에 할당된 메모리를 두 배로 할당함으로써 발생합니다. 이러한 할당은 정수 오버플로에 취약합니다.

그림 1은 RPC 런타임에서 디컴파일된 코드를 나타냅니다. 이 예제는 SIMPLE_DICT 구조체와 정수 오버플로가 트리거될 수 있는 취약한 코드 줄(강조 표시)에 대한 삽입 프로세스를 보여 줍니다.

그림 1은 RPC 런타임에서 디컴파일된 코드를 나타냅니다. 그림 1: SIMPLE_DICT 구조체 확장에서 정수 오버플로 발생

취약점 탐색

취약점을 트리거하려면 취약점을 트리거하는 근본 원인을 파악하고 취약한 함수에 대한 흐름의 존재 여부와 트리거하는 데 걸리는 시간을 파악해야 합니다.

간결한 설명을 위해 세 가지 취약점 중 하나인 대기열 데이터 구조체에 대해 알아보겠습니다. 다른 정수 오버플로는 본질적으로 유사하므로 다음 섹션에서 수행한 분석을 서로 바꿔 수행할 수 있습니다.

정수 오버플로의 이해

대기열은 간단한 FIFO(선입선출) 데이터 구조체입니다. RPC 런타임의 대기열은 대기열 항목의 배열, 현재 용량, 대기열에서 마지막 항목의 위치가 포함된 구조체를 사용해 구축됩니다. 

새로운 항목이 대기열에 추가되면(사용 가능한 슬롯이 있는 경우) 모든 항목이 배열에서 앞으로 이동하고, 새로운 항목이 배열의 시작 부분에 추가됩니다. 그런 다음 대기열에서 마지막 항목의 위치가 증가합니다.

대기열이 제거되면 마지막 항목이 당겨지고 마지막 항목의 위치가 감소합니다(그림 2). 

대기열 제거가 발생할 때의 스크린샷 그림 2: 대기열 및 대기열 제거 작업을 수행하는 동안의 대기열 구조체

앞서 설명한 것처럼 새로운 항목을 삽입할 때 취약점이 발생합니다. 동적 배열이 가득 차면 코드가 다음을 수행합니다.

  • 다음 크기의 새로운 배열을 할당합니다.
    CurrentCapacity * 2 * sizeof(QueueEntry)

  • 이전 항목을 새로운 배열에 복사

  • 오래된 항목의 배열을 해제

  • 용량을 두 배로 증대

32비트 시스템의 경우 새로운 배열 크기를 계산할 때 오버플로가 발생합니다.

  • 대기열을 0x1000000(!) 항목으로 채웁니다. 

  • 확장이 발생합니다. 새로운 할당의 크기가 0x10000000 * 16으로 계산됩니다.  이 오버플로로 새로운 할당 크기는 0이 됩니다

  • 길이가 0인 배열이 할당됩니다.

  • 코드는 이전 항목의 배열을 새로운 작은 배열로 복사합니다. 이로 인해 와일드 카피(선형 대형 카피)가 발생합니다.

64비트 시스템에서는 대규모 할당이 실패하기 때문에 이 취약점을 악용할 수 없습니다. 이렇게 하면 범위를 벗어난 쓰기를 트리거하지 않고 코드가 정상적으로 종료됩니다. 64비트 시스템은 이러한 취약점이 없지만 다른 정수 오버플로에 취약합니다 (SIMPLE_DICT 및 SIMPLE_DICT2).

코드 흐름

RPC 연결은 OSF_SCONNECTION 클래스를 사용해 나타냅니다. 각 연결은 여러 클라이언트 호출(OSF_SCALL)을 처리할 수 있지만 주어진 시간마다 하나의 호출만 허용되어 연결에서 실행되고 다른 호출은 대기합니다. 

따라서 대기열을 사용하는 흥미로운 함수는 OSF_SCONNECTION::MaybeQueueThisCall입니다.  이는 연결에 도달한 새로운 호출을 디스패치하는 과정의 일부로 호출됩니다. 이 경우 대기열은 다른 호출이 처리되는 동안 수신 호출을 " 보류"하는 데 사용됩니다.

따라서 클라이언트 호출을 차례로 전송해 대기열을 채울 수 있는 사용자 제어 방법이 있지만, 이 함수는 다음과 같은 사항이 충족돼야 합니다. 연결이 현재 호출을 처리하고 있습니다. 즉 대기열을 채우려면, 완료하는 데 시간이 걸리는 호출이 있어야 합니다. 호출이 처리되는 동안 디스패치 대기열을 채우는 여러 개의 새로운 호출을 보냅니다. 

완료하는 데 가장 오래 걸리는 함수 호출은 무엇일까요? 

  • 최고의 후보는 무한 루프를 일으킬 수 있는 함수입니다.

  • 두 번째로 좋은 후보는 인증 강제 취약점입니다. 서버가 우리와 연결되어 응답 시간을 제어할 수 있기 때문입니다.

  • 마지막 수단은 복잡한 논리가 있는 복합 함수나 많은 데이터를 처리하느라 완료하는 데 오랜 시간이 걸리는 함수입니다.

Akamai는 자체 인증 강제 취약점을 이용하기로 했습니다.

트리거하는 데 걸리는 시간

지금까지 대기열을 채우는 데 필요한 것과 실행 방법을 이해했습니다. 하지만 중요한 질문이 하나 있습니다. 이것이 실용적일까요?

정수 오버플로가 발생하는 변수를 최소한의 제어만 할 수 있으며, RefCount(참조 카운트) 오버플로와 유사하게 한 번에 하나만 증가시킬 수 있습니다. 이러한 종류의 정수 오버플로는 완전히 제어할 수 있는 두 변수가 더해지거나 곱해지는 경우 또는 추가된 크기를 어느 정도 제어할 수 있는 경우(예: 패킷 크기)의 정수 오버플로보다 좀 더 비효율적입니다.

앞에서 언급한 것처럼 0x10000000(~268M) 항목을 할당해야 합니다. 매우 많은 항목입니다.

제 머신에서 취약점을 트리거하려고 시도한 결과 초당 약 15~20건의 대기 호출이 발생했습니다. 따라서 평균 머신에서 트리거하려면 약 155일이 걸릴 것입니다. 초당 대기 호출 수는 증가할 것으로 예상했습니다. RPC 런타임이 너무 느린 이유는 무엇인가요? 멀티스레딩이 아닌가요? 

여러 스레드가 동일한 연결에 대해 서로 다른 호출을 동시에 처리하고 대기열에 배치한다고 가정했습니다. 방향을 몇 번 바꿔 보니 실제로 흐름이 약간 달라졌습니다.

MS-RPC 패킷 처리

호출이 디스패치되기 직전에 코드는 새로운 스레드를 스핀하고(필요한 경우) OSF_SCONNECTION::TransAsyncReceive를 호출합니다. TransAsyncReceive는 동일한 연결에서 요청을 수신하려고 합니다. 그런 다음 (CO_SubmitRead를 호출해) 새로운 스레드로 요청을 제출합니다. 

다른 스레드는 TppWorkerThread에서 요청을 선택하고 결국 ProcessReceiveComplete로 이어지며, 여기서 MaybeQueueThisCall을 호출해 SCALL을 디스패치 대기열에 배치합니다. 그런 다음 전파되어 이 연결에 대한 새로운 요청을 수신하려 합니다. 

따라서 여러 스레드가 실행 중일 수도 있지만 실제로는 하나의 스레드만 연결에 사용됩니다. 즉, 여러 스레드에서 동시에 대기열에 호출을 추가할 수 없습니다.

“나머지” 패킷

취약점을 악용하는 데 걸리는 시간을 최소화하기 위해 초당 더 많은 호출을 수행하는 방법을 찾고자 했습니다. 수신 코드를 반전할 때 패킷의 길이가 패킷의 실제 RPC 요청보다 크면 RPC 런타임에서 나머지를 저장합니다. 나중에 새로운 요청을 확인할 때는 소켓을 즉시 사용하지 않습니다. 먼저 "나머지" 패킷이 있는지 확인하고, 있다면 나머지 패킷의 새로운 요청을 처리합니다.

이렇게 하면 각각 최대 개수의 요청이 포함된 훨씬 적은 수의 패킷을 보낼 수 있습니다. 초당 대기 호출 수는 비교적 변함없이 유지되기 때문에 별 도움이 되지 않는 것 같습니다.

요약

이러한 취약점이 악용될 가능성은 낮을 것으로 예상하지만, 지난 1년간 MS-RPC를 리서치하면서 발견한 중요한 취약점 목록에 추가했습니다. 악용하기 어려운 취약점이라도 유능한(그리고 인내심 있는) 공격자에게는 기회가 된다는 점을 기억해야 합니다. 

MS-RPC는 수십 년간 존재해 왔지만, 여전히 발견 가능한 취약점이 있습니다. 

이번 리서치를 통해 다른 연구자들도 MS-RPC와 이로 인해 발생하는 공격표면을 살펴볼 수 있기를 바랍니다. 신속하게 대응하여 문제를 해결해 주신 Microsoft에 감사의 말씀을 전합니다.

Akamai의 GitHub 리포지터리 에는 여러분의 시작에 도움을 드릴 수 있는 툴과 기술이 가득합니다.



Akamai Wave Blue

에 의해 작성

Ben Barnea

May 26, 2023

Akamai Wave Blue

에 의해 작성

Ben Barnea

벤 바니아는 Akamai의 보안 연구원으로, Windows, Linux, 사물 인터넷, 모바일 등 다양한 아키텍처에서 저수준 보안 리서치와 취약점 리서치 수행에 관한 관심과 경험을 보유하고 있습니다. 그는 복잡한 메커니즘의 작동 원리, 특히 복잡한 메커니즘의 실패 방식을 배우는 것을 좋아합니다.