
Windows 시스템에서 DLL 하이재킹과 활성화 캐시 중독(activation cache poisoning)을 연계하여 중간 무결성(medium integrity)에서 높은 무결성(high integrity)으로 권한을 상승시키는 CVE-2024-6769에 대한 기술 문서 및 PoC입니다.
이 블로그는 두 가지 연쇄 버그에 관한 것입니다. 1단계는 ROOT 드라이브 리매핑으로 인한 DLL 하이재킹 버그이며, 2단계는 CSRSS 서버가 관리하는 활성화 캐시 중독 버그입니다.
1단계는 BlueFrost Security의 Nicolás Economou가 Ekoparty 2023에서 발표한 "I'm High"라는 프레젠테이션에서 자세히 소개되었습니다. 그는 당시 Microsoft가 아직 패치하지 않은 취약점을 악용하는 방법을 설명했습니다. 이를 통해 MEDIUM INTEGRITY 사용자가 제한된 HIGH PRIVILEGES를 가진 상태로 권한이 상승될 수 있었지만, 완전한 관리자가 되기 위한 모든 액세스 권한은 없었습니다.
2단계는 해당 컨퍼런스에서 발표되지 않았지만, 연구를 시작하기 위한 몇 가지 단계가 제안되었습니다.
먼저 서론적 맥락을 제공하기 위해 1단계를 검토하겠습니다. 그런 다음 제한된 HIGH INTEGRITY에서 완전한 관리자로의 전체 권한 상승을 달성하는 세부 사항을 다루는 2단계 연구에 대해 자세히 살펴보겠습니다. 여기에는 모든 Windows 버전을 위한 두 단계 모두의 완전한 동작 PoC가 포함되며, Windows 10, Windows 11, Windows Server 2022 및 Windows Server 2019에서 모든 업데이트가 적용된 상태로 성공적으로 테스트되었습니다.

이 단계의 유일한 요구 사항은 초기 프로세스가 MEDIUM INTEGRITY LEVEL에서 시작되고 사용자가 Administrators 그룹에 속한다는 것입니다.
1단계 악용은 다음 단계로 요약할 수 있습니다.
예: 디스크를 다음에서 리매핑
"C:\" → "C:\users\public"
이렇게 하면 "system32" 폴더도 다음에서 리매핑됩니다.
"C:\windows\system32" → "C:\users\public\windows\system32"
이러한 영향을 받는 프로그램 중 하나는 HIGH INTEGRITY LEVEL로 실행되지만 관리자 권한이 없는 CTFMON입니다.
일반적으로 CTFMON은 실제 system32 폴더에서 MsCtfMonitor.dll이라는 모듈을 로드하려고 하지만, ROOT 드라이브가 리매핑되었기 때문에 우리가 제어하는 가짜 system32에서 MsCtfMonitor.dll을 찾습니다. 이 폴더에는 동일한 이름의 조작된 DLL을 만들어 배치할 수 있습니다.
이 시점에서 가짜 system32 폴더에 우리 버전의 MsCtfMonitor.dll을 배치하면 해당 DoMsCtfMonitor 함수가 호출되어 HIGH INTEGRITY LEVEL에서 우리 코드를 실행합니다.




동시에, 해당 프로세스가 HIGH INTEGRITY LEVEL임에도 불구하고 관리자 권한이 없음을 확인할 수 있습니다.


Nicolas는 Ekoparty 프레젠테이션에서 악용을 완료하기 위한 다음 단계를 제안했습니다.


