퍼징은 이론만큼 실습도 중요하기 때문에, Fuzzing101을 통해 퍼징을 실습해보겠습니다.
본 포스팅에서는 Xpdf의 취약한 버전(3.02)과 최신 버전을 퍼징했으며, Fuzzing101 Exercise 1 - Xpdf에서 제공하는 환경과는 다르게 진행했으므로 유의하시기 바랍니다. 또한, 대부분의 오류는 경로 문제가 많기 때문에 실행 경로를 잘 확인하기 바랍니다.
Environment
본 포스팅에서는 Ubuntu 22.04.5 LTS와 AFL++ 4.33c 환경으로 실습을 진행했습니다. AFL++를 설치할 때, AFLplusplus/docs/INSTALL.md를 참조했습니다.
Xpdf
Xpdf는 PDF 파일을 다루기 위한 오픈 소스 소프트웨어 모음입니다. PDF 뷰어(viewer)를 포함하여, PDF 파일의 텍스트나 이미지를 추출하고 다른 형식으로 변환하는 다양한 도구를 제공합니다. 이번 포스팅의 타겟 프로그램은 pdftotext로, PDF 파일에서 텍스트 내용만 추출하여 일반 텍스트(.txt) 파일로 저장합니다.
Xpdf 3.02
Download and Build
mkdir fuzzing_xpdf && cd fuzzing_xpdf/sudo apt install build-essentialwget https://dl.xpdfreader.com/old/xpdf-3.02.tar.gztar -xvzf xpdf-3.02.tar.gzXpdf 3.02를 다운받기 위해, 디렉터리를 만들고 Xpdf 3.02 버전을 다운 받습니다.
cd xpdf-3.02sudo apt update && sudo apt install -y build-essential gcc./configure --prefix="/fuzzing_xpdf/install/"makemake installXpdf 3.02를 빌드합니다.
cd fuzzing_xpdfmkdir pdf_examples && cd pdf_exampleswget https://github.com/mozilla/pdf.js-sample-files/raw/master/helloworld.pdfwget https://www.melbpc.org.au/wp-content/uploads/2017/10/small-example-pdf-file.pdf/fuzzing_xpdf/install/bin/pdfinfo -box -meta /fuzzing_xpdf/pdf_examples/helloworld.pdf빌드된 Xpdf를 테스트하기 위해 PDF 샘플을 다운로드 하고, 테스트 합니다.

위 사진처럼 나타났으면 정상적으로 Xpdf가 빌드 되었습니다.
Meet AFL++
AFL++ 4.33c이 정상적으로 설치가 되었다면 Xpdf를 ASAN과 AFL++로 빌드합니다.
cd xpdf-3.02make cleanexport LLVM_CONFIG="llvm-config-14"CFLAGS="-fsanitize=address -g" CXXFLAGS="-fsanitize=address -g" LDFLAGS="-fsanitize=address"CC=/AFLplusplus/afl-clang-fast CXX=/AFLplusplus/afl-clang-fast++ ./configure --prefix="/fuzzing_xpdf/install/"makemake install추후 크래시를 분석할 때, 원활한 디버깅을 위해 -g 옵션을 설정하고 ASAN을 설정했습니다. 또한, 계측을 하기 위해 afl-clang-fast로 빌드해야 합니다.
Fuzz
ASAN과 AFL++로 빌드 했다면, afl-fuzz 명령어를 통해 Xpdf 3.02를 퍼징합니다.
AFL_USE_ASAN=1 afl-fuzz -i /fuzzing_xpdf/pdf_examples/ -o /fuzzing_xpdf/out/ -s 123 -m none -- /fuzzing_xpdf/install/bin/pdftotext @@ /fuzzing_xpdf/output각 명령어에 대한 설명은 다음과 같습니다.
AFL_USE_ASAN=1: ASAN 모드 활성화afl-fuzz: AFL++ 실행-i /fuzzing_xpdf/pdf_examples/: AFL++에 줄 초기 시드-o /fuzzing_xpdf/out/: 퍼징 결과물을 저장할 디렉터리queue/: 코드 커버리지 확장을 이끌어낸 모든 테스트 케이스crashes/: 프로그램 충돌(SIGSEGV, SIGABRT 등)을 일으킨 테스트 케이스hangs/: 지정된 시간(-t옵션 기본 1000 ms) 안에 완료되지 않아 타임아웃된 테스트 케이스
-s 123: 내부 난수 생성기의 시드를 고정(재현성 확보를 위해 설정)-m none: 메모리를 제한하지 않음--:--이전까지는 AFL++ 옵션,--이후는 타겟 프로그램과 인자@@: 변형된 입력 파일 PlaceHolder-- /fuzzing_xpdf/install/bin/pdftotext @@ /fuzzing_xpdf/output→/fuzzing_xpdf/install/bin/pdftotext /tmp/afl_tmp_file813 /fuzzing_xpdf/output
/fuzzing_xpdf/output: 타겟 프로그램에 넘기는 파일
Tip
여러 CPU 코어를 활용하여 Master-Slave(병렬) 구조를 사용하면 퍼징 효율을 극대화할 수 있습니다.
AFL_USE_ASAN=1 afl-fuzz -i /fuzzing_xpdf/pdf_examples/ -o /fuzzing_xpdf/out/ -s 123 -m none -M master -- /fuzzing_xpdf/install/bin/pdftotext @@ /fuzzing_xpdf/outputAFL_USE_ASAN=1 afl-fuzz -i /fuzzing_xpdf/pdf_examples/ -o /fuzzing_xpdf/out/ -s 123 -m none -S slave1 -- /fuzzing_xpdf/install/bin/pdftotext @@ /fuzzing_xpdf/outputAFL_USE_ASAN=1 afl-fuzz -i /fuzzing_xpdf/pdf_examples/ -o /fuzzing_xpdf/out/ -s 123 -m none -S slave2 -- /fuzzing_xpdf/install/bin/pdftotext @@ /fuzzing_xpdf/output

