Logo
Overview

Fuzzing 101 Exercise 2

August 7, 2025
9 min read

이번 포스팅에서는 Fuzzing101 Exercise 2 - libexif를 실습합니다. 이전 Xpdf처럼 libexif의 취약한 버전과 최신 버전을 AFL++로 퍼징했으며, Fuzzing101에서 제공하는 환경과 다르게 진행했으므로 실습을 진행하는데 유의하시기 바랍니다.

Environment

환경은 Ubuntu 22.04.5 LTSAFL++ 4.33c으로 실습을 진행했습니다.

libexif, exif

libexif는 EXIF(Exchangeable image file format) 데이터를 읽고, 쓰고, 수정하는 데 사용되는 오픈 소스 라이브러리입니다. libexif는 실행 파일이 아닌 C 라이브러리이므로, 이 라이브러리를 사용하는 exif 명령어와 같은 실행 프로그램을 퍼징 대상으로 삼아야 합니다. 물론 LibFuzzer로 직접 libexif를 퍼징할 수 있지만, AFL++를 실습하기 위해 exif명령어를 퍼징하도록 하겠습니다.

libexif 0.6.14, exif 0.6.15

Download and Build

Terminal window
mkdir fuzzing_libexif && cd fuzzing_libexif/
wget https://github.com/libexif/libexif/archive/refs/tags/libexif-0_6_14-release.tar.gz
tar -xzvf libexif-0_6_14-release.tar.gz
cd libexif-libexif-0_6_14-release/
sudo apt-get install autopoint libtool gettext libpopt-dev
autoreconf -fvi
./configure --enable-shared=no --prefix="/fuzzing_libexif/install/"
make
make install

libexif 0.6.14를 다운받기 위해, 디렉터리를 만들고 libexif 0.6.14를 다운 받습니다. 또한, 필요한 환경을 설치하고 libexif 0.6.14를 빌드합니다. 여기서 --enable-shared=no를 설정한 이유는 퍼징을 할 때, 의존성 문제 없이 단일 실행 파일을 만들기 위해 해당 명령어를 사용했습니다.

Note

--enable-shared=no: 공유 라이브러리(.so)가 아닌 정적 라이브러리(.a)를 만드는 명령어

Terminal window
cd fuzzing_libexif
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="/fuzzing_libexif/install/" PKG_CONFIG_PATH=/fuzzing_libexif/install/lib/pkgconfig
make
make install

앞서 이야기 했지만, AFL++로 퍼징하기 위해서는 libexif와 같은 라이브러리가 아닌 exif와 같은 실행 프로그램이 필요합니다. 그래서 exif 0.6.15를 다운받기 위해, 디렉터리를 만들고 exif 0.6.15를 다운받습니다. 또한, exif 0.6.15를 빌드합니다.

/fuzzing_libexif/install/bin/exif를 사용하여 exif가 잘 동작하는지 확인합니다.

exif0.6.15_test

위와 같이 나타나면 exif가 정상적으로 동작합니다.

Seed corpus

Terminal window
cd /fuzzing_libexif
wget https://github.com/ianare/exif-samples/archive/refs/heads/master.zip
unzip master.zip
/fuzzing_libexif/install/bin/exif /fuzzing_libexif/exif-samples-master/jpg/Canon_40D_photoshop_import.jpg

exif를 퍼징하기 전, 시드 파일을 다운하고 잘 동작하는지 확인합니다.

exif0.6.15_test2

위와 같이 나타난다면 정상적인 시드 파일입니다.

Meet AFL++

타겟 프로그램을 다운하고 빌드까지 했다면, 퍼징을 하기위해 계측코드 삽입이 필요합니다.

Terminal window
cd /fuzzing_libexif/libexif-libexif-0_6_14-release/
make clean
export LLVM_CONFIG="llvm-config-14"
CFLAGS="-fsanitize=address -g" CXXFLAGS="-fsanitize=address -g" LDFLAGS="-fsanitize=address"
CC=/AFLplusplus/afl-clang-lto ./configure --enable-shared=no --prefix="/fuzzing_libexif/install/"
make
make install

libexif에 계측 코드를 삽입하고, exif에도 계측 코드를 사용합니다.