단순해 보이지만, 리버싱과 디버깅에 많은 시간이 필요합니다.
이 공격 벡터에 대해 조금 더 파고들면서, 활성화 컨텍스트 캐시 중독이 일부 익스플로잇에서 사용되어 왔음이 분명해졌습니다. 따라서 추가적인 맥락과 통찰을 얻기 위해 이전에 악용이 어떻게 수행되었는지 배우는 것이 가치가 있습니다. 이 악용에 대한 자세한 내용은 Zero Day Initiative의 분석 자료인 *Activation Context Cache Poisoning: Exploiting CSRSS for Privilege Escalation*에서 확인할 수 있습니다.
활성화 캐시는 프로그램이 특정 버전을 요구하는 라이브러리를 로드하려고 할 때 사용됩니다.
예를 들어, 애플리케이션이 C:\Windows\System32\comctl32.dll을 로드하려고 할 때, 해당 위치의 comctl32.dll이 애플리케이션이 필요한 버전이라는 보장이 없습니다. 이것이 활성화 컨텍스트 캐시(Activation Contexts Cache)의 기본 사용 사례입니다. 프로그램은 새 활성화 컨텍스트 항목을 캐시에 추가하도록 CSRSS 서버에 요청을 보낼 수 있으며, 이를 통해 프로그램이 필요한 특정 라이브러리 버전을 로드할 수 있습니다.
이 목적을 위해 소위 매니페스트(manifest)가 사용되며, 이는 XML 형식입니다. 일반적으로 EXE 또는 DLL 파일에 리소스로 내장됩니다. 그렇지 않으면 Windows는 프로그램의 실행 파일이 있는 동일한 폴더에서 매니페스트 파일을 검색합니다.
위에서 언급한 URL에는 PATH TRAVERSAL 기술을 통해 공격자가 제어하는 디렉터리에서 advapi32.dll 라이브러리를 로드하도록 시스템을 속이는 것과 같은 오래된 익스플로잇에서 사용된 매니페스트 파일의 예가 있습니다.

물론, 사용되던 일부 공격 벡터는 패치되었고 일부 새로운 기술이 발견되었습니다. 또한 Windows 11 22H2의 2022년 10월 패치에는 새로운 검사가 추가되었습니다.
이 패치가 적용된 후, **활성화 컨텍스트(ACTX)**를 등록할 때의 검사는 캐시에 새 항목을 추가하는 프로세스의 RID가 이를 사용할 프로세스의 RID와 같거나 더 높은 경우에만 우회할 수 있습니다.
winnt.h에서 RID 값을 확인할 수 있습니다.

이 검사를 우회하기 위한 제안은 조작된 DLL이 실행되는 CTFMON 프로세스에서 활성화 컨텍스트 요청을 생성하는 것입니다. 이 조작된 DLL은 RID=0x3000을 가지며, 항목이 캐시에 추가된 후 RID=0x3000인 TCMSETUP이 tapi32.dll을 로드합니다.
단계를 따르려는 시도 동안, CreateActCtx를 사용하여 ACTX를 등록하기 위해 가능한 모든 조합을 시도했습니다. 그러나 항상 이를 회피하는 검사가 있었기 때문에 불가능했습니다.
이 함수는 사용자 영역에 있으며 kernel32.dll에서 내보내진다는 점에 유의해야 합니다. DLL을 메모리에서 패치하면 검사를 우회할 수 있습니다. 그다지 우아하지는 않지만 가능하며 작동할 것입니다.

Nicolas의 프레젠테이션 슬라이드는 LOW LEVEL을 사용하도록 제안합니다. 그러나 윙크하는 얼굴을 보면, 패치 없이 이 버그를 악용할 때 CreateActCtx를 사용하는 것이 더 나은 옵션이 아님이 분명했습니다.

**ALPC(Advanced Local Procedure Call)**는 Windows 운영 체제 내에서 고속으로 메시지를 전송하는 데 사용되는 프로세스 간 통신 메커니즘입니다. 표준 Windows API와 달리 ALPC는 애플리케이션에서 직접 사용할 수 없습니다. 대신 Windows 운영 체제의 구성 요소만 접근할 수 있는 내부 메커니즘입니다. (그리고 우리도요.)
추가 연구를 통해, 일부 오래된 캐시 중독 익스플로잇이 서버와 직접 통신하기 위해 ALPC를 사용한다는 것을 발견했습니다. 예는 Philip Tsukerman의 기사 *Activation Contexts—A Love Story*에서 볼 수 있습니다.
CsrClientCallServer 함수는 Win32 프로세스와 CSRSS 프로세스 간의 ALPC 인터페이스를 구현합니다.
따라서 서버 역할을 하는 CSRSS 프로세스에 CsrClientCallServer를 사용하여 호출을 시도해야 합니다.
이전 익스플로잇에서 예를 찾던 중, Packet Storm에서 관련 힙 버퍼 오버플로 문제에 대한 페이지를 발견했습니다.
CSRSS 서버가 올바른 패키지로 호출되면, 패킷은 sxssrv.dll 모듈에 속한 BaseSrvSxsCreateActivationContextFromMessage 함수에서 수신됩니다.
이 함수는 수신된 패킷에 대한 포인터 하나만을 인수로 받습니다. 이를 리버싱하기 위해 맞춤형 TotalMessage 구조체를 만들었습니다.
TotalMessage 구조체 패킷은 처음 0x40바이트가 HEADER이고, 그 뒤에 _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG 구조체인 내장 활성화 컨텍스트 메시지가 옵니다.
TotalMessage 구조체는 아래에서 확인할 수 있습니다.