퍼징 명령어가 오류 없이 정상적으로 동작하면 위와 같이 나타납니다. 이제 crashes가 나타나면 해당 크래시를 분석하면 됩니다.
분석
본격적인 분석을 하기 전에 원활한 분석을 위해, 타겟 프로그램을 AFL++로 빌드한 바이너리가 아닌 원본 상태의 바이너리로 만들어야 합니다.
cd xpdf-3.02make cleanCFLAGS="-g -O0" CXXFLAGS="-g -O0" ./configure --prefix="/fuzzing_xpdf/install/"makemake install위 과정 없이 AFL++로 빌드한 바이너리로 분석 한다면, 계측(instrumentation) 코드가 있어 분석하기 어렵습니다. 따라서 원활한 분석을 위해 타겟 프로그램을 원본 바이너리로 만들고 gdb에 크래시파일을 입력값으로 넣으면, 본격적인 분석을 시작할 수 있습니다.
Note
pdftotext의 사용 방식은 ./pdftotext input.pdf output.txt으로, input.pdf에 크래시파일을 주면 됩니다.
gdb --args /fuzzing_xpdf/install/bin/pdftotext [크래시파일]run위 명령어를 실행하면 아래처럼 나옵니다.

프로그램이 종료된 이유는 rsp + 0x24에 접근하려다가 종료되는 것을 알 수 있습니다. rsp + 0x24를 보기 위해 vmmap을 사용하면, rsp + 0x24주소는 매핑된 주소가 아닌데 프로그램이 접근하려 하기 때문에 프로그램이 비정상적으로 종료됩니다.

스택을 자세히 보기 위해 bt 명령어로 Stack Trace를 합니다.

위 사진처럼 특정 함수들이 무한 반복됩니다. 계속 반복되는 함수들을 도식화하면 다음과 같습니다.

