cd $HOME
mkdir fuzzing_xpdf && cd fuzzing_xpdf/
# Download Xpdf 3.02
wget https://dl.xpdfreader.com/old/xpdf-3.02.tar.gz
tar -xvzf xpdf-3.02.tar.gz
# Build Xpdf
cd xpdf-3.02
sudo apt update && sudo apt install -y build-essential gcc
./configure --prefix="$HOME/fuzzing_xpdf/install/"
make
make install
# Build Test
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
# Test
$HOME/fuzzing_xpdf/install/bin/pdfinfo -box -meta $HOME/fuzzing_xpdf/pdf_examples/helloworld.pdf
Xpdf? Xpdf는 PDF 파일을 보고 변환할 수 있는 무료 오픈 소스 소프트웨어 유틸리티 1995년 발표, PDF Reader와 유사함 구성요소 / PDF Viewer, PDFinfo, PDFimages, PDFtoText, PDFtoHTML ChatGPT 피셜로는 단순함과 고성능으로 많은 사용자와 개발자 사이에서 인기가 있다고 함
Ubuntu 20.04.2에서 llvm-11을 찾을 수가 없어서 llvm-12로 다운로드함. 별 차이는 없음.
# Install AFL
cd $HOME
git clone https://github.com/AFLplusplus/AFLplusplus && cd AFLplusplus
export LLVM_CONFIG="llvm-config-12" // 11로 설정되어있기 때문에 12로 변경
make distrib
sudo make install
LLVM? LLVM은 컴파일러의 기반구조이다. 프로그램을 컴파일 타임, 링크 타임, 런타임 상황에서 프로그램의 작성 언어에 상관없이 최적화를 쉽게 구현할 수 있도록 구성되어 있다. 대표적인 프로젝트로는 LLVM Code와 Clang이 있다고 한다.
make distrib 명령 실행 중에 에러가 뜬다.
unicorn 패키지를 설치 중에 오류가 발생한 것 같은데.. pip install unicorn으로 설치해주면 된다. 무시해도 상관 없는 듯 함. 잘 된다.
Xpdf 재컴파일
AFL에 포함된 Instrumentation을 사용하기 위해선 afl-clang-fast로 재컴파일 해야한다.
CC=$HOME/AFLplusplus/afl-clang-fast CXX=$HOME/AFLplusplus/afl-clang-fast++ ./configure --prefix="$HOME/fuzzing_xpdf/install/"
make
make install
-i dir / 인풋으로 사용할 입력들이 있는 디렉터리
-o dir / 크래쉬가 저장될 디렉터리
Execution control settings:
-P strategy - set fix mutation strategy: explore (focus on new coverage),
exploit (focus on triggering crashes). You can also set a
number of seconds after without any finds it switches to
exploit mode, and back on new coverage (default: 1000)
-p schedule - power schedules compute a seed's performance score:
explore(default), fast, exploit, seek, rare, mmopt, coe, lin
quad -- see docs/FAQ.md for more information
-f file - location read by the fuzzed program (default: stdin or @@)
-t msec - timeout for each run (auto-scaled, default 1000 ms). Add a '+'
to auto-calculate the timeout, the value being the maximum.
-m megs - memory limit for child process (0 MB, 0 = no limit [default])
-O - use binary-only instrumentation (FRIDA mode)
-Q - use binary-only instrumentation (QEMU mode)
-U - use unicorn-based instrumentation (Unicorn mode)
-W - use qemu-based instrumentation with Wine (Wine mode)
-X - use VM fuzzing (NYX mode - standalone mode)
-Y - use VM fuzzing (NYX mode - multiple instances mode)
Mutator settings:
-a type - target input format, "text" or "binary" (default: generic)
-g minlength - set min length of generated fuzz input (default: 1)
-G maxlength - set max length of generated fuzz input (default: 1048576)
-D - enable (a new) effective deterministic fuzzing
-L minutes - use MOpt(imize) mode and set the time limit for entering the
pacemaker mode (minutes of no new finds). 0 = immediately,
-1 = immediately and together with normal mutation.
Note: this option is usually not very effective
-c program - enable CmpLog by specifying a binary compiled for it.
if using QEMU/FRIDA or the fuzzing target is compiled
for CmpLog then use '-c 0'. To disable Cmplog use '-c -'.
-l cmplog_opts - CmpLog configuration values (e.g. "2ATR"):
1=small files, 2=larger files (default), 3=all files,
A=arithmetic solving, T=transformational solving,
X=extreme transform solving, R=random colorization bytes.
Fuzzing behavior settings:
-Z - sequential queue selection instead of weighted random
-N - do not unlink the fuzzing input file (for devices etc.)
-n - fuzz without instrumentation (non-instrumented mode)
-x dict_file - fuzzer dictionary (see README.md, specify up to 4 times)
Test settings:
-s seed - use a fixed seed for the RNG
-V seconds - fuzz for a specified time then terminate
-E execs - fuzz for an approx. no. of total executions then terminate
Note: not precise and can have several more executions.
Other stuff:
-M/-S id - distributed mode (-M sets -Z and disables trimming)
see docs/fuzzing_in_depth.md#c-using-multiple-cores
for effective recommendations for parallel fuzzing.
-F path - sync to a foreign fuzzer queue directory (requires -M, can
be specified up to 32 times)
-T text - text banner to show on the screen
-I command - execute this command/script when a new crash is found
-C - crash exploration mode (the peruvian rabbit thing)
-b cpu_id - bind the fuzzing process to the specified CPU core (0-...)
-e ext - file extension for the fuzz test input file (if needed)
많은 옵션이 있지만, Target에 사용하는 옵션들만 확인해보자.
-i 인풋으로 들어가는 파일들이 있는 디렉터리, 이 파일을 기반으로 입력값을 변조해서 Fuzzing을 하는 걸로 보임
-o crash가 발생했을 때에 들어간 input을 저장하는 디렉터리, input을 기반으로 mutate된 파일들이 저장됨
-s 시드 설정, 랜덤하게 설정할 수는 있는데, Fuzzing 101에서 추천하는건 123임.
@@ @@에 mutate된 값이 들어감 $HOME/fuzzing_xpdf/install/bin/pdftotext @@ $HOME/fuzzing_xpdf/output pdftotext에 변조된 값을 넣고, output 폴더에 결과값을 출력하겠다는 의미
실행파일인 pdftotext에 취약점을 찾기 위해서 Fuzzing을 수행한다. 그전에 어떤 역할을 하는지 한번 확인해보자.
pdf 파일 내용이다.
이 파일을 pdftotext에 넣어보면, 아래와 같이 출력된다.
Fuzzing 시작
ERROR 발생 시
sudo su
echo core >/proc/sys/kernel/core_pattern
exit
위의 명령어 입력 후에 돌아오면 잘 된다.
Fuzzing 결과
Crash 분석
AFL++를 통해서 찾은 입력 값을 다시 넣어보면 Crash가 다시 재현이 된다. 이를 통해서 분석해보자.
Instrumentation 적용된 것과 아닌 것의 차이 아래의 Instrumentation이 적용된 실행파일은 좀 더 상세한 결과를 볼 수 있다.
input 파일 확인 input으로 넣었을 때에 Crash가 발생하는 파일들이다. 한번 Windows에서 확인해보자
내용을 확인해봤을 때에는 아무이상 없어 보이는 pdf 파일이다. hxd로 한번 까보자.
HXD로 확인해봐도 이상한 점은 못찾겠다.
GDB를 통한 분석
run 후 backtrace 확인 Parser::makeStream -> Object::dictLookup -> XRef::fetch -> Parser::getObj -> Parser::makeStream -> ... 순으로 계속 반복된다. getObj에 break point를 설정하고 확인하자.
break point 설정
디버깅 peda가 많은 정보를 보여줘서 생각보다는 보기 편하다. backtrace에서 발견했던 순서로 반복될 때 어떤 값이 들어가는지 확인해보자
makeStream 이후로 발생하는데 FlateDecode라는 내용이 자꾸 들어가있다.
Object *Parser::getObj(Object *obj, Guchar *fileKey,
CryptAlgorithm encAlgorithm, int keyLength,
int objNum, int objGen) {
...
if (allowStreams && buf2.isCmd("stream")) {
if ((str = makeStream(obj, fileKey, encAlgorithm, keyLength,
objNum, objGen))) {
obj->initStream(str);
} else {
...
문제가 시작되는 부분의 코드다. PDF에서 Stream은 이미지나 첨부된 파일 내용들을 저장할 때 사용하는 데, 대부분 압축이 되어있다고 한다.
코드를 쭉 따라가보자.
```C++
Stream *Parser::makeStream(Object *dict, Guchar *fileKey,
CryptAlgorithm encAlgorithm, int keyLength,
int objNum, int objGen) {
...
// get length
dict->dictLookup("Length", &obj);
if (obj.isInt()) {
length = (Guint)obj.getInt();
obj.free();
} else {
error(getPos(), "Bad 'Length' attribute in stream");
obj.free();
return NULL;
}
...
inline Object *Object::dictLookup(char *key, Object *obj)
{ return dict->lookup(key, obj); }
Object *Dict::lookup(char *key, Object *obj) {
DictEntry *e;
return (e = find(key)) ? e->val.fetch(xref, obj) : obj->initNull(); // 여기서 다시 fetch 된다.
}
Object *XRef::fetch(int num, int gen, Object *obj) {
...
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;
}
parser->getObj(obj, encrypted ? fileKey : (Guchar *)NULL, // fetch 후 getObj
encAlgorithm, keyLength, num, gen);
obj1.free();
obj2.free();
obj3.free();
delete parser;
break;
...
PDF의 내용과 코드의 내용을 합쳐서 생각해보면 object 7번으로 가야하는데 자기 자신을 참조하고 있어서 무한재귀에 빠진다. 자기 자신인지 필터링을 하던지, 재귀에 제한을 걸어서 무한루프에 빠지지 않도록 하던지 둘중에 하나만 하면 될 것 같다.
결론
이라고 쓰고 패치노트 찾아보기긴 하지만
// xpdf 3.03
Object *Parser::getObj(Object *obj, GBool simpleOnly,
Guchar *fileKey,
CryptAlgorithm encAlgorithm, int keyLength,
int objNum, int objGen, int recursion) {
...
if (buf1.isEOF())
error(errSyntaxError, 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, recursion + 1))) {
obj->initStream(str);
} else {
obj->free();
obj->initError();
}
} else {
shift();
}
...
못보던 recursion이라는게 추가 되었고, 저 값이 500 이상되면 탈출하게 되어있다.