그리고 여기에 _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG 구조체**:**가 있습니다.
이 구조체 내부에는 언어 또는 CultureFallbacks, AssemblyDirectory, TextualAssemblyIdentity, AssemblyName에 해당하는 여섯 개의 UNICODE_STRING과, 각각 내부에 하나의 UNICODE_STRING을 포함하는 두 개의 _BASE_MSG_SXS_STREAM 구조체가 있습니다.
아래는 _BASE_MSG_SXS_STREAM 구조체입니다.

서버가 수락하는 유효한 패킷을 생성하는 것이 어렵기 때문에, 그 방법을 자세히 설명할 가치가 있습니다.
_BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG 내부의 Flags 필드 값은 많은 조합이 있기 때문에 매우 중요합니다. 올바른 플래그 값이 없으면 버그를 악용할 수 없습니다.
예를 들어, 제 MsCtfMonitor.dll 코드를 보겠습니다. 여러 번의 시도 끝에 이 버그 악용을 위한 유일한 올바른 flags 값은 0x41이라는 결론을 내렸습니다.

서로 다른 값의 조합은 잘못된 경로 플래그 값을 초래할 수 있습니다.

동일한 TotalMessage 구조체는 size=0x40바이트의 헤더를 가집니다. 나머지 0x1f8바이트는 _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG 구조체를 위해 예약되어 있습니다.
struct TotalMessage
{
signed __int64 pad[8];
_BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG message;
};
할당 크기는 0x40+0x1f8입니다.

그런 다음 문자열을 조합하고 tapi32.dll에 대한 활성화 캐시 컨텍스트를 수행했습니다. 이 DLL은 TCMSETUP이라는 프로세스가 로드하는 매우 드물게 사용되는 DLL입니다. 이 프로세스는 관리자와 동일한 권한을 가진 HIGH PRIVILEGES INTEGRITY LEVEL(RID=0x3000)입니다.

제 DLL 코드에서는 CaptureUnicodestring 함수가 호출됩니다. 이 함수는 결국 CsrCaptureMessageString을 호출하게 됩니다:
NTSTATUS CaptureUnicodeString(LPVOID CaptureBuffer, PSTR OutputString,
PCWSTR String, ULONG Length = 0) {
if (Length == 0) {
Length = lstrlenW(String);
}
return CsrCaptureMessageString(CaptureBuffer, (PCSTR)String, Length * 2,
Length * 2 + 2, OutputString);
}
이 단계는 패키지를 올바르게 준비하기 위해 필요하며, 시스템이 내 패킷의 문자열을 CSRSS 프로세스로 복사할 수 있게 해줍니다. 이렇게 하면 문자열이 유효한 상태로 유지되고 내 포인터가 해당 컨텍스트에서 유효한 포인터로 대체됩니다.
또한 "Tasks" 언어가 포함된 내장 XML 매니페스트를 추가했습니다. 이는 존재하지 않는 언어이지만 악용의 핵심이 될 것입니다. (이것에 대해 Nico에게 감사를 표합니다.)

내 코드에서 또 다른 중요한 세부 사항은 CaptureBuffer가 생성될 때입니다. CsrAllocateCaptureBuffer 함수에는 서버로 관리하고 복사할 UNICODE_STRING의 수를 정의하는 인수가 있습니다.
내 경우에는 “4”개의 문자열을 사용했습니다.

값이 “4”인 인수는 아래에 나와 있습니다.

활성화 서버에 도달하기 위해, CsrClientCallServer 함수는 위에서 언급한 오래된 익스플로잇과 동일한 ApiNumber 0x1001001E를 사용하여 내 MsCtfMonitor.dll에서 내 패킷을 보냅니다.
Geoff Chappell의 블로그는 CsrClientCallServer에 대한 자세한 내용을 제공합니다.

