이번 포스팅에서는 Fuzzing101 Exercise 2 - libexif를 실습합니다. 이전 Xpdf처럼 libexif의 취약한 버전과 최신 버전을 AFL++로 퍼징했으며, Fuzzing101에서 제공하는 환경과 다르게 진행했으므로 실습을 진행하는데 유의하시기 바랍니다.
Environment
환경은 Ubuntu 22.04.5 LTS와 AFL++ 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
mkdir fuzzing_libexif && cd fuzzing_libexif/wget https://github.com/libexif/libexif/archive/refs/tags/libexif-0_6_14-release.tar.gztar -xzvf libexif-0_6_14-release.tar.gzcd libexif-libexif-0_6_14-release/sudo apt-get install autopoint libtool gettext libpopt-devautoreconf -fvi./configure --enable-shared=no --prefix="/fuzzing_libexif/install/"makemake installlibexif 0.6.14를 다운받기 위해, 디렉터리를 만들고 libexif 0.6.14를 다운 받습니다. 또한, 필요한 환경을 설치하고 libexif 0.6.14를 빌드합니다. 여기서 --enable-shared=no를 설정한 이유는 퍼징을 할 때, 의존성 문제 없이 단일 실행 파일을 만들기 위해 해당 명령어를 사용했습니다.
Note
--enable-shared=no: 공유 라이브러리(.so)가 아닌 정적 라이브러리(.a)를 만드는 명령어
cd fuzzing_libexifwget https://github.com/libexif/exif/archive/refs/tags/exif-0_6_15-release.tar.gztar -xzvf exif-0_6_15-release.tar.gzcd exif-exif-0_6_15-release/autoreconf -fvi./configure --enable-shared=no --prefix="/fuzzing_libexif/install/" PKG_CONFIG_PATH=/fuzzing_libexif/install/lib/pkgconfigmakemake install앞서 이야기 했지만, AFL++로 퍼징하기 위해서는 libexif와 같은 라이브러리가 아닌 exif와 같은 실행 프로그램이 필요합니다. 그래서 exif 0.6.15를 다운받기 위해, 디렉터리를 만들고 exif 0.6.15를 다운받습니다. 또한, exif 0.6.15를 빌드합니다.
/fuzzing_libexif/install/bin/exif를 사용하여 exif가 잘 동작하는지 확인합니다.

위와 같이 나타나면 exif가 정상적으로 동작합니다.
Seed corpus
cd /fuzzing_libexifwget https://github.com/ianare/exif-samples/archive/refs/heads/master.zipunzip master.zip/fuzzing_libexif/install/bin/exif /fuzzing_libexif/exif-samples-master/jpg/Canon_40D_photoshop_import.jpgexif를 퍼징하기 전, 시드 파일을 다운하고 잘 동작하는지 확인합니다.

위와 같이 나타난다면 정상적인 시드 파일입니다.
Meet AFL++
타겟 프로그램을 다운하고 빌드까지 했다면, 퍼징을 하기위해 계측코드 삽입이 필요합니다.
cd /fuzzing_libexif/libexif-libexif-0_6_14-release/make cleanexport 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/"makemake installlibexif에 계측 코드를 삽입하고, exif에도 계측 코드를 사용합니다.
cd fuzzing_libexif/exif-exif-0_6_15-releasemake cleanexport 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/pkgconfigmakemake 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를 사용합니다. 더 자세한 내용은 아래 사진과 여기를 참조하면 됩니다.

Fuzz
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 @@
퍼징이 정상적으로 진행이 되었고, 이제 크래시를 분석하면 됩니다.
분석
원인 분석(Root Cause)를 하기 위해, 타겟 프로그램을 계측 코드가 삽입된 바이너리가 아닌 원본 바이너리 상태로 만들어야 합니다. libexif와 exif를 원본 상태로 만들기 위한 명령어는 다음과 같습니다.
cd /fuzzing_libexif/libexif-libexif-0_6_14-release/make cleanexport LLVM_CONFIG="llvm-config-14"CFLAGS="-g -O0" CXXFLAGS="-g -O0" ./configure --enable-shared=no --prefix="/fuzzing_libexif/install/"makemake installcd fuzzing_libexif/exif-exif-0_6_15-releasemake cleanexport 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/pkgconfigmakemake install원본 바이너리 상태로 만들었다면, gdb를 이용하여 크래시파일을 입력값으로 넣어 원인 분석(Root Cause)를 합니다.
gdb --args /fuzzing_libexif/install/bin/exif [크래시파일]run위 명령어를 실행하면 아래처럼 나옵니다.

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

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

