[Fuzzing101] Exercise 2

Sisyphus·2024년 11월 3일

Fuzzing101

목록 보기
2/7

Target

  • libexif 0.6.14
    • 이미지 파일에서 EXIF 메타데이터를 읽고 쓰는 데 사용되는 라이브러리
    • 디지털 카메라에서 촬영한 이미지 파일에 포함된 촬영 정보를 처리하는데 유용
  • CVE-2009-3895
    • exif-entry.c 파일의 exif_entry_fix 함수에서 발생한 Heap-based buffer overflow 취약점
  • CVE-2012-2836
    • exif-data.c 파일의 exif_data_load_data 함수에서 발생한 out-of-bounds read 취약점

Target Download & Build

Download

cd ~
mkdir fuzzing_libexif && cd fuzzing_libexif/
  • 작업 디렉터리 생성

wget https://github.com/libexif/libexif/archive/refs/tags/libexif-0_6_14-release.tar.gz
tar -zxvf libexif-0_6_14-release.tar.gz
  • libexif 0.6.14 다운로드

Build

cd libexif-libexif-0_6_14-release
sudo apt-get install doxygen autopoint
autoreconf -i
./configure --enable-shared=no --prefix="$HOME/fuzzing_libexif/install/"
make
make install
  • libexif 빌드하기

Test

ls
include  lib  share
❯ ls
libexif.a  libexif.la  libexif.so  libexif.so.12  libexif.so.12.1.0  pkgconfig
  • 테스트 하려고 하니까 바이너리가 없습니다.
  • 라이브러리라 라이브러리 파일만 존재합니다.
  • 퍼징을 하려면 프론트엔드가 필요할거 같습니다.

Frontend

FRONTENDS
---------

