Steganography Analysis With pngdump.py

    Published: 2025-04-26. Last Updated: 2025-04-26 06:45:13 UTC
    by Didier Stevens (Version: 1)
    0 comment(s)

    I like it when a diary entry like "Example of a Payload Delivered Through Steganography" is published: it gives me an opportunity to test my tools, in particular pngdump.py, a tool to analyze PNG files.

    A PNG file consists of a header followed by chunks. pngdump.py shows this (sample c2219ddbd3456e3df0a8b10c7bbdf018da031d8ba5e9b71ede45618f50f2f4b6):

    The IHDR chunk gives us information about the image: it's 31744 pixels wide and 1 pixel high. That's already unusual!

    There are 8 bits and the colortype is 6. That's RGBA (Red, Green, Blue and Alpha/Transparency). So there are 4 channels in total with a resolution of 8 bits, thus 4 bytes per pixel.

    The IDAT chunk contains the compressed (zlib) image: there's only one IDAT chunk in this image. And the line filter is None: this means that the pixel data is not encoded.

    So let's take a look at the decompressed raw pixel data:

    We see the letters M and Z: these letters are characteristic for the header of a PE file.

    And a bit further we see the letters "This program ...".

    So it's clear that a PE file is embedded in the pixel data.

    Every fourth byte is a byte of the PE file, and the first byte of the PE file is the second byte of the pixel data: so the PE file is embedded in the second channel of the pixel data.

    We can select these bytes with a tool like translate.py, that takes a Python function to manipulate the bytes it receives.

    data[1::4] is a Python slice that starts with the second byte (position 1), ends when there is no more data, and selects every fourth byte (a step of 4). This allows us to extract the second channel:

    pngdump.py's option -d performs a binary dump, piping the raw pixel data into translate.py. translate.py's option -f reads all the data in one go (by default, translate.py operates byte per byte). lambda data: data[11:4] is a Python function that takes the raw pixel data as input (data) and returns the bytes of the second channel via a Python slice that selects every fourth byte starting with the second byte of the raw pixel data.

    Finally, the extracted PE file is piped into file-magic.py to identify the file type: it is a .NET file, as Xavier explained.

    We can also check that it is a PE file with a tool like pecheck.py:

    And we can calculate the hashes with hash.py to look up the file on VirusTotal:

    This is the SHA256 of the extracted PE file: 8f4cea5d602eaa4e705ef62e2cf00f93ad4b03fb222c35ab39f64c24cdb98462.

    It's clear that the steganography works in this example: 0 detections for the PNG file on VirusTotal, and 49 detections for the embedded PE file.

     

     

     

    Didier Stevens
    Senior handler
    blog.DidierStevens.com

    Keywords:
    0 comment(s)

      Comments


      Diary Archives