본문 바로가기
Pwnable

(Fuzzing) Fuzzing101 - Xpdf

by snwo 2022. 8. 11.

최종 폴더구조는 다음과 같다

./xpdf_fuzz
|-- in
|-- out
|-- xpdf-3.02
`-- xpdf_build
wget https://dl.xpdfreader.com/old/xpdf-3.02.tar.gz
tar -xvzf xpdf-3.02.tar.gz

sudo apt-get install libfreetype6-dev libmotif-dev libxt-dev

xpdf 구버전으로 가져온다. 라이브러리없을수도 있으니 apt 로 설치한다

./configure
make
make install

이렇게 기본적으로빌드할 수 있다.

퍼징을 하기 위해서는, 빌드경로, 컴파일러, 세니타이져, 등을 설정해야한다.

export CC=~/AFLplusplus/afl-clang-fast 
export CXX=~/AFLplusplus/afl-clang-fast++ 
export CFLAGS="-fsanitize=address" 
export CXXFLAGS="-fsanitize=address" 
./configure --prefix=$PWD/../xpdf_build --with-freetype2-includes=/usr/include/freetype2

freetype 라이브러리땜에 에러나니 —with-freetype2-includes 으로 라이브러리경로를 명시해준다.

세니타이져는 cflags 으로 설정하면 된다

afl-fuzz -i ../in -o ../out -s 123 -- ./bin/pdftotext @@ ../output

퍼징돌릴 땐 이렇게 돌렸는데, 대충 옵션설명하면 아래와 같다

-i : input file
-o : output file
-s : seed 값 (여기선 123이 가장 잘 나온다고함)
-- : afl 옵션 종료, 이 다음부턴 바이너리인자 주면 됨
@@ : 파일로 입력
../output : pdftotext 결과로 저장될 파일, 필요없으니 아무거나 주면 된다

터미널 여러개 분할해놓고, broadcast ? 모드로 afl 명령어 입력하고 -S 으로 고유값 주면 코어 개수만큼 실행시킬 수 있다.

> xpdf_build ./bin/pdftotext ../out/default/crashes/id:000000,sig:11,src:000000,time:70002,execs:18325,op:havoc,rep:16 output

Error: PDF file is damaged - attempting to reconstruct xref table...
Error (1251): Illegal character '>'
Error (1259): Dictionary key must be a name object
Error (1262): Dictionary key must be a name object
Error (1264): Dictionary key must be a name object
Error (1268): Dictionary key must be a name object
Error (1271): Dictionary key must be a name object
Error (1278): Dictionary key must be a name object
AddressSanitizer:DEADLYSIGNAL
=================================================================
==30560==ERROR: AddressSanitizer: stack-overflow on address 0x7ffc4ce28ff8 (pc 0x7f856607a910 bp 0x000000000040 sp 0x7ffc4ce29000 T0)
    #0 0x7f856607a90f in __sanitizer::CombinedAllocator<__sanitizer::SizeClassAllocator64<__asan::AP64>, __sanitizer::SizeClassAllocatorLocalCache<__sanitizer::SizeClassAllocator64<__asan::AP64> >, __sanitizer::LargeMmapAllocator<__asan::AsanMapUnmapCallback, __sanitizer::LargeMmapAllocatorPtrArrayDynamic> >::Allocate(__sanitizer::SizeClassAllocatorLocalCache<__sanitizer::SizeClassAllocator64<__asan::AP64> >*, unsigned long, unsigned long) ../../../../src/libsanitizer/sanitizer_common/sanitizer_allocator_combined.h:37
    #1 0x7f856607affe in __asan::Allocator::Allocate(unsigned long, unsigned long, __sanitizer::BufferedStackTrace*, __asan::AllocType, bool) ../../../../src/libsanitizer/asan/asan_allocator.cc:451
    #2 0x7f85660777ec in __asan::asan_memalign(unsigned long, unsigned long, __sanitizer::BufferedStackTrace*, __asan::AllocType) ../../../../src/libsanitizer/asan/asan_allocator.cc:922
    #3 0x7f856615e545 in operator new(unsigned long) ../../../../src/libsanitizer/asan/asan_new_delete.cc:104
    #4 0x55c3f0b9f8d3 in FileStream::makeSubStream(unsigned int, int, unsigned int, Object*) /home/parallels/pg/xpdf_fuzz/xpdf-3.02/xpdf/Stream.cc:596
    #5 0x55c3f0c4fa71 in XRef::fetch(int, int, Object*) /home/parallels/pg/xpdf_fuzz/xpdf-3.02/xpdf/XRef.cc:809
    #6 0x55c3f0b8936c in Object::dictLookup(char*, Object*) /home/parallels/pg/xpdf_fuzz/xpdf-3.02/xpdf/Object.h:253
,,,
        #319 0x55c3f0b8936c in Parser::makeStream(Object*, unsigned char*, CryptAlgorithm, int, int, int) /home/parallels/pg/xpdf_fuzz/xpdf-3.02/xpdf/Parser.cc:156
    #320 0x55c3f0b8d932 in Parser::getObj(Object*, unsigned char*, CryptAlgorithm, int, int, int) /home/parallels/pg/xpdf_fuzz/xpdf-3.02/xpdf/Parser.cc:94
    #321 0x55c3f0c506bc in XRef::fetch(int, int, Object*) /home/parallels/pg/xpdf_fuzz/xpdf-3.02/xpdf/XRef.cc:823
    #322 0x55c3f0b8936c in Object::dictLookup(char*, Object*) /home/parallels/pg/xpdf_fuzz/xpdf-3.02/xpdf/Object.h:253
    #323 0x55c3f0b8936c in Parser::makeStream(Object*, unsigned char*, CryptAlgorithm, int, int, int) /home/parallels/pg/xpdf_fuzz/xpdf-3.02/xpdf/Parser.cc:156
    #324 0x55c3f0b8d932 in Parser::getObj(Object*, unsigned char*, CryptAlgorithm, int, int, int) /home/parallels/pg/xpdf_fuzz/xpdf-3.02/xpdf/Parser.cc:94
    #325 0x55c3f0c506bc in XRef::fetch(int, int, Object*) /home/parallels/pg/xpdf_fuzz/xpdf-3.02/xpdf/XRef.cc:823
    #326 0x55c3f0b8936c in Object::dictLookup(char*, Object*) /home/parallels/pg/xpdf_fuzz/xpdf-3.02/xpdf/Object.h:253
    #327 0x55c3f0b8936c in Parser::makeStream(Object*, unsigned char*, CryptAlgorithm, int, int, int) /home/parallels/pg/xpdf_fuzz/xpdf-3.02/xpdf/Parser.cc:156
    #328 0x55c3f0b8d932 in Parser::getObj(Object*, unsigned char*, CryptAlgorithm, int, int, int) /home/parallels/pg/xpdf_fuzz/xpdf-3.02/xpdf/Parser.cc:94
    #329 0x55c3f0c506bc in XRef::fetch(int, int, Object*) /home/parallels/pg/xpdf_fuzz/xpdf-3.02/xpdf/XRef.cc:823

SUMMARY: AddressSanitizer: stack-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_allocator_combined.h:37 in __sanitizer::CombinedAllocator<__sanitizer::SizeClassAllocator64<__asan::AP64>, __sanitizer::SizeClassAllocatorLocalCache<__sanitizer::SizeClassAllocator64<__asan::AP64> >, __sanitizer::LargeMmapAllocator<__asan::AsanMapUnmapCallback, __sanitizer::LargeMmapAllocatorPtrArrayDynamic> >::Allocate(__sanitizer::SizeClassAllocatorLocalCache<__sanitizer::SizeClassAllocator64<__asan::AP64> >*, unsigned long, unsigned long)

fetch → getObj →makeStream → dictLookup → fetch → …

이런식으로 재귀함수가 무한으로 호출돼서 스택이 터진다

#38800 Parser::makeStream (this=0x606000004700, dict=<optimized out>, fileKey=<optimized out>, encAlgorithm=<optimized out>, keyLength=<optimized out>, objNum=<optimized out>, objGen=<optimized out>) at Parser.cc:156
#38801 0x000055555586e933 in Parser::getObj (this=this@entry=0x606000004700, obj=obj@entry=0x7fffffffd860, fileKey=<optimized out>, encAlgorithm=<optimized out>, keyLength=<optimized out>, objNum=objNum@entry=7, objGen=<optimized out>) at Parser.cc:94
#38802 0x00005555559316bd in XRef::fetch (this=<optimized out>, num=<optimized out>, gen=<optimized out>, obj=<optimized out>) at XRef.cc:823
#38803 0x000055555586a36d in Object::dictLookup (this=0x7fffffffdbc0, obj=0x7fffffffd860, key=0x555555a0d720 "Length") at Object.h:253
#38804 Parser::makeStream (this=0x6060000046a0, dict=<optimized out>, fileKey=<optimized out>, encAlgorithm=<optimized out>, keyLength=<optimized out>, objNum=<optimized out>, objGen=<optimized out>) at Parser.cc:156
#38805 0x000055555586e933 in Parser::getObj (this=this@entry=0x6060000046a0, obj=obj@entry=0x7fffffffdbc0, fileKey=<optimized out>, encAlgorithm=<optimized out>, keyLength=<optimized out>, objNum=objNum@entry=7, objGen=<optimized out>) at Parser.cc:94
#38806 0x00005555559316bd in XRef::fetch (this=<optimized out>, num=<optimized out>, gen=<optimized out>, obj=<optimized out>) at XRef.cc:823
#38807 0x000055555586a36d in Object::dictLookup (this=0x7fffffffdf60, obj=0x7fffffffdbc0, key=0x555555a0d720 "Length") at Object.h:253
#38808 Parser::makeStream (this=0x606000004640, dict=<optimized out>, fileKey=<optimized out>, encAlgorithm=<optimized out>, keyLength=<optimized out>, objNum=<optimized out>, objGen=<optimized out>) at Parser.cc:156
#38809 0x000055555586e933 in Parser::getObj (this=this@entry=0x606000004640, obj=obj@entry=0x7fffffffdf60, fileKey=<optimized out>, encAlgorithm=<optimized out>, keyLength=<optimized out>, objNum=objNum@entry=7, objGen=<optimized out>) at Parser.cc:94
#38810 0x00005555559316bd in XRef::fetch (this=<optimized out>, num=<optimized out>, gen=<optimized out>, obj=obj@entry=0x7fffffffdf60) at XRef.cc:823
#38811 0x00005555558560ca in Object::fetch (this=this@entry=0x606000004368, xref=<optimized out>, obj=obj@entry=0x7fffffffdf60) at Object.cc:105
#38812 0x0000555555865842 in Page::displaySlice (this=0x606000004340, out=<optimized out>, hDPI=<optimized out>, vDPI=<optimized out>, rotate=<optimized out>, useMediaBox=<optimized out>, crop=<optimized out>, sliceX=<optimized out>, sliceY=<optimized out>, sliceW=<optimized out>, sliceH=<optimized out>, printing=<optimized out>, catalog=<optimized out>, abortCheckCbk=<optimized out>, abortCheckCbkData=<optimized out>) at Page.cc:314
#38813 0x0000555555866df7 in Page::display (this=<optimized out>, out=out@entry=0x60e000000040, hDPI=hDPI@entry=72, vDPI=vDPI@entry=72, rotate=rotate@entry=0, useMediaBox=useMediaBox@entry=0, crop=crop@entry=1, printing=printing@entry=0, catalog=0x60d000000110, abortCheckCbk=0x0, abortCheckCbkData=0x0) at Page.cc:264
#38814 0x00005555558738b6 in PDFDoc::displayPage (abortCheckCbkData=<optimized out>, abortCheckCbk=<optimized out>, printing=<optimized out>, crop=<optimized out>, useMediaBox=<optimized out>, rotate=<optimized out>, vDPI=<optimized out>, hDPI=<optimized out>, page=1, out=<optimized out>, this=<optimized out>) at Catalog.h:45
#38815 PDFDoc::displayPages (this=<optimized out>, out=<optimized out>, firstPage=<optimized out>, lastPage=<optimized out>, hDPI=<optimized out>, vDPI=<optimized out>, rotate=0, useMediaBox=0, crop=<optimized out>, printing=<optimized out>, abortCheckCbk=<optimized out>, abortCheckCbkData=<optimized out>) at PDFDoc.cc:330
#38816 0x00005555555facf0 in main (argc=<optimized out>, argc@entry=3, argv=argv@entry=0x7fffffffe328) at pdftotext.cc:237
#38817 0x00007ffff7067083 in __libc_start_main (main=0x5555555f8900 <main(int, char**)>, argc=3, argv=0x7fffffffe328, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe318) at ../csu/libc-start.c:308
#38818 0x00005555555fd33e in _start ()

사건의 발달은 Object::fetch 에서 XRef::fetch 함수를 호출할 때부터 시작한다.

이 함수는 특정 object 에서 다른 object 를 참조할 때 사용하는데, 해당 object 가 이상해서 무한재귀 발생

Root Cause Analysis


다른블로그에는 없길래 한 번 해봤당

위쪽이 크

일단 소스코드 보면, << 부터 안에있는걸 걸 >> 까지 가져와서 dict 에  add 하는데

이 부분에서 일단 꺽쇠가 사라져서 Length 가 속해있는 꺽쇠까지 가져와서 dict 에 넣는다.

근데 이 부분은 별로 문제될게 없다. 그래서 좀만 더 변형해보면 크래시가 더 나올 것 같다

 

위쪽이 크래시,밑이 정상

이 부분에서 재귀함수 루핑이 걸린다. (gdb 로 length, filter 계속 가져오는거 확인)

그래서 이부분 고쳐줬는뎅 아직도 재귀함수 루핑이 걸린다.

 

이부분 숫자가 다르길래 고쳐줬더니 루핑이 사라지고 포맷이 잘못되었다는 에러만 났다.

이부분을 보자.

사진에는 7 0 object, 8 0 object 두 개가 있다.

상호 참조에서 개체를 찾고 7 0파일에서 오프셋을 가져옵니다.
해당 오프셋으로 이동하여 객체 읽기를 시작하고 먼저 스트림 사전을 구문 분석하고,
at stream키워드는 객체가 스트림임을 인식하고 에 대한 간접 참조인 Length 값을 검색 합니다 .8 0
상호 참조에서 개체를 찾고 8 0 파일에서 오프셋을 가져옵니다.
해당 오프셋으로 이동하여 개체, 숫자를 읽습니다 267
7 0길이가 267이라는 것을 알고 있는 스트림 개체의 스트림 내용을 읽습니다 .

원본파일에서 pdf 프로세서가 개체를 읽는 방식이라고 한다.

7 0 object 의 length 는 8 0 object 에 명시해놔서 8 0 object 에서 길이를 가져와야 한다.

근데 Length 를 7 0 으로 수정하면 자기자신을 참조해 결국 Length 를 가져오기위해 자기자신을 계속 참조하게되어 무한재귀가 발생한다.

코드로 살펴보면 아래와 같다

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()); // [1]
    shift();
    if (buf1.isEOF() || buf1.isError()) {
      gfree(key);
      break;
    }
    obj->dictAdd(key, getObj(&obj2, fileKey, encAlgorithm, keyLength, // [2]
                 objNum, objGen));
      }
    }

else if (buf1.isInt()) {
    num = buf1.getInt();
    shift();
    if (buf1.isInt() && buf2.isCmd("R")) { // [3]
      obj->initRef(num, buf1.getInt());
      shift();
      shift();
    } else {
      obj->initInt(num);
    }

Parser::getObj 함수이다.

[1] 에서 << >> 사이에 있는 변수명을 가져와 [2] 에서 딕셔너리로 저장한다.

key:value 에서 value 는 마찬가지로 getObj 함수로 가져오는데

Length 7 0 R 형식으로 되어잇을 때 7 0 을 참조하는 값으로 저장한다.

if (allowStreams && buf2.isCmd("stream")) {
      if ((str = makeStream(obj, fileKey, encAlgorithm, keyLength,
                objNum, objGen))) {

파일상으로 해당부분 밑에 stream 이 존재하니 makeStream 함수로 스트림을 생성한다.

// get length
  dict->dictLookup("Length", &obj);

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();
}

Object *Object::fetch(XRef *xref, Object *obj) {
  return (type == objRef && xref) ?
         xref->fetch(ref.num, ref.gen, obj) : copy(obj);
}

makeStream 함수에서는 stream의 길이를 알기 위해 dictLookup 함수를 호출해 Length 의 reference 값을 찾는다.

그리고 해당 object 의 fetch method 호출, 인자는 reference 값 전달 (7 0)

        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,
           encAlgorithm, keyLength, num, gen);

XRef::fetch 함수 내용 중 이 부분에서 현재 object (7 0 obj)값을 가져와 확인한다.

인자로 들어온 cross reference 값과 현재 object 값과 같아야 한다.

Length 에 적힌 7 0 을 가져올거라 인자로 들어온 레퍼런스 값과 현재 object 가 동일하다

그리고 Parser::getObj 함수가 호출되고 무한반복된다~

 

최초로 해당 object 를 참조하는 부분이 여긴데, 이게 위해서 말한 사건의발달이다