일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- xz-utils
- 백도어
- kernel build
- rootfs
- Kernel
- kernel img
- CVE-2024-3094
- liblzma
- newbie
- kernel image
- cwe-506
- Today
- Total
ZZoMb1E
[Fuzzing 101] Xpdf (CVE-2019-13288) 본문
※ 자료들을 참고하여 분석을 진행하였기에 잘못된 부분이 있을지도 모릅니다.
※ 보완 혹은 수정해야 되는 부분이 있다면 알려주시면 확인 후 조치하도록 하겠습니다.
1. 분석대상
Xpdf이란?
PDF View 및 관련 명령들을 지원해주는 오픈 소스 프로그램
- pdftotext : PDF를 텍스트로 변환
- pdftops : PDF를 PostScript로 변환
- pdftoppm : PDf 페이지들을 netpbm 이미지 파일들로 변환
- pdftopng : PDF 페이지들을 PNG 이미지 파일들로 변환
- pdftohtml : PDF를 html로 변환
- pdfinfo : PDF 메타데이터 추출
- pdfimages : PDF 파일들로부터 원시 이미지(raw image)추출
- pdffonts : PDF 파일에 사용된 폰트 목록 나열
- pdfdetach : PDF 파일에 첨부된 파일들을 추출
2. CVE Code
CVE-2019-13288
조작된 파일을 통해 재귀함수를 무한적으로 발생시킬 수 있는 DoS 취약점
호출된 함수들은 스택 프레임에 할당되기 때문에 무한적으로 발생되는 재귀함수들에 의해 Stack OverFlow가 발생하여 파일이 비정상 종료된다.
CWE-674
Uncontrolled Recursion
제어되지 않은 재귀 함수의 반복은 메모리/스택에 Stack OverFlow를 유발한다
3. CVE 관련 정보
![](https://blog.kakaocdn.net/dn/ceLazf/btsAzCP6OOO/fR2NvNouVjABR3SRJ7HYDK/img.png)
5.5 등급의 취약점
https://nvd.nist.gov/vuln/detail/CVE-2019-13288
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-13288
https://cwe.mitre.org/data/definitions/674.html
4. 분석 환경 및 구현
분석 환경 : Ubuntu 20.04
실습을 위한 설치 과정
Xpdf 파일 실습할 디렉터리 경로 생성
cd $HOME
mkdir fuzzing_xpdf && cd fuzzing_xpdf/
make 및 gcc 등의 명령을 사용하기 위한 툴 설치
sudo apt install build-essential
취약한 Xpdf 버젼 설치 과정
wget https://dl.xpdfreader.com/old/xpdf-3.02.tar.gz
tar -xvzf xpdf-3.02.tar.gz
일반적인 build
cd xpdf-3.02
sudo apt update && sudo apt install -y build-essential gcc
./configure --prefix="$HOME/fuzzing_xpdf/install/"
make
make install
실습에 사용할 예제 파일 설치
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
예제 파일 확인 및 Xpdf 실행
$HOME/fuzzing_xpdf/install/bin/pdfinfo -box -meta $HOME/fuzzing_xpdf/pdf_examples/helloworld.pdf
![](https://blog.kakaocdn.net/dn/bzzKXB/btsAy6DLrnR/Gpqx61AmVBkVLOXWwP1SH1/img.png)
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
build
cd $HOME
git clone https://github.com/AFLplusplus/AFLplusplus && cd AFLplusplus
export LLVM_CONFIG="llvm-config-11"
make distrib
sudo make install
Xpdf를 AFL++ 이용하여 빌드 - 계측코드 삽입
기존 build 삭제
rm -r $HOME/fuzzing_xpdf/install
cd $HOME/fuzzing_xpdf/xpdf-3.02/
make clean
build
export LLVM_CONFIG="llvm-config-11"
CC=$HOME/AFLplusplus/afl-clang-fast CXX=$HOME/AFLplusplus/afl-clang-fast++ ./configure --prefix="$HOME/fuzzing_xpdf/install/"
make
make install
fuzzer 수행
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 : 입력에 사용할 예제의 디렉터리 경로 설정 옵션
-o : AFL++의 결과를 저장할 디렉터리 경로 설정 옵션
-s : 무작위 정적 시드 사용
@@ : 입력 파일 이름으로 대체할 대상 표시
![](https://blog.kakaocdn.net/dn/dPD1gG/btsAwroLIio/rnYyseOIKPCYpU56EDSMnk/img.png)
5. 루크 커즈 분석
![](https://blog.kakaocdn.net/dn/v7AyQ/btsAw3nwa2E/GAQzwEePhfsGAjuKCBTT91/img.png)
out/default/crashes/을 살펴보면 crash파일이 들어있다.
해당 crash 파일은 인자로 gdb를 다음과 같이 실행한다.
gdb -q --args ~/fuzzing_xpdf/install/bin/pdftotext ./id:000000,sig:11,src:000165,time:745537,execs:62405,op:havoc,rep:8
실행하면 crash가 발생하는 지점에서 bp가 걸리는 것을 볼 수 있다.
![](https://blog.kakaocdn.net/dn/b2u0Zh/btsAw6xL2z2/IBIWEkQKDR4ZSlik552OCK/img.png)
분석
![](https://blog.kakaocdn.net/dn/nHVPS/btsAx05gw9Q/NThTaFbGkwxOH2EJ4wxzGk/img.png)
bt로 살펴보면 아래의 함수들이 무한적으로 반복 호출되는 것을 볼 수 있다.
Parser::getObj, XRef::fetch, Object::dictLookup, Parser::makeStream
내용을 조금 살펴보면 Parser::getObj 함수가 objNum을 0x7로 계속 받고 있는 것을 알 수 있다.
문제의 부분을 살펴보겠다.
![](https://blog.kakaocdn.net/dn/bmHww2/btsAzdizW1y/sR1w2Ksa734jCOXWZ2DQuk/img.png)
stream 관련해서 보내는 부분인 것을 확인할 수 있다.
Dictionary 부분을 살펴보면 [ 7 0 R ]을 인자로 참조하는데, 7번 객체에서 7번인 자기 자신을 참조하는 과정이 만들어져 자기 자신을 계속해서 재귀 함수 호출하면서 스택의 범위를 벗어나게 된다.
코드 분석
먼저 Parser::getObj 코드를 문제가 되는 부분 위주로 살펴보겠다.
Parser::getObj
//PDF객체가 사전인 경우
else if (buf1.isCmd("<<")) {
// '<<' 기호 처리 이후의 위치로 이동
shift();
// obj객체를 사전으로 초기화
obj->initDict(xref);
// >>이 나올때까지 + 바이너리 파일이 종료될때까지 반복
while (!buf1.isCmd(">>") && !buf1.isEOF()) {
//만약 key가 이름 객체가 아니라면 오류 출력 및 shift로 위치 다음 처리 위치 이동
if (!buf1.isName()) {
error(getPos(), "Dictionary key must be a name object");
shift();
}
//key가 이름 객체이면 key에 이름을 복사 후 shift
else {
key = copyString(buf1.getName());
shift();
//만약 바이너리 파일이 종료되었거나 오류가 발생하면 key 해제 후 반복문 종료
if (buf1.isEOF() || buf1.isError()) {
gfree(key);
break;
}
//값이 정상적으로 존재하는 경우 값을 가져와서 사전에 key-value쌍을 추가
//이 값역시 pdf 객체 => getObj함수 재귀호출
obj->dictAdd(key, getObj(&obj2, fileKey, encAlgorithm, keyLength, objNum, objGen));
}
}
객체를 파싱하다가 <<를 만나면 >>을 만날 때까지 key와 value 형태로 데이터를 저장한다.
dictAdd()의 인자로 getObj의 반환값을 사용한다.
이는 value의 type이 고정된 것이 아니라 여러 종류를 가질 수 있기 때문에 이를 고려하기 위해서다.
이 과정이 끝나면 Stream을 처리하기 위한 코드가 수행된다.
//사전이 먼저 종료된 경우 오류 출력
if (buf1.isEOF())
error(getPos(), "End of file inside dictionary");
// stream objects are not allowed inside content streams or
// object streams
//토큰이 stream형식이고, steam이 허용되는 경우 실행
if (allowStreams && buf2.isCmd("stream")) {
//steam을 만들고 현재 객체에 설정
if ((str = makeStream(obj, fileKey, encAlgorithm, keyLength,
objNum, objGen))) {
obj->initStream(str);
}
//steam만드는데 실패하면 객체 및 오류객체 초기화
else {
obj->free();
obj->initError();
}
} else {
shift();
}
stream객체이면서 allowSreams이 참이면 makeStream()가 실행된다.
이때 인자들을 Parser::getObj()의 매개변수와 비교해보면
Object *Parser::getObj(Object *obj, Guchar *fileKey,
CryptAlgorithm encAlgorithm, int keyLength,
int objNum, int objGen) {
makeStream(obj, fileKey, encAlgorithm, keyLength,
objNum, objGen))
makeStream()는 Parser::getObj()의 매개변수들을 그대로 가져와 호출되는 것을 볼 수 있다.
Parser::makeStream
//pdf 파일에서 스트림 객체를 생성하는 함수
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();
//현제 lexer를 다음 줄로 이동시키고 스트림 시작 위치를 가져옴
// get length
//스트림의 길이를 검색 | key = 'Length'
dict->dictLookup("Length", &obj);
//'Length'의 value가 정수이면 그대로 사용, 그렇지 않으면 오류 출력
if (obj.isInt()) {
length = (Guint)obj.getInt();
obj.free();
} else {
error(getPos(), "Bad 'Length' attribute in stream");
obj.free();
return NULL;
}
Parser::makeStream()에서 stream의 길이를 검색하기 위해 dictLookup이라는 함수가 호출된다.
이때 key는 "Length"가 된다.
Object::dictLookup
inline Object *Object::dictLookup(char *key, Object *obj)
{ return dict->lookup(key, obj); }
//lookup이라는 메소드를 호출하여 키값을 찾음
dict의 lookup()을 호출하고 있다.
Dict::lookup
Object *Dict::lookup(char *key, Object *obj) {
DictEntry *e;
//find 메소드를 이용하여 key에 해당하는 value를 찾음
return (e = find(key)) ? e->val.fetch(xref, obj) : obj->initNull();
}
find()를 이용하여 key에 해당하는 value를 찾는 과정을 수행한다.
이때 값이 존재하면 val.fetch()를 수행하고, 존재하지 않으면 initNull()로 value를 초기화해준다.
Object::fetch
Object *Object::fetch(XRef *xref, Object *obj) {
return (type == objRef && xref) ?
xref->fetch(ref.num, ref.gen, obj) : copy(obj);
}
Object::fetch()를 살펴보면 obj의 타입이 objRef이고, xref가 존재/참이면 xref->fetch()를 호출하고 있다.
XRef::fetch
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;
}
//유효하면 entries에서 num에 해당하는 데이터를 가져와서 처리
e = &entries[num];
switch (e->type) {
//압축되지 않은 경우
case xrefEntryUncompressed:
//매개변수로 받은 값과 e->gen이 일치하는지 확인
if (e->gen != gen) {
goto err;
}
//Parser 실행 - offset으로부터 시작하ㅡ 서브 스트림 파싱
obj1.initNull();
parser = new Parser(this, new Lexer(this,str->makeSubStream(start + e->offset, gFalse, 0, &obj1)), gTrue);
//parser을 이용해 객체를 세번 가져옴
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가 원하는 객체를 가져옴 -> 매개변수에 암호화 된 객체를 해독하기 위한 정보 포함
parser->getObj(obj, encrypted ? fileKey : (Guchar *)NULL, encAlgorithm, keyLength, num, gen);
obj1.free();
obj2.free();
obj3.free();
delete parser;
break;
XRef::fetch()의 인자로 [7 0 R]로 참조된 [7 0 obj]의 7을 num으로 가져오게 된다.
이 인자를 다시 getObj()의 objNum 인자로 사용하게 되는 과정이 반복호출 되어 스택의 크기를 벗어나게 된다.
6. 패치 파악
패치된 버젼인 Xpdf 3.03과 취약한 버젼 Xpdf 3.02를 비교하였다.
// Max number of nested objects. This is used to catch infinite loops
// in the object structure.
#define recursionLimit 500
패치된 버젼에 위와 같이 recursionLimit이 새로 선언되어 있다.
recursionLimit은 최대 자원 검색 횟수를 제한하는 역할을 한다.
이로 인해 getObj()를 호출하는 과정에서 인자의 변화가 생겼다.
취약한 버젼 - Xpdf 3.02
Object *Parser::getObj(Object *obj, Guchar *fileKey,
CryptAlgorithm encAlgorithm, int keyLength,
int objNum, int objGen) {
패치된 버젼 - Xpdf 3.03
Object *Parser::getObj(Object *obj, GBool simpleOnly,
Guchar *fileKey,
CryptAlgorithm encAlgorithm, int keyLength,
int objNum, int objGen, int recursion) {
recursionLimit을 검사하기 위한 변수가 추가 되었다.
Parser::getObj()의 코드에도 일부분 수정이 되었다.
// dictionary or stream
} else if (!simpleOnly && recursion < recursionLimit && buf1.isCmd("<<")) {
shift();
obj->initDict(xref);
while (!buf1.isCmd(">>") && !buf1.isEOF()) {
if (!buf1.isName()) {
error(errSyntaxError, 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, gFalse,
fileKey, encAlgorithm, keyLength,
objNum, objGen, recursion + 1));
}
}
패치된 코드를 살펴보면 dictionary/stream인지를 검사하는 부분에 recursionLimit을 위한 변수와 simpleOnly가 0인 경우 실행이 된다는 조건이 추가되었다.
Parser::makeStream
Stream *Parser::makeStream(Object *dict, Guchar *fileKey,
CryptAlgorithm encAlgorithm, int keyLength,
int objNum, int objGen, int recursion) {
Object obj;
BaseStream *baseStr;
Stream *str;
Guint pos, endPos, length;
// get stream start position
lexer->skipToNextLine();
if (!(str = lexer->getStream())) {
return NULL;
}
pos = str->getPos();
Parser::makeStream()도 recursionLimit을 위한 매개 변수가 추가되었으며,
lexer->getStream()이 0이면 종료된다는 조건이 추가되었다.
getStream()코드를 살펴보면
// Get stream.
Stream *getStream()
{ return curStr.isNone() ? (Stream *)NULL : curStr.getStream(); }
stream이 존재하지 않는 경우 null을 반환하고, 존재하는 경우 getStream 메소드를 호출하여 stream 객체를 반환한다.
isNone()에 대한 코드는 Object.h를 살펴보면 찾을 수 있다.
GBool isNone() { return type == objNone; }
다시 Parser::makeStream코드를 살펴보겠다.
// get length
dict->dictLookup("Length", &obj, recursion);
if (obj.isInt()) {
length = (Guint)obj.getInt();
obj.free();
} else {
error(errSyntaxError, getPos(), "Bad 'Length' attribute in stream");
obj.free();
return NULL;
}
length의 값을 가져오고 나서 obg.free()를 수행하고 있다.
Object::free()
void Object::free() {
switch (type) {
...
}
#ifdef DEBUG_MEM
--numAlloc[type];
#endif
type = objNone;
}
free()에서 해제할 때 type을 objNone으로 설정해준다.
이로 인해 이미 해재된 객체를 다시 참조가 발생하는 경우 makeStream이 종료된다.
'STUDY > CVE && Fuzzing' 카테고리의 다른 글
[Fuzzing 101] libtiff (CVE-2016-9297) (1) | 2024.06.01 |
---|---|
[Fuzzing 101] TCPdump (CVE-2017-13028) (0) | 2024.01.19 |
[Fuzzing 101] libexif (CVE-2009-3895) (0) | 2024.01.16 |
[Fuzzing 101] libexif (CVE-2012-2836) (0) | 2024.01.08 |
[CVE] Curl (CVE-2023-38545) (0) | 2023.11.16 |