Here are a few frontends to libexif:
 - exif:     A small command-line utility to show EXIF information in JPEG
             files (https://github.com/libexif/exif).
 - gexif:    A GTK+ frontend for editing EXIF data
             (https://github.com/libexif/gexif).
 - gphoto2:  A command-line frontend to libgphoto2, a library to access a
             wide range of digital cameras (http://www.gphoto.org).
 - gtkam:    A GTK+ frontend to libgphoto2 (http://www.gphoto.org).
 - thirdeye: Digital photos organizer and driver for eComStation
             (http://ecomstation.ru/thirdeye).
 - digikam:  digital photo management application for KDE
             (https://www.digikam.org/)
  • 그냥 맨 위에 있는 exif를 다운로드해서 빌드하겠습니다.

Download exif

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

Build exif

cd exif-exif-0_6_15-release
sudo apt-get update
sudo apt-get install libexif-dev libpopt-dev
autoreconf -i
./configure --enable-shared=no --prefix="$HOME/fuzzing_libexif/install/" PKG_CONFIG_PATH="$HOME/fuzzing_libexif/install/lib/pkgconfig/"
make
make install
ls
bin  include  lib  share
❯ cd bin
❯ ls
exif
  • install 디렉터리에 가서 확인해보면 빌드된 exif 파일이 있습니다.

Testing

$HOME/fuzzing_libexif/install/bin/exif
Usage: exif [OPTION...] file
  -v, --version                   Display software version
  -i, --ids                       Show IDs instead of tag names
  -t, --tag=tag                   Select tag
      --ifd=IFD                   Select IFD
  -l, --list-tags                 List all EXIF tags
  -|, --show-mnote                Show contents of tag MakerNote
      --remove                    Remove tag or ifd
  -s, --show-description          Show description of tag
  -e, --extract-thumbnail         Extract thumbnail
  -r, --remove-thumbnail          Remove thumbnail
  -n, --insert-thumbnail=FILE     Insert FILE as thumbnail
  -o, --output=FILE               Write data to FILE
      --set-value=STRING          Value
  -m, --machine-readable          Output in a machine-readable (tab delimited) format
  -x, --xml-output                Output in a XML format
  -d, --debug                     Show debugging messages

Help options:
  -?, --help                      Show this help message
      --usage                     Display brief usage message
  • 잘 작동합니다.

Rebuild using afl-clang-lto

rm -r $HOME/fuzzing_libexif/install
cd $HOME/fuzzing_libexif/libexif-libexif-0_6_14-release
make clean
  • afl-clang-lto로 재빌드하기 위해 기존에 빌드한 버전을 삭제해줍니다.

export LLVM_CONFIG="llvm-config-11"
CC=afl-clang-lto ./configure --enable-shared=no --prefix="$HOME/fuzzing_libexif/install/"
make
make install
  • afl-clang-lto를 이용해서 재빌드 해줍니다.

cd ../exif-exif-0_6_15-release
make clean
CC=afl-clang-lto ./configure --enable-shared=no --prefix="$HOME/fuzzing_libexif/install/" PKG_CONFIG_PATH=$HOME/fuzzing_libexif/install/lib/pkgconfig
make
make install
  • exif도 다시 빌드해줍니다.

libexif

$HOME/fuzzing101/fuzzing_libexif/install/bin/exif $HOME/fuzzing101/fuzzing_libexif/exif-samples/jpg/Canon_40D_photoshop_import.jpg
EXIF tags in '/home/ion/fuzzing101/fuzzing_libexif/exif-samples/jpg/Canon_40D_photoshop_import.jpg' ('Motorola' byte order):
--------------------+----------------------------------------------------------
Tag                 |Value
--------------------+----------------------------------------------------------
Orientation         |top - left
x-Resolution        |300.00
y-Resolution        |300.00
Resolution Unit     |Inch
Software            |GIMP 2.4.5
Date and Time       |2008:07:31 10:05:49
Compression         |JPEG compression
x-Resolution        |72.00
y-Resolution        |72.00
Resolution Unit     |Inch
Color Space         |sRGB
PixelXDimension     |100
PixelYDimension     |77
--------------------+----------------------------------------------------------
  • 이미지 파일에서 메타데이터를 추출하는 소프트웨어 인거 같습니다.
  • 이를 위해 이미지 파일을 파싱하여 메타 데이터를 추출하고 이걸 가져오는 작업이 수행될거 같습니다.

Fuzzing

Sample file download

cd ..
git clone https://github.com/ianare/exif-samples.git
  • 퍼징에 사용할 샘플 파일을 다운로드 해줍니다.

Fuzzing

afl-fuzz -D -i $HOME/fuzzing_libexif/exif-samples/ -o $HOME/fuzzing_libexif/out/ -s 123 -- $HOME/fuzzing_libexif/install/bin/exif @@ $HOME/fuzzing_libexif/output

  • 퍼징을 돌리는데 AFL++ 4.22a 버전에서는 -D 옵션을 줘야 fuzzing strategy가 활성화 되기 때문에, -D 옵션을 줘서 실행시켜줍니다.

Root Cause Analysis - out-of-bound read

Debugging

❯ gdb-gef --args ~/fuzzing101/fuzzing_libexif/install/bin/exif ./crash0
gef➤  r
Program received signal SIGSEGV, Segmentation fault.
0x000055555557c7b0 in exif_get_sshort (buf=0x5556557adb95 <error: Cannot access memory at address 0x5556557adb95>, order=EXIF_BYTE_ORDER_MOTOROLA) at /root/fuzzing_libexif/libexif-libexif-0_6_14-release/libexif/exif-utils.c:92
92      /root/fuzzing_libexif/libexif-libexif-0_6_14-release/libexif/exif-utils.c: No such file or directory.
[ Legend: Modified register | Code | Heap | Stack | String ]
─────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x5556557adb95
$rbx   : 0x00005555557acb80  →  0x00005555557acc10  →  0x0000000000000000
$rcx   : 0x5556557adb96
$rdx   : 0x5556557adb95
$rsp   : 0x00007fffffffd120  →  0x0000000000000000
$rbp   : 0x00005555557acbb8  →  0x00005555557acbd0  →  0x0000000000000000
$rsi   : 0x0
$rdi   : 0x00005555557acb80  →  0x00005555557acc10  →  0x0000000000000000
$rip   : 0x000055555557c7b0  →  <exif_data_load_data+0690> movzx edx, BYTE PTR [rdx]
$r8    : 0xffffffff
$r9    : 0x0
$r10   : 0x00005555557adb80  →  0x0000000000000df0
$r11   : 0x00007ffff7eacce0  →  0x00005555557ae010  →  0x0000000010004300
$r12   : 0x00005555557acbd0  →  0x0000000000000000
$r13   : 0xffffffff
$r14   : 0x00005555557adb90  →  0x4d4d000066697845 ("Exif"?)
$r15   : 0x484
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
─────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffd120│+0x0000: 0x0000000000000000   ← $rsp
0x00007fffffffd128│+0x0008: 0x00005555557adb96  →  0xffffffff2a004d4d ("MM"?)
0x00007fffffffd130│+0x0010: 0x0000047e5556085a
0x00007fffffffd138│+0x0018: 0x00005555555a34d0  →  0x00005555555a6920  →  <__afl_area_initial+0000> add BYTE PTR [rax], al
0x00007fffffffd140│+0x0020: 0x00005555557acb80  →  0x00005555557acc10  →  0x0000000000000000
0x00007fffffffd148│+0x0028: 0x00005555557ac130  →  0x0000000000000002
0x00007fffffffd150│+0x0030: 0x00005555557acb30  →  0x0000000100000006
0x00007fffffffd158│+0x0038: 0x000000000000009c
───────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x55555557c7a3 <exif_data_load_data+0683> mov    BYTE PTR [rcx+0x446], dl
   0x55555557c7a9 <exif_data_load_data+0689> lea    rcx, [rax+0x1]
   0x55555557c7ad <exif_data_load_data+068d> mov    rdx, rax
 → 0x55555557c7b0 <exif_data_load_data+0690> movzx  edx, BYTE PTR [rdx]
   0x55555557c7b3 <exif_data_load_data+0693> movzx  ecx, BYTE PTR [rcx]
   0x55555557c7b6 <exif_data_load_data+0696> shl    edx, 0xa
   0x55555557c7b9 <exif_data_load_data+0699> lea    ecx, [rdx+rcx*4]
   0x55555557c7bc <exif_data_load_data+069c> lea    ecx, [rcx+rcx*2]
   0x55555557c7bf <exif_data_load_data+069f> jmp    0x55555557c7d9 <exif_data_load_data+1721>
───────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "exif", stopped 0x55555557c7b0 in exif_get_sshort (), reason: SIGSEGV
─────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x55555557c7b0 → exif_get_sshort(buf=0x5556557adb95 <error: Cannot access memory at address 0x5556557adb95>, order=EXIF_BYTE_ORDER_MOTOROLA)
[#1] 0x55555557c7b0 → exif_get_short(buf=0x5556557adb95 <error: Cannot access memory at address 0x5556557adb95>, order=EXIF_BYTE_ORDER_MOTOROLA)
[#2] 0x55555557c7b0 → exif_data_load_data(data=0x5555557acb80, d_orig=<optimized out>, ds_orig=<optimized out>)
[#3] 0x555555572a39 → exif_loader_get_data(loader=0x0)
[#4] 0x555555572a39 → main(argc=<optimized out>, argv=<optimized out>)
──────────────────────────────────────────────────────────────────────────────────────────────────────────────
  • SIGSEGV 가 발생했습니다.
  • trace를 보니 취약점이 발생했다고 하는 exif_data_load_data 함수가 존재합니다.
  • main 함수부터 올라가면서 봐보겠습니다.

main

	if (args) {
		while (*args) {
			ExifLoader *l;

			/*
			 * 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);
			exif_loader_unref (l);
			if (!ed || ! (ed->data || ed->size ||
						ed->ifd[EXIF_IFD_0]->count ||
						ed->ifd[EXIF_IFD_1]->count ||
						ed->ifd[EXIF_IFD_EXIF]->count ||
						ed->ifd[EXIF_IFD_GPS]->count ||
						ed->ifd[EXIF_IFD_INTEROPERABILITY]->count))
				exif_log (log, -1, "exif", _("'%s' does not "
							"contain EXIF data!"), *args);
  • 파일에서 EXIF 데이터를 읽어오고 만약 EXIF 데이터가 없다면 종료하는 코드입니다.
  • exif_loader_new 함수를 통해 l 에 EXIF 데이터를 읽어오기 위한 EXIF 로더를 로드합니다.
  • exif_loader_log 함수를 통해 logger를 붙입니다.
  • exif_loader_write_file 함수를 통해 인자로 들어온 이미지 파일의 데이터를 로더에 씁니다.
  • exif_loader_get_data 함수를 통해 EXIF 구조체에 EXIF 데이터를 얻어와 저장합니다.

exif_loader_get_data

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 데이터를 얻어와 반환하는 함수입니다.
  • exif_data_new_mem 함수를 통해 로더에 메모리를 할당합니다.
  • exif_data_log 함수를 통해 로더에 로거를 붙입니다.
  • exif_data_load_data 함수를 통해 로더의 buf 에 있던 데이터를 ExifData 변수인 ed 에 저장합니다.
  • ExifData를 반환합니다.

exif_data_load_data

exif_data_load_data (ExifData *data, const unsigned char *d_orig,
		     unsigned int ds_orig)
{
	unsigned int l;
	ExifLong offset;
	ExifShort n;
	const unsigned char *d = d_orig;
	unsigned int ds = ds_orig, len;

	if (!data || !data->priv || !d || !ds) 
		return;

	exif_log (data->priv->log, EXIF_LOG_CODE_DEBUG, "ExifData",
		  "Parsing %i byte(s) EXIF data...\n", ds);
  • 변수 선언, 인자 값 체크 등을 통해 EXIF 데이터를 전달하기 위한 준비를 합니다.

	/*
	 * 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;
	}
	.
	.
	.
  • EXIF 헤더를 찾는 부분인거 같은데 넘어가도 될거 같습니다.

	/*
	 * Verify the exif header
	 * (offset 2, length 6).
	 */
	if (ds < 6) {
		LOG_TOO_SMALL;
		return;
	}
	.
	.
  • 헤더를 검증하는 과정인데 넘어가도 될거 같습니다.

	/* Byte order (offset 6, length 2) */
	if (ds < 14)
		return;
	if (!memcmp (d + 6, "II", 2))
		data->priv->order = EXIF_BYTE_ORDER_INTEL;
	else if (!memcmp (d + 6, "MM", 2))
		data->priv->order = EXIF_BYTE_ORDER_MOTOROLA;
	else {
		exif_log (data->priv->log, EXIF_LOG_CODE_CORRUPT_DATA,
			  "ExifData", _("Unknown encoding."));
		return;
	}
  • Byte order를 확인하는 부분입니다.
  • Byte order는 Exif.. 다음에 나오기 6 바이트를 더한 뒤 2 바이트를 살펴봅니다.
  • 원래 우분트에서는 리틀 엔디안을 사용하지만, 순수 JPEG 이미지의 바이너리는 빅엔디안인 Motorala 방식을 사용하기 대문에, 제대로 된 파싱을 위해 Byte order를 확인해야 합니다.

	/* 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);
  • EXIF 데이터를 파싱하는 부분입니다.
  • ExifData 에서 offset 10만큼 이동해서 IFD 0 offset을 찾아냅니다.
  • EXIF는 IFD 0에 이미지 정보가 저장되어 있기 때문에, IFD 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);
	if (offset) {
		exif_log (data->priv->log, EXIF_LOG_CODE_DEBUG, "ExifData",
			  "IFD 1 at %i.", (int) offset);

		/* Sanity check. */
		if (offset > ds - 6) {
			exif_log (data->priv->log, EXIF_LOG_CODE_CORRUPT_DATA,
				  "ExifData", "Bogus offset.");
			return;
		}
  • IFD 1 offset을 읽어오는 함수입니다. IFD 1 에는 썸네일 이미지 정보가 존재합니다.
  • exif_get_short 함수에서 문제가 발생했습니다.

exif_get_short

ExifShort
exif_get_short (const unsigned char *buf, ExifByteOrder order)
{
	return (exif_get_sshort (buf, order) & 0xffff);
}
  • buf에서 바이트 오더에 따라 2바이트의 짧은 정수를 읽어옵니다.
  • 여기서 값을 읽을 때 잘못된 인자값이 들어와서 out-of-bound read 취약점이 발생한거 같습니다.
  • 취약점 발생 원인 파악을 위해 인자값 살펴보겠습니다.

byteorder 확인

  • Byteorder는 MOTOROLA로 알맞게 들어왔습니다.

offset 확인

  • offset 값을 확인해보면 0xffffffff 로 이상한 값이 들어와 있습니다.
  • offset 으로 인해 out-of-bound read 취약점이 발생했습니다.

offset 값 추적

다시 위로 올라가서 왜 offset 값에 이상한 값이 들어가게 되었는지 확인해보겠습니다.

	/* 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);
  • IFD 0 offset 값을 구하기 위해 Exif 시작 지점에서 부터 +10 위치의 값을 읽어 온 후 offset 에 저장합니다.

crash 파일에서 이 위치에 무슨 값이 있는지 확인해보면

  • 0xFFFF 값이 들어있습니다.

정상 파일에서는 무슨 값이 있는지 확인을 해보면

  • 0x0008 이 들어 있습니다.

➡️ 한눈에 봐도 크래쉬 파일에 들어있는 오프셋 값이 비정상적으로 보입니다. 그래서 해당 함수에서 offset 값을 얻어오고 해당 값을 기준으로 데이터를 읽을 때, offset 이 전체 EXIF 데이터 크기인 ds 보다 큰지 검증을 하지만 offset 값에 0xffff 가 들어오면서 정수형 오버플로우가 발생하여 값이 -1 이 되었습니다. 그래서 검증 절차를 통과하여 그대로 실행이 되어 out-of-bound read 취약점이 발생했습니다.


Patch - Out-of-bound Read

Patch

offset 값이 정수형 오버플로우가 발생하지 않도록 타입을 변경해주면 됩니다.

// exif-data.c
ExifLong offset;
// exif-utils.h
typedef uint64_t	ExifLong;          /* 4 bytes */
  • 원래 uint32_t 타입이었는데, 좀 더 큰 범위까지 커버 할 수 있도록 uint64_t 로 타입을 변경하였습니다.

Testing

❯ gdb-gef --args ~/fuzzing_libexif/install/bin/exif crash0
Reading symbols from /root/fuzzing_libexif/install/bin/exif...
Error while writing index for `/root/fuzzing_libexif/install/bin/exif': mkstemp: No such file or directory.
GEF for linux ready, type `gef' to start, `gef config' to configure
93 commands loaded and 5 functions added for GDB 12.1 in 0.00ms using Python engine 3.10
gef➤  r
Starting program: /root/fuzzing_libexif/install/bin/exif crash0
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
'crash0' does not contain EXIF data!
[Inferior 1 (process 12744) exited with code 01]
  • crash 파일을 넣어서 실험해보면 SIGSEGV 가 아닌 에러 처리로 인한 문자열이 출력되며 정상 종료되었습니다.

Root Cause Analysis - Heap-based buffer overflow

Debugging

Program received signal SIGSEGV, Segmentation fault.
0x0000555555578ed3 in exif_get_slong (b=<optimized out>, order=EXIF_BYTE_ORDER_INTEL) at /root/fuzzing_libexif/libexif-libexif-0_6_14-release/libexif/exif-utils.c:135
135     /root/fuzzing_libexif/libexif-libexif-0_6_14-release/libexif/exif-utils.c: No such file or directory.
[ Legend: Modified register | Code | Heap | Stack | String ]
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x1
$rbx   : 0x0
$rcx   : 0xd308
$rdx   : 0x6985
$rsp   : 0x00007fffffffd0d0  →  0x000000000000039a
$rbp   : 0x80000001
$rsi   : 0x1a610
$rdi   : 0x00005555557ae9f0  →  0x0000000000000000
$rip   : 0x0000555555578ed3  →  <exif_content_fix+0603> movzx r9d, BYTE PTR [r9]
$r8    : 0x5555557c9000
$r9    : 0x5555557c9001
$r10   : 0xee
$r11   : 0xbfb9ea25ec6404aa
$r12   : 0x00005555555a34d0  →  0x00005555555a6920  →  <__afl_area_initial+0000> add BYTE PTR [rax], al
$r13   : 0x00005555557ae990  →  0x000000040000a408
$r14   : 0xb
$r15   : 0x400
$eflags: [zero carry PARITY adjust SIGN trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffd0d0│+0x0000: 0x000000000000039a   ← $rsp
0x00007fffffffd0d8│+0x0008: 0x00005555557accb0  →  0x00005555557aea10  →  0x5555e5e05555e580
0x00007fffffffd0e0│+0x0010: 0x0000000000000701
0x00007fffffffd0e8│+0x0018: 0x0000555500000004
0x00007fffffffd0f0│+0x0020: 0x0000555500000002
0x00007fffffffd0f8│+0x0028: 0x00005555557acb80  →  0x00005555557acc10  →  0x00005555557ad290  →  0x00005555557acda0  →  0x000000020000010f
0x00007fffffffd100│+0x0030: 0x00005555557acb80  →  0x00005555557acc10  →  0x00005555557ad290  →  0x00005555557acda0  →  0x000000020000010f
0x00007fffffffd108│+0x0038: 0x00005555557ac130  →  0x0000000000000002
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x555555578ec4 <exif_content_fix+05f4> mov    BYTE PTR [r9+0x323], r10b
   0x555555578ecb <exif_content_fix+05fb> lea    r9, [r8+0x2]
   0x555555578ecf <exif_content_fix+05ff> add    r8, 0x3
 → 0x555555578ed3 <exif_content_fix+0603> movzx  r9d, BYTE PTR [r9]
   0x555555578ed7 <exif_content_fix+0607> shl    r9d, 0x8
   0x555555578edb <exif_content_fix+060b> movzx  ebx, BYTE PTR [r8]
   0x555555578edf <exif_content_fix+060f> or     ebx, r9d
   0x555555578ee2 <exif_content_fix+0612> test   eax, eax
   0x555555578ee4 <exif_content_fix+0614> je     0x555555578f41 <exif_content_fix+1649>
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "exif", stopped 0x555555578ed3 in exif_get_slong (), reason: SIGSEGV
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x555555578ed3 → exif_get_slong(b=<optimized out>, order=EXIF_BYTE_ORDER_INTEL)
[#1] 0x555555578ed3 → exif_get_long(buf=<optimized out>, order=EXIF_BYTE_ORDER_INTEL)
[#2] 0x555555578ed3 → exif_entry_fix(e=0x5555557ae990)
[#3] 0x555555578ed3 → fix_func(e=0x5555557ae990, data=0x0)
[#4] 0x555555578ed3 → exif_content_foreach_entry(content=0x5555557accb0, data=0x0, func=<optimized out>)
[#5] 0x555555578ed3 → exif_content_fix(c=0x5555557accb0)
[#6] 0x55555557eb9b → exif_data_foreach_content(data=0x5555557acb80, user_data=0x0, func=<optimized out>)
[#7] 0x55555557eb9b → exif_data_fix(d=0x5555557acb80)
[#8] 0x555555572a39 → exif_loader_get_data(loader=0x0)
[#9] 0x555555572a39 → main(argc=<optimized out>, argv=<optimized out>)
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
  • SIGSEGV 가 발생했습니다.
  • 취약점이 발생한 exif_entry_fix 함수가 보입니다.
  • 앞에서 분석하지 않은 부분부터 이어서 분석해보겠습니다.

exif_data_load_data

	/*
	 * If we got an EXIF_TAG_MAKER_NOTE, try to interpret it. Some
	 * cameras use pointers in the maker note tag that point to the
	 * space between IFDs. Here is the only place where we have access
	 * to that data.
	 */
	switch (exif_data_get_type_maker_note (data)) {
	case EXIF_DATA_TYPE_MAKER_NOTE_OLYMPUS:
		data->priv->md = exif_mnote_data_olympus_new (data->priv->mem);
		break;
	case EXIF_DATA_TYPE_MAKER_NOTE_PENTAX:
		data->priv->md = exif_mnote_data_pentax_new (data->priv->mem);
		break;
	case EXIF_DATA_TYPE_MAKER_NOTE_CANON:
		data->priv->md = exif_mnote_data_canon_new (data->priv->mem, data->priv->options);
		break;
	default:
		break;
	}
  • 제조사 노트를 해석하려고 시도하는 부분입니다. 일부 카메라에서는 제조사 노트 태그가 IFD 안의 공간을 가리키는 포인터를 사용할 수 있습니다. 이곳에서 해당 데이터에 접근할 수 있습니다.
  • swith 문을 돌며 각 제조사에 따라 적절한 함수를 호출하여 제조사 노트 데이터를 초기화합니다.

	/* 
	 * If we are able to interpret the maker note, do so.
	 */
	if (data->priv->md) {
		exif_mnote_data_log (data->priv->md, data->priv->log);
		exif_mnote_data_set_byte_order (data->priv->md,
						data->priv->order);
		exif_mnote_data_set_offset (data->priv->md,
					    data->priv->offset_mnote);
		exif_mnote_data_load (data->priv->md, d, ds);
	}
  • 제조사 노트를 성공적으로 해석한 경우
    • exif_mnote_data_log: 제조사 노트 데이터를 로그에 기록합니다.
    • exif_mnote_data_set_byte_order: 바이트 오더를 설정합니다.
    • exif_mnote_data_set_offset: 제조사 노트의 오프셋을 설정합니다.
    • exif_mnote_data_load: 데이터를 메모리에서 로드합니다.

	if (data->priv->options & EXIF_DATA_OPTION_FOLLOW_SPECIFICATION)
		exif_data_fix (data);
  • EXIF_DATA_OPTION_FOLLOW_SPECIFICATION 옵션이 설정되어 있으면 EXIF 데이터를 수정하는 함수를 호출합니다.
  • EXIF 사양에 따라 데이터를 정리하는 작업입니다.

exif_data_fix

void
exif_data_fix (ExifData *d)
{
	exif_data_foreach_content (d, fix_func, NULL);
}
  • exif_data_foreach_content 함수를 호출합니다.

exif_data_foreach_content

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

typedef struct _ByteOrderChangeData ByteOrderChangeData;
struct _ByteOrderChangeData {
	ExifByteOrder old, new;
};
  • EXIF_IFD_COUNT 만큼 각 IFD에 대해 exif_content_fix 함수 호출을 반복합니다.
  • 바이트 오더 변경을 위한 데이터를 저장하는 구조체를 선언합니다.

exif_content_fix

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);
  • c 가 NULL인 경우 함수를 종료합니다.
  • exif_data_get_data_type 을 호출하여 IFD를 가져오고, 데이터 타입을 결정합니다.

	/* First of all, fix all existing entries. */
	exif_content_foreach_entry (c, fix_func, NULL);
  • exif_content_foreach_entry 함수를 호출하여 모든 기존 항목을 수정하는 fix_func 함수를 적용합니다.

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);
}
  • content 또는 func 가 NULL 이면 함수를 종료합니다.
  • content->count 만큼 반복하여 func 함수를 호출합니다. 여기서는 fix_func 를 호출합니다.

fix_func

static void
fix_func (ExifEntry *e, void *data)
{
	exif_entry_fix (e);
}
  • exif_entry_fix 함수를 호출합니다.

exif_entry_fix

void
exif_entry_fix (ExifEntry *e)
{
	unsigned int i;
	ExifByteOrder o;
	ExifRational r;
	ExifSRational sr;

	if (!e || !e->priv) return;
  • e 또는 e->priv 값이 NULL이면 함수를 종료합니다.

	switch (e->tag) {
	
	/* These tags all need to be of format SHORT. */
	case EXIF_TAG_YCBCR_SUB_SAMPLING:
	case EXIF_TAG_SUBJECT_AREA:
	case EXIF_TAG_COLOR_SPACE:
	case EXIF_TAG_PLANAR_CONFIGURATION:
	case EXIF_TAG_SENSING_METHOD:
	case EXIF_TAG_ORIENTATION:
	case EXIF_TAG_YCBCR_POSITIONING:
	case EXIF_TAG_PHOTOMETRIC_INTERPRETATION:
	case EXIF_TAG_CUSTOM_RENDERED:
	case EXIF_TAG_EXPOSURE_MODE:
	case EXIF_TAG_WHITE_BALANCE:
	case EXIF_TAG_SCENE_CAPTURE_TYPE:
	case EXIF_TAG_GAIN_CONTROL:
	case EXIF_TAG_SATURATION:
	case EXIF_TAG_CONTRAST:
	case EXIF_TAG_SHARPNESS:
  • e->tag 에 따라 특정 태그로 분기합니다.

switch (e->format) {
case EXIF_FORMAT_LONG:
    if (!e->parent || !e->parent->parent) break;
    o = exif_data_get_byte_order(e->parent->parent);
  • EXIF 형식이 LONG인 경우에 부모 데이터에서 바이트 오더를 가져옵니다.

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));
  • LONG 형식의 데이터를 SHORT 형식으로 변환합니다.

e->format = EXIF_FORMAT_SHORT;
e->size = e->components * exif_format_get_size(e->format);
e->data = exif_entry_realloc(e, e->data, e->size);
  • 엔트리 형식을 SHROT으로 변경하고, 크기를 재설정합니다.
  • 메모리 재할당을 통해 새로운 데이터 크기를 맞춥니다.

exif_entry_log(e, EXIF_LOG_CODE_DEBUG,
    _("Tag '%s' was of format '%s' (which is "
    "against specification) and has been "
    "changed to format '%s'."),
    exif_tag_get_name(e->tag), 
    exif_format_get_name(EXIF_FORMAT_LONG),
    exif_format_get_name(EXIF_FORMAT_SHORT));
  • 형식이 변경되었음을 로그에 기록합니다.

bug trigger context

코드만 봐서는 감이 잘 오지 않습니다. 크래쉬 발생 상황에서의 context를 봐보겠습니다.

  • 일단 반복 횟수인 e->components2147483649 입니다.
  • 그리고 버그가 발생한 인덱스 i27020 입니다.
  • 아마 e->components 값이 비정상적으로 커서 ExifEntry 범위를 넘어선 곳까지 접근하여 값을 조작해서 Heap overflow가 발생한 것이 아닌가 하는 의심이듭니다.

검증을 위해 최적화를 해제하고 다시 빌드한 이후 변환하려는 값의 주소를 분석해보겠습니다.

rm -r $HOME/fuzzing_libexif/install
cd $HOME/fuzzing_libexif/libexif-libexif-0_6_14-release
make clean
  • 재빌드하기 위해 기존에 빌드한 버전을 삭제해줍니다.

export LLVM_CONFIG="llvm-config-11"
CC=afl-clang-lto CFLAGS="-O0 -g" ./configure --enable-shared=no --prefix="$HOME/fuzzing_libexif/test/"
make
make install
  • CFLAGS="-O0 -g" 옵션을 통해 최적화를 해제하고 빌드해줍니다.

cd ../exif-exif-0_6_15-release
make clean
CC=afl-clang-lto CFLAGS="-O0 -g"  ./configure --enable-shared=no --prefix="$HOME/fuzzing_libexif/install/" PKG_CONFIG_PATH=$HOME/fuzzing_libexif/test/lib/pkgconfig
make
make install
  • exif도 다시 빌드해줍니다.

다시 디버깅 context를 보면

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x55555558806b <exif_get_slong+00cb> add    cl, dl
   0x55555558806d <exif_get_slong+00cd> mov    BYTE PTR [rax+0x51c], cl
   0x555555588073 <exif_get_slong+00d3> mov    rax, QWORD PTR [rbp-0x8]0x555555588077 <exif_get_slong+00d7> movzx  eax, BYTE PTR [rax+0x3]
   0x55555558807b <exif_get_slong+00db> shl    eax, 0x18
   0x55555558807e <exif_get_slong+00de> mov    rcx, QWORD PTR [rbp-0x8]
   0x555555588082 <exif_get_slong+00e2> movzx  ecx, BYTE PTR [rcx+0x2]
   0x555555588086 <exif_get_slong+00e6> shl    ecx, 0x10
   0x555555588089 <exif_get_slong+00e9> or     eax, ecx
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "exif", stopped 0x555555588077 in exif_get_slong (), reason: SIGSEGV
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x555555588077exif_get_slong(b=0x5555557c4000 <error: Cannot access memory at address 0x5555557c4000>, order=EXIF_BYTE_ORDER_INTEL)
[#1] 0x555555587dbfexif_get_long(buf=0x5555557c4000 <error: Cannot access memory at address 0x5555557c4000>, order=EXIF_BYTE_ORDER_INTEL)
[#2] 0x55555557b82eexif_entry_fix(e=0x5555557a8380)
[#3] 0x55555557640dfix_func(e=0x5555557a8380, data=0x0)
[#4] 0x555555575d42exif_content_foreach_entry(content=0x5555557a5840, func=0x5555555763d0 <fix_func>, data=0x0)
[#5] 0x55555557614dexif_content_fix(c=0x5555557a5840)
[#6] 0x555555578be2fix_func(c=0x5555557a5840, data=0x0)
[#7] 0x555555578ca6exif_data_foreach_content(data=0x5555557a5710, func=0x555555578ab0 <fix_func>, user_data=0x0)
[#8] 0x555555578aa2exif_data_fix(d=0x5555557a5710)
[#9] 0x55555557780cexif_data_load_data(data=0x5555557a5710, d_orig=0x5555557a78e0 "Exif", ds_orig=0x7e5)
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤  vmmap 0x5555557c4000
[ Legend:  Code | Stack | Heap ]
Start              End                Offset             Perm Path
  • 예상대로 변환하려는 값이 정상적인 메모리 범위를 벗어난 값입니다.
  • e->components 에 비정상적으로 큰 값이 파싱되고 이후 해당 값을 기준으로 메모리에 접근하여 값을 조작하려고 하여 Heap Overflow가 발생하였습니다.

Patch - Heap Overflow

Patch

e->components 의 값을 검사하는 코드를 추가하면 됩니다.

#define MAX_COMPONENTS 100

case EXIF_FORMAT_LONG:
			if (!e->parent || !e->parent->parent) break;
			if (e->components > MAX_COMPONENTS) break;
			o = exif_data_get_byte_order (e->parent->parent);
			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));
  • e->components 최대 값을 100 정도로 제한하겠습니다.

Testing

❯ gdb-gef --args ~/fuzzing_libexif/install/bin/exif crash30
Reading symbols from /root/fuzzing_libexif/install/bin/exif...
Error while writing index for `/root/fuzzing_libexif/install/bin/exif': mkstemp: No such file or directory.
GEF for linux ready, type `gef' to start, `gef config' to configure
93 commands loaded and 5 functions added for GDB 12.1 in 0.00ms using Python engine 3.10
gef➤  r
Starting program: /root/fuzzing_libexif/install/bin/exif crash30
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
EXIF tags in 'crash30' ('Intel' byte order):
--------------------+----------------------------------------------------------
Tag                 |Value
--------------------+----------------------------------------------------------
Manufacturer        |OLYMPUS CoR
Orientation         |top - left
x-Resolution        |72.00
y-Resolution        |72.00
Resolution Unit     |Inch
Software            |GIMP 2.4.5
Date and Time       |2008:07:31 13:03:
Artist              |
YCbCr Positioning   |centered
Exif Version        |Exif Version 2.1
Date and Time (origi|
ISO Speed Ratings   |50
Corrupt data (ExifEntry):
The tag 'FileSource' contains an invalid number of components (64, expected 1).
File Source         |[Inferior 1 (process 31524) exited with code 01]
  • 더 이상 Heap Overflow로 인한 SIGSEGV가 발생하지 않습니다.

0개의 댓글