1. 환경설정 및 설치
디렉토리 생성
cd $HOME
mkdir fuzzing_xpdf && cd fuzzing_xpdf/
build 도구 설치
sudo apt install build-essential
xpdf 다운로드 및 설치
wget https://dl.xpdfreader.com/old/xpdf-3.02.tar.gz
tar -xvzf xpdf-3.02.tar.gz
cd xpdf-3.02
sudo apt update && sudo apt install -y build-essential gcc
./configure --prefix="$HOME/fuzzing_xpdf/install/"
make
make install
pdf 예제 다운로드
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
afl-clang-fast 컴파일러를 사용한 xpdf 빌드
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
2. Fuzzing 단계

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
- 위 bash명령어를 사용해 AFL Fuzzer를 실행시켰고, 약 40분 뒤에 unique crash가 발생함을 확인할 수 있다.
3. 결과 분석 (gdb)
- 앞서 찾은 unique crash를 분석하기 위해서 gdb의 인자로 xpdf가 위치한 경로와 Fuzzing의 결과물인 crash 파일의 경로로 설정한다. $HOME/fuzzing_xpdf/out/ 디렉토리 내의 해당 crash를 넘겨준다.
gdb--args ~/fuzzing_xpdf/install/bin/pdftotext ./id:000000,sig:11,src:000002,time:2353550,op:flip4,pos:799

- getObj 함수가 재귀적으로 계속 호출되어, 결국 Segmentation fault (SIGSEGV)가 발생했음을 확인할 수 있다.
4. 취약점 분석 및 패치
Parser::getObj :
// 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 {
Parser::makeStream :
// get length
dict->dictLookup("Length", &obj);
if (obj.isInt()) {
length = (Guint)obj.getInt();
obj.free();
} else {
Object::dictLookup :
inline Object *Object::dictLookup(char *key, Object *obj)
{ return dict->lookup(key, obj); }
Dict::lookup :
Object *Dict::lookup(char *key, Object *obj) {
DictEntry *e;
return (e = find(key)) ? e->val.fetch(xref, obj) : obj->initNull();
}
Object::fetch :
Object *Object::fetch(XRef *xref, Object *obj) {
return (type == objRef && xref) ?
xref->fetch(ref.num, ref.gen, obj) : copy(obj);
}
XRef::fetch :
delete parser;
goto err;
}
parser->getObj(obj, encrypted ? fileKey : (Guchar *)NULL,
encAlgorithm, keyLength, num, gen);
obj1.free();
obj2.free();
obj3.free();
- 위의 코드들에서 볼 수 있듯이, Parser::getObj -> Parser::makeStream -> Object::dictLookup -> Dict::lookup -> Object::fetch -> XRef::fetch -> Parser::getObj의 순서대로 실행흐름이 재귀된다.
취약점 패치 :
if (!simpleOnly && recursion < recursionLimit && buf1.isCmd("[")) {
shift();
obj->initArray(xref);
while (!buf1.isCmd("]") && !buf1.isEOF())
obj->arrayAdd(getObj(&obj2, gFalse, fileKey, encAlgorithm, keyLength,
objNum, objGen, recursion + 1));
if (buf1.isEOF())
error(errSyntaxError, getPos(), "End of file inside array");
shift();
- 조건문을 사용해서 recursion, recursionLimit 변수를 추가해 재귀를 판단하고 처리하도록 패치했다.