Fuzzing101_exercise 2

156·2025년 3월 18일

Fuzzing101

목록 보기
3/3
post-thumbnail

0. 문제 설명

Exercise 2
타겟 : libexif 0.6.14
1-day : CVE-2009-3895, CVE-2012-2836

학습 목표

  • 외부 애플리케이션을 사용하여 라이브러리를 퍼징하기 위해
  • afl-clang-lto를 사용하여 퍼징 성능을 향상하기 위해 (afl-clang-fast보다 더 빠르고 더 나은 결과를 제공하는 충돌 없는 계측 도구)
  • GDB 콘솔 대신 Eclipse IDE를 사용하여 트리아징을 더 쉽게 하기 위해
  • libexif 라이브러리를 사용하는 인터페이스 애플리케이션을 찾으세요.
  • EXIF 샘플로 시드 코퍼스(seed corpus)를 만드세요.
  • afl-clang-lto를 사용하여 libexif와 선택한 애플리케이션을 컴파일하여 퍼징할 준비를 하세요.
  • libexif를 퍼징하여 몇 가지 고유한 충돌(crash)을 발견하세요.
  • 충돌을 분석하여 각 취약점에 대한 PoC(개념 증명)를 찾으세요.
  • 발견된 문제를 수정하세요.

libexif란?
https://github.com/libexif/libexif
exif : 디지털 카메라나 스마트폰으로 찍은 사진 파일(JPEG 등)에 저장되는 메타데이터 ex) 촬영 날짜와 시간, 모델명 등
libexif : EXIF 데이터를 파싱, 편집 및 저장하기 위한 라이브러리

  • 순수 C언어로 작성
  • libexif를 사용하는 프론트엔드로는 exif, gexif, gphoto2, gtkam 등이 있다.

CVE-2009-3895 & CVE-2012-2836
CVE-2009-3895 : libexif 0.6.18의 libexif/exif-entry.c 파일에 있는 exif_entry_fix 함수(일명 태그 수정 루틴)에서 발생한 힙 기반 버퍼 오버플로우. 해당 취약점을 통해 공격자가 잘못된 EXIF를 이용하여 DoS를 유발하거나, 임의 코드를 실행할 수 있음.


CVE-2012-2836 : libexif 0.6.21 이하 버전의 exif-data.c 파일의 exif_data_load_data 함수에서 발생하는 out-of-bound 취약점. 조작된 EXIF 태그가 포함된 이미지를 통해 민감 정보 탈취 및 DoS 유발 가능

1. libexif 0.6.14 빌드

디렉토리 생성

mkdir fuzzing_libexif && cd fuzzing_libexif

libexif 0.6.14 다운로드

wget https://github.com/libexif/libexif/archive/refs/tags/libexif-0_6_14-release.tar.gz
tar -xvzf libexif-0_6_14-release.tar.gz
cd libexif-libexif-0_6_14-release
sudo apt-get install doxygen autopoint
autoreconf -i
./configure --enable-shared=no --prefix="$HOME/fuzzing101/fuzzing_libexif/install/"
make
make install

trouble shooting

make에서 에러발생

해결

sudo apt-get update
sudo apt-get install gettext autopoint

2. exif 설치

why exif?
이번 타겟인 libexif는 라이브러리이다. 라이브러리를 퍼징하기 위해서는 반드시 해당 라이브러리를 사용하는 소프트웨어가 필요하다. 따라서 libexif의 프론트앤드 중 하나인 exif를 설치하여 fuzzzing한다.

exif 설치

wget https://github.com/libexif/exif/archive/refs/tags/exif-0_6_15-release.tar.gz
tar -xzvf exif-0_6_15-release.tar.gz

빌드

cd exif-exif-0_6_15-release/
autoreconf -fvi
./configure --enable-shared=no --prefix="$HOME/fuzzing101/fuzzing_libexif/install/" PKG_CONFIG_PATH=$HOME/fuzzing101/fuzzing_libexif/install/lib/pkgconfig
make
make install

