
Overview
CVE-2019-13288는 조작된 파일로 인한 infinite recursion이 발생하는 취약점이다.
이 취약점의 내용은 함수가 재귀적으로 호출되면서 스택 메모리가 소진되고 프로그램 충돌로 이어지는 것이다.
Ubuntu20.04.02 LTS 환경에서 진행했다.
Download and build target
# target directory 생성
cd $HOME
mkdir fuzzing_xpdf && cd fuzzing_xpdf/
sudo apt install build-essential
# donwload target
wget <https://dl.xpdfreader.com/old/xpdf-3.02.tar.gz>
tar -xvzf xpdf-3.02.tar.gz
# configure 스크립트를 실행해 설치 환경을 설정한다.
# --prefix 옵션은 프로그램이 설치될 경로를 지정한다.
# make를 통해 소스 코드를 컴파일한다. configure에서 설정한 환경에 맞춰 xpdf가 빌드된다.
# make install을 통해 컴파일이 완료된 프로그램을 지정된 설치 경로에 설치한다.
cd xpdf-3.02
sudo apt update && sudo apt install -y build-essential gcc
./configure --prefix="$HOME/fuzzing_xpdf/install/"
make
make install
# fuzzer의 시드 입력을 위한 정상적인 test 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>
# pdfinfo를 통해 pdf 확인
$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 autoconf git flex bison libglib2.0-dev libpixman-1-dev python3-setuptools
sudo apt-get install -y lld-12 llvm-12 llvm-12-dev clang-12 || 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
sudo apt-get install python3-pip
sudo apt install unicorn cmake
pip3 install wheel
cd $HOME
git clone <https://github.com/AFLplusplus/AFLplusplus> && cd AFLplusplus
export LLVM_CONFIG="llvm-config-12"
make distrib
sudo make install
# 기존에 설치 완료된 프로그램을 삭제
rm -r $HOME/fuzzing_xpdf/install
cd $HOME/fuzzing_xpdf/xpdf-3.02/
make clean
# 대상 프로그램의 소스코드를 보유하고 있을 경우 AFL을 통해 컴파일이 가능하다.
# AFL을 통한 컴파일을 통해 코드 계측(instrumentation)이 가능하다.
# 코드 실행 흐름을 추적하고, 각 입력이 프로그램의 어떤 부분을 실행했는지 확인 가능하다.
# clang은 llvm project안에 속해 있는 컴파일러의 한 종류다.
export AFL_USE_ASAN=1
export LLVM_CONFIG="llvm-config-12"
export CC=$HOME/AFLplusplus/afl-clang-fast CXX=$HOME/AFLplusplus/afl-clang-fast++
export CFLAGS="-fsanitize=address -g" CXXFLAGS="-fsanitize=address -g" LDFLAGS="-fsanitize=address"
./configure --prefix="$HOME/fuzzing_xpdf/install/"
make
make install
sudo su
echo core > /proc/sys/kernel/core_pattern
exit
# -i : 입력 디렉토리를 지정하는 옵션, 퍼징에 사용할 기본 입력 파일들로 사용된다.
# -o : 출력 디렉토리를 지정하는 옵션, 퍼징 과정 중 발견한 충돌 및 버그 관련 정보, 생성된 입력 파일들을 저장한다.
# -s : 시드 값을 설정하는 옵션, 랜덤 입력값을 생성할 때 사용할 초기 시드 값을 지정한다.
# -- : AFL의 명령어 옵션과 실제 실행할 프로그램의 명령어를 구분하는 구분자다.
# @@ : AFL에서 입력 파일을 나타내는 자리표시자, 퍼징 중에 생성된 각 변형된 입력 파일을 @@ 자리에 대입한다.
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



