Target CVEs
Description
Compile
Download
git clone https://github.com/libexif/libexif.gitcd libexif && git checkout libexif-0_6_14-releaseBuild
这里由于我在 make 的时候有关生成文档的地方报错了,所以我手动将文档部分 patch 掉:
SUBDIRS = m4m po libexif test doc binarySUBDIRS = m4m po libexif test binaryautoreconf -fviCC=clang CXX=clang++ CFLAGS="-O0 -g -fno-inline -fno-builtin -fno-omit-frame-pointer" CXXFLAGS="$CFLAGS" ./configure --enable-shared=no --prefix="$PWD/../workshop/lib-debug/"make -j`nproc` && make installmake clean
CC=afl-clang-lto CXX=afl-clang-lto++ ./configure --enable-shared=no --prefix="$PWD/../workshop/lib-fuzz/"make -j`nproc` && make install由于 libexif 只是一个库,所以我们要 fuzz 它需要自己找个前端,或者手写 harness 。方便起见,我直接使用 exif 作为前端。
git clone https://github.com/libexif/exif.gitcd exif && git checkout exif-0_6_15-releaseautoreconf -fviCC=clang CXX=clang++ CFLAGS="-O0 -g -fno-inline -fno-builtin -fno-omit-frame-pointer" CXXFLAGS="$CFLAGS" PKG_CONFIG_PATH="$PWD/../workshop/lib-debug/lib/pkgconfig/" ./configure --enable-shared=no --prefix="$PWD/../workshop/exif-debug"make -j`nproc` && make installmake clean
CC=afl-clang-lto CXX=afl-clang-lto++ PKG_CONFIG_PATH="$PWD/../workshop/lib-fuzz/lib/pkgconfig/" ./configure --enable-shared=no --prefix="$PWD/../workshop/exif-fuzz"make -j`nproc` && make installSamples
#!/usr/bin/env python3
import structimport os
def pack_data(fmt, endian, *args): return struct.pack(('<' if endian == 'LE' else '>') + fmt, *args)
class ExifGenerator: def __init__(self, endian='LE'): self.endian = endian self.entries = [] self.data_blobs = b''
def add_entry(self, tag, data_type, count, value): self.entries.append({'tag': tag, 'type': data_type, 'count': count, 'value': value})
def build_ifd(self, start_offset, next_ifd_offset=0): ifd_data = pack_data('H', self.endian, len(self.entries)) data_offset = start_offset + 2 + (len(self.entries) * 12) + 4
entry_bytes = b'' blob_bytes = b''
for e in self.entries: val_bytes = b'' raw_blob = None
if e['type'] == 1: # BYTE if isinstance(e['value'], int): raw_blob = pack_data('B', self.endian, e['value']) else: raw_blob = e['value'] elif e['type'] == 2: # ASCII raw_blob = e['value'].encode('ascii') + b'\x00' elif e['type'] == 3: # SHORT if e['count'] == 1: raw_blob = pack_data('H', self.endian, e['value']) else: raw_blob = pack_data('H'*e['count'], self.endian, *e['value']) elif e['type'] == 4: # LONG if e['count'] == 1: raw_blob = pack_data('I', self.endian, e['value']) else: raw_blob = pack_data('I'*e['count'], self.endian, *e['value']) elif e['type'] == 5: # RATIONAL raw_blob = pack_data('II', self.endian, e['value'][0], e['value'][1]) elif e['type'] == 7: # UNDEFINED raw_blob = e['value'] elif e['type'] == 10: # SRATIONAL raw_blob = pack_data('ii', self.endian, e['value'][0], e['value'][1])
if len(raw_blob) <= 4: val_bytes = raw_blob.ljust(4, b'\x00') else: off = data_offset + len(blob_bytes) val_bytes = pack_data('I', self.endian, off) blob_bytes += raw_blob
entry_bytes += pack_data('HHII', self.endian, e['tag'], e['type'], e['count'], struct.unpack('<I' if self.endian == 'LE' else '>I', val_bytes)[0])
return ifd_data + entry_bytes + pack_data('I', self.endian, next_ifd_offset) + blob_bytes
def create_complex_exif(endian='LE'): # 1. Build Exif SubIFD exif = ExifGenerator(endian) exif.add_entry(0x9003, 2, 20, "2026:02:24 14:00:00") exif.add_entry(0x829a, 5, 1, (1, 100)) exif.add_entry(0x829d, 5, 1, (28, 10)) exif.add_entry(0x9204, 10, 1, (-1, 3)) exif.add_entry(0x9286, 7, 8, b"USERCOM\x00") exif_sub_data = exif.build_ifd(0) # Length check only first
# 2. Build GPS IFD gps = ExifGenerator(endian) gps.add_entry(0x0000, 1, 4, b'\x02\x02\x00\x00') gps.add_entry(0x0002, 5, 3, (1, 1)) # This will actually need 3 rationals, but let's keep it simple # Fix: GPS Latitude is 3 rationals gps.entries[-1] = {'tag': 0x0002, 'type': 5, 'count': 3, 'value': (40, 1, 30, 1, 15, 1)} gps_data = gps.build_ifd(0)
# 3. Build IFD0 ifd0 = ExifGenerator(endian) ifd0.add_entry(0x010e, 2, 11, "Fuzz Test") ifd0.add_entry(0x0110, 2, 11, "Gemini Cam") ifd0.add_entry(0x8769, 4, 1, 0) # ExifOffset ifd0.add_entry(0x8825, 4, 1, 0) # GPSInfo
# IFD0 fixed size: 2 + 4*12 + 4 = 54. Blobs: 11 + 11 = 22. Total = 76. ifd0_total_len = 76 exif_offset = 8 + ifd0_total_len gps_offset = exif_offset + len(exif_sub_data)
for e in ifd0.entries: if e['tag'] == 0x8769: e['value'] = exif_offset if e['tag'] == 0x8825: e['value'] = gps_offset
final_ifd0 = ifd0.build_ifd(8) final_exif = exif.build_ifd(exif_offset) final_gps = gps.build_ifd(gps_offset)
header = b'II\x2a\x00\x08\x00\x00\x00' if endian == 'LE' else b'MM\x00\x2a\x00\x00\x00\x08' return header + final_ifd0 + final_exif + final_gps
def save_corpus(name, data, add_exif_header=False): os.makedirs('corpus/exif', exist_ok=True) with open(os.path.join('corpus/exif', name), 'wb') as f: if add_exif_header: f.write(b'Exif\x00\x00') f.write(data) print(f"Created {name}")
if __name__ == "__main__": save_corpus('rich_le.exif', create_complex_exif('LE'), True) save_corpus('rich_be.exif', create_complex_exif('BE'), True)
SOI, APP1 = b'\xff\xd8', b'\xff\xe1' exif_payload = b'Exif\x00\x00' + create_complex_exif('LE') app1_data = APP1 + struct.pack('>H', len(exif_payload) + 2) + exif_payload save_corpus('rich_jpeg.jpg', SOI + app1_data + b'\xff\xd9') save_corpus('raw_tiff.exif', create_complex_exif('LE'), False)Fuzzing
AFLplusplus go work: afl-fuzz -i - -o out/ -s 1337 -- ./exif-fuzz/bin/exif @@.
这个 Gemini Cam 还挺滑稽的哈哈哈:

13 个 crashes,4 个 hangs,其中有这几种独立的 crash 情况:
Program received signal SIGSEGV, Segmentation fault.0x00007ffff7e595c1 in memcpy () from /usr/lib64/libc.so.6#0 0x00007ffff7e595c1 in memcpy () from /usr/lib64/libc.so.6#1 0x0000555555565553 in exif_data_load_data_thumbnail (data=0x5555555931e0, d=0x555555595986 "II*", ds=256, offset=4294967168, size=232) at exif-data.c:292#2 0x0000555555563d05 in exif_data_load_data_content (data=0x5555555931e0, ifd=EXIF_IFD_EXIF, d=0x555555595986 "II*", ds=256, offset=86, recursion_depth=1) at exif-data.c:381#3 0x0000555555563b36 in exif_data_load_data_content (data=0x5555555931e0, ifd=EXIF_IFD_0, d=0x555555595986 "II*", ds=256, offset=10, recursion_depth=0) at exif-data.c:361#4 0x0000555555563621 in exif_data_load_data (data=0x5555555931e0, d_orig=0x555555595980 "Exif", ds_orig=262) at exif-data.c:813#5 0x000055555556cb9b in exif_loader_get_data (loader=0x555555593190) at exif-loader.c:387#6 0x000055555555f73e in main (argc=2, argv=0x7fffffffdca8) at main.c:438
Program received signal SIGSEGV, Segmentation fault.0x000055555556e5bf in exif_get_slong (b=0x5555555b2000 <error: Cannot access memory at address 0x5555555b2000>, order=EXIF_BYTE_ORDER_INTEL) at exif-utils.c:137137 return ((b[3] << 24) | (b[2] << 16) | (b[1] << 8) | b[0]);#0 0x000055555556e5bf in exif_get_slong (b=0x5555555b2000 <error: Cannot access memory at address 0x5555555b2000>, order=EXIF_BYTE_ORDER_INTEL) at exif-utils.c:137#1 0x000055555556e44f in exif_get_long (buf=0x5555555b2000 <error: Cannot access memory at address 0x5555555b2000>, order=EXIF_BYTE_ORDER_INTEL) at exif-utils.c:167#2 0x0000555555565fd6 in exif_entry_fix (e=0x555555593460) at exif-entry.c:193#3 0x0000555555562d4d in fix_func (e=0x555555593460, data=0x0) at exif-content.c:231#4 0x00005555555629f3 in exif_content_foreach_entry (content=0x555555593270, func=0x555555562d30 <fix_func>, data=0x0) at exif-content.c:200#5 0x0000555555562bbc in exif_content_fix (c=0x555555593270) at exif-content.c:247#6 0x0000555555565431 in fix_func (c=0x555555593270, data=0x0) at exif-data.c:1169#7 0x000055555556507a in exif_data_foreach_content (data=0x5555555931e0, func=0x555555565390 <fix_func>, user_data=0x0) at exif-data.c:1031#8 0x0000555555564384 in exif_data_fix (d=0x5555555931e0) at exif-data.c:1176#9 0x0000555555563866 in exif_data_load_data (data=0x5555555931e0, d_orig=0x555555595980 "", ds_orig=82) at exif-data.c:871#10 0x000055555556cb9b in exif_loader_get_data (loader=0x555555593190) at exif-loader.c:387#11 0x000055555555f73e in main (argc=2, argv=0x7fffffffdca8) at main.c:438
Program received signal SIGSEGV, Segmentation fault.0x000055555556e382 in exif_get_sshort (buf=0x555655595985 <error: Cannot access memory at address 0x555655595985>, order=EXIF_BYTE_ORDER_INTEL) at exif-utils.c:9494 return ((buf[1] << 8) | buf[0]);#0 0x000055555556e382 in exif_get_sshort (buf=0x555655595985 <error: Cannot access memory at address 0x555655595985>, order=EXIF_BYTE_ORDER_INTEL) at exif-utils.c:94#1 0x000055555556e2ef in exif_get_short (buf=0x555655595985 <error: Cannot access memory at address 0x555655595985>, order=EXIF_BYTE_ORDER_INTEL) at exif-utils.c:104#2 0x0000555555563651 in exif_data_load_data (data=0x5555555931e0, d_orig=0x555555595980 "Exif", ds_orig=61) at exif-data.c:819#3 0x000055555556cb9b in exif_loader_get_data (loader=0x555555593190) at exif-loader.c:387#4 0x000055555555f73e in main (argc=2, argv=0x7fffffffdca8) at main.c:438hang 的情况比较唯一,全是卡在 exif_loader_write 这里:

Analysis
这个项目有 API 文档,不用自己猜函数功能了:libexif Project Documentation.
好的,继续玩 MC,快开学了,谁学啊!?
CVE-2012-2836
先分析一下这个样本,栈回溯如下:
#0 0x00007ffff7e595c1 in memcpy () from /usr/lib64/libc.so.6#1 0x0000555555565553 in exif_data_load_data_thumbnail (data=0x5555555931e0, d=0x555555595986 "II*", ds=256, offset=4294967168, size=232) at exif-data.c:292#2 0x0000555555563d05 in exif_data_load_data_content (data=0x5555555931e0, ifd=EXIF_IFD_EXIF, d=0x555555595986 "II*", ds=256, offset=86, recursion_depth=1) at exif-data.c:381#3 0x0000555555563b36 in exif_data_load_data_content (data=0x5555555931e0, ifd=EXIF_IFD_0, d=0x555555595986 "II*", ds=256, offset=10, recursion_depth=0) at exif-data.c:361#4 0x0000555555563621 in exif_data_load_data (data=0x5555555931e0, d_orig=0x555555595980 "Exif", ds_orig=262) at exif-data.c:813#5 0x000055555556cb9b in exif_loader_get_data (loader=0x555555593190) at exif-loader.c:387#6 0x000055555555f73e in main (argc=2, argv=0x7fffffffdca8) at main.c:438exif_loader_get_data 负责为 ExifData 结构体开辟空间,然后使用 exif_data_load_data 将内存中的 JPEG / EXIF header, byte order, fixed value 等信息加载到 ExifData 结构体中,进行一些简单的校验,并初始化 IFD 0 和 IFD 1。初始化 IFD 字段是通过调用 exif_data_load_data_content 实现的。