trouble shooting

configure에서 에러 뜸

해결

sudo apt-get install libpopt-dev


설치 완료

  • libexif 라이브러리를 사용하는 인터페이스 애플리케이션을 찾으세요.

→ 완료

3. seed corpus

seed corpus란?
퍼저에서 초기 입력값으로 사용되는 입력 파일들의 집합. 퍼저는 해당 데이터를 바탕으로 다양한 변형 데이터를 만들어 낸다.


libexif를 퍼징하기 위해서 들어갈 JPEG(exif가 포함된) 파일들이 Seed Corpus가 되는 것이다.

어떻게 수집하지?

  1. JPEG를 모아서 EXIF 데이터를 추가
  2. EXIF가 있는 JPEG를 모으기

github에 exif 라고 검색하니까 적지않은 샘플을 구할 수 있었다.


seed corpus를 구글링이나 github를 통해 수집하는 요령을 터득해야 할 것 같다.

seed corpus 설치

https://github.com/ianare/exif-samples

wget https://github.com/ianare/exif-samples/archive/refs/heads/master.zip
unzip master.zip

corpus 추출

순수 샘플 파일만 들어있지도 않고, 한 디렉토리에 모여있지도 않다.

corpus-exif 디렉토리를 생성하여 한 곳에 모아주자.

mkdir corpus-exif
cd exif-samples-master/jpg

find . -maxdepth 1 -type f -name "*.jpg" -exec cp {} $HOME/fuzzing101/fuzzing_libexif/corpus-exif/ \;
find . -maxdepth 1 -type f -name "*.tiff" -exec cp {} $HOME/fuzzing101/fuzzing_libexif/corpus-exif/ \;

  • EXIF 샘플로 시드 코퍼스(seed corpus)를 만드세요.

→ 완료

4. afl-clang-lto

링크텍스트

afl-clang-lto란?
afl의 고급 기능으로 링크 타임 최적화 기반의 퍼징 도구이다.


기존 AFL

  • 코드 블록 간의 연결(엣지)을 추적해서 새로운 경로를 찾으려고 하는데, 이 때 코드 블록 ID를 랜덤하게 설정하였다.
  • 코드 볼륨이 커져서 블록이 많아지면 경로가 충돌하여 새로운 경로를 제대로 찾지 못하는 상황이 발생한다.


    afl-clang-lto

  • 코드 블록 간의 ID를 겹치지 않게 많들어서 더 높은 커버리지를 낼 수 있으며, 더 빠른 속도를 낼 수 있다.
  • 또한 소스 코드를 컴파일 할 때가 아닌, 링크 단계에서 코드 계측을 수행하여 코드 성능을 향상시킨다.

libexif alf-clang-lto 컴파일

rm -r $HOME/fuzzing101/fuzzing_libexif/install
cd $HOME/fuzzing101/fuzzing_libexif/libexif-libexif-0_6_14-release/
make clean

export LLVM_CONFIG="llvm-config-14"
CC=$HOME/AFLplusplus/afl-clang-lto CXX=$HOME/AFLplusplus/afl-clang-lto++ ./configure --prefix="$HOME/fuzzing101/fuzzing_libexif/install/"
make
make install

exif 컴파일

cd $HOME/fuzzing101/fuzzing_libexif/exif-exif-0_6_15-release/
make clean

export LLVM_CONFIG="llvm-config-14"
CC=$HOME/AFLplusplus/afl-clang-lto CXX=$HOME/AFLplusplus/afl-clang-lto++ ./configure --prefix="$HOME/fuzzing101/fuzzing_libexif/install/" PKG_CONFIG_PATH=$HOME/fuzzing101/fuzzing_libexif/install/lib/pkgconfig
make
make install
💡

컴파일러 변경 및 xpdf 빌드