다음은 CsrClientCallServer 호출입니다.

그리고 다음은 내 DLL에서 빌드한 보낼 패키지입니다.

Manifest.Offset 값은 내 내장 XML 매니페스트를 가리킵니다.

활성화 프로세스를 기록하는 흥미로운 명령은 sxstrace이며, 대상 내의 관리자 콘솔에서 사용됩니다.
이 명령은 추적을 활성화하고 로그 결과를 sxstrace.etl에 저장합니다. (추적을 종료하려면 ENTER를 누르십시오.)
sxstrace trace -logfile:sxstrace.etl
그런 다음 원시 sxstrace.etl 파일을 읽을 수 있는 형식으로 변환할 수 있습니다.
sxstrace parse -logfile:sxstrace.etl -outfile:sxstrace.txt
패키지가 올바르다면, csrss 프로세스의 sxssrv 모듈에 있는 BaseSrvSxsCreateActivationContextFromMessage 함수에 도달해야 합니다. 따라서 원격 커널을 디버깅할 때는 이 프로세스로 컨텍스트를 전환해야 합니다. 그런 다음 중단점을 설정하려면 사용자 모드 심볼을 다시 로드해야 합니다.

저는 Windbg 플러그인과 함께 IDA PRO를 사용하여 커널을 디버깅했습니다.

BaseSrvSxsCreateActivationContextFromMessage에서 멈추면, RCX는 TotalMessage 구조체를 가리킵니다.

초기 0x40 HEADER 바이트(시스템이 클라이언트 프로세스의 PID 등 일부 값으로 채움) 다음에, _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG 구조체에 속하는 내 활성화 메시지를 볼 수 있습니다.

문자열을 가리키는 포인터는 보낼 때와 동일한 값을 가지지 않는다는 점에 유의하십시오.

그러나 이 포인터들은 문자열을 올바르게 가리켰습니다.

패킷이 클라이언트에서 서버로 전송될 때, 시스템은 내 프로세스의 문자열을 CSRSS 프로세스로 복사하고 내 패킷의 포인터가 해당 컨텍스트에서 유효하도록 변경했습니다.
그 후, BaseSrvSxsCreateActivationContextFromMessage 함수는 문자열이 유효한지 확인합니다.

루프에서 여섯 개의 문자열을 확인하지만, 검사를 완벽하게 통과합니다. 제 경우에는 네 개의 문자열만 전달했고 나머지 두 개는 0입니다.
다른 사소한 검사 후, 활성화 프로세스에서 가장 중요한 함수인 BaseSrvSxsCreateActivationContextFromStructEx를 호출합니다.

BaseSrvSxsCreateActivationContextFromStructEx에 도달하면, r8은 활성화 메시지인 _BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG를 가리킵니다.

flags 값을 평가합니다. 제 경우에는 값이 0xD에 대해 0x41이었습니다.

test 함수는 프로세서 아키텍처(1) 검증에 해당하는 플래그 옵션을 사용하여 우회할 수 있습니다.

그 후, 호출 프로세스의 RID를 가져와 이후 비교를 위해 저장합니다. 이 경우 CTFMON이 HIGH INTEGRITY LEVEL을 가지므로 RID는 0x3000입니다.

이 함수의 가장 중요한 부분은 BaseSrvActivationContextCacheLookupEntry 호출입니다.

tapi32.dll에 대한 항목이 있는지 확인하기 위해 활성화 컨텍스트 캐시를 검색합니다.
BaseSrvActivationContextCacheCompareEntries라는 함수를 호출하며, 이 함수는 활성화 메시지 항목의 특정 부분을 캐시의 모든 기존 항목과 비교합니다.

내 패킷에서 보낸 LastWriteTime 값을 모든 항목의 동일한 값과 비교합니다.
나는 이전에 tapi32.dll에서 GetFileTime을 사용하여 이 값을 계산하고 내 활성화 패키지 안에 넣어 보냈습니다.

tapi32.dll에 대한 항목이 없으므로 비교는 일치하지 않습니다. 예상대로 0xC0000225 오류를 반환합니다. 그 후 캐시에 추가하기에 적합한지 확인하기 위해 내 ACTX를 검사합니다.