Terminal window
cd fuzzing_libexif/exif-exif-0_6_15-release
make clean
export LLVM_CONFIG="llvm-config-14"
CFLAGS="-fsanitize=address -g" CXXFLAGS="-fsanitize=address -g" LDFLAGS="-fsanitize=address"
CC=/AFLplusplus/afl-clang-lto ./configure --enable-shared=no --prefix="/fuzzing_libexif/install/" PKG_CONFIG_PATH=/fuzzing_libexif/install/lib/pkgconfig
make
make install

이전 Fuzzing101 Exercise 1에는 afl-clang-fast를 사용했는데, Fuzzing101 Exercise 2에서는 afl-clang-lto를 사용합니다. afl-clang-fast는 퍼징 시 가장 호환적인 컴파일 방식으로 빠르게 컴파일 하고 안정적으로 계측할 수 있습니다. afl-clang-lto는 퍼징 시 가장 효율적인 컴파일 방식으로 퍼징 효율성을 극대화하고 싶을 때 사용합니다. 일반적으로 afl-clang-lto 방식으로 컴파일이 안될 때 afl-clang-fast를 사용합니다. 더 자세한 내용은 아래 사진과 여기를 참조하면 됩니다.

afl_clang_diagram

Fuzz

Terminal window
AFL_USE_ASAN=1 afl-fuzz -i /fuzzing_libexif/exif-samples-master/jpg/ -o /fuzzing_libexif/out/ -s 123 -m none -- /fuzzing_libexif/install/bin/exif @@

exif0.6.15_fuzz.png

퍼징이 정상적으로 진행이 되었고, 이제 크래시를 분석하면 됩니다.

분석

원인 분석(Root Cause)를 하기 위해, 타겟 프로그램을 계측 코드가 삽입된 바이너리가 아닌 원본 바이너리 상태로 만들어야 합니다. libexif와 exif를 원본 상태로 만들기 위한 명령어는 다음과 같습니다.

Terminal window
cd /fuzzing_libexif/libexif-libexif-0_6_14-release/
make clean
export LLVM_CONFIG="llvm-config-14"
CFLAGS="-g -O0" CXXFLAGS="-g -O0" ./configure --enable-shared=no --prefix="/fuzzing_libexif/install/"
make
make install
Terminal window
cd fuzzing_libexif/exif-exif-0_6_15-release
make clean
export LLVM_CONFIG="llvm-config-14"
CFLAGS="-g -O0" CXXFLAGS="-g -O0" ./configure --enable-shared=no --prefix="/fuzzing_libexif/install/" PKG_CONFIG_PATH=/fuzzing_libexif/install/lib/pkgconfig
make
make install

원본 바이너리 상태로 만들었다면, gdb를 이용하여 크래시파일을 입력값으로 넣어 원인 분석(Root Cause)를 합니다.

Terminal window
gdb --args /fuzzing_libexif/install/bin/exif [크래시파일]
run

위 명령어를 실행하면 아래처럼 나옵니다.

exif0.6.15_gdb.png

프로그램이 종료된 이유를 보면, Cannot access memory at address 0x55565558ec35라고 나옵니다.

exif0.6.15_vmmap

vmmap을 이용하여 0x55565558ec35 주소값을 보면, 해당 주소는 매핑된 주소가 아닌 것을 볼 수 있습니다.

exif0.6.15_disass.png

또한 0x5555555691e0 주소값을 보기위해 disass 명령어를 사용하면, exif_get_short에 매핑된 주소인 것을 알 수 있습니다.

함수의 흐름을 보기 위해 bt명령어를 사용하여 콜스택을 확인합니다.

exif0.6.15_bt.png

buf=0x55565558ec35 <error: Cannot access memory at address 0x55565558ec35>라는 메세지가 나타나고, 프로그램이 exif_get_sshort에서 비정상적으로 종료되는 것을 알 수 있습니다.

buf=0x55565558ec35을 보기 위해서는 mainexif_loader_get_dataexif_data_load_dataexif_get_short(여기까지만 봐도 됨) → exif_get_sshort 이렇게 순차적으로 분석해야 합니다.

먼저, main.c의 438번째 줄을 보면 다음과 같습니다.

...
l = exif_loader_new ();
exif_loader_log (l, log);
exif_loader_write_file (l, *args);
ed = exif_loader_get_data (l); // main.c:438
...

이를 분석하면 ExifLoader의 객체(l)가 파일에서 읽어 들인 원본 EXIF 데이터를 파싱하여, Exif 프로그램에서 다룰 수 있는 ExifData 구조체(ed)로 변환하는 역할을 합니다.