CC=$HOME/AFLplusplus/afl-clang-fast CXX=$HOME/AFLplusplus/afl-clang-fast++ ./configure --prefix="$HOME/fuzzing101/fuzzing_xpdf/install/"
  • CC CXX : 기본 컴파일러 변경
  • ./configure로 빌드

퍼저 실행

afl-fuzz -i $HOME/fuzzing101/fuzzing_libexif/corpus-exif/ -o $HOME/fuzzing101/fuzzing_libexif/out/ -s 123 -- $HOME/fuzzing101/fuzzing_libexif/install/bin/exif @@ $HOME/fuzzing101/fuzzing_libexif/output
💡

명령어 설명

afl-fuzz -i $HOME/fuzzing101/fuzzing_xpdf/pdf_examples/ \
         -o $HOME/fuzzing101/fuzzing_xpdf/out/ \
         -s 123 -- \
         $HOME/fuzzing101/fuzzing_xpdf/install/bin/pdftotext @@ $HOME/fuzzing101/fuzzing_xpdf/output

-i : 퍼징에 사용할 input 파일 경로 지정

-o : 퍼징 결과를 저장하는 디렉토리 → 크래시 파일이나 로그가 여깄음

-s 123 : 랜덤 시드를 고정하여 추후에 재현 가능을 보장

-- : 옵션과 인자 구분

$HOME/fuzzing101/fuzzing_xpdf/install/bin/pdftotext : 퍼징 대상 프로그램

@@ : 자동으로 생성한 입력 파일 → fuzzing input 파일

$HOME/fuzzing101/fuzzing_xpdf/output : 크래시 파일이나 버그 관련 정보가 저장

5. crash 분석_CVE-2012-2836


4시간째에 4개가 나왔고, 자고일어났더니 한개가 더 추가된 상황

분석 전에 exif 구조를 공부한는 것을 추천한다.

id:000000,sig:11,src:000009,time:7864,execs:12447,op:havoc,rep:2

gdb --args exif $HOME/fuzzing101/fuzzing_libexif/out/default/crashes/id:000000,sig:11,src:000009,time:7864,execs:12447,op:havoc,rep:2

image.png

5-1. exif-loader.c : 387

exif_loader_get_data

exif 데이터를 로드하는 함수같음

ExifData *exif_loader_get_data (ExifLoader *loader)
{
	ExifData *ed;

	if (!loader) 
		return NULL;

	ed = exif_data_new_mem (loader->mem);
	exif_data_log (ed, loader->log);
	exif_data_load_data (ed, loader->buf, loader->bytes_read);

	return ed
}

exif_data_new_mem으로 데이터를 로드할 공간을 할당하고

exif_data_log로 로그 확보

exif_data_load_data로 데이터를 읽는듯함

5-2. exif-data.c : 819

exif_data_load_data

exif_data_load_data (data=0x55555576c630, d_orig=<optimized out>, ds_orig=<optimized out>)

Exif 형식을 확인하다가 값을 하나씩 읽어오는 듯함

문제는 IFD 데이터를 읽어오는 데서 발생함

void exif_data_load_data (ExifData *data, const unsigned char *d_orig, unsigned int ds_orig)
.
.
.
	/* IFD 1 offset */
	if (offset + 6 + 2 > ds) {
		return;
	}
	n = exif_get_short (d + 6 + offset, data->priv->order);
	if (offset + 6 + 2 + 12 * n + 4 > ds) {
		return;
	}

d_orig : loader의 buffer → d로 재선언됨 → 데이터가 저장된 버퍼

ds_orig : loader의 bytes_read → ds로 재선언됨 → 읽어들인 바이트 수

해당 코드보다 위의 코드에서 Fixed Value (TIFF 헤더에 마커 값)을 확인하고 IFD 0을 파싱함

해당 코드는 IFD1 의 시작 주소를 계산하여 n에 저장하는 역할

