반응형
Recent Posts
Recent Comments
Link
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

ZZoMb1E_PWN

[Fuzzing 101] Xpdf (CVE-2019-13288) 본문

STUDY/CVE && Fuzzing

[Fuzzing 101] Xpdf (CVE-2019-13288)

ZZoMb1E 2023. 11. 18. 14:02
728x90
반응형

자료들을 참고하여 분석을 진행하였기에 잘못된 부분이 있을지도 모릅니다. 

※ 보완 혹은 수정해야 되는 부분이 있다면 알려주시면 확인 후 조치하도록 하겠습니다.
 

1. 분석대상


Xpdf이란?

PDF View 및 관련 명령들을 지원해주는 오픈 소스 프로그램

  • pdftotext : PDF를 텍스트로 변환
  • pdftops : PDF를 PostScript로 변환
  • pdftoppm : PDf 페이지들을 netpbm 이미지 파일들로 변환
  • pdftopng : PDF 페이지들을 PNG 이미지 파일들로 변환
  • pdftohtml : PDF를 html로 변환
  • pdfinfo : PDF 메타데이터 추출
  • pdfimages : PDF 파일들로부터 원시 이미지(raw image)추출
  • pdffonts : PDF 파일에 사용된 폰트 목록 나열
  • pdfdetach : PDF 파일에 첨부된 파일들을 추출

 

2. CVE Code


CVE-2019-13288

조작된 파일을 통해 재귀함수를 무한적으로 발생시킬 수 있는 DoS 취약점
호출된 함수들은 스택 프레임에 할당되기 때문에 무한적으로 발생되는 재귀함수들에 의해 Stack OverFlow가 발생하여 파일이 비정상 종료된다.

CWE-674

Uncontrolled Recursion
제어되지 않은 재귀 함수의 반복은 메모리/스택에 Stack OverFlow를 유발한다
 

3. CVE 관련 정보


5.5 등급의 취약점
 
https://nvd.nist.gov/vuln/detail/CVE-2019-13288
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-13288
https://cwe.mitre.org/data/definitions/674.html
 
 

4. 분석  환경 및 구현


분석 환경 : Ubuntu 20.04
 
 

실습을 위한 설치 과정


 
Xpdf 파일 실습할 디렉터리 경로 생성

cd $HOME
mkdir fuzzing_xpdf && cd fuzzing_xpdf/

 
make 및 gcc 등의 명령을 사용하기 위한 툴 설치 

sudo apt install build-essential

 
취약한 Xpdf 버젼 설치 과정

wget https://dl.xpdfreader.com/old/xpdf-3.02.tar.gz
tar -xvzf xpdf-3.02.tar.gz

 
일반적인 build

cd xpdf-3.02
sudo apt update && sudo apt install -y build-essential gcc
./configure --prefix="$HOME/fuzzing_xpdf/install/"
make
make install

 
 


실습에 사용할 예제 파일 설치

cd $HOME/fuzzing_xpdf
mkdir pdf_examples && cd pdf_examples
wget https://github.com/mozilla/pdf.js-sample-files/raw/master/helloworld.pdf
wget http://www.africau.edu/images/default/sample.pdf
wget https://www.melbpc.org.au/wp-content/uploads/2017/10/small-example-pdf-file.pdf

 
 
예제 파일 확인 및 Xpdf 실행

$HOME/fuzzing_xpdf/install/bin/pdfinfo -box -meta $HOME/fuzzing_xpdf/pdf_examples/helloworld.pdf

 
 


 
AFL++ 설치 과정
 
종속성 설치

sudo apt-get update
sudo apt-get install -y build-essential python3-dev automake git flex bison libglib2.0-dev libpixman-1-dev python3-setuptools
sudo apt-get install -y lld-11 llvm-11 llvm-11-dev clang-11 || sudo apt-get install -y lld llvm llvm-dev clang 
sudo apt-get install -y gcc-$(gcc --version|head -n1|sed 's/.* //'|sed 's/\..*//')-plugin-dev libstdc++-$(gcc --version|head -n1|sed 's/.* //'|sed 's/\..*//')-dev

 
build

cd $HOME
git clone https://github.com/AFLplusplus/AFLplusplus && cd AFLplusplus
export LLVM_CONFIG="llvm-config-11"
make distrib
sudo make install

 
 


 
Xpdf를 AFL++ 이용하여 빌드 - 계측코드 삽입
 
기존 build 삭제

rm -r $HOME/fuzzing_xpdf/install
cd $HOME/fuzzing_xpdf/xpdf-3.02/
make clean

 
 
build

