┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
CVE-2016-9297: LibTIFF
~ CuB3y0nd
# CVE-2016-9297

## Description

CVE: https://www.cve.org/CVERecord?id=CVE-2016-9297

# Compile

## Download

 Mitre  description  4.0.6  chall  description  4.
0.4 4.0.4 

```shellsession
git clone https://gitlab.com/libtiff/libtiff.git && cd libtiff
git checkout v4.0.4
```

## Build

```shellsession
mkdir ../libtiff-build-fuzz
cd ../libtiff-build-fuzz
CC=afl-clang-lto 
CXX=afl-clang-lto++ 
../libtiff/configure 
  --prefix="$(realpath ../libtiff-fuzz)" 
  --disable-shared
make clean && make -j`nproc` && make install

mkdir ../libtiff-build-fuzz-asan
cd ../libtiff-build-fuzz-asan
AFL_USE_ASAN=1 
CC=afl-clang-lto 
CXX=afl-clang-lto++ 
../libtiff/configure 
  --prefix="$(realpath ../libtiff-fuzz-asan)" 
  --disable-shared
make clean && AFL_USE_ASAN=1 make -j`nproc` && AFL_USE_ASAN=1 make install
```

make  `tif_predict.h` 

```c
#include "tiffio.h"
#include "tiffiop.h"
```

# Samples

