Exercise 2: libexif

  任务目标是获得 CVE-2009-3895 以及 CVE-2012-2836 两个漏洞。前者是一个堆溢出;后者是一个越界读取,可以实现 DoS,也可能导致信息泄露。

0x01 环境准备

  首先获取 libexif 0.6.14 的源码。在 Github 可以下载到:

Release libexif-0_6_14-release · libexif/libexif
properly update so name for API/ABI addition

  readme 中提到,libexif 自带有 exif 这个简单的前端程序(CLI),展示 JPEG 图片的 EXIF 信息。我们将其定为 fuzz 目标。

  编译 libexif:

apt install automake libtool autopoint gettext  # 重点是 autopoint
autoreconf -i
CC=/afl/afl-clang-lto ./configure --prefix=/work/out/
make
make install # 安装到 /work/out

  编译前端程序 exif:

git clone https://github.com/libexif/exif.git
cd exif
git reset --hard dcf592e6abfcb70f365e92211ebf80ceafe73e5a
# 回滚到 2007 年 5 月 17 日的一次 commit,以适配 libexif 版本
autoreconf -i

apt install libpopt-dev
CC=/afl/afl-clang-lto ./configure --prefix=/work/bin PKG_CONFIG_PATH=/work/out/lib/pkgconfig
make
make install # 安装到 /work/bin

0x02 生成 seed corpus

  下一步,我们需要寻找一些 jpg 文件,作为种子语料库(seed corpus)。笔者使用了两张文件。第一张来自 wikimedia,第二张是笔者用 GIMP 画的。

fuzzer :: /work/bin/bin » ./exif /work/inputs/cat.jpg
EXIF tags in '/work/inputs/cat.jpg' ('Motorola' byte order):
--------------------+----------------------------------------------------------
Tag                 |Value                                                     
--------------------+----------------------------------------------------------
Orientation         |top - left                                                
x-Resolution        |72.00                                                     
y-Resolution        |72.00                                                     
Resolution Unit     |Inch                                                      
Software            |Adobe Photoshop CS4 Windows                               
Date and Time       |2009:01:31 22:25:45                                       
Compression         |JPEG compression                                          
x-Resolution        |72.00                                                     
y-Resolution        |72.00                                                     
Resolution Unit     |Inch                                                      
Color Space         |Uncalibrated                                              
PixelXDimension     |1600                                                      
PixelYDimension     |1598                                                      
--------------------+----------------------------------------------------------
EXIF data contains a thumbnail (5620 bytes).

fuzzer :: /work/bin/bin » ./exif /work/inputs/abc.jpg
EXIF tags in '/work/inputs/abc.jpg' ('Intel' byte order):
--------------------+----------------------------------------------------------
Tag                 |Value                                                     
--------------------+----------------------------------------------------------
Image Description   |Created with GIMP                                         
x-Resolution        |300.00                                                    
y-Resolution        |300.00                                                    
Resolution Unit     |Inch                                                      
Software            |GIMP 2.10.30                                              
Date and Time       |2022:10:06 17:08:32                                       
New Subfile Type    |1                                                         
Image Width         |256                                                       
Image Length        |256                                                       
Bits per Sample     |8, 8, 8                                                   
Compression         |JPEG compression                                          
Photometric Interpre|YCbCr                                                     
Samples per Pixel   |3                                                         
User Comment        |Created with GIMP                                         
Color Space         |sRGB                                                      
Altitude            |0.00                                                      
--------------------+----------------------------------------------------------
EXIF data contains a thumbnail (4196 bytes).

0x03 fuzz

  这次 fuzz 的输入文件比较大,我们设置 AFL_TEMPDIR 以节省 SSD 寿命。

AFL_TMPDIR=/tmp /afl/afl-fuzz -i /work/inputs -o out -s 123 -- ./exif @@

  运行大约 1h 之后,获得 5 个 crash 和 1 个 hang。注意到 stablity 指标为 100%,说明这些 bug 可以稳定复现。

0x04 复现和修复

  首先在 -O0 -g 模式下编译:

# 对于 libexif
autoreconf -i
CC=gcc-12 CFLAGS="-O0 -g" ./configure --prefix=/work/build/libexif
make
make install

# 对于 exif
autoreconf -i
CC=gcc-12 CFLAGS="-O0 -g" ./configure PKG_CONFIG_PATH=/work/build/libexif/lib/pkgconfig --prefix=/work/build/exif
make
make install

  先在服务器上复现一下:

  然而,在本机复现时,这个 hang 样例的结果是 segmentation fault。修改编译参数为 -O2-O3 或去掉 -g,或者把编译器改成 clang,结果都仍然是 segmation fault。笔者选择在 debug 时再考虑这个问题。

crash 样例

  首先,我们看一下各个样例的 crash 现场。

// crash0
* thread #1, name = 'exif', stop reason = signal SIGSEGV: invalid address (fault address: 0x555555581a24)
  * frame #0: 0x00007ffff7ed7e90 libc.so.6`__memcpy_avx_unaligned_erms at memmove-vec-unaligned-erms.S:872
    frame #1: 0x00007ffff7f8f3b1 libexif.so.12`exif_data_load_data_thumbnail(data=0x0000555555561fa0, d="MM", ds=382, offset=302, size=4294967295) at exif-data.c:292:2
    frame #2: 0x00007ffff7f8f8d0 libexif.so.12`exif_data_load_data_content(data=0x0000555555561fa0, ifd=EXIF_IFD_1, d="MM", ds=382, offset=210, recursion_depth=0) at exif-data.c:381:6
    frame #3: 0x00007ffff7f90e98 libexif.so.12`exif_data_load_data(data=0x0000555555561fa0, d_orig="Exif", ds_orig=388) at exif-data.c:835:3
    frame #4: 0x00007ffff7f98829 libexif.so.12`exif_loader_get_data(loader=0x0000555555561f50) at exif-loader.c:387:2
    frame #5: 0x0000555555559eac exif`main(argc=2, argv=0x00007fffffffdf38) at main.c:438:9
    
// crash1
* thread #1, name = 'exif', stop reason = signal SIGSEGV: invalid address (fault address: 0x5556555648c3)
  * frame #0: 0x00007ffff7f99e0e libexif.so.12`exif_get_sshort(buf="", order=EXIF_BYTE_ORDER_MOTOROLA) at exif-utils.c:92:29
    frame #1: 0x00007ffff7f99e77 libexif.so.12`exif_get_short(buf="", order=EXIF_BYTE_ORDER_MOTOROLA) at exif-utils.c:104:10
    frame #2: 0x00007ffff7f90d94 libexif.so.12`exif_data_load_data(data=0x0000555555561fa0, d_orig="Exif", ds_orig=379) at exif-data.c:819:6
    frame #3: 0x00007ffff7f98829 libexif.so.12`exif_loader_get_data(loader=0x0000555555561f50) at exif-loader.c:387:2
    frame #4: 0x0000555555559eac exif`main(argc=2, argv=0x00007fffffffdf38) at main.c:438:9

// crash 2
* thread #1, name = 'exif', stop reason = signal SIGSEGV: invalid address (fault address: 0x5556555648c3)
  * frame #0: 0x00007ffff7f99e33 libexif.so.12`exif_get_sshort(buf="", order=EXIF_BYTE_ORDER_INTEL) at exif-utils.c:94:29
    frame #1: 0x00007ffff7f99e77 libexif.so.12`exif_get_short(buf="", order=EXIF_BYTE_ORDER_INTEL) at exif-utils.c:104:10
    frame #2: 0x00007ffff7f90d94 libexif.so.12`exif_data_load_data(data=0x0000555555561fa0, d_orig="Exif", ds_orig=410) at exif-data.c:819:6
    frame #3: 0x00007ffff7f98829 libexif.so.12`exif_loader_get_data(loader=0x0000555555561f50) at exif-loader.c:387:2
    frame #4: 0x0000555555559eac exif`main(argc=2, argv=0x00007fffffffdf38) at main.c:438:9