exif_get_short로 IFD1의 시작 주소를 읽어서 n에 저장

그 뒤 load_content라는 함수로 IFD 1의 데이터를 파싱

5-3. exif-utils.c : 104

exif_get_short

exif_get_short (buf=0x55565576d63e <error: Cannot access memory at address 0x55565576d63e>,order=EXIF_BYTE_ORDER_MOTOROLA)
ExifShort exif_get_short (const unsigned char *buf, ExifByteOrder order)
{
	return (exif_get_sshort (buf, order) & 0xffff);
}

exif_get_sshort 호출하고 끝냄

5-4. exif-utils.c : 92

exif_get_sshort

exif_get_sshort (buf=0x55565576d63e <error: Cannot access memory at address 0x55565576d63e>,order=EXIF_BYTE_ORDER_MOTOROLA)
ExifSShort exif_get_sshort (const unsigned char *buf, ExifByteOrder order)
{
	if (!buf) return 0;
        switch (order) {
        case EXIF_BYTE_ORDER_MOTOROLA:
                return ((buf[0] << 8) | buf[1]);
        case EXIF_BYTE_ORDER_INTEL:
                return ((buf[1] << 8) | buf[0]);
        }
	/* Won't be reached */
	return (0);
}

해결

buf=0x55565576d63e <error: Cannot access memory at address 0x55565576d63e

이걸 눈여겨 봐야할거같은데,

기존에 접근했던 0x55555576c630 영역은 heap

0x55565576d63e 도 heap 영역이네 ㅅㅂ?

둘 차이는 4110

0x55555576c630 + 6 + offset이니까

4104 넣은 격이 되는건데,

offset은 IFD 0의 시작 위치로 고정됨 즉, offset은 Exif 시작 지점이 아니라

Exif의 IFD 시작 지점임

ds의 값을 먼저 확인해보자.

b exif-data.c:713

	/*
	 * It can be that the data starts with the EXIF header. If it does
	 * not, search the EXIF marker.
	 */
	if (ds < 6) {
		LOG_TOO_SMALL;
		return;
	}

여기서 쉽게 확인 가능할 거 같다.

ds : 0x480 (1152)

또한 d의 기존 주소는 0x55555576c630 이며

n = exif_get_short (d + 6 + offset, data->priv->order);

해당 코드에서 offset을 더할 때 모종의 이유로 offset이 너무 커져서 1152 범위를 벗어나서 0x55565576d63e 를 침범한 out of bound 였던 것 같다.

기왕 하는김에 offset까지 확인해보자.

b exif-data.c:809

		exif_log (data->priv->log, EXIF_LOG_CODE_DEBUG, "ExifData",
			  "IFD 1 at %i.", (int) offset);


R8보면 0xffff8이다.

실제로 파일을 까봐도 터무니 없는 값이 들어있다

	/* IFD 1 offset */
	if (offset + 6 + 2 > ds) {
		return;
	}
	n = exif_get_short (d + 6 + offset, data->priv->order);
	if (offset + 6 + 2 + 12 * n + 4 > ds) {
		return;
	}

uint32_t는 0xffffffff까지 표현 가능한데, 0xfffff8 + 8 즉 0x100000000이 들어갔다.

다시말해 값이 overflow 나면서 offset이 음수로 들어가서 return이 안걸린 것이다.

ExifSShort exif_get_sshort (const unsigned char *buf, ExifByteOrder order)
{
	if (!buf) return 0;
        switch (order) {
        case EXIF_BYTE_ORDER_MOTOROLA:
                return ((buf[0] << 8) | buf[1]);
        case EXIF_BYTE_ORDER_INTEL:
                return ((buf[1] << 8) | buf[0]);
        }
	/* Won't be reached */
	return (0);
}

여기서 buf에 들어간 값은 이미 ds 값을 넘어섰기 때문에 발생한 out of bound인 것이다.

6. crash 분석_CVE-2009-3895