```python
#!/usr/bin/env python

import os
import subprocess
import struct

def run_cmd(cmd):
    try:
        subprocess.run(cmd, check=True, capture_output=True)
    except subprocess.CalledProcessError as e:
        print(f"Error running {' '.join(cmd)}: {e}")

def create_minimal_tiff(filename, width=8, height=8):
    # Minimal 8x8 BW TIFF (1 bit per pixel)
    # Header
    header = b'II' + struct.pack('<HI', 42, 8) # II, 42, offset to IFD=8

    # IFD
    num_entries = 11
    ifd_offset = 8
    next_ifd_offset = 0

    # Tags:
    # 256 (Width), 257 (Height), 258 (BitsPerSample), 259 (Compression),
    # 262 (Photometric), 273 (StripOffsets), 277 (SamplesPerPixel),
    # 278 (RowsPerStrip), 279 (StripByteCounts), 282 (XRes), 283 (YRes)

    # Each entry is 12 bytes
    entries = []
    def add_entry(tag, type, count, value):
        entries.append(struct.pack('<HHII', tag, type, count, value))

    add_entry(256, 3, 1, width)  # Width
    add_entry(257, 3, 1, height) # Height
    add_entry(258, 3, 1, 1)      # BitsPerSample
    add_entry(259, 3, 1, 1)      # Compression (None)
    add_entry(262, 3, 1, 1)      # Photometric (MinIsBlack)
    add_entry(273, 4, 1, 8 + 2 + num_entries*12 + 4 + 16) # StripOffsets (after 
IFD and Res values)
    add_entry(277, 3, 1, 1)      # SamplesPerPixel
    add_entry(278, 3, 1, height) # RowsPerStrip
    add_entry(279, 4, 1, (width * height + 7) // 8) # StripByteCounts
    add_entry(282, 5, 1, 8 + 2 + num_entries*12 + 4) # XRes (offset)
    add_entry(283, 5, 1, 8 + 2 + num_entries*12 + 4 + 8) # YRes (offset)

    ifd = struct.pack('<H', num_entries) + b''.join(entries) + struct.pack('<I',
 next_ifd_offset)

    # Rational values for XRes, YRes (72/1)
    res_values = struct.pack('<IIII', 72, 1, 72, 1)

    # Image data (all zeros for simplicity)
    data = b'x00' * ((width * height + 7) // 8)

    with open(filename, 'wb') as f:
        f.write(header)
        f.write(ifd)
        f.write(res_values)
        f.write(data)

def create_grayscale_tiff(filename, width=8, height=8, bpp=8):
    # Header
    header = b'II' + struct.pack('<HI', 42, 8)

    num_entries = 11
    entries = []
    def add_entry(tag, type, count, value):
        entries.append(struct.pack('<HHII', tag, type, count, value))

    data_offset = 8 + 2 + num_entries*12 + 4 + 16

    add_entry(256, 3, 1, width)  # Width
    add_entry(257, 3, 1, height) # Height
    add_entry(258, 3, 1, bpp)    # BitsPerSample
    add_entry(259, 3, 1, 1)      # Compression
    add_entry(262, 3, 1, 1)      # Photometric (MinIsBlack)
    add_entry(273, 4, 1, data_offset) # StripOffsets
    add_entry(277, 3, 1, 1)      # SamplesPerPixel
    add_entry(278, 3, 1, height) # RowsPerStrip
    add_entry(279, 4, 1, width * height * (bpp // 8)) # StripByteCounts
    add_entry(282, 5, 1, 8 + 2 + num_entries*12 + 4) # XRes
    add_entry(283, 5, 1, 8 + 2 + num_entries*12 + 4 + 8) # YRes

    ifd = struct.pack('<H', num_entries) + b''.join(entries) + struct.pack('<I',
 0)
    res_values = struct.pack('<IIII', 72, 1, 72, 1)
    data = b'x80' * (width * height * (bpp // 8)) # Gray data

    with open(filename, 'wb') as f:
        f.write(header)
        f.write(ifd)
        f.write(res_values)
        f.write(data)

def create_rgb_tiff(filename, width=8, height=8):
    header = b'II' + struct.pack('<HI', 42, 8)
    num_entries = 12
    entries = []
    def add_entry(tag, type, count, value):
        entries.append(struct.pack('<HHII', tag, type, count, value))

    bps_offset = 8 + 2 + num_entries*12 + 4
    res_offset = bps_offset + 6
    data_offset = res_offset + 16

    add_entry(256, 3, 1, width)  # Width
    add_entry(257, 3, 1, height) # Height
    add_entry(258, 3, 3, bps_offset) # BitsPerSample (3 values)
    add_entry(259, 3, 1, 1)      # Compression
    add_entry(262, 3, 1, 2)      # Photometric (RGB)
    add_entry(273, 4, 1, data_offset) # StripOffsets
    add_entry(277, 3, 1, 3)      # SamplesPerPixel
    add_entry(278, 3, 1, height) # RowsPerStrip
    add_entry(279, 4, 1, width * height * 3) # StripByteCounts
    add_entry(282, 5, 1, res_offset) # XRes
    add_entry(283, 5, 1, res_offset + 8) # YRes
    add_entry(284, 3, 1, 1)      # PlanarConfig (Contig)

    ifd = struct.pack('<H', num_entries) + b''.join(entries) + struct.pack('<I',
 0)
    bps_values = struct.pack('<HHH', 8, 8, 8)
    res_values = struct.pack('<IIII', 72, 1, 72, 1)
    data = b'xffx00x00' * (width * height) # Red image

    with open(filename, 'wb') as f:
        f.write(header)
        f.write(ifd)
        f.write(bps_values)
        f.write(res_values)
        f.write(data)

def create_palette_tiff(filename, width=8, height=8):
    header = b'II' + struct.pack('<HI', 42, 8)
    num_entries = 12
    entries = []
    def add_entry(tag, type, count, value):
        entries.append(struct.pack('<HHII', tag, type, count, value))

    cmap_offset = 8 + 2 + num_entries*12 + 4
    res_offset = cmap_offset + 256 * 3 * 2 # 256 colors, RGB, 2 bytes each
    data_offset = res_offset + 16

    add_entry(256, 3, 1, width)  # Width
    add_entry(257, 3, 1, height) # Height
    add_entry(258, 3, 1, 8)      # BitsPerSample
    add_entry(259, 3, 1, 1)      # Compression
    add_entry(262, 3, 1, 3)      # Photometric (Palette)
    add_entry(273, 4, 1, data_offset) # StripOffsets
    add_entry(277, 3, 1, 1)      # SamplesPerPixel
    add_entry(278, 3, 1, height) # RowsPerStrip
    add_entry(279, 4, 1, width * height) # StripByteCounts
    add_entry(282, 5, 1, res_offset) # XRes
    add_entry(283, 5, 1, res_offset + 8) # YRes
    add_entry(320, 3, 256 * 3, cmap_offset) # ColorMap

    ifd = struct.pack('<H', num_entries) + b''.join(entries) + struct.pack('<I',
 0)

    # Color map: 256 RGB values, each 2 bytes (16-bit)
    cmap = b''
    for i in range(256): cmap += struct.pack('<H', i * 256) # Red
    for i in range(256): cmap += struct.pack('<H', i * 256) # Green
    for i in range(256): cmap += struct.pack('<H', i * 256) # Blue

    res_values = struct.pack('<IIII', 72, 1, 72, 1)
    data = bytes([i % 256 for i in range(width * height)])

    with open(filename, 'wb') as f:
        f.write(header)
        f.write(ifd)
        f.write(cmap)
        f.write(res_values)
        f.write(data)

def main():
    corpus_dir = "corpus"
    if not os.path.exists(corpus_dir):
        os.makedirs(corpus_dir)

    base_tiff = os.path.join(corpus_dir, "base_bw.tif")
    create_minimal_tiff(base_tiff)

    base_gray8 = os.path.join(corpus_dir, "base_gray8.tif")
    create_grayscale_tiff(base_gray8, bpp=8)

    base_gray16 = os.path.join(corpus_dir, "base_gray16.tif")
    create_grayscale_tiff(base_gray16, bpp=16)

    base_rgb = os.path.join(corpus_dir, "base_rgb.tif")
    create_rgb_tiff(base_rgb)

    base_palette = os.path.join(corpus_dir, "base_palette.tif")
    create_palette_tiff(base_palette)

    bases = [base_tiff, base_gray8, base_gray16, base_rgb, base_palette]

    # Use tiffcp to create variations
    compressions = ["none", "lzw", "zip", "packbits", "zstd"]
    for base in bases:
        base_name = os.path.basename(base).split('.')[0]
        for comp in compressions:
            out = os.path.join(corpus_dir, f"{base_name}_{comp}.tif")
            run_cmd(["tiffcp", "-c", comp, base, out])

            out_tile = os.path.join(corpus_dir, f"{base_name}_{comp}_tile.tif")
            run_cmd(["tiffcp", "-c", comp, "-t", "-w", "8", "-l", "8", base, out
_tile])

    # Special compressions for BW
    for comp in ["g3", "g4"]:
        run_cmd(["tiffcp", "-c", comp, base_tiff, os.path.join(corpus_dir, f"bw_
{comp}.tif")])

    # JPEG for RGB
    run_cmd(["tiffcp", "-c", "jpeg", base_rgb, os.path.join(corpus_dir, "rgb_jpe
g.tif")])

    # BigTIFF
    run_cmd(["tiffcp", "-8", base_rgb, os.path.join(corpus_dir, "bigtiff_rgb.tif
")])

    # Byte order
    run_cmd(["tiffcp", "-B", base_rgb, os.path.join(corpus_dir, "big_endian_rgb.
tif")])

    # Planar configuration
    run_cmd(["tiffcp", "-p", "separate", base_rgb, os.path.join(corpus_dir, "sep
arate_rgb.tif")])

    # Add some tags
    tagged_tiff = os.path.join(corpus_dir, "tagged_rgb.tif")
    run_cmd(["cp", base_rgb, tagged_tiff])
    run_cmd(["tiffset", "-s", "315", "Fuzzer", tagged_tiff])

    # Broken cases
    # Circular IFD (from base_bw)
    circular = os.path.join(corpus_dir, "circular.tif")
    with open(base_tiff, 'rb') as f:
        data = bytearray(f.read())
    # Find next_ifd_offset (last 4 bytes of IFD)
    # IFD starts at 8, has 11 entries (11*12=132), + 2 bytes for count = 134.
    # next_ifd_offset is at index 142
    if len(data) >= 146:
        data[142:146] = struct.pack('<I', 8)
        with open(circular, 'wb') as f:
            f.write(data)

    # Huge dimensions
    huge_dim = os.path.join(corpus_dir, "huge_dim.tif")
    create_minimal_tiff(huge_dim)
    run_cmd(["tiffset", "-s", "256", "1000000", huge_dim])
    run_cmd(["tiffset", "-s", "257", "1000000", huge_dim])

    # Invalid compression tag
    invalid_comp = os.path.join(corpus_dir, "invalid_comp.tif")
    create_minimal_tiff(invalid_comp)
    run_cmd(["tiffset", "-s", "259", "65535", invalid_comp])

    print(f"Generated {len(os.listdir(corpus_dir))} samples in {corpus_dir}")

if __name__ == "__main__":
    main()
```


 AI `test/images/` 