서버는 내 XML 내장 매니페스트와 이를 가리키는 Manifest.Offset 주소를 읽어야 합니다. 그러나 이 새로운 컨텍스트에서는 아직 유효한 포인터가 아닙니다. 이 값을 사용하여 내 XML 내장 매니페스트가 어떻게 그리고 언제 읽히는지 확인하기 위해 이 값에 중단점을 설정할 가치가 있습니다.
CSRSS가 내 ACTX 요청에서 보낸 내 내장 XML 매니페스트를 어디에서 읽는지 확인하려면 Manifest.Offset에 중단점을 설정해야 합니다. 또한 멈출 때마다 다른 주소로 복사하는 경우 중단점을 추가해야 합니다.

Manifest.Offset 값 주소를 읽을 때 중단점에서 멈춥니다.
Manifest.Offset 필드에 배치된 주소는 해당 컨텍스트에 속하므로, 이 주소를 사용하여 NtReadVirtualMemory를 통해 CTFMON 프로세스에서 내 내장 XML 매니페스트를 읽습니다.

내 내장 XML 매니페스트가 읽혀 대상 버퍼로 복사됩니다.

CTFMON 프로세스 컨텍스트로 전환하여 내 내장 XML 매니페스트가 이전에 보낸 Manifest.Offset 주소에 있는지 확인합니다. 제 경우에는 0x7ff93a261470이었습니다.

내장 XML 매니페스트 읽기는 SxSGenerateActivationContext에서 호출됩니다. 캐시의 유효한 항목에서 찾을 수 없으므로 내장 매니페스트를 사용하여 이를 "생성"하려고 시도합니다.

그 시점부터 내 내장 XML 매니페스트 구문 분석을 시작합니다.
마지막 호출 스택을 살펴보고, 버퍼가 완전히 채워졌을 때 멈추도록 RtlReadOutOfProcessMemoryStream 호출에 중단점을 설정하기로 결정했습니다.

이제 "Tasks" 문자열에 대한 액세스에 중단점을 배치하여 서버가 이를 읽거나 처리할 때 멈출 수 있습니다.

다음은 내장 XML 매니페스트 안의 tasks 문자열입니다.

읽기와 복사 과정에서 여러 번 멈춥니다.

문자열 "tasks"를 와이드 문자로 변환할 때 CharEncoder::wideCharFromUtf8에서 멈춥니다.

그런 다음 XML 파서에서 멈춥니다.

함수 이름 parseAttributes가 의미하듯이 XML 속성 구문 분석을 계속합니다.

그런 다음 ValidateElementAttributes에서 호출된 memcpy에서 멈춥니다.

복사가 일어나는 곳에 또 다른 중단점을 배치할 수 있습니다.

함수 이름 SxspValidateLanguageAttribute가 의미하듯이 언어 속성을 검증합니다.

다시 memcpy에서 멈추지만, 이번에는 SxspCreateAssemblyIdentityfromIdentityElement에서 호출됩니다.

한 번 더 memcpy에서 멈춥니다. 이번에는 SxsInsertAssemblyIdentityAttribute+0xc48에서 호출됩니다.
그런 다음 SxsInsertAssemblyIdentityAttribute에서 멈춥니다:

이번에는 BufferedStream::prepairForInput에서 memcpy를 마지막으로 호출합니다:

그런 다음 여기에서 tasks 문자열을 읽습니다:

그런 다음, 여기에서 읽습니다:


여기에서 계속 읽습니다:


이 함수들의 이름이 제 관심을 끌었습니다. ProbingCandidate라는 이름에는 SXS txt 로그 파일에 사용된 것과 같은 단어(probing manifests)가 포함되어 있습니다.

다시 여기서 멈춥니다:

다음으로, GetFileAttributesExW를 사용하여 SXS txt 로그에 언급된 첫 번째 파일이 존재하는지 확인합니다. 존재하지 않으므로 0을 반환합니다.

파일 확인 순서는 로그 파일에서 볼 수 있습니다:

두 번째 파일은 tasks 폴더의 tapi32.dll 경로이므로 존재하지 않습니다:

거기서부터 tasks에서 tapi32.manifest를 "probing"하는 것처럼 보입니다:

그런 다음 CProbedAssemblyInformation::ProbeManifestExistence에 도달합니다:


내 매니페스트 파일이 tasks 폴더에 존재하는지 확인합니다. 존재하므로 오류를 반환하지 않습니다:

자, "tasks" 폴더에 있는 tapi32.manifest를 찾았습니다.
서버는 제 임베디드 XML 매니페스트에 포함된 "tasks" 언어 값 때문에 system32의 "tasks" 하위 폴더에서 매니페스트 파일을 검색하도록 강제되었습니다:


경로가 사용되는 위치를 확인하기 위해 중단점을 계속 배치하면, EncodingStream::Read에서 멈추고 여기서 tapi32.manifest 파일 내용을 읽습니다.

다음으로, TAPI32.manifest 파일 내용을 파싱합니다. 오류가 있으면 SXS TRACE 로그에 표시되므로 수정하기가 더 쉽습니다.

제 TAPI32.manifest 파일이 올바르게 파싱되면, 오류 없이 BaseSrvSxsCreateActivationContextFromStructEx로 돌아갑니다. 이렇게 하면 FAILED 문자열이 포함된 메시지가 출력되지 않습니다.
제 경우에는 Activation Context 생성이 제 TAPI32.manifest 파일을 사용하여 성공했습니다.


그런 다음 제 항목이 캐시에 삽입되는 호출에 도달했습니다.
아무 문제 없이 전달되어 0을 반환합니다. 이것은 올바른 값이며, 조작된 TAPI32.manifest가 포함된 항목이 성공적으로 삽입됩니다.

제 항목은 Activation 캐시에 포함되고 서버는 CTFMON의 DLL 호출에 OK 응답을 반환합니다.
로그 txt 파일은 전체 프로세스를 보여줍니다.
임베디드 XML 매니페스트를 읽습니다. 언어가 "Tasks"이므로 system32의 "Tasks" 하위 폴더에서 새 매니페스트 파일을 검색합니다. 이는 언어가 "en-us"로 설정된 경우 system32의 "en-us" 하위 폴더에서 매니페스트를 검색하는 것과 동일합니다.

SXS 로그 txt 파일에는 "Activation Context generation succeeded" 메시지가 표시됩니다!

제 ACTX 항목이 캐시에 추가된 후, tcmsetup.exe가 실행되면 tapi32.dll을 로드하고, 제 매니페스트 파일을 사용하여 imm32.dll을 로드해야 합니다.
그러나 그렇게 간단하지 않습니다. 로드를 방해할 수 있는 몇 가지 검사가 있기 때문에 imm32.dll을 로드할 수 없습니다.
검사는 동일한 BaseSrvSxsCreateActivationContextFromStructEx 함수에 대한 이후 호출에서 수행되므로, 모든 중단점을 제거하고 이 함수에만 하나만 남겨 두십시오.
거기서 콘솔에서 TCMSETUP.EXE를 실행할 수 있습니다. 다만 제 PoC는 Activation Cache 중독이 완료된 후 MsCtfMonitor.dll에서 TCMSETUP을 실행합니다:

중단점에서 여러 번 멈춥니다. 멈출 때마다 r8이 가리키는 구조체를 확인하여 tapi32.dll과 관련된 요청인지 확인합니다.

다른 모듈에 대한 많은 중지 후, TCMSETUP.exe에 대한 요청이 나타납니다:

호출 스택에서 프로세스가 생성되는 순간부터 온 것임을 알 수 있습니다. TCMSETUP에 대한 activation cache에 항목이 있는지 확인하기 위해 호출됩니다.
TAPI32.dll에 대한 호출이 도착할 때까지 계속 실행합니다. 그 전에 TCMSETUP에 대한 여러 호출이 있을 것입니다.

마지막으로 도착하는 패킷은 제가 DLL에서 캐시에 항목을 삽입할 때 이전에 만든 패킷과 매우 유사해야 합니다. 그러나 이제는 TCMSETUP이 TAPI32.dll을 로드하려고 할 때 멈춥니다.

이 시점에서 이 패키지에서 몇 가지 중요한 값들을 발견했습니다.

