CVE-2012-2836(libexif) 분석
개요
EXIF (Exchangeable Image File Format) : 디지털 카메라 등에서 사용되는 이미지 파일 메타데이터 포맷
libexif : EXIF 데이터를 파싱하고, 수정하고, 저장하기 위한 라이브러리
- CVE-2012-2836 : 0.6.21,
exif-data.c
의exif_load_data
함수에서 발생하는out of bounds read
취약점
타겟 설치
- libexif 설치
libexif
설치 후 사용하기 편하도록 이름을 변경해주었다.
1
2
3
$ 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
$ mv libexif-0_6_14-release.tar.gz libexif
- exif 설치
libexif
는 라이브러리이기 때문에 libexif
를 사용할 프로그램을 설치해야 한다.
CLI 기반이며, 프로그램 크기가 작은 exif
를 선택하였다.
exif
도 동일하게 이름을 변경해주었다.
1
2
3
$ 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
$ mv exif-0_6_15-release.tar.gz exif
타겟 빌드
공식 도큐멘트에 올라온 빌드 방법으로 빌드하였다.
- libexif 빌드
1
2
3
4
5
$ cd /root/libexif
$ autoreconf -i
$ ./configure --enable-shared=no --prefix=/root/install
$ make
$ make install
- exif 빌드
libexif
의 위치를 지정해주어야 빌드를 완료할 수 있다.
PKG_CONFIG_PATH
를 통해 libexif.pc
의 위치를 지정해줄 수 있다.
먼저 libexif.pc
의 위치를 찾아주었다.
1
2
$ find /root/install/ -name libexif.pc
/root/install/lib/pkgconfig/libexif.pc
1
2
3
4
5
$ cd /root/exif
$ autoreconf -i
$ ./configure --enable-shared=no --prefix=/root/install PKG_CONFIG_PATH=/root/install/lib/pkgconfig
$ make
$ make install
autoreconf
: 생성된 구성파일 업데이트-f : 이전의 모든 파일을 사용하지 않음으로 간주
-v : 자세한 보고 처리
-i : 누락된 보조 파일 설치
정상적으로 설치되었다.
샘플 다운로드
1
2
3
$ wget https://github.com/ianare/exif-samples/archive/refs/heads/master.zip
$ unzip master.zip
$ mv exif-samples-master samples
해당 샘플을 타겟 프로그램에 전달해보았다.
afl-clang-lto
이번에는 afl-clang-fast
가 아닌 afl-clang-lto
를 이용하여 퍼징을 진행하라고 한다.
먼저 lto
란 Link Time Optimization
로 약자에서 알 수 있듯이 링크타임에 최적화를 진행하는 것을 말한다.
AFL++
은 컴파일 중에 블록 ID를 랜덤으로 설정한다. 따라서 계측된 위치가 많아질 수록 엣지에서의 충돌이 많아지게 된다.
이러한 방식은 새로운 path를 발견하지 못하고 퍼징의 효율은 줄어들게 된다는 문제점이 발생한다.
따라서 AFL++
은 다음과 같은 솔루션을 선택하였다.
- 모든 파일이 사전 컴파일 되었을 때 링크타임에 계측 코드가 삽입된다.
- 링크타임에 계측 코드를 삽입하기 위해
lto
모드를 이용하여 컴파일한다. - 시스템 링커가 아닌
afl-ld
라는 자체 링커를 사용한다.
결과로 llvm
모드의 10~25% 정도의 속도 향상을 보였으며, 충돌 없는 엣지 커버리지를 보장할 수 있게 되었다.
lto
의 특성상 컴파일 타임이 더 길어질 수도 있다.
afl 공식 문서에는 다음과 같은 컴파일러 선택을 도와주는 다이어그램이 존재한다.
| clang/clang++ 11+ is available | --> use LTO mode (afl-clang-lto/afl-clang-lto++)
+--------------------------------+ see [instrumentation/README.lto.md](instrumentation/README.lto.md)
|
| if not, or if the target fails with LTO afl-clang-lto/++
|
v
+---------------------------------+
| clang/clang++ 3.8+ is available | --> use LLVM mode (afl-clang-fast/afl-clang-fast++)
+---------------------------------+ see [instrumentation/README.llvm.md](instrumentation/README.llvm.md)
|
| if not, or if the target fails with LLVM afl-clang-fast/++
|
v
+--------------------------------+
| gcc 5+ is available | -> use GCC_PLUGIN mode (afl-gcc-fast/afl-g++-fast)
+--------------------------------+ see [instrumentation/README.gcc_plugin.md](instrumentation/README.gcc_plugin.md) and
[instrumentation/README.instrument_list.md](instrumentation/README.instrument_list.md)
|
| if not, or if you do not have a gcc with plugin support
|
v
use GCC mode (afl-gcc/afl-g++) (or afl-clang/afl-clang++ for clang)
AFL을 이용하여 컴파일
이전 빌드 파일 삭제
1
2
3
4
5
6
7
$ rm -r /root/install
$ cd /root/libexif
$ make clean
$ cd /root/exif
$ make clean
컴파일
1
2
3
4
5
$ export LLVM_CONFIG="llvm-config-11"
$ cd /root/libexif
$ CC=afl-clang-lto ./configure --enable-shared=no --prefix=/root/install/
$ make
$ make install
1
2
3
4
$ cd /root/exif
$ CC=afl-clang-lto ./configure --enable-shared=no --prefix=/root/install/ PKG_CONFIG_PATH=/root/install/lib/pkgconfig
$ make
$ make install
AFL 실행
1
afl-fuzz -i /root/samples/jpg/ -o /root/out/ -s 123 -- /root/install/bin/exif @@
크래시 분석
Github 에서는 Eclipse CDT를 사용하여 디버깅 하는 방법을 소개하였지만, 현재 쓰고 있는 환경 (macos + Docker) 때문에 Eclipse를 사용하지 못하므로 GDB를 사용하였다.
입력하기 편하게 크래시명을 변경해주었다.
1
2
3
$ mv id\:000000\,sig\:11\,src\:000301\,time\:53563\,execs\:61713\,op\:havoc\,rep\:16 crash-0
$ mv id\:000001\,sig\:11\,src\:000166+000222\,time\:72057\,execs\:82797\,op\:splice\,rep\:8 crash-1
$ mv id\:000002\,sig\:11\,src\:000337\,time\:123556\,execs\:136809\,op\:havoc\,rep\:16 crash-2
gdb
실행
1
$ gdb --args /root/install/bin/exif /root/out/default/crashes/crash-0
1
2
3
4
5
6
7
8
9
(gdb) r
Starting program: /root/install/bin/exif /root/out/default/crashes/crash-0
warning: Error disabling address space randomization: Operation not permitted
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Program received signal SIGSEGV, Segmentation fault.
0x000056211b1bea56 in exif_get_sshort (buf=0x56221cd189e4 <error: Cannot access memory at address 0x56221cd189e4>, order=EXIF_BYTE_ORDER_MOTOROLA) at /root/libexif/libexif/exif-utils.c:92
92 return ((buf[0] << 8) | buf[1]);
exif_get_sshort
함수에서 Segementation Fault
가 발생한 것을 확인하였다.
콜스택을 확인해보자.
1
2
3
4
5
6
7
8
(gdb) bt
#0 0x000056211b1bea56 in exif_get_sshort (buf=0x56221cd189e4 <error: Cannot access memory at address 0x56221cd189e4>, order=EXIF_BYTE_ORDER_MOTOROLA)
at /root/libexif/libexif/exif-utils.c:92
#1 exif_get_short (buf=0x56221cd189e4 <error: Cannot access memory at address 0x56221cd189e4>, order=EXIF_BYTE_ORDER_MOTOROLA)
at /root/libexif/libexif/exif-utils.c:104
#2 exif_data_load_data (data=0x56211cd179d0, d_orig=<optimized out>, ds_orig=<optimized out>) at exif-data.c:819
#3 0x000056211b1b4020 in exif_loader_get_data (loader=<optimized out>) at /root/libexif/libexif/exif-loader.c:387
#4 main (argc=<optimized out>, argv=<optimized out>) at main.c:438
exif
프로그램 내에서 이미지 파일의 exif
데이터를 불러오기 위해 libexif
의 함수를 호출한다.
1
2
3
4
5
6
7
8
/*
* Try to read EXIF data from the file.
* If there is no EXIF data, exit.
*/
l = exif_loader_new ();
exif_loader_log (l, log);
exif_loader_write_file (l, *args);
ed = exif_loader_get_data (l);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
IFD0
과 IFD1
의 offset을 가져오는 부분이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 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);
if (offset + 6 + 2 + 12 * n + 4 > ds) {
return;
}
offset = exif_get_long (d + 6 + offset + 2 + 12 * n, data->priv->order);
첫번째 offset은 TIFF
헤더 내부의 첫번째 IFD
를 가리키는 데이터를 통해 가져온다.
또한 IFD0
의 오프셋을 가지고 IFD1
을 구하는 것을 확인할 수 있다.
첫번째 오프셋에 매우 큰 값을 전달할 경우 이후 로직에서 Out of bounds
취약점이 발생하게 된다.
1
2
3
4
5
ExifShort
exif_get_short (const unsigned char *buf, ExifByteOrder order)
{
return (exif_get_sshort (buf, order) & 0xffff);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
}
해당 로직에서 허용되지 않는 영역에 shift 연산을 시도하면서 Segementation Fault
가 발생된다.
실제 EXIF
포맷을 가진 정상적인 파일에서 첫번째 IFD
의 오프셋만 변경하면 Segmentation Fault
가 발생되는 것을 확인할 수 있다.
취약점 패치
1
2
3
4
5
6
/* 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;
jpeg
의 APP1
섹션은 64KiB를 넘을 수 없으므로 길이가 아무리 크더라도 강제로 64KiB로 변경하도록 패치되었다.