# Fuzzing



 Mitre  references [Bug 2590 - CVE-2016-9297: segfault in _TIFFP
rintField (tif_print.c:127)](http://bugzilla.maptools.org/show_bug.cgi?id=2590) 


> Triggered in libtiff 4.0.6 with AFL and ASAN. Only crashes if I LD_PRELOAD AFL
's libdislocator (more info: https://github.com/mirrorer/afl/tree/master/libdisl
ocator).
>
> `LD_PRELOAD=/root/afl-2.35b/libdislocator/libdislocator.so ./tiffinfo -i test0
00`

 fuzz  `tiffinfo` 
便线 fuzz

```shellsession
mkdir -p /dev/shm/{normal,asan}

AFL_TMPDIR=/dev/shm/asan 
afl-fuzz -i corpus 
         -o outs 
         -m none 
         -M asan 
         -- ../libtiff-fuzz-asan/bin/tiffinfo -Dcjrsw @@

AFL_TMPDIR=/dev/shm/normal 
afl-fuzz -i corpus 
         -o outs 
         -m none 
         -S normal 
         -- ../libtiff-fuzz/bin/tiffinfo -Dcjrsw @@
```

ASAN 线 crashes ……
# Analysis

## Coverage

 chall 使 [lcov](https://github.com/linux-test-project/lcov) 
 llvm-cov lcov 

 cov 

```shellsession
mkdir ../libtiff-build-cov
cd libtiff-build-cov
CC=clang 
CXX=clang++ 
CFLAGS="-fprofile-instr-generate -fcoverage-mapping" 
CXXFLAGS="$CFLAGS" 
../libtiff/configure 
  --prefix="$(realpath ../libtiff-cov)" 
  --disable-shared
make clean && AFL_USE_ASAN=1 make -j`nproc` && AFL_USE_ASAN=1 make install
```

使

```python
#!/usr/bin/env python

import os
import subprocess
import glob
import argparse

def run_cmd(cmd, env=None):
    """Run a shell command and return the output."""
    try:
        result = subprocess.run(cmd, shell=True, env=env, capture_output=True, t
ext=True)
        return result.returncode, result.stdout, result.stderr
    except Exception as e:
        return 1, "", str(e)

def main():
    parser = argparse.ArgumentParser(description="Generate llvm-cov report from 
AFL++ samples.")
    parser.add_argument("--bin", required=True, help="Path to the instrumented b
inary.")
    parser.add_argument("--samples", required=True, help="Directory containing A
FL++ samples (e.g., output/default/queue).")
    parser.add_argument("--out", default="coverage_report", help="Directory to s
ave the HTML report (default: coverage_report).")
    parser.add_argument("--profraw-dir", default="profraws", help="Directory to 
store intermediate .profraw files.")
    parser.add_argument("--args", default="", help="Additional arguments to pass
 to the binary (use {} as placeholder for sample path).")

    args = parser.parse_args()

    # Create directories
    if not os.path.exists(args.profraw_dir):
        os.makedirs(args.profraw_dir)
    if not os.path.exists(args.out):
        os.makedirs(args.out)

    # Find all samples
    samples = glob.glob(os.path.join(args.samples, "*"))
    # Filter only files (AFL++ directories might have subfolders)
    samples = [s for s in samples if os.path.isfile(s)]

    if not samples:
        print(f"No samples found in {args.samples}")
        return

    print(f"Found {len(samples)} samples. Running coverage...")

    # Set up environment for LLVM profile generation
    # %p is PID, %m is binary hash to avoid collisions
    env = os.environ.copy()

    for i, sample in enumerate(samples):
        profraw_path = os.path.abspath(os.path.join(args.profraw_dir, f"sample_{
i}.profraw"))
        env["LLVM_PROFILE_FILE"] = profraw_path

        # Construct command
        if "{}" in args.args:
            cmd = f"{args.bin} {args.args.format(sample)}"
        else:
            cmd = f"{args.bin} {args.args} {sample}"

        print(f"[{i+1}/{len(samples)}] Running: {cmd}", end="r")
        run_cmd(cmd, env=env)

    print("nMerging profile data...")
    profdata_file = "merged.profdata"
    profraw_pattern = os.path.join(args.profraw_dir, "*.profraw")
    merge_cmd = f"llvm-profdata merge -sparse {profraw_pattern} -o {profdata_fil
e}"
    rc, stdout, stderr = run_cmd(merge_cmd)

    if rc != 0:
        print(f"Error merging data: {stderr}")
        return

    print(f"Generating HTML report in {args.out}...")
    report_cmd = f"llvm-cov show {args.bin} -instr-profile={profdata_file} -form
at=html -output-dir={args.out}"
    rc, stdout, stderr = run_cmd(report_cmd)

    if rc != 0:
        print(f"Error generating report: {stderr}")
        return

    # Also generate a summary report
    print("nCoverage Summary:")
    summary_cmd = f"llvm-cov report {args.bin} -instr-profile={profdata_file}"
    rc, stdout, stderr = run_cmd(summary_cmd)
    print(stdout)

    print(f"Report generated successfully in directory: {args.out}")
    print(f"You can view it by opening {os.path.join(args.out, 'index.html')} in
 a browser.")

if __name__ == "__main__":
    main()
```

```shellsession
./gen_coverage.py --bin ./bin/tiffinfo --args "-Dcjrsw {}" --samples ../libtiff-
workshop/outs/asan/queue
```

 web 
 libtiffchall 
 fuzz  AI  fuzzer 
 Fuzzing 101  CVE  crash 
 crash