_BASE_SXS_CREATE_ACTIVATION_CONTEXT_MSG 구조체의 시작 부분에서 위쪽으로 0x40 바이트를 확장하여 TotalMessage 구조체를 할당합니다. TAPI32.dll에 대한 요청을 하는 프로세스의 PID는 DLL을 로드하려는 TCMSETUP입니다.

컨텍스트를 TCMSETUP 프로세스로 변경하면, Manifest.Offset 값이 일부 임베디드 XML 매니페스트를 가리키는 것을 볼 수 있습니다.


NOTEPAD에서 tapi32.dll을 열어 수신된 XML 임베디드 매니페스트가 파일에 포함된 것과 동일한지 확인합니다.
TCMSETUP은 이전에 파일 리소스를 읽어 매니페스트를 가져와 임베디드 XML 매니페스트로 패킷에 넣습니다.
그 후, BaseSrvSxsCreateActivationContextFromStructEx에서 호출되는 BaseSrvActivationContextCacheCompareEntries 함수에 의해 다시 비교가 수행됩니다. 이제 제 tapi32.dll 항목도 캐시에 있습니다.

BaseSrvActivationContextCacheCompareEntries는 루프 내에서 호출되어 실제 요청을 Activation Context Cache의 모든 항목(제 항목 포함)과 비교합니다.
먼저 두 LastWriteTime 값을 비교합니다. 값이 같으므로 더 많은 값을 계속 비교합니다.
이 LastWriteTime 값은 매우 중요합니다. 값이 다르면 제 캐시 항목이 버려지고 제 imm32.dll은 로드되지 않습니다.
계속 진행하여 다음 검사에서 멈춥니다.

이제, 두 값 모두 0x7c여야 하는 ResourceName 값을 확인합니다.

그런 다음 실제 ACTX 패킷의 언어("en-us")를 제 캐시 항목의 언어와 비교합니다. 제 캐시 항목의 언어도 "en-us"입니다.

제 패키지에도 동일한 언어 값이 있습니다:

그런 다음 프로세서 아키텍처를 비교합니다. 이 경우 두 값 모두 9입니다:


그런 다음 두 Manifest.path를 비교합니다.

System Directory 값을 사용하여 하드코딩 없이 동일한 경로를 만들었습니다:

그런 다음 AssemblyDirectory를 비교합니다. 이것도 동일합니다:


모든 비교가 올바르면 0을 반환합니다. 이는 제 항목이 activation cache에서 발견되었고 사용됨을 의미합니다.
첫 번째로 제 요청을 보내 항목을 추가할 때는 TAPI32.dll에 대한 캐시 항목이 없었기 때문에 비교가 오류를 반환했다는 점을 기억하십시오. 제 항목이 이전에 추가되었으므로 이제 0을 반환합니다.
그 후, TCMSETUP의 RID와 CTFMON의 RID를 비교합니다. 둘 다 RID = 0x3000이므로 프로세스가 계속됩니다.
RID 패치에 대한 전체 설명은 Zero Day Initiative의 블로그에서 확인할 수 있습니다.

이 패치의 코드는 다음과 같습니다:

R15에는 호출자 TCMSETUP = 0x3000의 RID가 있고, buffer에는 CTFMON 프로세스의 RID=0x3000이 저장되어 있습니다.
앞서 말했듯이 Microsoft는 2022년 10월에 이 RID 확인 패치를 추가했습니다.
이 패치가 구현된 후, **MEDIUM INTEGRITY LEVEL PROCESS (0x2000)**에서 동일한 MsCtfMonitor.dll을 사용하여 tapi32.dll 항목을 캐시에 추가하려고 하면 항목이 캐시에 추가되기는 하지만 실패합니다. 호출자 프로세스의 RID 0x2000이 저장되고, TCMSETUP을 RID=0x3000으로 실행하여 imm32을 로드하려고 하면 RID가 비교되어 항목이 제거되기 때문입니다.
그 가상의 경우, R15에는 imm32 로드를 위해 tapi32.dll을 로드하도록 요청한 TCMSETUP 프로세스의 RID=0x3000이 있고, "buffer" 변수에는 항목을 캐시에 추가한 MEDIUM INTEGRITY LEVEL 프로세스의 RID=0x2000이 저장되어 있을 것입니다.

