공부/JUN STUDY

[Fuzzing101] Exercise 1 CVE-2019-13288 분석 (커맨드 분석하기)

JUNFUTURE 2022. 9. 15. 12:44

 

추가

2024.5.12 : 아래 링크에 가보시면 더 많은 시리즈 자료를 확인할 수 있습니다.

아래 링크

 

서론

https://github.com/antonio-morales/Fuzzing101

 

GitHub - antonio-morales/Fuzzing101: An step by step fuzzing tutorial. A GitHub Security Lab initiative

An step by step fuzzing tutorial. A GitHub Security Lab initiative - GitHub - antonio-morales/Fuzzing101: An step by step fuzzing tutorial. A GitHub Security Lab initiative

github.com

Fuzzing101의 Exercise 1에 등장하는 CVE-2019-13288 분석해본다.

타겟은 오픈소스인 xpdf이고 퍼저는 AFL++를 이용한다.

 

해당 프로그램 내에 특정 함수가 무한반복되어 호출되는 recusrion 오류가 있었고

이는 stack memory를 가득채워 DOS(Denial Of Service)를 유발한다.

 

구체적으로 해당 프로그램의 Parser::getObj 에서 recusion이 발생했으며 자세한 내용은 아래에 있다.

1. 환경설정

해당 실습을 위해선

1. xpdf 설치

2. AFL++ 설치

3. gdb등 리눅스 ELF 디버거 설치

가 필요하다.

1. xpdf 설치

타겟 프로그램인 xpdf이다.

모든 프로그램은 다운 -> 설치를 해야하는데,

이때 다운은 인터넷에서 받아오는거, 설치는 컴파일하고 실행할 수있는 상태로 바꾸는 과정이다.

 

아래 명령어로 다운 받을 수 있다.

#dependencies 다운로드
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

#빌드를 위한 dependencies 다운
sudo apt update && sudo apt install -y build-essential gcc

#컴파일 결과 위치 설정
./configure --prefix="$HOME/fuzzing_xpdf/install/"

#컴파일
make
make install

Fuzzing101 자료에서 $HOME 환경변수를 이용하는데,

도커나 vm같은 환경에서 $HOME 환경변수가 잘 설정되어 있지않아 있을 수도 있으니,

미리 확인하는게 좋다.

 

이때 중요한게 아래 명령어인데,

이때 이 install/bin안에 프로그램들이 설치되고,

그 프로그램을 통해 xpdf의 기능들을 실행시킬 수 있다.

#컴파일 결과 위치 설정
./configure --prefix="$HOME/fuzzing_xpdf/install/"

때문에 --prefix 뒤의 경로를 잘 설정해주는게 매우 중요하다.

 

이후에는 샘플 pdf들을 다운받아준다.

pdf를 실행시켜주는 프로그램이니 당연히 샘플 pdf가 필요하다.

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

xpdf도 설치했겠다, 샘플 pdf도 설치했겠다, 그럼 이제 xpdf를 실행시킬 수 있다.

$HOME/fuzzing_xpdf/install/bin/pdfinfo -box -meta $HOME/fuzzing_xpdf/pdf_examples/helloworld.pdf

위 명령어를 통해 실행 시킬 수 있는데 핵심은

/install/bin/ 경로에 있는 pdfinfo를 helloworld.pdf라는 인자를 주어 실행한다는 뜻이다.

pdfinfo -box -meta helloworld.pdf

요정도로 요약해볼 수 있겠다.

실행하면 요래된다. 잘 실행되는 모습.

 

2. AFL++ 설치

다음으로는 AFL++를 설치하면된다. 근데 AFL++ 설치는 꽤 어렵다.

왜냐하면 AFL++를 다운로드외에도 타겟 프로그램을 컴파일 해주어야하기 때문이다.

 

이는  coverage 측정을 위한 instrumentation을 위함인데, coverage-guided fuzzer의 개념에 대해 공부해보면 이해할 수 있다.

 

타겟 프로그램 컴파일 초기화

즉 xpdf를 다시금 컴파일 해주어야한다는 뜻이다.

#컴파일 결과물 삭제
rm -r $HOME/fuzzing_xpdf/install

#컴파일 내용 삭제
cd $HOME/fuzzing_xpdf/xpdf-3.02/
make clean

컴파일 결과와 내용을 싹 삭제한다.

 

AFL++ 설치

아래 명령어를 통해 AFL++를 설치할 수 있다.

sudo apt-get update
sudo apt-get install -y build-essential python3-dev automake git flex bison libglib2.0-dev libpixman-1-dev python3-setuptools
sudo apt-get install -y lld-11 llvm-11 llvm-11-dev clang-11 || 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

도커도 있기는 한데, AFL++의 버전 차이로 인해 필요한 컴파일러가 없을 수도있으니 직접 설치하는게 좋다.

#git clone
git clone https://github.com/AFLplusplus/AFLplusplus && cd AFLplusplus

#환경변수설정
export LLVM_CONFIG="llvm-config-11"

#AFL++ 컴파일
make distrib
sudo make install

 

