AFL++实战其四

练习四 - LibTIFF

版本:libtiff 4.0.4

漏洞编号:CVE-2016-9297

CVE-2016-9297 是一个越界读取漏洞,可以通过精心设计的 TIFF_SETGET_C16ASCII 或 TIFF_SETGET_C32_ASCII 标签值触发。

越界读取是指程序读取数据超过预期缓冲区的结束点或开始时发生的漏洞。

因此,它允许远程攻击者实施拒绝服务,或可能从进程内存中获取潜在敏感信息


下载LibTIFF

1
2
3
4
mkdir $HOME/fuzz_main/
cd $HOME/fuzz_main/
wget https://github.com/libsdl-org/libtiff/archive/refs/tags/v4.0.4.tar.gz
tar -xzvf v4.0.4.tar.gz

测试覆盖范围

一般命令行类工具都提供了额外参数来实现不同额外功能,对于我们 fuzz 来说,能通过参数组合启用程序越多的功能越好,意味着这会覆盖更多的代码。

我们将采用工具 LCOV 来测试覆盖率

首先我们得安装 LCOV:

1
sudo apt install lcov

使用--coverage标志编译 libTIFF:

1
2
3
4
5
cd $HOME/fuzz_main/
cd $HOME/fuzz_main/libtiff-4.0.4/
CFLAGS="--coverage" LDFLAGS="--coverage" ./configure --prefix="$HOME/fuzz_main/cov_install/" --disable-shared
make
make install

通过以下输入收集代码的覆盖率:

1
2
3
4
5
6
7
mkdir $HOME/fuzz_main/fuzz/
cd $HOME/fuzz_main/fuzz/
lcov --zerocounters --directory $HOME/fuzz_main/libtiff-4.0.4 # 清零代码计数器,如果你不想多条测试命令覆盖率被叠加就得在每次收集覆盖率前执行这条命令
lcov --capture --initial --directory $HOME/fuzz_main/libtiff-4.0.4 --output-file app.info --ignore-errors source # 建立覆盖率数据,表示无输入下的默认覆盖率,加--ignore-errors source 的原因是因为lcov有时候会碰到记录编译时的临时文件而报错
$HOME/fuzz_main/install/bin/tiffinfo -D -j -c -s -r -w $HOME/fuzz_main/libtiff-4.0.4/test/images/logluv-3c-16b.tiff # 这是测试语句,你可以改变它的参数,但是每次改变都需要清零计数器
lcov --no-checksum --directory $HOME/fuzz_main/libtiff-4.0.4 --capture --output-file app2.info # 这是将当前测试语句的覆盖率生成信息文件
genhtml --highlight --legend -output-directory ./app2/ ./app2.info # 这是将覆盖率的信息文件与源代码合成可视化网页,其中保护覆盖率以及具体哪行代码未被覆盖

如果操作正确,你可以点开 app2 下的 index.htm :

我们可以尝试另外一条命令:

1
2
3
4
5
lcov --zerocounters --directory $HOME/fuzz_main/libtiff-4.0.4 
lcov --capture --initial --directory $HOME/fuzz_main/libtiff-4.0.4 --output-file app.info --ignore-errors source
$HOME/fuzz_main/install/bin/tiffinfo -D -d -j -c -s -w $HOME/fuzz_main/libtiff-4.0.4/test/images/logluv-3c-16b.tiff
lcov --no-checksum --directory $HOME/fuzz_main/libtiff-4.0.4 --capture --output-file app1.info
genhtml --highlight --legend -output-directory ./app1/ ./app1.info

此时点开 app1 下的 index.html :

明显第二次覆盖率达到了 Functions:13.0% ; Lines: 8.0%

所以我们 fuzz 时就应该使用第二次测试用的参数。


准备fuzz

1
2
3
4
5
6
cd $HOME/fuzz_main/libtiff-4.0.4/
make clean
export LLVM_CONFIG="llvm-config-11"
CC=afl-clang-lto ./configure --prefix="$HOME/fuzz_main/install/" --disable-shared
AFL_USE_ASAN=1 make -j4
AFL_USE_ASAN=1 make install

使用以下命令 fuzz:

1
afl-fuzz -m none -i $HOME/fuzz_main/libtiff-4.0.4/test/images/ -o ./out -s 123 -- $HOME/fuzz_main/install/bin/tiffinfo -D -d -j -c -s -w @@

这次 fuzz 应该非常快,不出10分钟应该会有十几个独特崩溃。


源码分析