재귀 함수로 인한 버그는 익스플로잇이 잘 안되고 DoS가 많기 때문에 ‘재귀함수가 계속 반복되기 때문에, 프로그램이 종료된다’라고 분석해도 괜찮습니다. 하지만 원인 분석(Root Cause) 연습을 해야하기 때문에, 어떠한 이유 때문에 재귀함수가 계속 반복되는지 알아 보겠습니다.
Parser::getObj(Parser.cc:94)
if ((str = makeStream(obj, fileKey, encAlgorithm, keyLength, objNum, objGen)))먼저 Parser::getObj()를 보면 파서가 특정 부분을 읽어 객체(스트림)을 처리하기 위해 makeStream()을 호출합니다.
Parser::makeStream(Parser.cc:156)
dict->dictLookup("Length", &obj);스트림을 처리하기 위해 스트림 데이터의 길이(Length)를 알아야 합니다. 따라서 dictLookup()을 호출합니다.
Object::dictLookup(./Object.h:253)
{ return dict->lookup(key, obj); }dictLookup()에서 스트림 데이터 길이를 조회하고 길이를 반환합니다. 만약 스트림의 길이가 간접 참조 값(ex. 11 0 R)이면, 해당 객체 내용을 가져오기 위해 XRef::fetch()를 호출합니다.
Note
XRef는 PDF 파일의 모든 객체 위치를 담고 있는 참조 테이블
XRef::fetch(XRef.cc:823)
parser->getObj(obj, encrypted ? fileKey : (Guchar *)NULL, encAlgorithm, keyLength, num, gen);XRef::fetch()에서 참조된 객체 내용을 가져오는데, 참조된 객체 내용을 분석하기 위해 새로운 Parser를 만들고 getObj()를 호출합니다. 이렇게 파서는 A를 분석하기 위해 B를 가져오고(fetch), B를 분석하기 위해 C를 가져오는(fetch) 과정이 계속 반복되면 프로그램이 비정상적으로 종료됩니다. 이렇게 getObj()가 제대로 정리되지 않아 rsp가 계속 감소되고, 메모리 매핑이 되지 않은 구역까지 rsp가 감소하여 프로그램이 비정상적으로 종료됩니다.
이러한 재귀함수 문제를 해결하기 위한 방법은 아래 내용에서 자세히 다루겠습니다.
Xpdf 4.05
Download, Build, and Meet AFL++
mkdir fuzzing_new_xpdf && cd fuzzing_new_xpdf/wget https://dl.xpdfreader.com/xpdf-4.05.tar.gztar -xvzf xpdf-4.05.tar.gz && cd xpdf-4.05mkdir build && cd buildexport LLVM_CONFIG="llvm-config-14"CFLAGS="-fsanitize=address -g" CXXFLAGS="-fsanitize=address -g" LDFLAGS="-fsanitize=address"CC=/AFLplusplus/afl-clang-fast CXX=/AFLplusplus/afl-clang-fast++ cmake -DCMAKE_BUILD_TYPE=Release ..makesudo make install/fuzzing_new_xpdf/xpdf-4.05/build/xpdf/pdfinfo -box -meta /fuzzing_xpdf/pdf_examples/helloworld.pdfXpdf 4.05를 다운받기 위해, 디렉터리를 만들고 Xpdf 4.05 버전을 다운하고 빌드합니다. Xpdf 3.02와는 빌드 과정이 달라졌는데, 자세한 내용은 Xpdf 4.05의 INSTALL문서를 참조하면 됩니다. 또한 Xpdf 4.05에서 사용한 예시 파일은 Xpdf 3.02에서 사용한 파일을 사용합니다.
Note
Xpdf의 최신버전 확인은 여기에서 할 수 있습니다.

위 사진처럼 나타났으면 정상적으로 Xpdf 4.05가 정상적으로 빌드 되었습니다.
Fuzz
afl-fuzz 명령어를 통해 Xpdf 4.05를 퍼징합니다.
AFL_USE_ASAN=1 afl-fuzz -i /fuzzing_xpdf/pdf_examples/ -o /fuzzing_new_xpdf/out/ -s 123 -m none -- /fuzzing_new_xpdf/xpdf-4.05/build/xpdf/pdfinfo @@ /fuzzing_new_xpdf/output
퍼징 명령어를 작성하고 동작이 잘 되었지만.. 5일이 지나도 크래시가 발견되지 않아서 퍼징을 중단했습니다. 이처럼 구버전이 아니라 최신 버전의 Xpdf를 퍼징할 수 있습니다.
패치
Xpdf 3.02는 재귀함수가 계속 호출되어 프로그램이 비정상적으로 종료된 모습을 볼 수 있었습니다. Xpdf 4.05 버전에서 달라진 점을 보니, Parser.cc에서 #define recursionLimit 500처럼 재귀함수 반복에 대한 제한이 생긴 것을 볼 수 있습니다. 자세한 내용은 Xpdf 3.02의 Parser.cc와 Xpdf 4.05의 Parser.cc를 참조하면 됩니다.
또한, Xpdf 3.02에 대한 취약점은 CVE-2019-13288로 해당 CVE를 참고하시면 됩니다.
Conclusion
이번 시간에는 AFL++ 4.33c를 사용하여 Xpdf 3.02와 Xpdf 4.05를 퍼징했습니다. Xpdf 3.02에서 크래시를 발견하고 원인 분석을 하고 패치 방향까지 살펴 보았습니다. Xpdf 4.05는 퍼징 동작까지 확인이 되었지만, 크래시가 발견되지 않았습니다. 다음 Fuzzing101 포스팅은 Fuzzing101 Exercise 2를 포스팅할 예정입니다.