최신 Windows 버전에서는 캐시에 항목 추가를 요청하는 프로세스가 실행자보다 낮은 권한이면 캐시 중독이 작동하지 않으며 항목이 제거됩니다. 이 패치 이전에 릴리스된 이전 버전은 모든 RID에서 문제없이 작동합니다.

이 경우로 돌아가면, RID 검사를 통과하고 두 프로세스 모두 동일한 RID=0x3000을 가집니다. 결과적으로 항목은 삭제되지 않고 오류 없이 계속됩니다.
서버는 TCMSETUP에 응답을 반환합니다. tapi32.dll을 로드할 때 제 항목과 tapi32.manifest 파일을 사용하며, 이 파일은 tasks 폴더에서 imm32.dll을 로드합니다.
이것이 LoadLibrary부터 TCMSETUP이 tapi32.dll을 로드할 때 activation cache에 요청하는 지점까지의 전체 경로입니다.

BasepCreateActCtx가 CSRSS 서버에 요청을 보내는 주체입니다. IMM32.dll 모듈을 결국 로드하는 시점을 확인하려면 시도가 필요합니다.
kernel32.dll을 보면 CsrBasepCreateActCtxCommon을 호출합니다. 내부에는 제 DLL에서 캐시 항목을 삽입하기 위해 만든 것과 유사한 서버 호출이 있습니다.

제 것과 동일한 ApiNumber를 사용합니다.
TCMSETUP을 실행할 때, 제 tapi32.manifest 파일이 수락된 후 서버에서 반환될 때 해당 위치에 중단점을 배치할 수 있습니다.

CsrBasepCreateActCtxCommon에서 서버 호출이 발생할 때까지의 전체 호출 스택입니다.

호출 스택의 일부 함수 반환 지점에 중단점을 배치합니다.

멈추면 imm32.dll이 "tasks" 폴더에서 로드된 것을 관찰할 수 있습니다:

PROCESS MONITOR를 사용하여 TCMSETUP이 "tasks" 폴더에서 IMM32.dll을 로드한다는 것을 검증할 수 있습니다.

방금 실행된 CMD 프로세스는 HIGH 권한을 가집니다.

또한 Administrator와 동일한 권한을 가집니다.
이러한 권한으로 이제 관리자 권한 상승이 필요한 모든 프로그램을 설치하고 임의의 폴더에 쓸 수 있습니다. 예를 들어 SYSTEM32 또는 프로그램 설치 폴더에 쓰는 것이 가능하며, 아래 VIDEO DEMO에서 확인할 수 있습니다.
다음은 악용 이전의 권한입니다(무결성 수준 Medium, Administrator 아님):

그리고 다음은 악용 이후의 권한입니다(무결성 수준 High, FULL Administrator):

이 시점에서 일부 조작된 DLL을 시스템 폴더에 넣어 쉽게 SYSTEM으로 권한을 상승시킬 수 있는 좋은 기회입니다.


여기에서 비디오 보기 및 기능 PoC는 여기에 있습니다
조작된 ACTX 메시지를 CSRSS 서버로 보냈습니다.
이 ACTX 메시지에는 해당 메시지를 가리키는 offset이 있는 임베디드 XML 매니페스트가 포함되어 있었습니다.
서버가 이를 수신하면 해당 offset을 사용하여 CTFMON 프로세스 컨텍스트에서 임베디드 XML 매니페스트를 읽었습니다.
임베디드 XML 매니페스트가 파싱되었습니다. 수락되면 외부 폴더에서 두 번째 외부 매니페스트를 로드하려고 시도했습니다.
읽을 폴더는 제가 제어하는 임베디드 XML 매니페스트의 언어 필드에 따라 달라졌습니다.
제 경우 임베디드 XML 매니페스트의 언어가 "tasks"였습니다. 그 이유로 system32의 "tasks" 하위 디렉터리에서 외부 매니페스트를 검색하여 찾았습니다.
제가 만든 tapi32.manifest 파일을 파싱하고 이를 수락하여 동일한 "tasks" 폴더에서 외부 IMM32.dll을 로드할 수 있게 되었습니다.
Nicolas Economou에게 감사드립니다. 그의 발표가 제 연구와 이 블로그 게시물 발행의 출발점이었기 때문입니다.
Ricardo Narvaja