// crash 3
(lldb) thread backtrace
* thread #1, name = 'exif', stop reason = signal SIGSEGV: invalid address (fault address: 0x5556555648e4)
  * frame #0: 0x00007ffff7f99e0e libexif.so.12`exif_get_sshort(buf="", order=EXIF_BYTE_ORDER_MOTOROLA) at exif-utils.c:92:29
    frame #1: 0x00007ffff7f99e77 libexif.so.12`exif_get_short(buf="", order=EXIF_BYTE_ORDER_MOTOROLA) at exif-utils.c:104:10
    frame #2: 0x00007ffff7f90d94 libexif.so.12`exif_data_load_data(data=0x0000555555561fa0, d_orig="\xff\xff\xd8\xff\xd8\xff\xff\xff\xd8\xff\xe0", ds_orig=278) at exif-data.c:819:6
    frame #3: 0x00007ffff7f98829 libexif.so.12`exif_loader_get_data(loader=0x0000555555561f50) at exif-loader.c:387:2
    frame #4: 0x0000555555559eac exif`main(argc=2, argv=0x00007fffffffdf38) at main.c:438:9

// crash 4
* thread #1, name = 'exif', stop reason = signal SIGSEGV: invalid address (fault address: 0x555555581aa5)
  * frame #0: 0x00007ffff7ed7e90 libc.so.6`__memcpy_avx_unaligned_erms at memmove-vec-unaligned-erms.S:872
    frame #1: 0x00007ffff7f8f3b1 libexif.so.12`exif_data_load_data_thumbnail(data=0x0000555555561fa0, d="II*", ds=12638, offset=380, size=4294967295) at exif-data.c:292:2
    frame #2: 0x00007ffff7f8f8d0 libexif.so.12`exif_data_load_data_content(data=0x0000555555561fa0, ifd=EXIF_IFD_0, d="II*", ds=12638, offset=10, recursion_depth=0) at exif-data.c:381:6
    frame #3: 0x00007ffff7f90d63 libexif.so.12`exif_data_load_data(data=0x0000555555561fa0, d_orig="\xff\xff\xff\xff\xff\xff\xd8\xff\xff\xd8\xff\xe0", ds_orig=12695) at exif-data.c:813:2
    frame #4: 0x00007ffff7f98829 libexif.so.12`exif_loader_get_data(loader=0x0000555555561f50) at exif-loader.c:387:2
    frame #5: 0x0000555555559eac exif`main(argc=2, argv=0x00007fffffffdf38) at main.c:438:9

  显然,这些 crash 可以分为两类:

  • 调用 exif_data_load_data_thumbnail 时崩溃。样例 0、4 是这种情况。
  • 调用 exif_get_sshort 时崩溃。样例 1、2、3 是这张情况。

  首先以 crash0 作为输入,来分析第一类。libexif 文档描述了内部各个函数的用途,给我们的分析带来了很大方便。简要浏览执行过程:

  1. main 函数,载入文件,然后调用 exif_loader_get_data 生成 ExifData
  2. exif_loader_get_data 函数为一个 ExifData 申请空间,然后调用 exif_data_load_data 读取 loader->buf 地址的字节流,长度为 loader->bytes_read
  3. exif_data_load_data 经过校验文件头等一系列步骤之后,解析 IFD 0,然后解析 IFD 1,此过程调用 exif_data_load_data_content
  4. exif_data_load_data_content 函数有一个防无限递归的检查。检查通过之后,读取 entry 数量,对于每一个 entry,按 tag 进行路由。若 tag 为 EXIF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH0x0202,则读入 thumbnail_length,此时若 thumbnail_offset 已经被设置,则调用 exif_data_load_data_thumbnail 去 parse 缩略图。注意到此时读入的 thumbnail_length 达到 4294967295,明显不合理。
  5. exif_data_load_data_thumbnail 调用 memcpy 复制数据,由于 len 过大导致 segmentation fault。

  于是粗略判断,bug 原因在于读取缩略图时不检查 len。现在开始以 crash1 为样本分析第二类 crash:

  1. 与第一类 crash 一致,main 调用 exif_loader_get_data
  2. 与第一类 crash 一致,exif_data_load_data 被调用
  3. 调用 exif_data_load_data 时,成功通过了第一类 crash 的崩溃地点 exif_data_load_data_content,然而在接下来调用 exif_get_short(d + 6 + offset, data->priv->order) 读取一个 short 时崩溃。注意到这里的 offset 为4294967293,明显异常。接下来越界读取,导致崩溃。

  动态调试,追踪 offset 的变化:

	/* IFD 0 offset */
	offset = exif_get_long (d + 10, data->priv->order);
	// 读入 offset = 4294967293,异常
    
	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);
	// 此函数是第一类 crash 崩溃处,但第二类 crash 样本可以通过此地
    
	/* IFD 1 offset */
	if (offset + 6 + 2 > ds) {
		return;
	}
	n = exif_get_short (d + 6 + offset, data->priv->order);
	// 第二类 crash 崩溃

  因此,第一、二类 crash 的原因,都是读入一个 offset 或 len 之后不加检查,导致越界读取。由于这个输入非常好控制(在文件内修改特定位置的 4 个字节即可),攻击者可以轻易做到:

  • 要求程序读取指定的固定地址,泄露某些信息(地址空间随机化能缓解此问题)
  • 要求程序读取一个合法地址块的起始位置,导致超时
  • 要求程序读取非法地址,造成崩溃

  但是,第二类 crash 中,这个 offset 是如何通过的 (offset + 6 + 2 > ds) 越界检查?注意到  offset 的类型是 uint32_t,而$$4294967293+6+2 \equiv 5 \pmod {2^{32}} $$

  于是 offset 的取值落在 $[-8, -1]$ 时,offset+8 会溢出 uint32,让这个检查失败。不得不说这是一个非常巧妙的 corner case。

  修复方法:给这个 if 添加条件,判断这个溢出。

	/* IFD 1 offset */
	if (offset > 0xffffff00 || offset + 6 + 2 > ds) {
		return;
	}

  成功修复第二类 crash:

▲ 修复后,第二类 crash 正常返回

  接下来去修复第一类 crash。观察 exif_data_load_data_thumbnail 函数:

static void
exif_data_load_data_thumbnail (ExifData *data, const unsigned char *d,
			       unsigned int ds, ExifLong offset, ExifLong size)
{
	if (ds < offset + size) {
		exif_log (data->priv->log, EXIF_LOG_CODE_DEBUG, "ExifData",
			  "Bogus thumbnail offset and size: %i < %i + %i.",
			  (int) ds, (int) offset, (int) size);
		return;
	}
	if (data->data) 
		exif_mem_free (data->priv->mem, data->data);
	data->size = size;
	data->data = exif_data_alloc (data, data->size);
	if (!data->data) 
		return;
	memcpy (data->data, d + offset, data->size);
}

  不难发现这里崩溃的原因也是 offset + size 溢出 uint32。修复方法:

if ((uint64_t)ds < (uint64_t)offset + (uint64_t)size) {
		// ...

  成功修复第一类 crash:

▲ crash0,返回正常信息
▲ crash4,返回正常信息

hang 样例

  看一下 backtrace:

// ....
    frame #130945: 0x00007ffff7f985eb libexif.so.12`exif_loader_write(eld=0x0000555555561f50, buf="\xf3\x94UUUU", len=0) at exif-loader.c:301:9
    frame #130946: 0x00007ffff7f985eb libexif.so.12`exif_loader_write(eld=0x0000555555561f50, buf="\xf3\x94UUUU", len=0) at exif-loader.c:301:9
    frame #130947: 0x00007ffff7f985eb libexif.so.12`exif_loader_write(eld=0x0000555555561f50, buf="\xf3\x94UUUU", len=0) at exif-loader.c:301:9
    frame #130948: 0x00007ffff7f985eb libexif.so.12`exif_loader_write(eld=0x0000555555561f50, buf="\U00000002\U00000001\nH\xf4H", len=20) at exif-loader.c:301:9
    frame #130949: 0x00007ffff7f97f1d libexif.so.12`exif_loader_write_file(l=0x0000555555561f50, path="./hang/data0") at exif-loader.c:120:8
    frame #130950: 0x0000555555559e9d exif`main(argc=2, argv=0x00007fffffffdf38) at main.c:437:4
    frame #130951: 0x00007ffff7d60d90 libc.so.6`__libc_start_call_main(main=(exif`main at main.c:321:1), argc=2, argv=0x00007fffffffdf38) at libc_start_call_main.h:58:16
    frame #130952: 0x00007ffff7d60e40 libc.so.6`__libc_start_main_impl(main=(exif`main at main.c:321:1), argc=2, argv=0x00007fffffffdf38, init=0x00007ffff7ffd040, fini=<unavailable>, rtld_fini=<unavailable>, stack_end=0x00007fffffffdf28) at libc-start.c:392:3
    frame #130953: 0x0000555555557625 exif`_start + 37

  这是一个无限递归。追踪执行过程:

  1. main 函数调用 exif_loader_write_file 读取文件。尽管它的名字里面有 write,但实际上它的功能是读取
  2. exif_loader_write_file 内,打开一个文件描述符,从文件内每次读 1024 字节进 buffer,并调用 exif_loader_write 把 buffer 内的信息导入 ExifLoader
  3. exif_loader_write 函数,首先通过一个状态机,确保完全读入 exif 信息。在函数的末尾,开发者宣称“如果抵达这个位置,则 buffer 没有大到能存完所有数据。向 buffer 填充新的数据”,于是递归调用自己

  注意到:第一次调用 exif_loader_write 的末尾, len=20;从第二次调用开始,始终有 len=0。然而, 我们看此函数第一次被调用,是在exif_loader_write_file

while (1) {
		size = fread (data, 1, sizeof (data), f);
		if (size <= 0) 
			break;
		if (!exif_loader_write (l, data, size)) 
			break;
	}

  显然,开发者并不期望 size=0。修复方案是:

unsigned char
exif_loader_write (ExifLoader *eld, unsigned char *buf, unsigned int len)
{
	unsigned int i;

	// if (!eld || (len && !buf)) 
	// 	return 0;
    
    if (!eld || (len && !buf) || len == 0) 
	return 0;
    
    // ...

  修复成功:

▲ 修复后,对正常 jpg 可以成功解析,对于 hang 样本报错并返回

  官方 fix:commit f885

0x05 复盘:被漏掉的 CVE-2009-3895

  我们寻找到了 5 个 crash 和 1 个 hang。其中 crash 对应 CVE-2012-2836;而 hang 对应的漏洞并非 CVE-2009-3895。翻阅 git 记录,Google Security Team 指出了此漏洞,而作者在 2007 年 12 月修复了这个 bug。

2007-12-14 Lutz Mueller <lutz@users.sourceforge.net> · libexif/libexif@f885d85
Bug pointed out by Meder Kydyraliev, Google Security Team: * libexif/exif-loader.c: (exif_loader_write) Ignore buffers of zero length.

  简而言之:我们漏掉了 Fuzzing101 编者所期望的 CVE-2009-3895,而找到了另一个 bug。

  然而,我们的 fuzzer 现在已经运行了 5 小时多,last new find 是两小时前。似乎很难再获取到新的 crash 了。

  应当反思我们的语料集,seed coupus 是否有问题。我们只提供了两个 jpg 文件,其中一个是网上找的,一个是自己画的。那么,我们的语料集中,不存在存储了相机信息的图片。除了相机信息之外,可能还有其他的 exif 数据被我们忽略了。

  笔者用 iPhone 拍摄了一张照片。发现 Exif 信息非常丰富:

  将这张照片加入语料集之后,再次 fuzz。

  睡一觉醒来,发现有收获:

  在 6h+ 的 fuzz 之后,我们获得了 8 个 crash。逐一调试,发现最后一个样本,即 crash7,能触发 CVE-2009-3895 所描述的 exif_entry_fix 函数:

  追踪执行过程。

  1. 与本文前述样本相同,main 调用 exif_loader_get_data
  2. exif_data_load_data 被调用。程序通过了本文前述两个漏洞的 fail 位置,抵达函数尾部,调用 exif_data_fix
  3. exif_data_fix 调用 exif_data_foreach_content(d, fix_func, NULL)
  4. exif_data_foreach_content 是一个 mapper,对 ExifData *d 里面的 ifd[] 施加函数 fix_func
  5. exif-data.c 里面的 static 函数)fix_func 对 ifd 进行路由,若 ifd 不为 EXIF_IFD_1,则调用 exif_content_fix(c)
  6. exif_content_fix(ExifContent *c) 调用 exif_content_foreach_entry(c, fix_func, NULL) ,按代码注释,其作用是“fix all existing entries”
  7. exif_content_foreach_entry (ExifContent *content, ExifContentForeachEntryFunc func, void *data) 也是个 mapper。调用 fix_func(content->entries[i], data)
  8. exif-content.c 里面的 static 函数)fix_funcexif_entry_fix(e) 的包装
  9. exif_entry_fix (ExifEntry *e) 调用 exif_get_long(e->data + i * exif_format_get_size (EXIF_FORMAT_LONG), o)
  10. exif_get_long 实际被 call 时的参数是 buf=0x555555581000, order=INTEL,这里 buf 已经是非法地址
  11. 尝试在非法的 buf 中读取,产生 segmentation fault

  来追踪步骤 10 里面这个 buf 的来历。它是 e->data + i * exif_format_get_size (EXIF_FORMAT_LONG),而此时 e->data0x555555563950exif_format_get_size (EXIF_FORMAT_LONG) 返回的是常量 4。此处 i=30124。相关代码:

