TerminalFix: PNG Steganography

    Published: 2026-09-21. Last Updated: 2026-09-21 10:25:05 UTC
    by Didier Stevens (Version: 1)
    0 comment(s)

    Microsoft Security Research published an interesting blog post "TerminalFix campaign deploys a reverse tunnel through multistage intrusion" about a malware campaign. The aspect that I want to take a closer look at, is the fact that the threat actors used PNG files with steganography. I reached out to the researchers and they kindly shared the IOCs for the PNG files with me.

    The first image is a small PNG file (f5f1eb6d43dd61d5b069c250e5c666384f7417d0c95014773bf9edf8ff13bebe).

    Analysis with pngdump.py reveals that this is a proper PNG file:

    There is no data appended to the end of the image. The image starts with the expected header, and has the 3 type of chunks one expects to find in a minimal PNG file: IHDR, IDAT and IEND. There is no metadata that can contain a payload. The IDAT chunk contains the pixels of the image: encoded and ZLIB compressed. The fact that pngdump was able to decompress the IDAT data, is another indication that this is a PNG image with valid IDAT data. It consists of 111 lines and 112 columns. All of the filters of the scanlines are known filters: yet another indication that this is valid data. Thus everything indicates that this is a valid image. If it contains a payload, then it hides in the pixels, and we can confirm that steganography is used.

    Let's take a look at the decompressed IDAT data (the scanlines that make up the image):

    We can see strings like MZ, .text, .data, ... All strings often found inside a Windows executable (PE file). It's very likely that there is a PE file hiding in the pixel data, but what we see here is the encoded pixel data.

    The picture consists of 111 scanlines. The first byte is the type of the filter. Value 01, seen at the start of the decompressed data, indicates that the first line is encoded with the SUB (subtract) filter. 8 bits are used to encode pixel data, and the colortype is 6, e.g., RGBA. This means that there are 4 bytes for each pixel: 1 for Red, 1 for Green, 1 for Blue and 1 for Alpha (transparency). 112 columns times 4 bytes gives 448 bytes per scanline.

    A filter of type SUB means that the RGBA values of a pixel are substracted from the RGBA values of the previous pixel (except for the first pixel). Thus this is a transformation that needs to be applied to obtain the raw pixel data. And SUB is not the only filter, there are 5 different filter types.

    But the easiest way to obtain the raw bitmat data is to use option -R (--raw):

    We can now see the familiar DOS stub "This program ...".

    The first 8 bytes of this raw bitmap is actually the length of the embedded PE file.

    It's stored little-endian:

    We can use this length (49720) to carve out the PE file and calculate its hashes:

    The embedded PE file is a genuine Microsoft executable. That executable, LockScreenContentServer.exe, is used for sideloading.

    So the payload is indeed stored inside the PNG file using steganography. Usually, when using steganography, only some of the available bits will be used to store the payload, and the others will be left untouched. If this is done with least significant bits, the medium can still be rendered, and is hard to visually distinguish from the original medium.

    But in this campaign, the threat actors decided to use all of the available bits. The original medium is lost, and the PNG looks like this when rendered:

    The 2 other PNG files contain the malicious payload (a DLL), stored in 2 parts, also using all the bits.

    This image contains the first part:

    And this image contains the second part:

    Concatenating both parts gives the payload, a malicious DLL.

     

    Didier Stevens
    Senior handler
    blog.DidierStevens.com

    Keywords:
    0 comment(s)
    ISC Stormcast For Monday, September 21st, 2026 https://isc.sans.edu/podcastdetail/10102

      Comments


      Diary Archives