
NtCopyFileChunk स्टैक बफर ओवरफ्लो POC
इस रिपॉजिटरी में एक POC है जो निष्पादित होने पर स्टैक OOB राइट ट्रिगर करता है, जिससे सिस्टम क्रैश हो जाता है। यह भेद्यता एक अत्यंत दिलचस्प और दुर्लभ अवशेष है जो कर्नेल प्रोग्रामिंग की जटिलताओं और विशिष्टताओं को प्रदर्शित करती है, विशेष रूप से ऑब्जेक्ट प्रबंधन में। इसके अलावा, यह भेद्यता Windows 11 24h2, Windows 10 22h2/21h2 के कर्नेल को प्रभावित करती है, लेकिन Windows 11 22h2/23h2 को नहीं, जो रुचि को और बढ़ाता है।
12 नवंबर, 2024 को पैच किया गया
Windows कर्नेल सिस्कॉल फ़ंक्शन NtCopyFileChunk में एक स्टैक-आधारित बफर ओवरफ्लो भेद्यता (अधिक तकनीकी रूप से, OOB राइट) मौजूद है।
NtCopyFileChunk एक ही सिस्कॉल में दो ऑपरेशन करने की अनुमति देता है: स्रोत फ़ाइल को पढ़ना और गंतव्य फ़ाइल में लिखना।
NT_COPYFILE_DATA_BUFFER एक संरचना है जिसमें कॉपी करने के लिए आवश्यक सब कुछ शामिल होता है। कृपया ध्यान दें कि यह संरचना रिवर्स इंजीनियरिंग के माध्यम से प्राप्त की गई थी, और इसका नाम गढ़ा गया है। इसलिए, इसके बारे में सचेत रहें।
struct NT_COPYFILE_DATA_BUFFER // sizeof=0x48
{
DWORD64 UnknownQword1;
DWORD64 UnknownQword2;
DWORD64 UnknownQword3;
DWORD64 UnknownQword4;
PIRP WriteIrp;
PDEVICE_OBJECT HighestDeviceObject;
PFILE_OBJECT DestFileObject;
PFILE_OBJECT SourceFileObject;
DWORD64 SourceOffsetQuadPart;
};
फ़ंक्शन स्यूडोकोड में कुछ इस तरह दिखता है:
// Pseudocode for the function nt!NtCopyFileChunk in win11 24h2
__int64 __fastcall NtCopyFileChunk(
void* SourceHandle,
void* DestHandle,
void* UserInputHandleEvent,
struct _IO_STATUS_BLOCK* IoStatusBlock,
ULONG Length,
__int64 SourceOffset,
struct _KTHREAD** DestOffset,
ULONG* SourceKey,
_DWORD* DestKey,
int Flags)
{
[...]
NTSTATUS Status;
char is_alertable_io;
DWORD64 SourceOffsetStack;
struct _KTHREAD* DestOffsetValue;
_OBJECT_HANDLE_INFORMATION* HandleInformation;
NT_COPYFILE_DATA_BUFFER* DataBuffer_3;
PVOID UserInputEventObject;
_FILE_OBJECT* pSourceFileObject;
PIRP WriteIrp;
struct _KEVENT StackEvent; // [1]
[...]
memset(&StackEvent, 0, sizeof(StackEvent));
DataBuffer = (NT_COPYFILE_DATA_BUFFER*)ExAllocatePool2(0x43u, Length + sizeof(NT_COPYFILE_DATA_BUFFER), 'pCoI');
ArbDataBuffer = DataBuffer + sizeof(NT_COPYFILE_DATA_BUFFER); //point after NT_COPYFILE_DATA_BUFFER
// Reference source file by handle
ret = IopReferenceFileObject(SourceHandle, 1u, PreviousMode, (PVOID*)&DataBuffer_2->SourceFileObject, 0);
if (ret < 0)
goto RET;
// Reference destination file by handle
ret = ObReferenceFileObjectForWrite(
(ULONG_PTR)DestHandle,
PreviousMode,
(_FILE_OBJECT*)&DataBuffer->DestFileObject,
(_OBJECT_HANDLE_INFORMATION*)&HandleInformation);
[...]
//Fill ArbDataBuffer with data that we will write to the dest file
ret = IopPopulateCopyWriteWorkerData(
(__int64)DestFileObj,
(__int64)IoStatusBlock,
(__int64)ArbDataBuffer,
Length,
v28,
(__int64)pSourceFileObject,
UserInputHandleEvent_1,
DestOffset,
DestKey,
SHIDWORD(HandleInformation),
(__int64)&DataBuffer_2->WriteIrp);
[...]
if (DestFileObj->Flags & FO_SYNCHRONOUS_IO)
{
// [2]
KeInitializeEvent(&StackEvent, SynchronizationEvent, 0);
// [3]
DataBuffer_3->WriteIrp->UserEvent = &StackEvent; //WriteIrp contains pointer to stack event!
DataBuffer->WriteIrp->Flags |= IRP_MJ_WRITE;
}
else
{
//for asynchronous mode, we don't need that
[...]
}
UserInputEventObject = 0;
// [4]
ret = ObReferenceObjectByHandle(
UserInputHandleEvent,
2u,
(POBJECT_TYPE)ExEventObjectType,
PreviousMode,
&UserInputEventObject,
0);
if (ret >= 0)
{
//If the user has submitted the correct event, we proceed to the main logic for copying one file to another.
//I have omitted that section of code for simplicity.
KeResetEvent((PRKEVENT)UserInputEventObject);
goto NEXT_PATH_TO_READ_FILE_QUERY;
}
RET:
//Here it is! Free the DataBuffer structure (remember WriteIrp, which contains a pointer to the stack event).
// [5]
if (ArbDataBuffer)
IopFreeCopyObjectsFromDataBuffer((__int64)ArbDataBuffer, 1);
if (UserInputEventObject_1)
ObfDereferenceObject(UserInputEventObject_1);
return (unsigned int)ret;
}
[1] में, हम StackEvent देख सकते हैं, जो हमारा समस्या ऑब्जेक्ट है। [2] में, यदि गंतव्य फ़ाइल सिंक्रोनस मोड में खोली गई थी, तो कर्नेल गंतव्य फ़ाइल पर लिखने के ऑपरेशन के लिए सिंक्रोनस रूप से प्रतीक्षा करने हेतु स्टैक इवेंट का उपयोग करता है। ऐसा करने के लिए, यह उपयोगकर्ता द्वारा पारित इवेंट के बजाय स्टैक इवेंट पर IopWaitForSynchronousIoEvent (स्यूडोकोड में नहीं दिखाया गया) का उपयोग करता है। पहले, कर्नेल स्टैक इवेंट की प्रतीक्षा करता है, और उसके बाद ही उपयोगकर्ता द्वारा पारित इवेंट को अपडेट करता है। इसी प्रकार, [3] में, आप देख सकते हैं कि WriteIrp की UserEvent स्टैक इवेंट की ओर इशारा करती है। हालाँकि, क्या होगा यदि हम सही अनुरोध बनाते हैं लेकिन गलत इनपुट इवेंट पारित करते हैं? [4] में, हम देख सकते हैं कि यह इनपुट इवेंट को कैसे संदर्भित करता है, जहाँ हम एक अमान्य हैंडल (उदाहरण के लिए, मान 1) पारित कर सकते हैं। और फिर [5] में मेमोरी साफ़ की जाती है। यहीं पर सबसे दिलचस्प चीज़ घटित होती है।
आइए IopFreeCopyObjectsFromDataBuffer फ़ंक्शन का विश्लेषण करें और देखें कि irp साफ़ होने पर क्या होता है। आइए WriteIrp->UserEvent पर करीब से नज़र डालें।
void __fastcall IopFreeCopyObjectsFromDataBuffer(__int64 ArbDataBuffer, char to_clear_irp)
{
NT_COPYFILE_DATA_BUFFER *DataBuffer;
PFILE_OBJECT SourceFileObject;
PIRP WriteIrp;
PFILE_OBJECT DestFileObject;
DataBuffer = (NT_COPYFILE_DATA_BUFFER *)(ArbDataBuffer - 0x48);
if ( to_clear_irp )
{
WriteIrp = DataBuffer->WriteIrp;
DestFileObject = DataBuffer->DestFileObject;
if ( WriteIrp )
{
IopFreeIrpExtension((__int64)DataBuffer->WriteIrp, 9, 1);
//We are moving deeper, closely monitoring UserEvent
IopExceptionCleanupEx((ULONG_PTR)DestFileObject, WriteIrp, WriteIrp->UserEvent, 0, 0);
return;
}
if ( DestFileObject )
ObfDereferenceObjectWithTag(DataBuffer->DestFileObject, 0x746C6644u);
}
SourceFileObject = DataBuffer->SourceFileObject;
if ( SourceFileObject )
ObfDereferenceObjectWithTag(SourceFileObject, 0x746C6644u);
ExFreePoolWithTag(DataBuffer, 0);
}
LONG_PTR __fastcall IopExceptionCleanupEx(ULONG_PTR DestFileObject, PIRP Irp, PVOID UserEvent, PVOID P, char a5)
{
[...]
if ( Irp )
{
MasterIrp = Irp->AssociatedIrp.MasterIrp;
if ( MasterIrp )
ExFreePoolWithTag(MasterIrp, 0);
[...]
IoFreeIrp(Irp);
}
//Oh, that's it! But how can you decrement the reference counter for a stack object that doesn't have an OBJECT_HEADER?
//Vuln!
if ( UserEvent )
ObfDereferenceObject(UserEvent);
if ( P )
ExFreePoolWithTag(P, 0);
[...]
}
ठीक है, कर्नेल UserEvent पर ObfDereferenceObject निष्पादित करता है। लेकिन यहाँ क्या गलत है? बात यह है, जैसा कि मैं इस पूरे समय जोर देता रहा हूँ, कि यह स्टैक पर स्थित एक KEVENT है। इसके पास OBJECT_HEADER नहीं है और परिणामस्वरूप, एक संदर्भ काउंटर भी नहीं है, क्योंकि इसका जीवनकाल स्टैक फ्रेम द्वारा सीमित होता है। लेकिन ZwCreateEvent फ़ंक्शन आपको इवेंट के लिए ऐसा हेडर बनाने की अनुमति देता, क्योंकि तब मेमोरी सिस्टम पूल में आवंटित होती है, और काउंटर शून्य होने पर उसे मुक्त करने की आवश्यकता होती है। हालाँकि, हमारे मामले में इसका उपयोग नहीं किया जाता है। OBJECT_HEADER हमेशा प्रत्येक ऑब्जेक्ट से पहले स्थित होता है। यही कारण है कि OOB राइट होता है, क्योंकि ObfDereferenceObject नेगेटिव ऑफसेट 0x30 को संदर्भित करता है और एक काल्पनिक संदर्भ गणना को घटाता है। यह आपको NtCopyFileChunk स्टैक फ्रेम के लोकल वेरिएबल्स में स्थित किसी चीज़ को मनमाने ढंग से घटाने की अनुमति देता है। यह पूरी तरह से यादृच्छिक है कि वहाँ कौन से लोकल वेरिएबल होंगे। इसके अलावा, यदि काउंटर शून्य हो जाता है, तो कर्नेल स्टैक को ऐसे मुक्त करने का प्रयास करेगा जैसे कि वह सिस्टम पूल हो...
यह सबसे महत्वपूर्ण प्रश्न है। यह हमें समझने में सक्षम बनाएगा कि ऐसी स्पष्ट भेद्यता इन परिस्थितियों में कैसे उत्पन्न हो सकती थी।
आइए Windows 11 23h2 में ntoskrnl के अंदर NtCopyFileChunk के भीतर के ठीक उसी कोड को देखें। 24h2 के लिए NtCopyFileChunk के पूर्वोल्लिखित स्यूडोकोड में, इवेंट को [2] पर आरंभ किया गया है।
*(_QWORD *)&ObjectAttributes.Length = 48;
memset(&ObjectAttributes.Attributes + 1, 0, 20);
ObjectAttributes.RootDirectory = 0;
ObjectAttributes.Attributes = PreviousMode == 0 ? 0x200 : 0;
ObjectAttributes.ObjectName = 0;
//Not even a stack event! It has an OBJECT_HEADER and stores a reference counter.
status = ZwCreateEvent(&EventHandle, 0x1F0003u, &ObjectAttributes, SynchronizationEvent, 0);
जैसा कि हम देख सकते हैं, Windows 11 23h2/22h2 में, स्टैक इवेंट के बजाय, ZwCreateEvent और OBJECT_HEADER के माध्यम से पूल में आवंटित एक इवेंट का उपयोग किया जाता है। इसलिए, यहाँ कोई भेद्यता नहीं है। ObfDereferenceObject ऐसे ऑब्जेक्ट पर एक अनुमत ऑपरेशन है। संयोगवश, 24h2 में उन्होंने इस भेद्यता को ठीक इसी तरह पैच किया है। समस्या की जड़ विभिन्न संस्करणों में कोड के डीसिंक्रनाइज़ेशन में थी। शायद किसी चरण में, इंजीनियरों ने मेमोरी बचाने के लिए स्टैक पर एक इवेंट परिभाषित करने का निर्णय लिया (यह मानक अभ्यास है)। लेकिन वे भूल गए कि IopFreeCopyObjectsFromDataBuffer क्लीनअप फ़ंक्शन एक संदर्भ गणना घटाव करता है, क्योंकि यदि ऐसा नहीं होता, तो 23h2 में एक लीक होता। यह आश्चर्यजनक है कि NtCopyFileChunk कोड Windows 11 के सभी संस्करणों के लिए सामान्य नहीं है। यह कर्नेल विकास की जटिलता को उजागर करता है और दिखाता है कि गंभीर गलतियाँ करना कितना आसान है।
Microsoft के अपने आकलन के अनुसार, इस भेद्यता का शोषण 'More Likely' है। इस कथन ने शुरू में मेरी रुचि बढ़ाई। तो आइए सच्चाई का पता लगाएं।
आइए उस स्टैक फ्रेम पर ध्यान दें जिस पर OOB एक्सेस होते हैं।
struct _KTHREAD* DestOffset; //-0x30
OBJECT_HANDLE_INFORMATION HandleInformation; // -0x28
NT_COPYFILE_DATA_BUFFER* DataBuffer; // -0x20
PVOID UserInputEventObject; // -0x18
_FILE_OBJECT* pSourceFileObject; //-0x10
PIRP WriteIrp; //-0x8
struct _KEVENT StackEvent; // our vuln event
और ObfDereferenceObject हमारे स्टैक फ्रेम तक कैसे पहुँचता है।
;rcx = ptr to StackEvent
ObfDereferenceObject proc near
...
;rdi = beginning of OBJECT_HEADER,
;but in our case it is ptr to DestOffset(user controlled)
lea rdi, [rcx-30h]
...
mov rbx, -1
lock xadd [rdi], rbx ; decrement DestOffset
sub rbx, 1
jg short RET ;if refcnt was >= 2, dont delete object, just exit
;The inevitable path to freeing the object, it will crash the system...
mov rcx, [rdi+8] ;rcx = HandleInformation(_OBJECT_HANDLE_INFORMATION struct)
test rcx, rcx
jnz short BSOD ; HandleInformation must be zero, or we will BSOD
test rbx, rbx ;check for negative reference counter
js short BSOD
...more code...
जैसा कि आप देख सकते हैं, यहाँ BSOD पैदा करने के कई तरीके हैं। हम भी बहुत भाग्यशाली हैं कि DestOffset _OBJECT_HEADER.PointerCount के साथ ओवरलैप होता है, जो NtCopyFileChunk कॉल (7वाँ पैरामीटर) का हिस्सा है। इस प्रकार, यदि हम 2 से बड़ा कोई सकारात्मक DestOffset पारित करते हैं, तो हम केवल DestOffset को घटाते हैं, और यह वहाँ कुछ नहीं करेगा, हम शांति से फ़ंक्शन से बाहर निकल जाएँगे। लेकिन यदि हम DestOffset = 1 पारित करते हैं, तो कर्नेल हमारे स्टैक इवेंट को सिस्टम पूल के रूप में मुक्त करने का प्रयास करेगा।
सिस्टम पूल मुक्त होने से पहले, _OBJECT_HEADER.TypeIndex में निर्दिष्ट ऑब्जेक्ट के प्रकार के विशिष्ट कॉलबैक को कॉल किया जाता है, जो UserInputEventObject के साथ ओवरलैप होता है, जिसे हम भी नियंत्रित करते हैं, जिसका अर्थ है कि हम सिस्टम को किसी भी वांछित ऑब्जेक्ट प्रकार को स्वीकार करने के लिए धोखा दे सकते हैं। यह संभावनाओं की एक पूरी श्रृंखला खोलता है। हमारा मुख्य लक्ष्य फ्री पूल ऑपरेशन से पहले RIP को हाईजैक करना है, उदाहरण के लिए, ऑब्जेक्ट टाइप कन्फ्यूजन के माध्यम से, कॉलबैक को कहीं ओवरराइट करने के लिए।
लेकिन एक बहुत ही अप्रिय चीज़ है जो हमारी योजनाओं को रद्द कर देती है। जैसा कि आप ऊपर देख सकते हैं, _OBJECT_HEADER.HandleCount की उपस्थिति के लिए एक जाँच होती है, और यदि यह गैर-शून्य है, तो कर्नेल BSOD बुलाता है। हमारे मामले में, यह HandleInformation के साथ ओवरलैप होता है। और चूँकि यह उपयोगकर्ता-नियंत्रित पैरामीटर नहीं है, इसे शून्य के बराबर कैसे बनाया जाए, इस पर आगे शोध की आवश्यकता है।
//0x8 bytes (sizeof)
struct _OBJECT_HANDLE_INFORMATION
{
ULONG HandleAttributes; //0x0
ULONG GrantedAccess; //0x4
};
जैसा कि पता चलता है, यह संरचना DestHandle से संबंधित है। इसका तात्पर्य है कि हमें गंतव्य पर शून्य GrantedAccess के साथ एक हैंडल खोलना होगा। इसका यह भी अर्थ है कि हमारे DestHandle के HANDLE_TABLE_ENTRY.GrantedAccessBits शून्य होने चाहिए। हालाँकि, चूँकि हमें इसे लिखने के लिए भी खोलना होगा (ObReferenceFileObjectForWrite), यह किसी भी स्थिति में शून्य नहीं हो सकता।
यह हमें एक गतिरोध की ओर ले जाता है।