for (i = 0; i < e->components; i++)
    exif_set_short (
        e->data + i * 2,
        o,
        (ExifShort) exif_get_long (e->data + i * 4, o));

  动态调试发现 e->componets = 1073741828,异常。动态调试观察 e->components 的变化,发现是在  exif_data_load_data 中,调用 exif_data_load_data_content 后被改为 1073741828

  考虑修复这个漏洞。注意到 ExifEntry 有一个 size 属性,注释称“Number of bytes in the buffer at data. This must be no less than exif_format_get_size(format)*components”,所以显然读取 data buffer 时,不应该超过 size 界限。所以有了修复方案:在 exif_entry_fix 开始 i=0 to e->components 循环之前,判断 e->components 是否符合这一约定。patch:

switch (e->format) {
    case EXIF_FORMAT_LONG:
        if (!e->parent || !e->parent->parent) break;
        
        // patch: 判断 e->components 是否符合约定
        if(e->components * exif_format_get_size(EXIF_FORMAT_LONG) > e->size) break;

        o = exif_data_get_byte_order (e->parent->parent);
        for (i = 0; i < e->components; i++)
		// ...

  修复成功:

  然而,当笔者去验证这个漏洞是否确为 CVE-2009-3895 时,发现该 CVE 只影响 libexif 0.6.18 版本

Date: Thu, 19 Nov 2009 11:05:49 -0500 (EST)
From: Josh Bressers <bressers@...hat.com>
To: oss-security <oss-security@...ts.openwall.com>
Cc: coley <coley@...re.org>
Subject: CVE assignment (libexif)

I'm giving libexif CVE-2009-3895. I've not seen an ID for this yet.

Only libexif version 0.6.18 is affected, all other versions are safe. http://article.gmane.org/gmane.comp.graphics.libexif.devel/806
http://bugs.gentoo.org/show_bug.cgi?id=293190

Thanks.
-- JB

  漏洞的发现者,Alex Legler 也在 Gentoo Bugzilla 中指出,“Only libexif version 0.6.18 is affected by this flaw. Version 0.6.17 and previous and 0.6.19 and later are not affected.”

  然而,Fuzzing101 文档提供的任务是要我们在 libexif 0.6.14 中寻找这个 CVE。双方说法是矛盾的。观察 Fuzzing101 官方题解,他们找到的漏洞也是本章节我们找到的漏洞,而非 CVE-2009-3895。官方题解提到的 patch commit 是 2012 年的 8ce7 commit

  本章节找到的漏洞,于 99df commit 存在,2007 年 5 月 8a79 commit 修复:

0x06 总结

  • 初始语料集非常重要。应当尽可能覆盖各种各样的情况。