根据错误输出我们可以看到这又是一个堆溢出漏洞。

我们根据代码调用追踪到 tif_print.c:126 :

1
2
3
4
5
6
7
8
9
10
11
12
// ...
else if(fip->field_type == TIFF_DOUBLE)
fprintf(fd, "%f", ((double *) raw_data)[j]);
else if(fip->field_type == TIFF_ASCII) { // tif_print.c:126
fprintf(fd, "%s", (char *) raw_data);
break;
}
else {
fprintf(fd, "<unsupported data type in TIFFPrint>");
break;
}
// ...

这里raw_data指向的是 0x5555555d8ee0 ,该位置的值:

1
2
3
4
5
6
7
8
9
10
11
-exec x/20gx 0x5555555d8ee0
0x5555555d8ee0: 0x00005555555d424d 0x00005555555d42b0
0x5555555d8ef0: 0x00005555555d42e0 0x0000000000000021
0x5555555d8f00: 0x0000000000000008 0x00005555555d4370
0x5555555d8f10: 0x00005555555d43a0 0x0000000000000021
0x5555555d8f20: 0x0000000000000008 0x00005555555d4430
0x5555555d8f30: 0x00005555555d4460 0x0000000000000411
0x5555555d8f40: 0x3536206761542020 0x756769203a303330
0x5555555d8f50: 0x203a6e6f69746172 0x6920656c676e6973
0x5555555d8f60: 0x616c70206567616d 0x00005555550a656e
0x5555555d8f70: 0x00005555555d45e0 0x00005555555d4610

我们发现一旦输出这里的值0x00005555555d424d就会包含一个 PIE 地址,有泄漏 PIE 基地址的风险。

那么为什么要输出这里的值呢?

经过调试断点发现当断点在setByteArray(第二段崩溃链发现)时继续运行就会去到上面那个输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
static void
setByteArray(void** vpp, void* vp, size_t nmemb, size_t elem_size)
{
if (*vpp)
_TIFFfree(*vpp), *vpp = 0;
if (vp) {
tmsize_t bytes = (tmsize_t)(nmemb * elem_size);
if (elem_size && bytes / elem_size == nmemb)
*vpp = (void*) _TIFFmalloc(bytes); // 关键点
if (*vpp)
_TIFFmemcpy(*vpp, vp, bytes);
}
}

经过调用链我们可以发现_TIFFmalloc(bytes)最终会调用malloc申请bytes大小的空间。

在我调试这里的时候bytes = 1,这意味着经过程序只需要一个字节空间,但是 Linux 系统会分配 0x20 大小的空间。

并且后面可以直接看到如果申请成功,vp会复制到申请到的内存中,而这个vp = 0x4d ,这也就是上面泄漏的0x00005555555d424d的最后一个字节。

修复

我觉得可以在复制到目标之前清空目标地址的空间内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void
setByteArray(void** vpp, void* vp, size_t nmemb, size_t elem_size)
{
if (*vpp)
_TIFFfree(*vpp), *vpp = 0;
if (vp) {
tmsize_t bytes = (tmsize_t)(nmemb * elem_size);
if (elem_size && bytes / elem_size == nmemb)
*vpp = (void*) _TIFFmalloc(bytes+1); // 关键点
if (*vpp){
_TIFFmemcpy(*vpp, vp, bytes);
*((char *)(*vpp) + bytes) = '\x00';
}
}
}

实际上官方补丁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static void
setByteArray(void** vpp, void* vp, size_t nmemb, size_t elem_size)
{
if (*vpp)
_TIFFfree(*vpp), *vpp = 0;
if (vp) {
// 正确的溢出检查:乘法前检查
if (nmemb == 0 || elem_size == 0) // 排除0
return;

// 关键:检查乘法是否会溢出
if (nmemb > SIZE_MAX / elem_size) // 溢出检查!
return; // 或错误处理

tmsize_t bytes = (tmsize_t)(nmemb * elem_size);

// 双重检查
if (bytes / elem_size != nmemb)
return;

*vpp = (void*) _TIFFmalloc(bytes+1);
if (*vpp) {
_TIFFmemcpy(*vpp, vp, bytes);
((char*)(*vpp))[bytes] = '\0'; // 安全的null终止
}
}
}

好吧我确实没有想到nmemb * elem_size可能会溢出。

不过我的补丁至少暂时有效 QvQ。


AFL++实战其四
https://zlsf-zl.github.io/2026/04/08/AFL-实战其四/
作者
ZLSF
发布于
2026年4月8日
许可协议