CVE-2012-2836 in libexif(out-of-bounds read)
CVE-2009-3895 haven't found

前期准备

测试用例

跟着教程来就行,不再赘述

Fuzzing

本次使用了 AFL++ 内置的 afl-clang-lto 所具备的链接时优化(Link-Time Optimization,LTO)功能来优化编译以得到更好的 fuzzing 效果,其比我们上次所用的 afl-clang-fast 更优秀

CC=afl-clang-lto ...

然后就可以开始 fuzzing 了

熟悉的界面

调试之前需要做一些准备
之前调试尝试跟踪变量的时候发现很多变量显示 ,给分析过程带来很大麻烦
这是因为编译器的优化会导致变量被优化掉,需要在 configure 前增加个参数降低优化程度

CFLAGS=“-O0 -g”

调试环境依然选择 VS Code + gdb

crash 1

#0  0x000000000022c56f in exif_get_sshort (buf=0x1004558e5 <error: Cannot access memory at address 0x1004558e5>, order=EXIF_BYTE_ORDER_MOTOROLA) at exif-utils.c:92
#1  exif_get_short (buf=0x1004558e5 <error: Cannot access memory at address 0x1004558e5>, order=EXIF_BYTE_ORDER_MOTOROLA) at exif-utils.c:104
#2  exif_data_load_data (data=0x4537b0, d_orig=<optimized out>, ds_orig=<optimized out>) at exif-data.c:819
#3  0x0000000000221886 in exif_loader_get_data (loader=<optimized out>) at /home/asuka/fuzzing101/fuzzing_libexif/libexif-libexif-0_6_14-release/libexif/exif-loader.c:387
#4  main (argc=<optimized out>, argc@entry=2, argv=<optimized out>, argv@entry=0x7fffffffdb28) at main.c:438
#5  0x00007ffff7cb9d90 in __libc_start_call_main (main=main@entry=0x21f100 <main>, argc=argc@entry=2, argv=argv@entry=0x7fffffffdb28) at ../sysdeps/nptl/libc_start_call_main.h:58
#6  0x00007ffff7cb9e40 in __libc_start_main_impl (main=0x21f100 <main>, argc=2, argv=0x7fffffffdb28, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffdb18) at ../csu/libc-start.c:392
#7  0x000000000021a9f5 in _start ()

main.c

    // ...
    args = poptGetArgs (ctx);

    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);      // here
            // ...

exif-loader.c

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);      // here in

    return ed;
}

前面都是读取照片的 EXIF 信息,没什么问题

exif-data.c

    // ...
    /* IFD 0 offset */
    offset = exif_get_long (d + 10, data->priv->order);     // offset = 4294967295(-1)
    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) {      // check offset
        return;
    }
    n = exif_get_short (d + 6 + offset, data->priv->order);     // here in
    // ...

到了这里发现 offset 的值是 0xffffffff,明显是不合理的
跟进看看发生了什么

exif-utils.c

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

此时 b 指向的地址是 0x44d8ea,看看内存信息