exif_loader_get_data를 보기 위해 exif-loader.c의 387번째 줄을 살펴봅니다.

...
ed = exif_data_new_mem (loader->mem);
exif_data_log (ed, loader->log);
exif_data_load_data (ed, loader->buf, loader->bytes_read); // exif-loader.c:387
...

exif_data_load_data()ExifLoader가 파일에서 읽어온 EXIF 바이트 데이터(raw data)를 실제로 파싱하여, 프로그램이 이해하고 사용할 수 있도록 구조화된 ExifData 객체로 변환하는 핵심 파싱 함수입니다. 각 인자에 대한 설명은 다음과 같습니다.

  • ed: 파싱된 EXIF 정보가 저장될 변수
  • loader->buf: 파싱할 원본 데이터가 들어있는 버퍼
  • loader->bytes_read: loader->buf에 저장된 데이터의 크기(바이트 수)

이제 exif_data_load_data를 보기 위해 exif-data.c의 819번째 줄을 확인합니다.

...
/* IFD 1 offset */
if (offset + 6 + 2 > ds) {
return;
}
n = exif_get_short (d + 6 + offset, data->priv->order); // exif-data.c:819
...

n = exif_get_short (d + 6 + offset, data->priv->order);는 IFD0의 시작 지점에서 그 안에 포함된 태그의 개수를 읽어와 n 변수에 저장하는 IFD1 파싱의 가장 첫 단계입니다. 즉, exif-data.c의 819번째 줄은 IFD1 위치를 찾기 위해 IFD0 위치를 확인하는 과정입니다. exif_get_short()의 인자에 대한 설명은 다음과 같습니다.

  • d + 6 + offset: IFD0의 시작 위치
    • d: EXIF 데이터 청크(chunk)의 시작 주소를 가리키는 포인터
    • + 6: EXIF 데이터 청크에서 6바이트를 더함 → Exif 식별자(Exif\0\0)를 건너뛰고 TIFF 헤더의 시작점으로 포인터를 이동
    • offset: TIFF 헤더 시작점부터 IFD0가 시작되는 거리
  • data->priv->order: EXIF 데이터 내의 바이트 값을 어떤 바이트 순서로 해석할지 결정하는 플래그
Note

IFD는 EXIF의 핵심 구성 요소이고, EXIF는 TIFF(Tagged Image File Format) 파일 형식 기반으로 만들어졌습니다. TIFF는 ‘태그(Tag)‘를 사용하여 데이터를 저장하는데, IFD는 바로 이 태그들의 집합을 담는 컨테이너 역할을 합니다.

exif_get_short를 보기 위해 exif-utils.c의 104번째 줄을 확인합니다.

...
ExifShort
exif_get_short (const unsigned char *buf, ExifByteOrder order)
{
return (exif_get_sshort (buf, order) & 0xffff); // exif-utils.c:104
}
...

exif_get_short()exif_get_sshort()를 호출합니다. exif_get_sshort()에 들어갈 인자는 다음과 같습니다.

  • buf: 파싱할 원본 데이터
  • order: 메모리 바이트 해석 정보(리틀엔디언/빅엔디언)

buf=0x55565558ec35 <error: Cannot access memory at address 0x55565558ec35>라는 메세지가 나오고 프로그램이 비정상적으로 종료된 것을 보면, buf에 문제가 있다는 것을 알 수 있습니다. exif-data.c의 819번째 줄을 보면 d + 6 + offsetbuf가 되는 것을 볼 수 있습니다. 즉, d가 문제거나 offset에 문제가 있다는 것을 알 수 있습니다.

buf(d + 6 + offset)가 문제가 있기 때문에, exif_data_load_data(exif-data.c의 819번째 줄)를 자세히 봐야 합니다.

...
/* IFD 0 offset */
offset = exif_get_long (d + 10, data->priv->order);
exif_log (data->priv->log, EXIF_LOG_CODE_DEBUG, "ExifData", "IFD 0 at %i.", (int) offset);
/* Parse the actual exif data (usually offset 14 from start) */
exif_data_load_data_content (data, EXIF_IFD_0, d + 6, ds - 6, offset, 0);
/* IFD 1 offset */
if (offset + 6 + 2 > ds) {
return;
}
n = exif_get_short (d + 6 + offset, data->priv->order); // exif-data.c:819
...