AFL++ 컴파일러(afl-clang-fast)를 이용한 타겟 프로그램 재컴파일

위와 같은 방법으로 AFL++를 설치하면

이와 같이 여러 파일들이 보인다.

이때 afl-fuzz가 바로 퍼저이고, afl-clang-fast와 같은 것들은 컴파일러이다.

아까 이야기했던대로 coverage instrumentation를 위해서 AFL++가 직접 개발한 컴파일러로

xpdf(타겟)을 다시금 컴파일 해주어야하는데, 우리는 afl-clang-fast를 사용할 것이다.

#afl++ 컴파일러를 이용한 컴파일을 위한 환경설정
export LLVM_CONFIG="llvm-config-11"

#컴파일러 변경
CC=$HOME/AFLplusplus/afl-clang-fast CXX=$HOME/AFLplusplus/afl-clang-fast++ ./configure --prefix="$HOME/fuzzing_xpdf/install/"

#컴파일
cd $HOME/fuzzing_xpdf/xpdf-3.02/

make
make install

이때 가장 중요한 명령어가 바로 이 명령어인데

CC=$HOME/AFLplusplus/afl-clang-fast \
CXX=$HOME/AFLplusplus/afl-clang-fast++ \
./configure --prefix="$HOME/fuzzing_xpdf/install/"

CC = cc (= gcc , C compiler)

CXX = g++ (C++ compiler)

이다.

 

즉, 위 명령어를 통해 컴파일러를 AFL이 개발한 alf-clang-fast로 변경하겠다는 뜻이다.이후 ./configure를 통해 다시금 xpdf 컴파일을 진행한다. 이때 ./configure은 당연히 xpdf를 다운받은 그 폴더에서 진행해야한다.

make 
make install


을 입력하고

일케 기분좋은 형광색 [ + ] 가 많이 보이면 성공적으로 컴파일이 진행되고있는 것이다.

퍼저 실행

여기까지 됐다면 퍼저 실행을 위한 모든 준비가 끝났고, 퍼저를 실행하기만 하면된다.근데 실행 명령어도 만만치 않은데, 차근차근 살펴보자

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
  • -i indicates the directory where we have to put the input cases (a.k.a file examples)
  • -o indicates the directory where AFL++ will store the mutated files
  • -s indicates the static random seed to use
  • @@ is the placeholder target's command line that AFL will substitute(바꾸다) with each input file name

라고한다.

이떄 @@ 설명이 어려운데, 파일을 입력으로 받는 프로그램의 경우 파일을 이제 AFL++퍼저가 자동으로 생성해서 퍼징 중에 input으로 넣을텐데, 그렇기에 그 파일이 들어가는 부분이라고 알려주는 것이다. @@이 없으면 stdin을 처리하는 것으로 간주한다.

pdfinfo helloworld.pdf

로 실행하는 프로그램을 AFL++로 실행하려면

pdfinfo @@

이래 되는 셈이다.

Fuzzing101의 예시 커맨드 말고 간단히 보면 아래와 같이 실행하는 셈이고

sudo afl-fuzz -i $HOME/Fuzzing101/fuzzing_xpdf/pdf_examples/ -o $HOME/Fuzzing101/fuzzing_xpdf/out3/ -s 123 $HOME/Fuzzing101/fuzzing_xpdf/install/bin/pdftotext @@

더 간단히 보면 아래와 같다.

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

즉 옵션들을 다 빼면 ./pdftotext @@ 를 계속해서 자동으로 실행해줄 뿐인 명령어이다.

 

 

참고로 Fuzzing101 자료에

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

다음과 같이 나와있는데, 이건 아래와 같이 쓰인 셈이고

./pdftotext hello_world.pdf ./output

pdf를 text로 바꿔주는 xpdf의 결과를 담아주는 ASCII text 파일이다. (별로 신경쓰지 않아도 된다)

AFL++ 최종 실행 커맨드

sudo afl-fuzz -i $HOME/Fuzzing101/fuzzing_xpdf/pdf_examples/ -o $HOME/Fuzzing101/fuzzing_xpdf/out3/ -s 123 $HOME/Fuzzing101/fuzzing_xpdf/install/bin/pdftotext @@

예쁘게 잘 돌아가고있는 모습이다. 돌돌돌돌...

3. crash 분석

그렇게 퍼저를 돌돌돌 돌리다 보면 crash가 발견될때가 있는데,

이때 output 폴더로 설정해둔 경로아래에 /default/crashes로 가보면

다음과 같이 파일들이 생겨있는 것을 볼 수 있다.

이게 뭔가 싶겠지만 파일 이름들이다.

우리는 pdf 파일을 실행해주는 xpdf에 샘플 pdf를 넣고 퍼징했으니

당연히 PDF 파일이 crash 폴더에 담겨있는 것을 확인할 수 있다.

 

Crash reproduction

이걸 그대로 타겟 프로그램에 넣어보자.

 

이를 reproduce라고 표현하고,

이때 성공적으로 crash가 발생하는 상황을 crash reproduce에 성공했다고 표현한다.

 