-exec x/20c 0x44d8e0
0x44d8e0:    69 'E'  120 'x' 105 'i' 102 'f' 0 '
-exec x/20c 0x44d8e0
0x44d8e0:    69 'E'  120 'x' 105 'i' 102 'f' 0 '\000'    0 '\000'    77 'M'  77 'M'
0x44d8e8:    0 '\000'    42 '*'  -1 '\377'   -1 '\377'   -1 '\377'   -1 '\377'   0 '\000'    12 '\f'
0x44d8f0:    1 '\001'    0 '\000'    3 '\003'    0 '\000'
-exec x/a 0x44d8ea
0x44d8ea:    0x10c00ffffffff
0' 0 '
-exec x/20c 0x44d8e0
0x44d8e0:    69 'E'  120 'x' 105 'i' 102 'f' 0 '\000'    0 '\000'    77 'M'  77 'M'
0x44d8e8:    0 '\000'    42 '*'  -1 '\377'   -1 '\377'   -1 '\377'   -1 '\377'   0 '\000'    12 '\f'
0x44d8f0:    1 '\001'    0 '\000'    3 '\003'    0 '\000'
-exec x/a 0x44d8ea
0x44d8ea:    0x10c00ffffffff
0' 77 'M' 77 'M' 0x44d8e8: 0 '
-exec x/20c 0x44d8e0
0x44d8e0:    69 'E'  120 'x' 105 'i' 102 'f' 0 '\000'    0 '\000'    77 'M'  77 'M'
0x44d8e8:    0 '\000'    42 '*'  -1 '\377'   -1 '\377'   -1 '\377'   -1 '\377'   0 '\000'    12 '\f'
0x44d8f0:    1 '\001'    0 '\000'    3 '\003'    0 '\000'
-exec x/a 0x44d8ea
0x44d8ea:    0x10c00ffffffff
0' 42 '*' -1 '7' -1 '7' -1 '7' -1 '7' 0 '
-exec x/20c 0x44d8e0
0x44d8e0:    69 'E'  120 'x' 105 'i' 102 'f' 0 '\000'    0 '\000'    77 'M'  77 'M'
0x44d8e8:    0 '\000'    42 '*'  -1 '\377'   -1 '\377'   -1 '\377'   -1 '\377'   0 '\000'    12 '\f'
0x44d8f0:    1 '\001'    0 '\000'    3 '\003'    0 '\000'
-exec x/a 0x44d8ea
0x44d8ea:    0x10c00ffffffff
0' 12 '\f' 0x44d8f0: 1 '
-exec x/20c 0x44d8e0
0x44d8e0:    69 'E'  120 'x' 105 'i' 102 'f' 0 '\000'    0 '\000'    77 'M'  77 'M'
0x44d8e8:    0 '\000'    42 '*'  -1 '\377'   -1 '\377'   -1 '\377'   -1 '\377'   0 '\000'    12 '\f'
0x44d8f0:    1 '\001'    0 '\000'    3 '\003'    0 '\000'
-exec x/a 0x44d8ea
0x44d8ea:    0x10c00ffffffff
1' 0 '
-exec x/20c 0x44d8e0
0x44d8e0:    69 'E'  120 'x' 105 'i' 102 'f' 0 '\000'    0 '\000'    77 'M'  77 'M'
0x44d8e8:    0 '\000'    42 '*'  -1 '\377'   -1 '\377'   -1 '\377'   -1 '\377'   0 '\000'    12 '\f'
0x44d8f0:    1 '\001'    0 '\000'    3 '\003'    0 '\000'
-exec x/a 0x44d8ea
0x44d8ea:    0x10c00ffffffff
0' 3 '
-exec x/20c 0x44d8e0
0x44d8e0:    69 'E'  120 'x' 105 'i' 102 'f' 0 '\000'    0 '\000'    77 'M'  77 'M'
0x44d8e8:    0 '\000'    42 '*'  -1 '\377'   -1 '\377'   -1 '\377'   -1 '\377'   0 '\000'    12 '\f'
0x44d8f0:    1 '\001'    0 '\000'    3 '\003'    0 '\000'
-exec x/a 0x44d8ea
0x44d8ea:    0x10c00ffffffff
3' 0 '
-exec x/20c 0x44d8e0
0x44d8e0:    69 'E'  120 'x' 105 'i' 102 'f' 0 '\000'    0 '\000'    77 'M'  77 'M'
0x44d8e8:    0 '\000'    42 '*'  -1 '\377'   -1 '\377'   -1 '\377'   -1 '\377'   0 '\000'    12 '\f'
0x44d8f0:    1 '\001'    0 '\000'    3 '\003'    0 '\000'
-exec x/a 0x44d8ea
0x44d8ea:    0x10c00ffffffff
0' -exec x/a 0x44d8ea 0x44d8ea: 0x10c00ffffffff

就是 EXIF 后面的几个 0xff 引起了错误,看看这张图片的信息

然而原图的信息是这样的

另一个问题是:0xffffffff 是如何通过offset + 6 + 2 > ds的?
ds 的定义如下,ds_orgi 就是 loader->bytes_read,大约是 1100 左右

void
exif_data_load_data (ExifData *data, const unsigned char *d_orig,
             unsigned int ds_orig)
{
    // ...
    unsigned int ds = ds_orig;

而 offset + 6 + 2 = 4294967295 + 8 溢出后等于 7,所以只要 offset 的值在 [-8, -1] 时就能通过检查,非常巧妙

exif-utils.c

ExifShort
exif_get_short (const unsigned char *buf, ExifByteOrder order)
{
    return (exif_get_sshort (buf, order) & 0xffff);
}
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]);    // line:92 crash
        case EXIF_BYTE_ORDER_INTEL:
                return ((buf[1] << 8) | buf[0]);
        }

    /* Won't be reached */
    return (0);
}

数组就是在这里越界了

所以这个漏洞是读取 offset 时没有做好边界检查导致的
复现非常容易,只要把 0x002a 后面的四个字节的值按图片的字节序(MOTOROLA,MM 和 INTEL,II)改到 [-8, -1] 之间即可
修复也非常容易,加个对 offset 单独的检查条件即可

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

crash 2