if (offset + 6 + 2 > ds) { return; }를 보면 offset + 6 + 2ds(전체 데이터)보다 크면 프로그램이 종료되는데, 프로그램은 종료되지 않고 n = exif_get_short (d + 6 + offset, data->priv->order); 지점을 통과합니다. 따라서, offset + 6 + 2ds보다 작은 것을 알 수 있습니다.

exif0.6.15_register

디버거로 offset + 6 + 2 > ds를 확인해보면, offset + 6 + 2은 0x7이고 ds는 0x484입니다. 하지만 offset + 6 + 2는 최소 0x8 이상의 값이 나와야 하는데 0x7입니다. 따라서 offset부분이 문제가 있다는 것을 알 수 있습니다.

정확한 offset 값을 알기 위해, exif_data_load_data_content (data, EXIF_IFD_0, d + 6, ds - 6, offset, 0);에 있는 offset을 확인해야 합니다.

exif0.6.15_offset

exif_data_load_data_content()를 확인해보면 unsigned int offset = 0x00000000ffffffff인 것을 알 수 있습니다. unsigned int 자료형의 최대값은 0xffffffff입니다. 그래서 if (offset + 6 + 2 > ds)에서 경계값을 확인할 때, interoverflow가 일어나 offset + 6 + 2값이 7이 되었고 ds보다 작기 때문에 프로그램이 종료되지 않고 n = exif_get_short() 지점을 통과 했습니다.

다시 프로그램이 비정상적으로 종료된 것을 생각해보면, buf=0x55565558ec35 <error: Cannot access memory at address 0x55565558ec35>로 프로그램이 종료 되었습니다. bufd + 6 + offset인 값인데, d + 60x000055555558ec36이고 offset0x00000000ffffffff입니다. 즉, d + 6 + offset = 0x55565558ec35값을 갖게 되는데, vmmap으로 확인하면 할당되지 않는 공간이라서 프로그램이 비정상적으로 종료가 됩니다.

exif0.6.15_vmmap

이러한 offset 문제를 해결하기 위한 방법은 아래에서 자세히 다루겠습니다.

libexif 0.6.25, exif 0.6.22

Download and Build

취약한 버전으로 퍼징을 실습해봤으니, 최신 버전에서 퍼징을 실습해보겠습니다.

Terminal window
mkdir fuzzing_new_libexif && cd fuzzing_new_libexif/
git clone https://github.com/libexif/libexif.git
cd libexif
autoreconf -fvi
./configure --enable-shared=no --prefix="/fuzzing_new_libexif/install/"
make
make install

libexif 0.6.25를 다운받기 위해, 디렉터리를 만들고 libexif 0.6.25를 다운받습니다. 또한, 필요한 환경을 설치하고 libexif 0.6.25를 빌드합니다.

Terminal window
cd fuzzing_new_libexif
git clone https://github.com/libexif/exif.git
cd exif
autoreconf -fvi
./configure --enable-shared=no --prefix="/fuzzing_new_libexif/install/" PKG_CONFIG_PATH=/fuzzing_new_libexif/install/lib/pkgconfig
make
make install

exif 0.6.22를 다운하고 빌드합니다. 취약한 버전과 최신 버전에서 빌드 차이점은 없기 때문에, 오류가 난다면 취약한 버전에서 빌드한 것을 참고하면 됩니다.

Note

libexif 0.6.25exif 0.6.22의 최신버전 링크입니다.

Terminal window
/fuzzing_new_libexif/install/bin/exif
/fuzzing_new_libexif/install/bin/exif /fuzzing_libexif/exif-samples-master/jpg/Canon_40D_photoshop_import.jpg

최신 버전의 exif가 잘 동작하는 것을 확인하기 위해서 위에 나온 명령어를 작성하면, 기존에 작동 테스트한 것처럼 나와야 합니다.(테스트 파일은 취약한 버전에서 사용한 시드 파일을 사용합니다.)

Meet AFL++

타겟 프로그램을 다운하고 빌드까지 했다면, 퍼징을 하기위해 AFL 계측코드를 삽입합니다.

Terminal window
cd /fuzzing_new_libexif/libexif
make clean
export LLVM_CONFIG="llvm-config-14"
CFLAGS="-fsanitize=address -g" CXXFLAGS="-fsanitize=address -g" LDFLAGS="-fsanitize=address"
CC=/AFLplusplus/afl-clang-lto ./configure --enable-shared=no --prefix="/fuzzing_new_libexif/install/"
make
make install

libexif에 계측 코드를 삽입하고, exif에도 계측 코드를 삽입 합니다.

