본격적으로 fuzzing을 해보자.
CVE-2019-13288
Description
In Xpdf 4.01.01, the Parser::getObj() function in Parser.cc may cause infinite recursion via a crafted file. A remote attacker can leverage this for a DoS attack. This is similar to CVE-2018-16646.
해당 취약점은 Xpdf 4.01.01에서 Parser.cc의 Parser::getObj()에서 발생한 무한 재귀 문제점이다.
build
우선 해당 버전의 xpdf를 빌드하자.
$ cd $HOME
$ mkdir fuzzing_xpdf && cd fuzzing_xpdf/
$ wget https://dl.xpdfreader.com/old/xpdf-3.02.tar.gz
$ tar -zxvf xpdf-3.02.tar.gz
$ cd xpdf-3.02/
빌드 전에 AFL++ 에서 지원하는 complie-time instrumentation에 대해서 보면
고급 기능인 afl-clang-lto 는 llvm clang 컴파일러 11버전 이상에서 지원된다. 버전이 낮은 경우 afl-clang-fast 나 afl-gcc-fast 등을 사용할 수 없다.
여기서는 llvm 11버전을 기준으로 afl-clang-fast++ 옵션을 적용해서 xpdf를 빌드해보자.
afl-clang-fast complier를 이용해서 빌드를 하자.
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
export LLVM_CONFIG=”llvm-config-11” : LLVM_CONFIG 환경 변수를 설정한다. 이 변수는 LLVM 컴파일러 및 도구 관련 설정 정보를 제공하는 프로그램을 가리킨다. 여기서 llvm-config-11 을 사용하도록 설정한 것이다. 이는 LLVM 11버전의 llvm-config 명령을 사용한다는 의미이다.
CC=$HOME/AFLplusplus/afl-clang-fast : CC 환경 변수를 설정한다. 이 변수는 C 컴파일러를 지정한다. 여기서는 afl-clang-fast 라는 C 컴파일러를 사용하도록 설정했다.
CXX=$HOME/AFLplusplus/afl-clang-fast++ : CXX 환경 변수를 설정한다. 이 변수는 C++ 컴파일러를 지정한다. 여기서는 afl-clang-fast++ 라는 컴파일러이다.
./configure --prefix="$HOME/fuzzing_xpdf/install/" : configure 스크립트는 소프트웨어를 현재 시스템에 맞게 설정하고 빌드할 수 있도록 도와준다. --prefix 옵션을 사용하여 소프트웨어를 설치할 디렉토리를 지정한다. 여기서는 $HOME/fuzzing_xpdf/install/ 디렉토리에 소프트웨어를 설치하도록 설정했다.
잘 되는것을 확인할 수 있다.
Fuzzing start
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
위 명령어로 fuzzing이 시작된다. (마지막에 output은 해당 프로그램 자체의 input에 따른 결과물 파일의 경로지정이다.)
fuzzing 옵션을 간략하게 설명하면 다음과 같다.
afl-fuzz -i input -o output -- target -option @@
기본 구조는 위와 같다.
옵션은 다음과 같다.
input: 사용자가 준비한 입력데이터들이 있는 디렉토리
output: 결과물을 저장하는 디렉토리
-- target: fuzzing하고자 하는 프로그램
-option: target의 여러 옵션을 설정할 수 있다.
@@: 파일에서 입력을 가져오는 프로그램의 경우 '@@'를 사용하여 파일을 저장할 위치를 표시한다.
-i : input 케이스를 넣어야 하는 디렉토리를 나타낸다. (-i input)
-o : AFL++가 변이된 case를 저장하는 디렉토리를 나타낸다. (-o output)
-s : 사용할 정적 시드를 나타낸다. (-s 123)
-D : deterministic mutation이 enable된다.
어느정도 crash가 끄면 ctrl + c 로 종료하자.
해당 위치에서 crash 들을 확인 할 수 있다.
해당 crash를 바이너리에 넣고 돌려보자
./install/bin/pdftotext ./out/default/crashes/[crash된 pdf이름] ./output
segmentation fault가 발생했다.
gdb로 디버깅하기 전 symbolic stack strace를 얻으려면 rebuild를 해야한다.
rm -r $HOME/fuzzing_xpdf/install
cd $HOME/fuzzing_xpdf/xpdf-3.02/
make clean
CFLAGS="-g -O0" CXXFLAGS="-g -O0" ./configure --prefix="$HOME/fuzzing_xpdf/install/"
make
make install
Debugging
디버깅을 해보자.
gdb --args ./install/bin/pdftotext ./out/default/crashes/id:000000,sig:11,src:001712,time:1231194,execs:286208,op:havoc,rep:7 ./output
backtrace로 함수 호출을 보자
Parser::getObj …함수가 무한 재귀된 것을 확인할 수 있다.
결국 stack을 모두 채우고 rsp가 stack 영역을 넘어서 터졌다.
함수는 다음과 같이 반복된다.
- getObj (Parser.cc)
- makeStream (Parser.cc)
- dictLookup (Object.h)
- lookup (Dict.cc)
- fetch (Object.cc)
- fetch (XRef.cc)
- getObj (반복) (Parser.cc)
이 순서로 무한반복에 빠진다. 그 이유를 한 번 알아보자.
우선 input corpus의 format인 pdf의 구조에 대해 알아야 한다.
PDF를 구성하는 4가지 구성요소
- Object
PDF 문서는 여러 작은 데이터 오브젝트로 구성된 모임이다. - File Structure
파일 구조는 PDF파일내에 오브젝트들이 어떻게 저장되고 어떻게 접근되고 어떻게 업데이트 되는지 결정한다. - Document Structure
문서 구조는 여러 간단한 오브젝트들이 어떻게 PDF의 각 문서를 구성하고 배치되는지 설명한다. - Content Streams
콘텐츠 스트림은 PDF 문서의 외곽이나 그래프 요소들을 묘사하는 일련의 명령들을 가지고 있다.
간접 오브젝트에 쓰이는 간접 참조는 6 0 R 과 같이 숫자 2개와 R 이 붙게 되는데 이는 [OBJECT NUMBER] [GENERATE NUMBER] R 을 의미한다.
문제의 시작이 되는 Parser::getObj 이다.
// Parser.cc
Object *Parser::getObj(Object *obj, Guchar *fileKey,
CryptAlgorithm encAlgorithm, int keyLength,
int objNum, int objGen) {
char *key;
Stream *str;
Object obj2;
int num;
DecryptStream *decrypt;
GString *s, *s2;
int c;
// refill buffer after inline image data
if (inlineImg == 2) {
buf1.free();
buf2.free();
lexer->getObj(&buf1);
lexer->getObj(&buf2);
inlineImg = 0;
}
// array
if (buf1.isCmd("[")) {
shift();
obj->initArray(xref);
while (!buf1.isCmd("]") && !buf1.isEOF())
obj->arrayAdd(getObj(&obj2, fileKey, encAlgorithm, keyLength,
objNum, objGen));
if (buf1.isEOF())
error(getPos(), "End of file inside array");
shift();
// ***** [1] ******
// dictionary or stream
} else if (buf1.isCmd("<<")) {
shift();
obj->initDict(xref);
while (!buf1.isCmd(">>") && !buf1.isEOF()) {
if (!buf1.isName()) {
error(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, fileKey, encAlgorithm, keyLength,
objNum, objGen));
}
}
if (buf1.isEOF())
error(getPos(), "End of file inside dictionary");
// stream objects are not allowed inside content streams or
// object streams
if (allowStreams && buf2.isCmd("stream")) {
if ((str = makeStream(obj, fileKey, encAlgorithm, keyLength,
objNum, objGen))) {
obj->initStream(str);
} else {
obj->free();
obj->initError();
}
} else {
shift();
}
// ... 생략
}
[1] 에서 << 와 >> 사이에 있는 것들을 가져와 dictionary로 만든다.
여기서 중요한 것은 7 0 이다. 7 0 R 로 되어있으니 간접 오브젝트이다.
7 0 오브젝트의 length는 8 0 오브젝트에 명시되어있어서 8 0 object를 참조하여 길이를 가져와야 하는데 7 0 을 참조한다고 해서 자기 자신을 참조해서 결국 Length를 가져오기 위해 무한 루프에 빠지게 된다.
스트림 오브젝트는 길이 제한이 없기 때문에 반드시 길이 선언을 해야한다.
그리고 아래 stream이 존재하니 makeStream 함수로 스트림을 생성한다.
// Parser.cc
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();
// ****** [1] ******
// get length
dict->dictLookup("Length", &obj);
if (obj.isInt()) {
length = (Guint)obj.getInt();
obj.free();
} else {
// ... 생략
[1]에서 Length를 가져오기 위해 dictLookup을 실행한다.
인자로 7 0 을 준다.
// Object.h
inline Object *Object::dictLookup(char *key, Object *obj)
{ return dict->lookup(key, obj); }
Object.h에 dict→lookup(key, obj) 로 리턴된다.
// Dict.cc
Object *Dict::lookup(char *key, Object *obj) {
DictEntry *e;
return (e = find(key)) ? e->val.fetch(xref, obj) : obj->initNull();
}
XRef::fetch 를 return 한다.
// XRef.cc
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;
}
e = &entries[num];
switch (e->type) {
case xrefEntryUncompressed:
if (e->gen != gen) {
goto err;
}
obj1.initNull();
parser = new Parser(this,
new Lexer(this,
str->makeSubStream(start + e->offset, gFalse, 0, &obj1)),
gTrue);
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;
} // ****** [1] *******
parser->getObj(obj, encrypted ? fileKey : (Guchar *)NULL,
encAlgorithm, keyLength, num, gen);
obj1.free();
obj2.free();
obj3.free();
delete parser;
break;
// ... 생략
XRef.cc의 [1]에서 다시 getObj를 실행하면서 무한 반복이 된다.
Patch
무한 재귀호출을 방지하기 위해 recursion라는 변수를 추가해 recursionLimit를 넘으면 함수를 실행시키지 않도록 설정해준다.
<틀린 부분이 있다면 비난과 욕설을 해주세요>
'Fuzzing > CVE 분석' 카테고리의 다른 글
Fuzzing101 Exercise5 (1) | 2024.06.14 |
---|---|
Fuzzing101 Exercise4 (0) | 2024.05.10 |
Fuzzing101 Exercise3 (1) | 2024.04.19 |
Fuzzing101 Exercise2_2 (0) | 2024.03.13 |
Fuzzing101 Exercise2_1 (1) | 2024.02.26 |