또한 0x5555555691e0 주소값을 보기위해 disass 명령어를 사용하면, exif_get_short에 매핑된 주소인 것을 알 수 있습니다.
함수의 흐름을 보기 위해 bt명령어를 사용하여 콜스택을 확인합니다.

buf=0x55565558ec35 <error: Cannot access memory at address 0x55565558ec35>라는 메세지가 나타나고, 프로그램이 exif_get_sshort에서 비정상적으로 종료되는 것을 알 수 있습니다.
buf=0x55565558ec35을 보기 위해서는 main → exif_loader_get_data → exif_data_load_data → exif_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번째 줄을 확인합니다.
...ExifShortexif_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 + offset이 buf가 되는 것을 볼 수 있습니다. 즉, 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 + 2가 ds(전체 데이터)보다 크면 프로그램이 종료되는데, 프로그램은 종료되지 않고 n = exif_get_short (d + 6 + offset, data->priv->order); 지점을 통과합니다. 따라서, offset + 6 + 2는 ds보다 작은 것을 알 수 있습니다.

디버거로 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을 확인해야 합니다.

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>로 프로그램이 종료 되었습니다. buf는 d + 6 + offset인 값인데, d + 6은 0x000055555558ec36이고 offset은 0x00000000ffffffff입니다. 즉, d + 6 + offset = 0x55565558ec35값을 갖게 되는데, vmmap으로 확인하면 할당되지 않는 공간이라서 프로그램이 비정상적으로 종료가 됩니다.

이러한 offset 문제를 해결하기 위한 방법은 아래에서 자세히 다루겠습니다.
libexif 0.6.25, exif 0.6.22
Download and Build
취약한 버전으로 퍼징을 실습해봤으니, 최신 버전에서 퍼징을 실습해보겠습니다.
mkdir fuzzing_new_libexif && cd fuzzing_new_libexif/git clone https://github.com/libexif/libexif.gitcd libexifautoreconf -fvi./configure --enable-shared=no --prefix="/fuzzing_new_libexif/install/"makemake installlibexif 0.6.25를 다운받기 위해, 디렉터리를 만들고 libexif 0.6.25를 다운받습니다. 또한, 필요한 환경을 설치하고 libexif 0.6.25를 빌드합니다.
cd fuzzing_new_libexifgit clone https://github.com/libexif/exif.gitcd exifautoreconf -fvi./configure --enable-shared=no --prefix="/fuzzing_new_libexif/install/" PKG_CONFIG_PATH=/fuzzing_new_libexif/install/lib/pkgconfigmakemake installexif 0.6.22를 다운하고 빌드합니다. 취약한 버전과 최신 버전에서 빌드 차이점은 없기 때문에, 오류가 난다면 취약한 버전에서 빌드한 것을 참고하면 됩니다.
Note
libexif 0.6.25와 exif 0.6.22의 최신버전 링크입니다.
/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 계측코드를 삽입합니다.
cd /fuzzing_new_libexif/libexifmake cleanexport 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/"makemake installlibexif에 계측 코드를 삽입하고, exif에도 계측 코드를 삽입 합니다.
cd /fuzzing_new_libexif/exifmake cleanexport 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/pkgconfigmakemake installFuzz
ASAN과 AFL++로 빌드 했다면, afl-fuzz 명령어를 통해 퍼징을 진행합니다.
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 @@
퍼징 명령어를 작성하고 동작이 잘 되었지만.. 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-fast나 afl-clang-lto를 사용합니다.
Fuzzing101 Exercise 2에서는 CVE-2012-2836뿐만 아니라, Heap-based buffer overflow 취약점이 발견된 CVE-2009-3895도 있습니다. 하지만 위 취약점까지 Root Cause 분석을 한다면 포스팅이 너무 길어짐으로, CVE-2009-3895를 참조하시면 됩니다.
References
Footnotes
-
IFD(Image File Directory): 사진 파일에서 다양한 메타데이터를 체계적으로 정리하고 저장하는 데이터 구조 ↩