Terminal window
cd /fuzzing_new_libexif/exif
make clean
export LLVM_CONFIG="llvm-config-14"
CFLAGS="-fsanitize=address -g" CXXFLAGS="-fsanitize=address -g" LDFLAGS="-fsanitize=address"
CC=/AFLplusplus/afl-clang-lto ./configure --enable-shared=no --prefix="/fuzzing_new_libexif/install/" PKG_CONFIG_PATH=/fuzzing_new_libexif/install/lib/pkgconfig
make
make install

Fuzz

ASAN과 AFL++로 빌드 했다면, afl-fuzz 명령어를 통해 퍼징을 진행합니다.

Terminal window
AFL_USE_ASAN=1 afl-fuzz -i /fuzzing__libexif/exif-samples-master/jpg/ -o /fuzzing_new_libexif/out/ -m none -- /fuzzing_libexif/install/bin/exif @@

exif0.6.22_fuzz

퍼징 명령어를 작성하고 동작이 잘 되었지만.. 5일이 지나도 크래시가 발견되지 않아서 퍼징을 중단했습니다. 이처럼 구버전이 아니라 최신 버전의 exif를 퍼징할 수 있습니다.

패치

exif0.6.15에서 offset이 unsigned int 자료형의 최대값이라서 경계값 검사할 때 interoverflow가 발생합니다. 이는 경계값을 통과하는 문제가 있었습니다. 이를 통해 buf가 유효하지 않은 주소를 가리키게 되면서 프로그램이 비정상적으로 종료 되었습니다.

offset의 크기를 검사하는 부분을 추가하면 위 문제를 해결할 수 있습니다. 그래서 최신 버전(libexif 0.6.25)에서 패치된 부분을 보면 다음과 같습니다.

...
/* The JPEG APP1 section can be no longer than 64 KiB (including a
16-bit length), so cap the data length to protect against overflow
in future offset calculations */
fullds = ds;
if (ds > 0xfffe)
ds = 0xfffe;
...
/* ds is restricted to 16 bit above, so offset is restricted too, and offset+8 should not overflow. */
if (offset > ds || offset + 6 + 2 > ds)
return;
/* Parse the actual exif data (usually offset 14 from start) */
exif_data_load_data_content (data, EXIF_IFD_0, d + 6, ds - 6, offset, 0);
/* IFD 1 offset */
n = exif_get_short (d + 6 + offset, data->priv->order);
/* offset < 2<<16, n is 16 bit at most, so this op will not overflow */
if (offset + 6 + 2 + 12 * n + 4 > ds)
return;
...

JPEG 표준에 따라 APP1 섹션 최대 크기를 65534(0xfffe)바이트로 제한하여 후속 계산에서 overflow 가능성을 차단했습니다. 즉, ds의 최대 범위가 65534바이트이기 때문에 offset > ds에서 offset 자체가 데이터 범위(ds)를 벗어나면 즉시 종료됩니다.

관련 취약점은 CVE-2012-2836를 참고하시면 됩니다.

Conclusion

이번 포스팅에서는 AFL++ 4.33c를 이용하여 libexif와 exif를 퍼징했습니다. libexif를 타겟을 잡았지만, libexif는 라이브러리기 때문에 libexif 라이브러리를 사용하는 프로그램인 exif를 퍼징 했습니다. 이처럼 라이브러리를 AFL++로 퍼징할 때는 직접 퍼징할 수 없고 해당 라이브러리를 사용하는 프로그램(명령어)을 퍼징하면 됩니다. 만약 라이브러리를 직접 퍼징하고 싶다면, LibFuzzer를 사용하면 됩니다.

또한 기존 AFL 계측코드를 삽입할 때,afl-clang-fast를 사용했지만, 이제는 위에 나온 다이어그램을 참조하거나 퍼징 환경에 따라 afl-clang-fastafl-clang-lto를 사용합니다.

Fuzzing101 Exercise 2에서는 CVE-2012-2836뿐만 아니라, Heap-based buffer overflow 취약점이 발견된 CVE-2009-3895도 있습니다. 하지만 위 취약점까지 Root Cause 분석을 한다면 포스팅이 너무 길어짐으로, CVE-2009-3895를 참조하시면 됩니다.

References

[1] Exercise 2 - libexif

Footnotes

  1. IFD(Image File Directory): 사진 파일에서 다양한 메타데이터를 체계적으로 정리하고 저장하는 데이터 구조