Post

CVE-2012-2836(libexif) 분석

개요


EXIF (Exchangeable Image File Format) : 디지털 카메라 등에서 사용되는 이미지 파일 메타데이터 포맷

libexif : EXIF 데이터를 파싱하고, 수정하고, 저장하기 위한 라이브러리

  • CVE-2012-2836 : 0.6.21, exif-data.cexif_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 : 누락된 보조 파일 설치

0

정상적으로 설치되었다.

샘플 다운로드


1
2
3
$ wget https://github.com/ianare/exif-samples/archive/refs/heads/master.zip
$ unzip master.zip
$ mv exif-samples-master samples

해당 샘플을 타겟 프로그램에 전달해보았다.

1

afl-clang-lto


이번에는 afl-clang-fast가 아닌 afl-clang-lto를 이용하여 퍼징을 진행하라고 한다.

먼저 ltoLink Time Optimization로 약자에서 알 수 있듯이 링크타임에 최적화를 진행하는 것을 말한다.


AFL++은 컴파일 중에 블록 ID를 랜덤으로 설정한다. 따라서 계측된 위치가 많아질 수록 엣지에서의 충돌이 많아지게 된다.
이러한 방식은 새로운 path를 발견하지 못하고 퍼징의 효율은 줄어들게 된다는 문제점이 발생한다.


따라서 AFL++은 다음과 같은 솔루션을 선택하였다.

  • 모든 파일이 사전 컴파일 되었을 때 링크타임에 계측 코드가 삽입된다.
  • 링크타임에 계측 코드를 삽입하기 위해 lto모드를 이용하여 컴파일한다.
  • 시스템 링커가 아닌 afl-ld 라는 자체 링커를 사용한다.


결과로 llvm 모드의 10~25% 정도의 속도 향상을 보였으며, 충돌 없는 엣지 커버리지를 보장할 수 있게 되었다.

lto의 특성상 컴파일 타임이 더 길어질 수도 있다.

afl 공식 문서에는 다음과 같은 컴파일러 선택을 도와주는 다이어그램이 존재한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
+--------------------------------+
| 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 @@

2

크래시 분석


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;
}


IFD0IFD1의 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을 구하는 것을 확인할 수 있다.

3TIFF Header


4EXIF Data Structure


첫번째 오프셋에 매우 큰 값을 전달할 경우 이후 로직에서 Out of bounds 취약점이 발생하게 된다.

5

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가 발생되는 것을 확인할 수 있다.

6

7

8

취약점 패치


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;

jpegAPP1 섹션은 64KiB를 넘을 수 없으므로 길이가 아무리 크더라도 강제로 64KiB로 변경하도록 패치되었다.

This post is licensed under CC BY 4.0 by the author.