#0  0x0000000000228f2a in exif_get_slong (b=<optimized out>, order=EXIF_BYTE_ORDER_MOTOROLA) at exif-utils.c:135
#1  exif_get_long (buf=<optimized out>, order=EXIF_BYTE_ORDER_MOTOROLA) at exif-utils.c:167
#2  exif_entry_fix (e=0x455840) at exif-entry.c:193
#3  fix_func (e=0x455840, data=0x0) at exif-content.c:231
#4  exif_content_foreach_entry (content=0x4538e0, data=0x0, func=<optimized out>) at exif-content.c:200
#5  exif_content_fix (c=<optimized out>) at exif-content.c:247
#6  0x000000000022f692 in fix_func (c=<optimized out>, data=<optimized out>) at exif-data.c:1159
#7  0x000000000022d748 in exif_data_foreach_content (data=0x4537b0, user_data=0x0, func=<optimized out>) at exif-data.c:1031
#8  exif_data_fix (d=0x4537b0) at exif-data.c:1176
#9  exif_data_load_data (data=0x4537b0, d_orig=<optimized out>, ds_orig=<optimized out>) at exif-data.c:871
#10 0x0000000000221886 in exif_loader_get_data (loader=<optimized out>) at /home/asuka/fuzzing101/fuzzing_libexif/libexif-libexif-0_6_14-release/libexif/exif-loader.c:387
#11 main (argc=<optimized out>, argc@entry=2, argv=<optimized out>, argv@entry=0x7fffffffdb28) at main.c:438
#12 0x00007ffff7cb9d90 in __libc_start_call_main (main=main@entry=0x21f100 <main>, argc=argc@entry=2, argv=argv@entry=0x7fffffffdb28) at ../sysdeps/nptl/libc_start_call_main.h:58
#13 0x00007ffff7cb9e40 in __libc_start_main_impl (main=0x21f100 <main>, argc=2, argv=0x7fffffffdb28, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffdb18) at ../csu/libc-start.c:392
#14 0x000000000021a9f5 in _start ()

第二个 crash 没能分析出来,网上说和第一个相似,经过实际调试后我认为不尽然

crash 3

#0  0x00007ffff7e3f586 in __memmove_evex_unaligned_erms () at ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:874
#1  0x000000000022f395 in exif_data_load_data_thumbnail (data=0x4537b0, d=0x4558e6 "II*", ds=7204, offset=2036, size=4294967295) at exif-data.c:292
#2  exif_data_load_data_content (data=<optimized out>, ifd=<optimized out>, d=<optimized out>, ds=<optimized out>, offset=794, recursion_depth=<optimized out>) at exif-data.c:381
#3  0x000000000022c6d2 in exif_data_load_data (data=0x4537b0, d_orig=<optimized out>, ds_orig=<optimized out>) at exif-data.c:835
#4  0x0000000000221886 in exif_loader_get_data (loader=<optimized out>) at /home/asuka/fuzzing101/fuzzing_libexif/libexif-libexif-0_6_14-release/libexif/exif-loader.c:387
#5  main (argc=<optimized out>, argc@entry=2, argv=<optimized out>, argv@entry=0x7fffffffdb28) at main.c:438
#6  0x00007ffff7cb9d90 in __libc_start_call_main (main=main@entry=0x21f100 <main>, argc=argc@entry=2, argv=argv@entry=0x7fffffffdb28) at ../sysdeps/nptl/libc_start_call_main.h:58
#7  0x00007ffff7cb9e40 in __libc_start_main_impl (main=0x21f100 <main>, argc=2, argv=0x7fffffffdb28, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffdb18) at ../csu/libc-start.c:392
#8  0x000000000021a9f5 in _start ()

前略,exif-data.c