퍼저가 crash라고 판단한 인풋들 중에 실제로는 crash가 아닌 경우도 있고,

crash 발현이 되었다가 안되었다가 불안정한 경우도 있다.

 

안정적인 reproduce가 되는 건 기분이 매우 좋다.

 

크래쉬 터진 파일 이름을 복잡하니까 crash1로 바꿔줬고

sudo ./pdftotext ./reproduce/crash1 ./output

을 입력하니 아래와 같이 시원하게 reproduce가 되었다.

매번 실행할때마다 되는거 보니 reproduce 안정성이 좋다.

Crash 분석

reproduce가 잘 되는 것을 확인했으면 이제 크래쉬를 분석해보면 된다.

이때 프로그램이 돌아가는 환경에 맞춰 디버깅을 진행하면된다.

디버깅 환경을 갖추는 것도 매우 중요하다.

 

xpdf의 경우 리눅스 바이너리를 제공하고

심지어 not-stripped(안 벗겨진) ELF64bit 바이너리이기에 GDB를 사용해도 된다.

 

이때 또 분석을 할때에는 불필요한 instrumentation 코드까지 디버깅 할 필요는 없기 때문에

다시금 타겟 컴파일을 수행한다.

#기존 컴파일 결과&내용 삭제
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

여기서 CFLAGS CXXFLAGS 는 컴파일 시에 최적화옵션을 주는 기능이라고한다.

gcc -g -Oo

g++ -g -Oo

와 같이 실행하게 해주는 셈이라고 한다.

 

gdb를 연결해서 분석하면(그냥 r만 눌러도) 아래와 같은 장면을 마주할 수 있는데

이때 가장 처음 입력해볼 명령어는 bt이다.

오류가 발생하는 시점 전까지 실행된 함수들을 보여주는 것인데 (스택 프레임 보기)

어떤 함수의 어느부분(c언어 라인까지)에서 함수가 호출되었는지 보여준다.

 

오류가 발생한 위치를 보여주는게 아니라 프로그램의 실행 흐름(함수의 호출과정)을 보여주는 것이다.

이를 쫓아가다보면 어디에서 문제가 발생했는지. 즉, 어떤 파일의 어떤 함수를 분석해야할지 알 수 있다.

 

bt 명령어를 통해 확인해보면 

아래의 함수들이 계속해서 반복 실행되는 것을 확인할 수 있다.

여기서 error->__fprintf->__vfprintf_internal 함수 흐름을 무시하는 이유는

에러를 감지하고 이를 사용자에게 알리려 실행된 커널 함수이기 때문에 무시한다.

 

우리가 분석할 타겟 프로그램은 xpdf (Parser.cc, XRef.cc, Lexer.cc.. 의 파일만 유의미)라는 것을 잊지말자.

#52645 0x00005555555fda69 in Parser::getObj (this=0x555555bfd790, obj=0x7fffffd5cb40, fileKey=0x0, encAlgorithm=cryptRC4, keyLength=0, objNum=7, objGen=0) at Parser.cc:94
#52646 0x00005555556217dc in XRef::fetch (this=0x5555556ca2b0, num=7, gen=0, obj=0x7fffffd5cb40) at XRef.cc:823
#52647 0x00005555555f8b8e in Object::fetch (this=0x555555bfd3b8, xref=0x5555556ca2b0, obj=0x7fffffd5cb40) at Object.cc:106
#52648 0x000055555559c4f6 in Dict::lookup (this=0x555555bfd360, key=0x55555564aa64 "Length", obj=0x7fffffd5cb40) at Dict.cc:76
#52649 0x00005555555f9843 in Object::dictLookup (this=0x7fffffd5cdc0, key=0x55555564aa64 "Length", obj=0x7fffffd5cb40) at Object.h:253
#52650 0x00005555555fde39 in Parser::makeStream (this=0x555555bfd2b0, dict=0x7fffffd5cdc0, fileKey=0x0, encAlgorithm=cryptRC4, keyLength=0, objNum=7, objGen=0) at Parser.cc:156
#52651 0x00005555555fda69 in Parser::getObj (this=0x555555bfd2b0, obj=0x7fffffd5cdc0, fileKey=0x0, encAlgorithm=cryptRC4, keyLength=0, objNum=7, objGen=0) at Parser.cc:94

이에 다음과 같은 순서로 파일들을 뒤지며 분석을해보면

Parser::getObj -> Parser::makeStream -> Object::dictLookup -> Dict::lookup -> Object::fetch -> XRef::fetch -> Parser::getObj

Parser::getObj
Parser::makeStream
Object::dictLookup
Dict::lookup
Object::fetch
XRef::fetch

결국에 꼬리에 꼬리를 물고 다시금 Parser::getObj를 호출하는 구조를 확인할 수 있다.

패치 확인

무한 재귀호출을 방지하기위해 패치된 부분을 확인해보면

왼 : 기존 오 : 패치

 

recursion이라는 변수를 추가해 recursionLimit를 넘으면

꼬리에 꼬리를 무는 함수를 실행시키지 않도록 설정해준 부분을 확인할 수 있다.

귀엽게도