Exercise 2: libexif
任务目标是获得 CVE-2009-3895 以及 CVE-2012-2836 两个漏洞。前者是一个堆溢出;后者是一个越界读取,可以实现 DoS,也可能导致信息泄露。
0x01 环境准备
首先获取 libexif 0.6.14 的源码。在 Github 可以下载到:
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 文档描述了内部各个函数的用途,给我们的分析带来了很大方便。简要浏览执行过程:
main
函数,载入文件,然后调用exif_loader_get_data
生成ExifData
exif_loader_get_data
函数为一个ExifData
申请空间,然后调用exif_data_load_data
读取loader->buf
地址的字节流,长度为loader->bytes_read
exif_data_load_data
经过校验文件头等一系列步骤之后,解析 IFD 0,然后解析 IFD 1,此过程调用exif_data_load_data_content
exif_data_load_data_content
函数有一个防无限递归的检查。检查通过之后,读取 entry 数量,对于每一个 entry,按 tag 进行路由。若 tag 为EXIF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH
即0x0202
,则读入thumbnail_length
,此时若thumbnail_offset
已经被设置,则调用exif_data_load_data_thumbnail
去 parse 缩略图。注意到此时读入的thumbnail_length
达到4294967295
,明显不合理。exif_data_load_data_thumbnail
调用memcpy
复制数据,由于 len 过大导致 segmentation fault。
于是粗略判断,bug 原因在于读取缩略图时不检查 len。现在开始以 crash1 为样本分析第二类 crash:
- 与第一类 crash 一致,
main
调用exif_loader_get_data
- 与第一类 crash 一致,
exif_data_load_data
被调用 - 调用
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。观察 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:
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
这是一个无限递归。追踪执行过程:
main
函数调用exif_loader_write_file
读取文件。尽管它的名字里面有write
,但实际上它的功能是读取exif_loader_write_file
内,打开一个文件描述符,从文件内每次读 1024 字节进 buffer,并调用exif_loader_write
把 buffer 内的信息导入ExifLoader
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;
// ...
修复成功:
官方 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。
简而言之:我们漏掉了 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
函数:
追踪执行过程。
- 与本文前述样本相同,
main
调用exif_loader_get_data
exif_data_load_data
被调用。程序通过了本文前述两个漏洞的 fail 位置,抵达函数尾部,调用exif_data_fix
exif_data_fix
调用exif_data_foreach_content(d, fix_func, NULL)
exif_data_foreach_content
是一个 mapper,对ExifData *d
里面的ifd[]
施加函数fix_func
- (
exif-data.c
里面的 static 函数)fix_func
对 ifd 进行路由,若 ifd 不为EXIF_IFD_1
,则调用exif_content_fix(c)
exif_content_fix(ExifContent *c)
调用exif_content_foreach_entry(c, fix_func, NULL)
,按代码注释,其作用是“fix all existing entries”exif_content_foreach_entry (ExifContent *content, ExifContentForeachEntryFunc func, void *data)
也是个 mapper。调用fix_func(content->entries[i], data)
- (
exif-content.c
里面的 static 函数)fix_func
是exif_entry_fix(e)
的包装 exif_entry_fix (ExifEntry *e)
调用exif_get_long(e->data + i * exif_format_get_size (EXIF_FORMAT_LONG), o)
exif_get_long
实际被 call 时的参数是buf=0x555555581000, order=INTEL
,这里buf
已经是非法地址- 尝试在非法的
buf
中读取,产生 segmentation fault
来追踪步骤 10 里面这个 buf
的来历。它是 e->data + i * exif_format_get_size (EXIF_FORMAT_LONG)
,而此时 e->data
为 0x555555563950
, exif_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 总结
- 初始语料集非常重要。应当尽可能覆盖各种各样的情况。