static void
exif_data_load_data_content (ExifData *data, ExifIfd ifd,
                 const unsigned char *d,
                 unsigned int ds, unsigned int offset, unsigned int recursion_depth)
{
    ExifLong o, thumbnail_offset = 0, thumbnail_length = 0;
    ExifShort n;
    ExifEntry *entry;
    unsigned int i;
    ExifTag tag;

    if (!data || !data->priv) 
        return;
    if ((ifd < 0) || (ifd >= EXIF_IFD_COUNT)) 
        return;

    if (recursion_depth > 150) {
        exif_log (data->priv->log, EXIF_LOG_CODE_CORRUPT_DATA, "ExifData",
              "Deep recursion detected!");
        return;
    }

    /* Read the number of entries */
    if (offset >= ds - 1) 
        return;
    n = exif_get_short (d + offset, data->priv->order);
    exif_log (data->priv->log, EXIF_LOG_CODE_DEBUG, "ExifData",
              "Loading %i entries...", n);
    offset += 2;

    /* Check if we have enough data. */
    if (offset + 12 * n > ds) 
        n = (ds - offset) / 12;

    for (i = 0; i < n; i++) {

        tag = exif_get_short (d + offset + 12 * i, data->priv->order);
        switch (tag) {
        case EXIF_TAG_EXIF_IFD_POINTER:
        case EXIF_TAG_GPS_INFO_IFD_POINTER:
        case EXIF_TAG_INTEROPERABILITY_IFD_POINTER:
        case EXIF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH:
        case EXIF_TAG_JPEG_INTERCHANGE_FORMAT:
            o = exif_get_long (d + offset + 12 * i + 8,     // o = 4294967295
                       data->priv->order);
            switch (tag) {
            // ...
            case EXIF_TAG_JPEG_INTERCHANGE_FORMAT_LENGTH:
                thumbnail_length = o;
                if (thumbnail_offset && thumbnail_length)
                    exif_data_load_data_thumbnail (data, d,
                                       ds, thumbnail_offset,    // ds = 7204, thumbnail_offset=2036
                                       thumbnail_length);       // thumbnail_length = o = 4294967295
            // ...
static void
exif_data_load_data_thumbnail (ExifData *data, const unsigned char *d,
                   unsigned int ds, ExifLong offset, ExifLong size)
{
    if (ds < offset + size) {                                           // check
        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);    // crash
}

offset + 4294967295 溢出为 (offset - 1) < ds,故而若不检查 size,只需要 offset < ds + 1 就能触发漏洞

至于这个巨大的 size 是怎么来的没分析出来,可能是前面读 data 的时候越界了

修复方案如下

- if (ds < offset + size) {
+ if ((uint64_t)ds < (uint64_t)offset + (uint64_t)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;
}

hangs

hangs 是指由于特殊原因导致超时(如死循环、无限递归等)的用例

#0  0x0000000000220378 in exif_loader_write (eld=<optimized out>, buf=0x7fffffffd210 "", len=0) at /home/asuka/fuzzing101/fuzzing_libexif/libexif-libexif-0_6_14-release/libexif/exif-loader.c:264
#1  exif_loader_write_file (l=<optimized out>, path=<optimized out>) at /home/asuka/fuzzing101/fuzzing_libexif/libexif-libexif-0_6_14-release/libexif/exif-loader.c:120
#2  main (argc=<optimized out>, argc@entry=2, argv=<optimized out>, argv@entry=0x7fffffffdb38) at main.c:437
#3  0x00007ffff7cb9d90 in __libc_start_call_main (main=main@entry=0x21f100 <main>, argc=argc@entry=2, argv=argv@entry=0x7fffffffdb38) at ../sysdeps/nptl/libc_start_call_main.h:58
#4  0x00007ffff7cb9e40 in __libc_start_main_impl (main=0x21f100 <main>, argc=2, argv=0x7fffffffdb38, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffdb28) at ../csu/libc-start.c:392
#5  0x000000000021a9f5 in _start ()

先来看看这个只有两行的神奇玩意,这到底是怎么生成出来的()

导致这个 hangs 的原因是无限递归

前略,exif-loader.c

void
exif_loader_write_file (ExifLoader *l, const char *path)
{
    FILE *f;
    int size;
    unsigned char data[1024];

    if (!l) 
        return;

    f = fopen (path, "rb");
    if (!f) {
        exif_log (l->log, EXIF_LOG_CODE_NONE, "ExifLoader",
              _("The file '%s' could not be opened."), path);
        return;
    }
    while (1) {
        size = fread (data, 1, sizeof (data), f);
        if (size <= 0)                              // check size
            break;
        if (!exif_loader_write (l, data, size))     // here in
            break;
    }
    fclose (f);
}
unsigned char
exif_loader_write (ExifLoader *eld, unsigned char *buf, unsigned int len)
{
    // ...
    /*
     * First fill the small buffer. Only continue if the buffer
     * is filled. Note that EXIF data contains at least 12 bytes.
     */

    // ...
    /*
     * If we reach this point, the buffer has not been big enough
     * to read all data we need. Fill it with new data.
     */
    eld->b_len = 0;
    return exif_loader_write (eld, buf, len);       // hangs
}

流程如下

  1. main函数调用exif_loader_write_file读取文件
  2. exif_loader_write_file打开一个文件描述符,从文件内每次读 1024 字节进 buffer,并调用exif_loader_write把 buffer 内的信息导入ExifLoader
  3. exif_loader_write函数首先经过一个状态机确保完全读入 exif 信息,如果抵达函数的末尾则证明 buffer 没有大到能存完所有数据,于是递归向 buffer 填充新的数据

观察到递归函数的入口前有判定 len 是否为 0,说明开发者不希望 len 为 0
第一次调用函数时 len 值为 32,但经过跟踪发现 len 在递归的过程中逐渐减为 0,但函数内部没有对 len 进行任何判定,说明状态机对状态的判定遗漏导致了无限递归

官方的修复方案

/*
     * If we reach this point, the buffer has not been big enough
     * to read all data we need. Fill it with new data.
     */
+    if (!len)
+        return 1;
    eld->b_len = 0;
    return exif_loader_write (eld, buf, len);       // hangs
}

CVE-2009-3895

这个漏洞并没有找到,求证后发现原因是此漏洞并不影响我们 fuzzing 的版本,如果想了解可以参看大佬的文章