# crash가 발생한 입력 파일을 모두 대입해 오류 메시지를 crash.log 파일에 저장한다.
for file in ~/fuzzing_xpdf/out/default/crashes/*; do echo Input: $file >> ~/fuzzing_xpdf/crash.log; ~/fuzzing_xpdf/install/bin/pdftotext $file ~/fuzzing_xpdf/output 2>> ~/fuzzing_xpdf/crash.log; done;
stackoverflow가 발생한 것을 확인할 수 있다.

Parser::makeStream
Parser::getObj
XRef::fetch
Object::fetch
Dict::lookup
Object::dictLookup
위 함수가 반복적으로 실행됨을 확인할 수 있다.
# symbolic stack trace를 위해 CFLAGS, CXXFLAGS를 설정 후 XPdf를 rebuild 한다.
# -g : 디버깅 심볼을 포함하여 컴파일
# -O0 : 최적화를 하지 않고 컴파일
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
gdb --args $HOME/fuzzing_xpdf/install/bin/pdftotext $HOME/fuzzing_xpdf/out/default/crashes/<your_filename> $HOME/fuzzing_xpdf/output


_int_malloc에서 SIGSEGV가 발생함을 알 수 있다.




getObj 함수를 호출한 뒤로 crash가 발생한 것을 알 수 있다.
getObj → makeStream → dictLookup → lookup → fetch → fetch → makeSubStream → new → __GI___libc_malloc → _int_malloc 순서로 호출된다.

rsp가 stack 메모리를 벗어나는 영역을 참조하려고 해서 생긴 에러다.


PDFDoc::displayPages 호출 후에 getObj 함수에서 infinite recursion 발생하는 것을 확인할 수 있다.

PDF(Portable Document Format) File Structure Analysis
Stream 오브젝트는 길이 제한이 없어서 /Length 엔트리를 통해 스트림 데이터의 바이트 길이를 정의한다.
여기서 간접 오브젝트를 통해 그 값을 정하는데, 7 0 R 자신을 참조하는 불상사가 벌어진 것이다.
// 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로 만든다.
여기서 Length 엔트리에서 7 0 R 간접 오브젝트를 참조하는데, 7 0은 자기 자신이니까 자신을 참조하게 되는 것이다.
// 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를 가져오기 위해 obj(7 0)를 참조해 dictLookup을 실행한다.
// Object.h
inline Object *Object::dictLookup(char *key, Object *obj)
{ return dict->lookup(key, obj); }
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;
// ... 생략
[1]에서 obj(7 0)을 다시 참조해서 getObj를 실행하면서 infinite recursion이 발생한다.

위와 같이 recursionLimit를 추가해 infinite recursion을 예방하도록 패치됐다.
정리
Fuzzing
Fuzzing 대상은 input을 받으면서 실행되는 모든 코드들이다.
문서 리더기, pdf reader 등 입력을 받을 수 있는 모든 프로그램들이 대상이다.
Fuzzing을 하기 위해서는 사용자가 넣고자 하는 input 파일들을 미리 준비해야한다.
텍스트 파일, 이미지 파일, 동영상 파일 등, fuzzing하고자 하는 프로그램이 받을 수 있는 입력 데이터에 해당하는 확장자면 된다.
입력 데이터가 많다고 좋은 것이 아니기 때문에, afl-cmin을 활용해 도움이 되지 않는 input을 걸러낼 수 있다.
configure, make, make install
configure는 소스 파일에 대한 환경 설정을 해주는 명령어다.
내게 필요한 도구가 다 있는지 라이브러리는 다 있는지 확인하고 빌드 환경을 만들어주는 스크립트다. 여기서 —prefix 옵션은 설치 디렉토리를 변경하는 옵션이다.
make는 소스를 컴파일하는 것으로 이 과정이 끝나면 설치파일이 생성된다.
make install은 make를 통해 만들어진 설치 파일을 설치하는 과정이다. build 된 프로그램을 실행 할 수 있게 파일들을 알맞은 위치에 복사를 한다.
Other build systems
configure 파일을 사용하는 프로그램도 있지만, 다른 빌드 시스템의 경우(cmake) afl compiler를 적용하는 방법이 다르다.
cmake의 경우
mkdir build; cd build; cmake -DCMAKE_C_COMPILER=afl-cc -DCMAKE_CXX_COMPILER=afl-c++ ..
ASAN (Adress Sanitizer)
C/C++ 언어로 작성된 소프트웨어의 메모리 오류를 검출하기 위한 툴이다.
ASAN은 메모리 액세스 오버플로우, 메모리 누수, 사용 후 해제 등의 오류를 검출할 수 있다.
AFL_USE_ASAN=1을 통해서 address sanitizer를 활성화시킨다. 그 외에도 다른 기능들도 있다.

ASAN을 사용하면 virtual memory를 많이 잡아먹기 때문에 afl-fuzz가 기본적으로 설정해 놓은 메모리 제한을 넘길 수 있다. 이를 방지하기 위해 afl-fuzz -m none 옵션을 통해 메모리 제한을 해제할 수 있다.
# 메모리 제한 해제
afl-fuzz -m none -i input -o output -- bin/target -someopt @@
Parallelized Fuzzing
parallelized fuzzing은 Fuzz의 효율성과 속도를 높이기 위해 여러 인스턴스를 동시에 실행할 수 있는 AFL의 기능을 의미한다.
parallelized fuzzing에서 AFL은 input을 여러 부분으로 나누고 각 부분을 다른 Fuzz 프로세스에 할당한다. 각 프로세스는 독립적으로 실행되고 테스트 케이스를 생성하며, 테스트 케이스는 Fuzz 프로세스를 추진하기 위해 시스템에 다시 전달한다. 그 뒤 각 프로세스의 결과를 결합해 테스트 중인 시스템의 동작에 대한 포괄적인 output을 생성한다.
Master Fuzzer
afl-fuzz -m none -i ./afl_in -o afl_out -s 123 -x ./dictionaries/xml.dict -D -M master -- ./xmllint --memory --noenc --nocdata --dtdattr --loaddtd --valid --xinclude @@
Slave Fuzzer
afl-fuzz -m none -i ./afl_in -o afl_out -s 234 -S slave1 -- ./xmllint --memory --noenc --nocdata --dtdattr --loaddtd --valid --xinclude @@
'조민우' 카테고리의 다른 글
| IPv6 Stateless Address Auto-Configuration (SLAAC) (0) | 2025.01.31 |
|---|

stellarflare 님의 블로그 입니다.
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!