export LLVM_CONFIG="llvm-config-11"
CC=$HOME/AFLplusplus/afl-clang-fast CXX=$HOME/AFLplusplus/afl-clang-fast++ ./configure --prefix="$HOME/fuzzing_xpdf/install/"
make
make install

 


fuzzer 수행
 

afl-fuzz -i $HOME/fuzzing_xpdf/pdf_examples/ -o $HOME/fuzzing_xpdf/out/ -s 123 -- $HOME/fuzzing_xpdf/install/bin/pdftotext @@ $HOME/fuzzing_xpdf/output

-i : 입력에 사용할 예제의 디렉터리 경로 설정 옵션
-o : AFL++의 결과를 저장할 디렉터리 경로 설정 옵션
-s : 무작위 정적 시드 사용
@@ : 입력 파일 이름으로 대체할 대상 표시
 

 
 
 

5. 루크 커즈 분석


out/default/crashes/을 살펴보면 crash파일이 들어있다.
 
해당 crash 파일은 인자로 gdb를 다음과 같이 실행한다.

gdb -q --args ~/fuzzing_xpdf/install/bin/pdftotext ./id:000000,sig:11,src:000165,time:745537,execs:62405,op:havoc,rep:8

 
실행하면 crash가 발생하는 지점에서 bp가 걸리는 것을 볼 수 있다.

 
 
 

분석


 
bt로 살펴보면 아래의 함수들이 무한적으로 반복 호출되는 것을 볼 수 있다.
Parser::getObj, XRef::fetch,  Object::dictLookup, Parser::makeStream
 
내용을 조금 살펴보면 Parser::getObj 함수가 objNum을 0x7로 계속 받고 있는 것을 알 수 있다.
문제의 부분을 살펴보겠다.

 
stream 관련해서 보내는 부분인 것을 확인할 수 있다.
Dictionary 부분을 살펴보면 [ 7 0 R ]을 인자로 참조하는데, 7번 객체에서 7번인 자기 자신을 참조하는 과정이 만들어져 자기 자신을 계속해서 재귀 함수 호출하면서 스택의 범위를 벗어나게 된다.
 

코드 분석


먼저 Parser::getObj 코드를 문제가 되는 부분 위주로 살펴보겠다.

 
Parser::getObj