id:000004,sig:11,src:000004,time:38012888,execs:97687631,op:havoc,rep:16

gdb --args exif $HOME/fuzzing101/fuzzing_libexif/out/default/crashes/id:000004,sig:11,src:000004,time:38012888,execs:97687631,op:havoc,rep:16


exif_entry_fixexif_data_load_data 둘 다 보인다.

하나씩 분석해보자.

6-1. exif-loader.c : 387

exif_loader_get_data

exif 데이터를 로드하는 함수같음

ExifData *exif_loader_get_data (ExifLoader *loader)
{
	ExifData *ed;

	if (!loader) 
		return NULL;

	ed = exif_data_new_mem (loader->mem);
	exif_data_log (ed, loader->log);
	exif_data_load_data (ed, loader->buf, loader->bytes_read);

	return ed
}

6-2. exif-data.c : 871

exif_data_load_data

이미지에서 exif 데이터를 찾아서 파싱하는거 같음

	if (data->priv->options & EXIF_DATA_OPTION_FOLLOW_SPECIFICATION)
		exif_data_fix (data);

각 데이터 파싱 후 data fix

6-3. exif-data.c

exif_data_fix

exif_data_foreach_content 호출, 별 다른 처리 X

exif_entry_fix 함수를 호출하여 잘못된 항목 수정

IFD에 필수로 존재해야하는 태그 추가

불필요 태그 제거 등의 작업을 수행

void
exif_data_fix (ExifData *d)
{
	exif_data_foreach_content (d, fix_func, NULL);
}

6-4. exif-data.c

exif_data_foreach_content

EXIF 데이터의 모든 IFD(이미지 파일 디렉토리)를 순회하면서 func를 실행

func는 어디서부터 오는지 모르겠음

void exif_data_foreach_content (ExifData *data, ExifDataForeachContentFunc func,void *user_data)
{
	unsigned int i;

	if (!data || !func)
		return;

	for (i = 0; i < EXIF_IFD_COUNT; i++)
		func (data->ifd[i], user_data);
}

아마 저 func가 여러개 존재하고, exif_content_fix 로 채택되어서 실행된 듯 함

6-5. exif-content.c

exif_content_fix

EXIF의 IFD를 검증하고 수정, 필수 태그가 없으면 추가.

