03 April 2024
Claudio Contin
Introduction
When writing malicious payloads for red teaming, it is common to hide the shellcode, rather than including it in plan within the payload itself. Commonly used techniques to hide it are:
- Download it from a remote web server, SMB share, FTP server, etc.
- Include it within the payload itself in an encoded or encrypted way, and decrypt it at runtime
The below Steganography description is extracted from Wikipedia:
Steganography (/ˌstɛɡəˈnɒɡrəfi/ ⓘ STEG-ə-NOG-rə-fee) is the practice of representing information within another message or physical object, in such a manner that the presence of the information is not evident to human inspection. In computing/electronic contexts, a computer file, message, image, or video is concealed within another file, message, image, or video. The word steganography comes from Greek steganographia, which combines the words steganós (στεγανός), meaning "covered or concealed", and -graphia (γραφή) meaning "writing"
Using images, different techniques can be used for steganography, such as Least Significant Bit, Masking, Filtering, etc.
I was interested in testing this technique against some top tier EDRs to determine how stealthy and effective this technique is.
For my tests I decided to proceed with the Least Significant Bit technique using PNG images.
Least Significant Bit (LSB)
In images, a pixel is defined by the Red, Green and Blue (RGB) channels. Each channel, for each pixel, is represented by 1 byte (8 bits).
When simply changing the last bit of each channel, the resulting colour remains identical to the human eye.
Many tools exist that use the LSB technique, such as https://github.com/RenanTKN/pylsb. The tool guide explains how bytes/bits can be encoded within the RGB channels:
If we want to encode a single letter "a", the decimal value for this letter according to the table is 97, in binary: 01100001.
To store 3 bits per pixel, only changing the last bit, we'll need 3 pixels to encode the 'a' in our image.
The first pixel will store 0 1 1, on the lasts bits of each RGB channel, the second will store 0 0 0, and the third will store 0 1 (the blue channel will not be needed for this one).
Joining the last bits of these pixels we'll have: 01100001, converting this binary to decimal will result in 97, which is "a" according to the ASCII table.
Given the requirements, it is clear that the original image needs to be big enough to store the actual payload, at least 8 times bigger than the payload. If the image is too small, not enough pixels are present to store the whole shellcode.
Tooling
Doing a quick online search, I could not identify any existing proof of concept of loaders that use steganography as a way of hiding the shellcode.
Several open source projects instead exist to perform stego operations. I decided to use https://github.com/7thSamurai/steganography as a starting point, as it is well implemented, it supports several features and it can be used for Windows, Linux and MacOS. The tool is written in C++.
The actual code I used is a fork of the project (https://github.com/tothi/steganography/), which splits some of the encryption and image encoding/decoding functionality into a separate library. The tool functionality is untouched.
For this post I only focused on the Windows platform, but the same concepts could be applied for other operating system.
For compilation and development I used Visual Studio with the Desktop development with C++ features.
To start, I ensured that the tool worked without any modifications. I cloned the repository and use the the Visual Studio Developer Command Prompt and executed:
The command above generates the Visual Studio project files. To open the project in Visual Studio, simply click the
steganography.sln
file.
The main executable is part of the steganography
project (main.cpp
), whereas the zlib
and stb
are the dependencies.The
stb
is the library copied from the https://github.com/nothings/stb and allows for handling images, such as image loading/decoding from file/memory. The following image types are supported by the project: JPG, PNG, TGA, BMP, PSD, GIF, HDR, PIC. Specifically, the stb_image
and stb_image_write
components are used.The
zlib
project I believe is based on the https://github.com/madler/zlib repository, and it is used for CRC calculation.In Visual Studio, compile the solution.
To test that everything works as expected, test the tool with a simple shellcode, such as the plain msfvenom
calculator binary shellcode.
Download a PNG image that you will use to embed the shellcode. In this example I used a PNG of 1MB in size, and 1200x1200 in actual pixel size. Open Command Prompt, navigate to the Release
folder of the project, and execute the below to encode the shellcode into the image. When prompted with a password, insert any character you wish to use for it:
The -i
argument points to the original image, the -e
is the shellcode file and the -o
is the output image filename that will have the shellcode encoded in it.
The result will be a new image, bigger in size compared to the original one.
When comparing the two images, there will be no noticeable visual differences.
To test that you can retrieve the embedded bytes, use the tool with the decode option, supplying the same password that was used during the encoding process. The result should be the original shellcode encoded in the previous step.
Core functionality
The tool core functionality resides in the image.cpp
file. The file defines functions to load and save images from and to disk, and to encode and decode bytes using the stego LSB technique. This code interfaces with the stb
library for handling PNG image related operations, such as loading the image from disk, and saving it upon encoding the data in it. The default encoding level used by the tool is Low
, which we leave as it is. The header definition of the image is show below.
The encoding level is not documented in the project README, but based on the source, we can assume that the default Low
operates on the last bit of the channels, which is what we are after. The Med
operates on the last 2 bits, whereas the High
operates on the last 4 bits.
The main.cpp
of the steganography
parses the arguments, and perform the encode/decode operations, as well as encrypting the data embedded, using a password key derivation algorithm to calculate the encryption key. Inspecting the file encode
function implementation, we can better understand how the data is stored within the image: int encode(Image &image, const std::array
. The logic:
- Checks that the original image file exists and loads it
- Checks that the image is big enough for embedding the provided data (shellcode in this case)
- Calculates a random offset where the data will be stored in the image
- Calculates the CRC of the data
- Creates an Header structure (Struct) (defined in the main.cpp itself) to store the signature, the EncodingLevel used, the offset calculated in the prior step, the size of the data to encode and the CRC value
- Generates a random salt and IV (initialisation vector) used for encryption
- Derives the key based on the password provided, the salt and the IV
- Encrypts the Header structure (Struct) using the AES (Advanced Encryption Standard) symmetric algorithm and the key derived from the password
- Encrypts the data (shellcode in this case) using AES and the same key derived from the password
- Writes the encrypted header and data in the image, and saves it on disk
encode
function is displayed below:Based on this implementation, we can determine that at the beginning of the image, the program firstly encodes the Salt, then the IV, and finally the encrypted header. Then, the actual encrypted data is encoded at the random offset calculated in the previous steps and stored within the header structure itself. The below shows what the data looks like within the modified image:
| Salt | IV | Header | .... | Data | .... |
The decode
function loads the image with the embedded data from disk, extracts the Salt and IV from the image, calculates the encryption key based on the Salt, IV and provided password, decrypts the Header which includes the size and offset of the actual data, extracts the data, decrypts it, checks that the CRC value and signature match with the ones stored in the Header, and stores the original data on disk.
Loader idea
The tool does an amazing job at encrypting and storing the data and metadata encrypted within the image. Modifying the tool as it to build a loader around the full feature set should not be a difficult task, but I wanted to simply the logic, removing the encryption parts, the CRC calculation, etc. Removing these features would also help with removing some dependencies and reducing the compiled program size.
The goal was to simply have a program that:
- Downloads an image from a web server
- Extracts the shellcode embedded in the image
- Executes the shellcode
Implementation
Image encoder
First, I modified the program that encodes the data in the image, to remove the encryption logic and the CRC calculation. The code still uses the Header structure to store the size and the offset of the data. The structure could be simplified to only include these two values, but given the relatively small size of the structure, I decided to just leave it as it is.
With the changes applied, the Header and shellcode are encoded in the image using the pattern shown below:
| Header | ... | Data | ... |
For testing that the data was correctly embedded in the image, I also tweaked the decode
function to match the new logic: decode the Header, find the offset and size of the shellcode, decode the shellcode and save it to disk. With the encoding logic ready, I then moved to the loader implementation.
Loader
Since I wanted to keep the logic as simple as possible, I managed to completely remove the zlib
library, all the code related to encryption, CRC, randomisation, command line argument parsing and the stb_image_write
that is needed only if needing to save the image on disk. The partial content of the new CMakeList.txt
of the project is:
The image.cpp
also needed to be modified, to remove all the code related to encoding and saving of the image, and to add a new function that would allow to load a PNG image from memory, rather than from disk. The new image.hpp
is shown below.
The decode
function remains untouched.
The load_from_memory
takes an std::vector
parameter (the download function I implemented returns a std::vector
). The function uses an existing stb
function (stbi_load_from_memory
) that allows to load a PNG in memory. The function implementation is shown below.
The stb_image.c
has been modified with the following content:
Defining these MACROS allows to compile the stb
library only with the support of PNG image type related functions. This helped reducing the size of the library by more than 100KB.
The rest of the changes were made of course to the main.cpp
file. The remaining of the logic changes are related to the download of the image from a web server, the load of the PNG image in memory based on the data retrieved from the download (Image::load_from_memory
covered above), the decode of the Header structure from retrieved by decoding the beginning of the image (first 64 bytes), and the final decoding of the actual shellcode, prior its execution. I will not cover the implementation of those, as they should be self-explanatory.
Results
At Tier Zero Security we tested the resulting executable against Microsoft Defender, Microsoft Defender for Endpoint and another top tier EDR. For the tests, we generated a default Havoc C2 raw shellcode using the Havoc client and embedded it in a PNG using the modified steganography
project, as described earlier.
The program was not detected as malicious or suspicious, and its execution was not stopped, resulting in a Havoc C2 beacon. We also tried to compile and execute the program as a DLL, and the program executed successfully without detection.
Below is the execution of the executable on a host running Microsoft Defender for Endpoint with default settings.
Proof of concept
The video below shows the loader stego DLL delivered via phishing that persists and executes on every user login. The same was attempted against another top tier EDR, with the same result. The payload was not blocked and no alerts were generated.
Conclusion
The findings suggest that leveraging steganography to embed malicious shellcode within images offers an effective means to evade detection by AVs and EDRs.
With many web applications facilitating image upload/download, concealing malicious payloads within images becomes a straightforward task.
Such techniques could present a significant challenge to the blue team's detection capabilities if they remain unaware of this potential threat.