//PDF객체가 사전인 경우
else if (buf1.isCmd("<<")) {
	// '<<' 기호 처리 이후의 위치로 이동
    shift();
	// obj객체를 사전으로 초기화
    obj->initDict(xref);

	// >>이 나올때까지 + 바이너리 파일이 종료될때까지 반복
    while (!buf1.isCmd(">>") && !buf1.isEOF()) {
	  //만약 key가 이름 객체가 아니라면 오류 출력 및 shift로 위치 다음 처리 위치 이동
      if (!buf1.isName()) {
				error(getPos(), "Dictionary key must be a name object");
				shift();
      }

		 	//key가 이름 객체이면 key에 이름을 복사 후 shift
			else {
					key = copyString(buf1.getName());
					shift();
					//만약 바이너리 파일이 종료되었거나 오류가 발생하면 key 해제 후 반복문 종료
					if (buf1.isEOF() || buf1.isError()) {
					  gfree(key);
					  break;
					}
				//값이 정상적으로 존재하는 경우 값을 가져와서 사전에 key-value쌍을 추가
				//이 값역시 pdf 객체 => getObj함수 재귀호출
				obj->dictAdd(key, getObj(&obj2, fileKey, encAlgorithm, keyLength, objNum, objGen));
      }
    }

객체를 파싱하다가 <<를 만나면 >>을 만날 때까지 key와 value 형태로 데이터를 저장한다.
 
dictAdd()의 인자로 getObj의 반환값을 사용한다.
이는 value의 type이 고정된 것이 아니라 여러 종류를 가질 수 있기 때문에 이를 고려하기 위해서다.
 
이 과정이 끝나면 Stream을 처리하기 위한 코드가 수행된다.
 

		//사전이 먼저 종료된 경우 오류 출력
		if (buf1.isEOF())
	      error(getPos(), "End of file inside dictionary");

        // stream objects are not allowed inside content streams or
        // object streams
		//토큰이 stream형식이고, steam이 허용되는 경우 실행
   		if (allowStreams && buf2.isCmd("stream")) {
			//steam을 만들고 현재 객체에 설정
   		   if ((str = makeStream(obj, fileKey, encAlgorithm, keyLength,
			    objNum, objGen))) {
						obj->initStream(str);
		      } 
			//steam만드는데 실패하면 객체 및 오류객체 초기화
			else {
						obj->free();
						obj->initError();
		      }
	    } else {
	      shift();
    }

stream객체이면서 allowSreams이 참이면 makeStream()가 실행된다.
 
이때 인자들을 Parser::getObj()의 매개변수와 비교해보면

Object *Parser::getObj(Object *obj, Guchar *fileKey,
		       CryptAlgorithm encAlgorithm, int keyLength,
		       int objNum, int objGen) {

 

 makeStream(obj, fileKey, encAlgorithm, keyLength,
			    objNum, objGen))

makeStream()는 Parser::getObj()의 매개변수들을 그대로 가져와 호출되는 것을 볼 수 있다.
 


Parser::makeStream

//pdf 파일에서 스트림 객체를 생성하는 함수
Stream *Parser::makeStream(Object *dict, Guchar *fileKey,
			   CryptAlgorithm encAlgorithm, int keyLength,
			   int objNum, int objGen) {
  Object obj;
  BaseStream *baseStr;
  Stream *str;
  Guint pos, endPos, length;
  // 임시 객체, 기본 스트림, 스트림, 위치, 종료 위치, 길이

  // get stream start position
  lexer->skipToNextLine();
  pos = lexer->getPos();
	//현제 lexer를 다음 줄로 이동시키고 스트림 시작 위치를 가져옴

  // get length
  //스트림의 길이를 검색  | key = 'Length'
  dict->dictLookup("Length", &obj);
	
  //'Length'의 value가 정수이면 그대로 사용, 그렇지 않으면 오류 출력
  if (obj.isInt()) {
    length = (Guint)obj.getInt();
    obj.free();
  } else {
    error(getPos(), "Bad 'Length' attribute in stream");
    obj.free();
    return NULL;
  }

Parser::makeStream()에서 stream의 길이를 검색하기 위해 dictLookup이라는 함수가 호출된다.
이때 key는 "Length"가 된다.
 


Object::dictLookup

inline Object *Object::dictLookup(char *key, Object *obj)
  { return dict->lookup(key, obj); }
//lookup이라는 메소드를 호출하여 키값을 찾음

dict의 lookup()을 호출하고 있다.
 

Dict::lookup

Object *Dict::lookup(char *key, Object *obj) {
  DictEntry *e;

  //find 메소드를 이용하여 key에 해당하는 value를 찾음
  return (e = find(key)) ? e->val.fetch(xref, obj) : obj->initNull();
}

find()를 이용하여 key에 해당하는 value를 찾는 과정을 수행한다.
이때 값이 존재하면 val.fetch()를 수행하고, 존재하지 않으면 initNull()로 value를 초기화해준다.
 


Object::fetch

Object *Object::fetch(XRef *xref, Object *obj) {
  return (type == objRef && xref) ?
         xref->fetch(ref.num, ref.gen, obj) : copy(obj);
}

Object::fetch()를 살펴보면 obj의 타입이 objRef이고, xref가 존재/참이면 xref->fetch()를 호출하고 있다.
 
 

XRef::fetch

Object *XRef::fetch(int num, int gen, Object *obj) {
  XRefEntry *e;
  Parser *parser;
  Object obj1, obj2, obj3;

  // check for bogus ref - this can happen in corrupted PDF files
  //객체 번호가 유효한지 확인
  if (num < 0 || num >= size) {
    goto err;
  }

  //유효하면 entries에서 num에 해당하는 데이터를 가져와서 처리
  e = &entries[num];
  switch (e->type) {

	  //압축되지 않은 경우
	  case xrefEntryUncompressed:
		//매개변수로 받은 값과 e->gen이 일치하는지 확인
	    if (e->gen != gen) {
	      goto err;
	    }
		
		//Parser 실행 - offset으로부터 시작하ㅡ 서브 스트림 파싱
	    obj1.initNull();
	    parser = new Parser(this, new Lexer(this,str->makeSubStream(start + e->offset, gFalse, 0, &obj1)), gTrue);
			
		//parser을 이용해 객체를 세번 가져옴
	    parser->getObj(&obj1);
	    parser->getObj(&obj2);
	    parser->getObj(&obj3);

		//객체가 올바른 값인지 확인
	    if (!obj1.isInt() || obj1.getInt() != num ||
	        !obj2.isInt() || obj2.getInt() != gen ||
	        !obj3.isCmd("obj")) {
	      obj1.free();
	      obj2.free();
	      obj3.free();
	      delete parser;
	      goto err;
	    }
		
		//parser가 원하는 객체를 가져옴 -> 매개변수에 암호화 된 객체를 해독하기 위한 정보 포함
	    parser->getObj(obj, encrypted ? fileKey : (Guchar *)NULL, encAlgorithm, keyLength, num, gen);
	    obj1.free();
	    obj2.free();
	    obj3.free();
	    delete parser;
	    break;

XRef::fetch()의 인자로 [7 0 R]로 참조된 [7 0 obj]의 7을 num으로 가져오게 된다.
이 인자를 다시 getObj()의 objNum 인자로 사용하게 되는 과정이 반복호출 되어 스택의 크기를 벗어나게 된다.
 
 

6. 패치 파악


패치된 버젼인 Xpdf 3.03과 취약한 버젼 Xpdf 3.02를 비교하였다.
 

// Max number of nested objects.  This is used to catch infinite loops
// in the object structure.
#define recursionLimit 500

패치된 버젼에 위와 같이 recursionLimit이 새로 선언되어 있다.
recursionLimit은 최대 자원 검색 횟수를 제한하는 역할을 한다.
 


이로 인해 getObj()를 호출하는 과정에서 인자의 변화가 생겼다.
취약한 버젼 - Xpdf 3.02

Object *Parser::getObj(Object *obj, Guchar *fileKey,
		       CryptAlgorithm encAlgorithm, int keyLength,
		       int objNum, int objGen) {

패치된 버젼 - Xpdf 3.03

Object *Parser::getObj(Object *obj, GBool simpleOnly,
		       Guchar *fileKey,
		       CryptAlgorithm encAlgorithm, int keyLength,
		       int objNum, int objGen, int recursion) {

recursionLimit을 검사하기 위한 변수가 추가 되었다.
 
Parser::getObj()의 코드에도 일부분 수정이 되었다.

// dictionary or stream
  } else if (!simpleOnly && recursion < recursionLimit && buf1.isCmd("<<")) {
    shift();
    obj->initDict(xref);
    while (!buf1.isCmd(">>") && !buf1.isEOF()) {
      if (!buf1.isName()) {
	error(errSyntaxError, getPos(),
	      "Dictionary key must be a name object");
	shift();
      } else {
	key = copyString(buf1.getName());
	shift();
	if (buf1.isEOF() || buf1.isError()) {
	  gfree(key);
	  break;
	}
	obj->dictAdd(key, getObj(&obj2, gFalse,
				 fileKey, encAlgorithm, keyLength,
				 objNum, objGen, recursion + 1));
      }
    }

패치된 코드를 살펴보면 dictionary/stream인지를 검사하는 부분에 recursionLimit을 위한 변수와 simpleOnly가 0인 경우 실행이 된다는 조건이 추가되었다.
 


Parser::makeStream

Stream *Parser::makeStream(Object *dict, Guchar *fileKey,
			   CryptAlgorithm encAlgorithm, int keyLength,
			   int objNum, int objGen, int recursion) {
  Object obj;
  BaseStream *baseStr;
  Stream *str;
  Guint pos, endPos, length;

  // get stream start position
  lexer->skipToNextLine();
  if (!(str = lexer->getStream())) {
    return NULL;
  }
  pos = str->getPos();

Parser::makeStream()도 recursionLimit을 위한 매개 변수가 추가되었으며,
lexer->getStream()이 0이면 종료된다는 조건이 추가되었다.
 
getStream()코드를 살펴보면

// Get stream.
  Stream *getStream()
    { return curStr.isNone() ? (Stream *)NULL : curStr.getStream(); }

stream이 존재하지 않는 경우 null을 반환하고, 존재하는 경우 getStream 메소드를 호출하여 stream 객체를 반환한다.
 
isNone()에 대한 코드는 Object.h를 살펴보면 찾을 수 있다.

GBool isNone() { return type == objNone; }

 
 
다시 Parser::makeStream코드를 살펴보겠다.

// get length
  dict->dictLookup("Length", &obj, recursion);
  if (obj.isInt()) {
    length = (Guint)obj.getInt();
    obj.free();
  } else {
    error(errSyntaxError, getPos(), "Bad 'Length' attribute in stream");
    obj.free();
    return NULL;
  }

length의 값을 가져오고 나서 obg.free()를 수행하고 있다.
 
Object::free()

void Object::free() {
  switch (type) {
  ...
  }
#ifdef DEBUG_MEM
  --numAlloc[type];
#endif
  type = objNone;
}

free()에서 해제할 때 type을 objNone으로 설정해준다.
 
이로 인해 이미 해재된 객체를 다시 참조가 발생하는 경우 makeStream이 종료된다.


 

728x90
반응형