void exif_content_fix (ExifContent *c)
{
	ExifIfd ifd = exif_content_get_ifd (c);
	ExifDataType dt;
	ExifTag t;
	ExifEntry *e;

	if (!c) return;

	dt = exif_data_get_data_type (c->parent);

	/* First of all, fix all existing entries. */
	exif_content_foreach_entry (c, fix_func, NULL);

	/*
	 * Then check for existing tags that are not allowed and for
	 * non-existing mandatory tags.
	 */
	for (t = 0; t <= 0xffff; t++) {
		switch (exif_tag_get_support_level_in_ifd (t, ifd, dt)) {
		case EXIF_SUPPORT_LEVEL_MANDATORY:
			if (exif_content_get_entry (c, t)) break;
			exif_log (c->priv->log, EXIF_LOG_CODE_DEBUG, "exif-content",
					"Tag '%s' is mandatory in IFD '%s' and has therefore been added.",
					exif_tag_get_name_in_ifd (t, ifd), exif_ifd_get_name (ifd));
			e = exif_entry_new ();
			exif_content_add_entry (c, e);
			exif_entry_initialize (e, t);
			exif_entry_unref (e);
			break;
		case EXIF_SUPPORT_LEVEL_NOT_RECORDED:
			e = exif_content_get_entry (c, t);
			if (!e) break;
			exif_log (c->priv->log, EXIF_LOG_CODE_DEBUG, "exif-content",
					"Tag '%s' is not recoreded in IFD '%s' and has therefore been "
					"removed.", exif_tag_get_name_in_ifd (t, ifd),
					exif_ifd_get_name (ifd));
			exif_content_remove_entry (c, e);
			break;
		case EXIF_SUPPORT_LEVEL_OPTIONAL:
		default:
			break;
		}
	}

IFD를 수정하여 규격에 맞게 정리

6-6. exif-content.c

exif_content_foreach_entry

void exif_content_foreach_entry (ExifContent *content, ExifContentForeachEntryFunc func, void *data)
{
	unsigned int i;

	if (!content || !func)
		return;

	for (i = 0; i < content->count; i++)
		func (content->entries[i], data);
}

IFD 디렉토리 내의 모든 태그를 순회하면서 fix_func를 실행

6-7. exif-content.c

fix_func

exif_entry_fix 실행시키고 만다.

static void fix_func (ExifEntry *e, void *data)
{
	exif_entry_fix (e);
}

6-8. exif-entry.c

exif_entry_fix

IFD 안에는 여러 엔트리들이 존재하는데, 이 엔트리들은 각각의 테그를 가지고 있다.

exif_entry_fix 함수는 각각의 엔트리들을 테그별로 분류하여 어떠한 처리를 한다.

예를 들어,

해당 테그들은 SHORT 포멧이 필요하다고 한다.

해당 테그들에 대한 처리는

for (i = 0; i < e->components; i++) {
    exif_set_short(e->data + i * exif_format_get_size(EXIF_FORMAT_SHORT), o, (ExifShort) exif_get_long(e->data + i * exif_format_get_size(EXIF_FORMAT_LONG), o)
    );
}

6-9. exif-utils.c

exif_get_long

ExifLong exif_get_long (const unsigned char *buf, ExifByteOrder order)
{
        return (exif_get_slong (buf, order) & 0xffffffff);
}
typedef uint32_t	ExifLong;          /* 4 bytes */

6-10. exif-utils.c

exif_get_slong

ExifSLong exif_get_slong (const unsigned char *b, ExifByteOrder order)
{
	if (!b) return 0;
        switch (order) {
        case EXIF_BYTE_ORDER_MOTOROLA:
                return ((b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3]);
        case EXIF_BYTE_ORDER_INTEL:
                return ((b[3] << 24) | (b[2] << 16) | (b[1] << 8) | b[0]);
        }

	/* Won't be reached */
	return (0);
}

slong 만큼의 길이를 get하는 거 같음

typedef int32_t		ExifSLong;         /* 4 bytes */
 return ((b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3]);

여기서 터진다.

해결

우선 처음 보는 개념인 ExifEntry부터 알아보자.

struct _ExifEntry {
    ExifTag tag;              // EXIF 태그 ID
    ExifFormat format;        // 데이터 형식 (예: ASCII, SHORT, LONG 등)
    unsigned long components; // 데이터의 구성 요소 개수

    unsigned char *data;      // 실제 데이터 포인터
    unsigned int size;        // 데이터 크기 (바이트 단위)

    /* Content containing this entry */
    ExifContent *parent;      // 이 엔트리가 속한 EXIF 콘텐츠

    ExifEntryPrivate *priv;   // 내부적으로 사용하는 private 데이터
};

간략히 설명해서 Exif의 엔트리 안에는 Format별로 여러 데이터를 저장할 수 있다.

이때 저장할 데이터의 개수를 components 변수에 저장하게 된다.

for (i = 0; i < e->components; i++) {
    exif_set_short(e->data + i * exif_format_get_size(EXIF_FORMAT_SHORT), o, (ExifShort) exif_get_long(e->data + i * exif_format_get_size(EXIF_FORMAT_LONG), o)
    );
}

위 반복문에서 i의 최대 크기는 e→components로 결정된다.

위에서 확인한 Exif의 크기보다 훨씬 큰 값을 가진다.
해당 값으로 인해 i가 너무 큰 값을 가지게 되어 exif 데이터보다 큰 곳을 참조하면서 발생하는 취약점이다.

0개의 댓글