根据这个 switch 就可以看出来,很显然,load data content 大致应该就是将 exif 图片的各个字段信息加载进去。
继续调试,我们在 EXIF_TAG_EXIF_IFD_POINTER 这个 case 调用了 exif_data_load_data_content,此时传入的 ifd 为 EXIF_IFD_EXIF,而我们后续也是在这里面执行 exif_data_load_data_thumbnail 时崩溃的,所以要重点分析一下这部分。
首先确定一下在第几次循环的时候崩溃,我们直接 c 让它崩,然后切换到对应栈帧查看 i,发现是第 13 次调用了 exif_data_load_data_thumbnail:

然后通过 if i == 13 下条件断点,定位到触发崩溃的循环索引继续分析。由于这里有两个调用 exif_data_load_data_thumbnail 的位置,而我们不知道具体会调用哪一个,因此如果我们下的那个断点不是实际的调用点,那就停不下来了。所以这里两个位置都需要下一个断点:


最后我们发现它执行的是 381 行处的代码,整洁起见,之前 374 行处的断点就可以删掉了。

步入继续,发现致使 memcpy 崩溃的原因是 rsi 无法解引用:

接下来重点看这个 exif_data_load_data_thumbnail 的逻辑:
// ► 0x555555563d00 <exif_data_load_data_content+1168> call exif_data_load_data_thumbnail <exif_data_load_data_thumbnail>// rdi: 0x555555593240 —▸ 0x5555555932d0 —▸ 0x5555555934a0 —▸ 0x555555593420 ◂— 0x200000110// rsi: 0x555555595986 ◂— 0x8002a4949 /* 'II*' */// rdx: 0x100// rcx: 0xffffff80// r8: 0xe8
static voidexif_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);}通过调试,我们可知它根本不会进入那三个 if branch,也就是说,这个函数只执行了如下三行代码:
data->size = size;data->data = exif_data_alloc (data, data->size);memcpy (data->data, d + offset, data->size);即将 size 设置为 r8,将 data 设置为 exif_data_alloc 分配出来的值,而 src 则是 d + offset 的值,即 rsi + 0xffffff80:

显然,这个 offset 特别大,而经调试,我们的 rsi 其实是合法值,那么问题就处在 offset 上了。
回溯发现,在给 exif_data_load_data_thumbnail 传参时,offset 的值就有问题了:

通过查看二进制数据,我们很容易定位到一个类似的数据:

尝试直接将其修改为 0xCAFEBABE,然后再调试一下。调试之前我们可以先使用 save breakpoints bps 将断点信息保存到 bps 文件,之后通过 source bps 加载。


可见,我们可以通过控制图片的内部数据信息来控制 memcpy 的行为,至于具体怎么利用,利用后有什么效果,我就不分析了。直接确定一下 CVE ID,发现这个 CVE-2012-2836 的描述比较符合我们的分析结果。
Fix
CVE-2012-2836
调试发现,这个非法 offset 来自 exif_get_long 函数,我们深入分析一下它的逻辑。

首先调用入口是:
case EXIF_TAG_JPEG_INTERCHANGE_FORMAT: o = exif_get_long (d + offset + 12 * i + 8, data->priv->order);所以它是取 d + offset + 12 * i + 8 这个偏移处的值。查阅文档,我们可知它就是返回一个 uint32_t 类型,而它内部使用的 exif_get_slong 会返回一个 int32_t 类型:
ExifLongexif_get_long (const unsigned char *buf, ExifByteOrder order){ return (exif_get_slong (buf, order) & 0xffffffff);}
ExifSLongexif_get_slong (const unsigned char *b, ExifByteOrder order){ if (!b) return 0; switch (order) { case EXIF_BYTE_ORDER_MOTOROLA: return ((b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3]); case EXIF_BYTE_ORDER_INTEL: return ((b[3] << 24) | (b[2] << 16) | (b[1] << 8) | b[0]); }
/* Won't be reached */ return (0);}虽然通过 & 0xffffffff 截断处理了整数溢出问题,但是 0xffffffff 依旧可以表示一个巨大的范围,大概 4 GB?所以问题就在于,它没检查偏移是否合理。
解决方法很简单,只要将 offset 限制在 ds 的范围内即可。ds 代表了 data size, 也就是整个图像数据的大小边界。超过 ds 肯定是不对的。
case EXIF_TAG_JPEG_INTERCHANGE_FORMAT: o = exif_get_long(d + offset + 12 * i + 8, data->priv->order); if (o >= ds) { exif_log(data->priv->log, EXIF_LOG_CODE_CORRUPT_DATA, "ExifData", "Illegal offset value detected!"); abort(); }