├── .gitignore ├── CMakeLists.txt ├── README.md ├── include ├── bititerator.h ├── compression.h └── pngstego.h ├── lib ├── bititerator.cpp ├── compression.cpp └── pngstego.cpp ├── src ├── decoder.cpp └── encoder.cpp └── testdata ├── README.md ├── imagetest.png ├── imagetest2.png ├── imagetest3.png ├── imagetest4.png ├── imagetest5.png ├── imagetest6.png ├── imagetest7.png └── imagetest8.png /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | build 3 | CMakeFiles 4 | **/*.dSYM 5 | **/.DS_Store -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | 3 | project(png_stego 4 | VERSION 0.1 5 | DESCRIPTION "PNG Stego POC" 6 | LANGUAGES CXX) 7 | 8 | include_directories(include) 9 | 10 | add_executable(png_encoder 11 | src/encoder.cpp 12 | lib/bititerator.cpp 13 | lib/compression.cpp 14 | lib/pngstego.cpp 15 | ) 16 | 17 | add_executable(png_decoder 18 | src/decoder.cpp 19 | lib/bititerator.cpp 20 | lib/compression.cpp 21 | lib/pngstego.cpp 22 | ) 23 | 24 | target_link_libraries(png_encoder PRIVATE z) 25 | target_link_libraries(png_decoder PRIVATE z) 26 | 27 | target_compile_features(png_encoder PRIVATE cxx_std_17) 28 | target_compile_features(png_decoder PRIVATE cxx_std_17) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Info 2 | 3 | A PNG Stego encoder/decoder, written as an acompaniment to my blog post on the subject: 4 | 5 | https://blog.xpnsec.com/png-steganography/ 6 | 7 | This is a POC tool meant to show the concept of hiding data in the IDAT chunk of a PNG file. It is not meant to be a fully featured tool and may be buggy. 8 | 9 | ## Building 10 | 11 | To build the encoder and decoder, you'll need: 12 | 13 | * CMake 14 | * zlib 15 | 16 | We can then build with: 17 | 18 | ``` 19 | mkdir build 20 | cd build 21 | cmake .. 22 | cmake --build . 23 | ``` 24 | 25 | ## Running 26 | 27 | ### Encoder 28 | 29 | ``` 30 | ./png_encoder ../testdata/imagetest.png payload 31 | ``` 32 | 33 | This will create a new PNG as `output.png` 34 | 35 | ### Decoder 36 | 37 | ``` 38 | ./png_decoder ./output.png 39 | ``` -------------------------------------------------------------------------------- /include/bititerator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | class BitIterator { 4 | public: 5 | BitIterator(char *data); 6 | BitIterator(char *data, int dataLength); 7 | bool getNextBit(bool &eof); 8 | void reset(); 9 | bool hasNext(); 10 | void addBit(char bit); 11 | char* getData(); 12 | 13 | private: 14 | char *data; 15 | int dataLength; 16 | int index; 17 | }; 18 | -------------------------------------------------------------------------------- /include/compression.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class Compression { 7 | public: 8 | Compression(); 9 | bool compress(unsigned char *data, int length, std::function callback); 10 | bool decompress(unsigned char *data, int length, int maxOutputLength, std::function callback); 11 | private: 12 | z_stream inflatestrm; 13 | z_stream deflatestrm; 14 | }; 15 | -------------------------------------------------------------------------------- /include/pngstego.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "compression.h" 7 | #include "bititerator.h" 8 | 9 | #define LONG_BITSWAP(x) (((x) >> 24) | (((x) >> 8) & 0xff00) | (((x) << 8) & 0xff0000) | ((x) << 24)) 10 | 11 | #define PNG_SIGNATURE 0xa1a0a0d474e5089 12 | #define PNG_SIGNATURE_LENGTH 8 13 | 14 | #define IHDR 0x52444849 15 | #define PLTE 0x45544c50 16 | #define IDAT 0x54414449 17 | #define IEND 0x444e4549 18 | 19 | #define PNG_LENGTH_FIELD_SIZE 4 20 | #define PNG_TYPE_FIELD_SIZE 4 21 | #define PNG_CRC_FIELD_SIZE 4 22 | 23 | #define PNG_CHUNK_HEADER_SIZE (PNG_LENGTH_FIELD_SIZE + PNG_TYPE_FIELD_SIZE) 24 | #define PNG_CHUNK_FOOTER_SIZE PNG_CRC_FIELD_SIZE 25 | 26 | struct Chunk { 27 | uint32_t length; 28 | uint32_t type; 29 | uint8_t *data; 30 | uint32_t crc; 31 | } __attribute__((packed)); 32 | 33 | typedef struct __attribute__((packed)) _IHDR { 34 | uint32_t width; 35 | uint32_t height; 36 | uint8_t bitDepth; 37 | uint8_t colorType; 38 | uint8_t compressionMethod; 39 | uint8_t filterMethod; 40 | uint8_t interlaceMethod; 41 | } Chunk_IHDR; 42 | 43 | typedef struct __attribute__((packed)) _PLTE { 44 | uint8_t red; 45 | uint8_t green; 46 | uint8_t blue; 47 | } Chunk_PLTE; 48 | 49 | enum PNGStegoMode { 50 | ENCODE, 51 | DECODE 52 | }; 53 | 54 | class PNGStego { 55 | public: 56 | PNGStego(const char *filename, PNGStegoMode encodingMode); 57 | ~PNGStego(); 58 | 59 | bool load(); 60 | char* encode(char *data, int dataLen, int *outLength); 61 | char* decode(); 62 | 63 | private: 64 | void compress(unsigned char *data, int length, std::function callback); 65 | char* processIHDR(char *input, int *inputOffset, char *output, int *outputOffset); 66 | char* processIDAT(char *input, int *inputOffset, char *output, int *outputOffset); 67 | char* processIEND(char *input, int *inputOffset, char *output, int *outputOffset); 68 | char* processUnknownChunk(char *input, int *inputOffset, char *output, int *outputOffset); 69 | void handleFilters(); 70 | void unfilterSub(unsigned char *scanline, int length); 71 | void unfilterUp(unsigned char *scanline, unsigned char *previousScanline, int length); 72 | void unfilterAverage(unsigned char *scanline, unsigned char *previousScanline, int length); 73 | void unfilterPaeth(unsigned char *scanline, unsigned char *previousScanline, int length); 74 | void filterSub(unsigned char *scanline, int length); 75 | void filterUp(unsigned char *scanline, unsigned char *previousScanline, int length); 76 | void filterAverage(unsigned char *scanline, unsigned char *previousScanline, int length); 77 | void filterPaeth(unsigned char *scanline, unsigned char *previousScanline, int length); 78 | unsigned char paethPredictor(unsigned char a, unsigned char b, unsigned char c); 79 | void encodeDataIntoScanline(unsigned char *scanline, int scanlineLength); 80 | void decodeDataFromScanline(unsigned char *scanline, int scanlineLength); 81 | 82 | // Input image filename 83 | const char *filename; 84 | 85 | // Loaded input image data 86 | char *fileContent; 87 | int fileLength; 88 | 89 | // Data to be hidden in image 90 | char *inputData; 91 | int inputDataLength; 92 | 93 | // Length of each scanline in bytes 94 | int scanlineLength; 95 | 96 | // Uncompressed image data 97 | char *uncompressedData; 98 | int uncompressedDataLength; 99 | 100 | // Dimensions of the image 101 | int width; 102 | int height; 103 | int bytesPerPixel; 104 | 105 | // Operating mode 106 | PNGStegoMode mode; 107 | 108 | Compression *compression; 109 | BitIterator *bitIterator; 110 | }; 111 | -------------------------------------------------------------------------------- /lib/bititerator.cpp: -------------------------------------------------------------------------------- 1 | #include "bititerator.h" 2 | 3 | BitIterator::BitIterator(char *data) { 4 | this->data = data; 5 | this->dataLength = -1; 6 | this->index = 0; 7 | } 8 | 9 | BitIterator::BitIterator(char *data, int dataLength) { 10 | this->data = data; 11 | this->dataLength = dataLength; 12 | this->index = 0; 13 | } 14 | 15 | void BitIterator::reset() { 16 | this->index = 0; 17 | } 18 | 19 | bool BitIterator::getNextBit(bool &eof) { 20 | 21 | char *buffer = this->data; 22 | int bufferLength = this->dataLength; 23 | 24 | if (this->index >= bufferLength * 8) { 25 | eof = true; 26 | return false; 27 | } 28 | 29 | int byteIndex = this->index / 8; 30 | int bitIndex = this->index % 8; 31 | 32 | int bit = (buffer[byteIndex] >> (7 - bitIndex)) & 1; 33 | this->index++; 34 | 35 | return bit; 36 | } 37 | 38 | bool BitIterator::hasNext() { 39 | return this->index < this->dataLength * 8; 40 | } 41 | 42 | void BitIterator::addBit(char bit) { 43 | 44 | if (this->dataLength < this->index) { 45 | // Buffer is full, so we stop ingesting 46 | return; 47 | } 48 | 49 | int byteIndex = this->index / 8; 50 | int bitIndex = this->index % 8; 51 | 52 | if (bit) { 53 | this->data[byteIndex] |= (1 << (7 - bitIndex)); 54 | } else { 55 | this->data[byteIndex] &= ~(1 << (7 - bitIndex)); 56 | } 57 | 58 | this->index++; 59 | } 60 | 61 | char* BitIterator::getData() { 62 | return this->data; 63 | } -------------------------------------------------------------------------------- /lib/compression.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "compression.h" 7 | 8 | Compression::Compression() { 9 | memset(&this->inflatestrm, 0, sizeof(z_stream)); 10 | memset(&this->deflatestrm, 0, sizeof(z_stream)); 11 | } 12 | 13 | bool Compression::compress(unsigned char *data, int length, std::function callback) { 14 | 15 | int ret; 16 | unsigned char *buffer; 17 | uLongf bufferLength; 18 | 19 | bufferLength = compressBound(length); 20 | buffer = (unsigned char *)malloc(bufferLength); 21 | 22 | ret = ::compress((Bytef *)buffer, (uLongf *)&bufferLength, (Bytef *)data, length); 23 | if (ret != Z_OK) { 24 | printf("[!] Error: compress returned %d\n", ret); 25 | return false; 26 | } 27 | 28 | callback((char *)buffer, bufferLength); 29 | free(buffer); 30 | 31 | return true; 32 | } 33 | 34 | bool Compression::decompress(unsigned char *data, int length, int maxOutputLength, std::function callback) { 35 | 36 | int ret; 37 | unsigned char *buffer; 38 | int bufferLength = maxOutputLength; 39 | int outputLength; 40 | 41 | this->inflatestrm.avail_in = length; 42 | this->inflatestrm.next_in = (Bytef *)data; 43 | 44 | if (this->inflatestrm.state == NULL) { 45 | this->inflatestrm.zalloc = Z_NULL; 46 | this->inflatestrm.zfree = Z_NULL; 47 | this->inflatestrm.opaque = Z_NULL; 48 | ret = inflateInit(&this->inflatestrm); 49 | if (ret != Z_OK) { 50 | printf("Error: inflateInit returned %d\n", ret); 51 | return false; 52 | } 53 | } 54 | 55 | buffer = (unsigned char *)malloc(bufferLength); 56 | this->inflatestrm.avail_out = bufferLength; 57 | this->inflatestrm.next_out = buffer; 58 | 59 | ret = inflate(&this->inflatestrm, Z_SYNC_FLUSH); 60 | if (ret != Z_OK && ret != Z_STREAM_END) { 61 | printf("Error: inflate returned %d\n", ret); 62 | return false; 63 | } 64 | 65 | outputLength = bufferLength - this->inflatestrm.avail_out; 66 | callback((char *)buffer, outputLength); 67 | free(buffer); 68 | 69 | if (this->inflatestrm.avail_in != 0) { 70 | printf("Error: inflate did not consume all input\n"); 71 | return false; 72 | } 73 | 74 | inflateEnd(&this->inflatestrm); 75 | 76 | return true; 77 | } 78 | -------------------------------------------------------------------------------- /lib/pngstego.cpp: -------------------------------------------------------------------------------- 1 | #include "pngstego.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | PNGStego::PNGStego(const char *filename, PNGStegoMode encodingMode) { 12 | this->filename = filename; 13 | this->uncompressedData = NULL; 14 | this->uncompressedDataLength = 0; 15 | this->compression = new Compression(); 16 | this->mode = encodingMode; 17 | } 18 | 19 | PNGStego::~PNGStego() { 20 | 21 | if (this->uncompressedData != NULL) { 22 | free(this->uncompressedData); 23 | } 24 | 25 | if (this->fileContent != NULL) { 26 | free(this->fileContent); 27 | } 28 | 29 | delete this->compression; 30 | delete this->bitIterator; 31 | } 32 | 33 | // Load data from the PNG file into memory 34 | bool PNGStego::load() { 35 | FILE *f = fopen(this->filename, "rb"); 36 | if (f == NULL) { 37 | return false; 38 | } 39 | 40 | fseek(f, 0, SEEK_END); 41 | this->fileLength = ftell(f); 42 | fseek(f, 0, SEEK_SET); 43 | this->fileContent = (char *)malloc(this->fileLength); 44 | if (this->fileContent == NULL) { 45 | return false; 46 | } 47 | 48 | fread(this->fileContent, this->fileLength, 1, f); 49 | fclose(f); 50 | 51 | return true; 52 | } 53 | 54 | // Filters a scanline using the sub filter type 55 | // scanline - the scanline to filter 56 | // length - the length of the scanline 57 | void PNGStego::filterSub(unsigned char *scanline, int length) { 58 | int i; 59 | unsigned char previousPixel; 60 | 61 | unsigned char *origScanline = (unsigned char *)malloc(length); 62 | memcpy(origScanline, scanline, length); 63 | 64 | for (i = 0; i < length; i++) { 65 | if (i < bytesPerPixel) { 66 | previousPixel = 0; 67 | scanline[i] -= previousPixel; 68 | } else { 69 | previousPixel = origScanline[i - bytesPerPixel]; 70 | scanline[i] -= previousPixel; 71 | } 72 | } 73 | 74 | free(origScanline); 75 | } 76 | 77 | // Unfilters a scanline using the sub filter type 78 | // scanline - the scanline to unfilter 79 | // length - the length of the scanline 80 | void PNGStego::unfilterSub(unsigned char *scanline, int length) { 81 | int i; 82 | unsigned char previousPixel; 83 | 84 | for (i = 0; i < length; i++) { 85 | if (i < bytesPerPixel) { 86 | previousPixel = 0; 87 | scanline[i] += previousPixel; 88 | } else { 89 | previousPixel = scanline[i - bytesPerPixel]; 90 | scanline[i] += previousPixel; 91 | } 92 | } 93 | } 94 | 95 | // Filters a scanline using the up filter type 96 | // scanline - the scanline to filter 97 | // previousScanline - the previous scanline 98 | // length - the length of the scanline 99 | void PNGStego::filterUp(unsigned char *scanline, unsigned char *previousScanline, int length) { 100 | int i; 101 | unsigned char previousPixel; 102 | 103 | for (i = 0; i < length; i++) { 104 | previousPixel = previousScanline[i]; 105 | scanline[i] -= previousPixel; 106 | } 107 | } 108 | 109 | // Unfilters a scanline using the up filter type 110 | // scanline - the scanline to unfilter 111 | // previousScanline - the previous scanline 112 | // length - the length of the scanline 113 | void PNGStego::unfilterUp(unsigned char *scanline, unsigned char *previousScanline, int length) { 114 | int i; 115 | unsigned char previousPixel; 116 | 117 | for (i = 0; i < length; i++) { 118 | previousPixel = previousScanline[i]; 119 | scanline[i] += previousPixel; 120 | } 121 | } 122 | 123 | // Filters a scanline using the average filter type 124 | // scanline - the scanline to filter 125 | // previousScanline - the previous scanline 126 | // length - the length of the scanline 127 | void PNGStego::filterAverage(unsigned char *scanline, unsigned char *previousScanline, int length) { 128 | int i; 129 | unsigned char previousPixel; 130 | unsigned char previousPixelUp; 131 | 132 | unsigned char *origScanline = (unsigned char *)malloc(length); 133 | memcpy(origScanline, scanline, length); 134 | 135 | for (i = 0; i < length; i++) { 136 | if (i < bytesPerPixel) { 137 | previousPixel = 0; 138 | previousPixelUp = previousScanline[i]; 139 | scanline[i] -= (previousPixel + previousPixelUp) / 2; 140 | } else { 141 | previousPixel = origScanline[i - bytesPerPixel]; 142 | previousPixelUp = previousScanline[i]; 143 | scanline[i] -= (previousPixel + previousPixelUp) / 2; 144 | } 145 | } 146 | 147 | free(origScanline); 148 | } 149 | 150 | // Unfilters a scanline using the average filter type 151 | // scanline - the scanline to unfilter 152 | // previousScanline - the previous scanline 153 | // length - the length of the scanline 154 | void PNGStego::unfilterAverage(unsigned char *scanline, unsigned char *previousScanline, int length) { 155 | int i; 156 | unsigned char previousPixel; 157 | unsigned char previousPixelUp; 158 | 159 | for (i = 0; i < length; i++) { 160 | if (i < bytesPerPixel) { 161 | previousPixel = 0; 162 | previousPixelUp = previousScanline[i]; 163 | scanline[i] += (previousPixel + previousPixelUp) / 2; 164 | 165 | } else { 166 | previousPixel = scanline[i - bytesPerPixel]; 167 | previousPixelUp = previousScanline[i]; 168 | scanline[i] += (previousPixel + previousPixelUp) / 2; 169 | } 170 | } 171 | } 172 | 173 | // Filters a scanline using the paeth filter type 174 | // scanline - the scanline to filter 175 | // previousScanline - the previous scanline 176 | // length - the length of the scanline 177 | void PNGStego::filterPaeth(unsigned char *scanline, unsigned char *previousScanline, int length) { 178 | int i; 179 | unsigned char previousPixel; 180 | unsigned char previousPixelUp; 181 | unsigned char previousPixelUpLeft; 182 | 183 | unsigned char *origScanline = (unsigned char *)malloc(length); 184 | memcpy(origScanline, scanline, length); 185 | 186 | for (i = 0; i < length; i++) { 187 | if (i < bytesPerPixel) { 188 | previousPixel = 0; 189 | previousPixelUp = previousScanline[i]; 190 | previousPixelUpLeft = 0; 191 | scanline[i] -= this->paethPredictor(previousPixel, previousPixelUp, previousPixelUpLeft); 192 | } else { 193 | previousPixel = origScanline[i - bytesPerPixel]; 194 | previousPixelUp = previousScanline[i]; 195 | previousPixelUpLeft = previousScanline[i - bytesPerPixel]; 196 | scanline[i] -= this->paethPredictor(previousPixel, previousPixelUp, previousPixelUpLeft); 197 | } 198 | } 199 | 200 | free(origScanline); 201 | } 202 | 203 | // Unfilters a scanline using the paeth filter type 204 | // scanline - the scanline to unfilter 205 | // previousScanline - the previous scanline 206 | // length - the length of the scanline 207 | void PNGStego::unfilterPaeth(unsigned char *scanline, unsigned char *previousScanline, int length) { 208 | int i; 209 | unsigned char previousPixel; 210 | unsigned char previousPixelUp; 211 | unsigned char previousPixelUpLeft; 212 | 213 | for (i = 0; i < length; i++) { 214 | if (i < bytesPerPixel) { 215 | previousPixel = 0; 216 | previousPixelUp = previousScanline[i]; 217 | previousPixelUpLeft = 0; 218 | scanline[i] += this->paethPredictor(previousPixel, previousPixelUp, previousPixelUpLeft); 219 | } else { 220 | previousPixel = scanline[i - bytesPerPixel]; 221 | previousPixelUp = previousScanline[i]; 222 | previousPixelUpLeft = previousScanline[i - bytesPerPixel]; 223 | scanline[i] += this->paethPredictor(previousPixel, previousPixelUp, previousPixelUpLeft); 224 | } 225 | } 226 | } 227 | 228 | // Returns the paeth predictor for the given values 229 | unsigned char PNGStego::paethPredictor(unsigned char a, unsigned char b, unsigned char c) { 230 | int p = a + b - c; 231 | int pa = abs(p - a); 232 | int pb = abs(p - b); 233 | int pc = abs(p - c); 234 | 235 | if (pa <= pb && pa <= pc) { 236 | return a; 237 | } else if (pb <= pc) { 238 | return b; 239 | } else { 240 | return c; 241 | } 242 | } 243 | 244 | // Uses the bit iterator to add decoded bits from the image 245 | void PNGStego::encodeDataIntoScanline(unsigned char *scanline, int scanlineLength) { 246 | int i; 247 | bool eof = false; 248 | 249 | if (!this->bitIterator->hasNext()) { 250 | return; 251 | } 252 | 253 | for (i = 0; i < scanlineLength; i++) { 254 | char bit = this->bitIterator->getNextBit(eof); 255 | if (eof) { 256 | return; 257 | } 258 | 259 | if (bit) { 260 | scanline[i] |= 1; 261 | } else { 262 | scanline[i] &= 0xFE; 263 | } 264 | } 265 | } 266 | 267 | // Combines bits from the image into the bit iterator 268 | void PNGStego::decodeDataFromScanline(unsigned char *scanline, int scanlineLength) { 269 | int i; 270 | 271 | for (i = 0; i < scanlineLength; i++) { 272 | this->bitIterator->addBit(scanline[i] & 1); 273 | } 274 | } 275 | 276 | // Walks through the decompressed IDAT chunks and processes the filters 277 | void PNGStego::handleFilters() { 278 | 279 | std::queue filterQueue; 280 | char *prevUncompressedData = NULL; 281 | 282 | prevUncompressedData = (char *)malloc(this->uncompressedDataLength); 283 | memcpy(prevUncompressedData, this->uncompressedData, this->uncompressedDataLength); 284 | 285 | printf("[*] Running unfilters...\n"); 286 | 287 | // First we process the filters for each scanline 288 | for (int i = 0; i < this->uncompressedDataLength; i += this->scanlineLength) { 289 | 290 | filterQueue.push(this->uncompressedData[i]); 291 | 292 | switch(this->uncompressedData[i]) { 293 | case 0: 294 | printf("[*] Scanline Filter: None\n"); 295 | break; 296 | 297 | case 1: 298 | printf("[*] Scanline Filter: Sub\n"); 299 | this->unfilterSub((unsigned char *)this->uncompressedData + i + 1, this->scanlineLength - 1); 300 | this->uncompressedData[i] = 0; 301 | break; 302 | 303 | case 2: 304 | printf("[*] Scanline Filter: Up\n"); 305 | this->unfilterUp((unsigned char *)this->uncompressedData + i + 1, (unsigned char *)this->uncompressedData + i + 1 - this->scanlineLength, this->scanlineLength - 1); 306 | this->uncompressedData[i] = 0; 307 | break; 308 | 309 | case 3: 310 | printf("[*] Scanline Filter: Average\n"); 311 | this->unfilterAverage((unsigned char *)this->uncompressedData + i + 1, (unsigned char *)this->uncompressedData + i + 1 - this->scanlineLength, this->scanlineLength - 1); 312 | this->uncompressedData[i] = 0; 313 | break; 314 | 315 | case 4: 316 | printf("[*] Scanline Filter: Paeth\n"); 317 | this->unfilterPaeth((unsigned char *)this->uncompressedData + i + 1, (unsigned char *)this->uncompressedData + i + 1 - this->scanlineLength, this->scanlineLength - 1); 318 | this->uncompressedData[i] = 0; 319 | break; 320 | 321 | default: 322 | printf("[*] Scanline Filter: Unknown (%d)\n", *((unsigned char *)uncompressedData + i)); 323 | break; 324 | } 325 | } 326 | 327 | for (int i = 0; i < this->uncompressedDataLength; i += this->scanlineLength) { 328 | if (this->mode == PNGStegoMode::ENCODE) { 329 | this->encodeDataIntoScanline((unsigned char *)this->uncompressedData + i + 1, this->scanlineLength - 1); 330 | } else { 331 | this->decodeDataFromScanline((unsigned char *)this->uncompressedData + i + 1, this->scanlineLength - 1); 332 | } 333 | } 334 | 335 | // DERP: Thanks to @au5_mate for spotting that this was in the wrong place... 336 | if (this->mode == PNGStegoMode::DECODE) { 337 | return; 338 | } 339 | 340 | char *origBytes = (char*)malloc(this->uncompressedDataLength); 341 | memcpy(origBytes, this->uncompressedData, this->uncompressedDataLength); 342 | 343 | printf("[*] Re-running filters...\n"); 344 | 345 | // And then reapply the filter (we'll just repeat from the beginning) 346 | for (int i = 0; i < this->uncompressedDataLength; i += this->scanlineLength) { 347 | 348 | int filter = filterQueue.front(); 349 | filterQueue.pop(); 350 | 351 | switch(filter) { 352 | case 0: 353 | printf("[*] Scanline Filter: None\n"); 354 | break; 355 | 356 | case 1: 357 | printf("[*] Scanline Filter: Sub\n"); 358 | this->filterSub((unsigned char *)this->uncompressedData + i + 1, this->scanlineLength - 1); 359 | this->uncompressedData[i] = 1; 360 | break; 361 | 362 | case 2: 363 | printf("[*] Scanline Filter: Up\n"); 364 | this->filterUp((unsigned char *)this->uncompressedData + i + 1, (unsigned char *)origBytes + i + 1 - this->scanlineLength, this->scanlineLength - 1); 365 | this->uncompressedData[i] = 2; 366 | break; 367 | 368 | case 3: 369 | printf("[*] Scanline Filter: Average\n"); 370 | this->filterAverage((unsigned char *)this->uncompressedData + i + 1, (unsigned char *)origBytes + i + 1 - this->scanlineLength, this->scanlineLength - 1); 371 | this->uncompressedData[i] = 3; 372 | break; 373 | 374 | case 4: 375 | printf("[*] Scanline Filter: Paeth\n"); 376 | this->filterPaeth((unsigned char *)this->uncompressedData + i + 1, (unsigned char *)origBytes + i + 1 - this->scanlineLength, this->scanlineLength - 1); 377 | this->uncompressedData[i] = 4; 378 | break; 379 | 380 | default: 381 | printf("[*] Scanline Filter: Unknown (%d)\n", *((unsigned char *)uncompressedData + i)); 382 | break; 383 | } 384 | } 385 | 386 | if (prevUncompressedData != NULL) { 387 | free(prevUncompressedData); 388 | } 389 | 390 | if (origBytes != NULL) { 391 | free(origBytes); 392 | } 393 | } 394 | 395 | // Processes a IHDR chunk 396 | // input - the input buffer containing PNG data 397 | // inputOffset - the offset into the input buffer where the IHDR chunk starts 398 | // output - the output buffer of our new image 399 | // outputOffset - the offset into the output buffer where we should start writing 400 | // returns a new output buffer as we most likely will need to realloc 401 | char* PNGStego::processIHDR(char *input, int *inputOffset, char *output, int *outputOffset) { 402 | 403 | Chunk_IHDR *ihdr; 404 | uint32_t chunkLength; 405 | uint32_t chunkType; 406 | uint32_t chunkCRC; 407 | char *chunkData; 408 | 409 | chunkLength = LONG_BITSWAP(*(uint32_t *)(input + *inputOffset)); 410 | chunkType = LONG_BITSWAP(*(uint32_t *)((input + *inputOffset + PNG_LENGTH_FIELD_SIZE))); 411 | chunkData = input + *inputOffset + PNG_CHUNK_HEADER_SIZE; 412 | chunkCRC = LONG_BITSWAP(*(uint32_t *)((input + *inputOffset + PNG_CHUNK_HEADER_SIZE + chunkLength))); 413 | 414 | ihdr = (Chunk_IHDR *)(chunkData); 415 | 416 | printf("[*] Dimensions: %dx%d\n", LONG_BITSWAP(ihdr->width), LONG_BITSWAP(ihdr->height)); 417 | printf("[*] Bit Depth: %d\n", ihdr->bitDepth); 418 | printf("[*] Color Type: %d\n", ihdr->colorType); 419 | printf("[*] Compression: %d\n", ihdr->compressionMethod); 420 | printf("[*] FilterMethod: %d\n", ihdr->filterMethod); 421 | printf("[*] Interlace: %d\n", ihdr->interlaceMethod); 422 | 423 | if (ihdr->compressionMethod != 0) { 424 | printf("[!] Error: Compression method must be 0\n"); 425 | exit(1); 426 | } 427 | 428 | if (ihdr->filterMethod != 0) { 429 | printf("[!] Error: Filter method must be 0\n"); 430 | exit(1); 431 | } 432 | 433 | if (ihdr->interlaceMethod != 0) { 434 | // Not currently implemented, because CBA until I see it in the wild 435 | printf("[!] Error: Interlace method must be 0\n"); 436 | exit(1); 437 | } 438 | 439 | this->width = LONG_BITSWAP(ihdr->width); 440 | this->height = LONG_BITSWAP(ihdr->height); 441 | 442 | switch(ihdr->colorType) { 443 | case 0: 444 | printf("[*] IHDR Color Type: Grayscale\n"); 445 | this->scanlineLength = LONG_BITSWAP(ihdr->width) + 1; 446 | this->bytesPerPixel = (1 * ihdr->bitDepth) / 8; 447 | break; 448 | 449 | case 2: 450 | printf("[*] IHDR Color Type: RGB\n"); 451 | this->scanlineLength = LONG_BITSWAP(ihdr->width) * 3 + 1; 452 | this->bytesPerPixel = (3 * ihdr->bitDepth) / 8; 453 | break; 454 | 455 | case 3: 456 | printf("[*] IHDR Color Type: Palette\n"); 457 | printf("[!] Pallate not currently supported so results may be screwy!\n"); 458 | this->scanlineLength = LONG_BITSWAP(ihdr->width) + 1; 459 | this->bytesPerPixel = (1 * ihdr->bitDepth) / 8; 460 | break; 461 | 462 | case 4: 463 | printf("[*] IHDR Color Type: Grayscale + Alpha\n"); 464 | this->scanlineLength = LONG_BITSWAP(ihdr->width) * 2 + 1; 465 | this->bytesPerPixel = (2 * ihdr->bitDepth) / 8; 466 | break; 467 | 468 | case 6: 469 | printf("[*] IHDR Color Type: RGB + Alpha\n"); 470 | this->scanlineLength = LONG_BITSWAP(ihdr->width) * 4 + 1; 471 | this->bytesPerPixel = (4 * ihdr->bitDepth) / 8; 472 | break; 473 | } 474 | 475 | printf("[*] Maximum Data Size: %d bytes\n", (this->width * this->height * this->bytesPerPixel)); 476 | if ((this->width * this->height * this->bytesPerPixel) < this->inputDataLength) { 477 | printf("[!] Error: Payload is too large to fit in the image\n"); 478 | exit(1); 479 | } 480 | 481 | // If we are decoding, we don't write out any data 482 | if (this->mode == PNGStegoMode::DECODE) { 483 | *inputOffset += PNG_CHUNK_HEADER_SIZE + chunkLength + PNG_CRC_FIELD_SIZE; 484 | return output; 485 | } 486 | 487 | output = (char *)realloc(output, *outputOffset + PNG_CHUNK_HEADER_SIZE + chunkLength + PNG_CRC_FIELD_SIZE); 488 | if (output == NULL) { 489 | printf("[!] Error: realloc failed\n"); 490 | return NULL; 491 | } 492 | 493 | // Copy the IHDR chunk to our stego output 494 | memcpy(output + *outputOffset, input + *inputOffset, PNG_CHUNK_HEADER_SIZE + chunkLength + PNG_CRC_FIELD_SIZE); 495 | 496 | // Update the offsets 497 | *inputOffset += PNG_CHUNK_HEADER_SIZE + chunkLength + PNG_CRC_FIELD_SIZE; 498 | *outputOffset += PNG_CHUNK_HEADER_SIZE + chunkLength + PNG_CRC_FIELD_SIZE; 499 | 500 | return output; 501 | } 502 | 503 | // Processes a IEND chunk 504 | // input - the input buffer containing PNG data 505 | // inputOffset - the offset into the input buffer where the IEND chunk starts 506 | // output - the output buffer of our new image 507 | // outputOffset - the offset into the output buffer where we should start writing 508 | // returns a new output buffer as we most likely will need to realloc 509 | char* PNGStego::processIEND(char *input, int *inputOffset, char *output, int *outputOffset) { 510 | 511 | char *chunkData = NULL; 512 | uint32_t chunkLength; 513 | uint32_t chunkType; 514 | uint32_t chunkCRC; 515 | 516 | chunkLength = LONG_BITSWAP(*(uint32_t *)(input + *inputOffset)); 517 | chunkType = LONG_BITSWAP(*(uint32_t *)((input + *inputOffset + PNG_LENGTH_FIELD_SIZE))); 518 | chunkData = input + *inputOffset + PNG_CHUNK_HEADER_SIZE; 519 | chunkCRC = LONG_BITSWAP(*(uint32_t *)((input + *inputOffset + PNG_CHUNK_HEADER_SIZE + chunkLength))); 520 | 521 | // Now we need to handle all that IDAT stuff 522 | this->handleFilters(); 523 | 524 | // If we are decoding, we return here 525 | if (this->mode == PNGStegoMode::DECODE) { 526 | *inputOffset += PNG_CHUNK_HEADER_SIZE + chunkLength + PNG_CRC_FIELD_SIZE; 527 | return output; 528 | } 529 | 530 | this->compression->compress((unsigned char *)this->uncompressedData, this->uncompressedDataLength, [&output, outputOffset](char *compressedData, int compressedLength) { 531 | 532 | printf("[*] Compressed Length: %d bytes\n", compressedLength); 533 | output = (char*)realloc(output, *outputOffset + compressedLength + PNG_CHUNK_HEADER_SIZE + PNG_CRC_FIELD_SIZE); 534 | 535 | *(uint32_t *)(output + *outputOffset) = LONG_BITSWAP(compressedLength); 536 | *outputOffset += PNG_LENGTH_FIELD_SIZE; 537 | 538 | *(uint32_t *)(output + *outputOffset) = IDAT; 539 | *outputOffset += PNG_TYPE_FIELD_SIZE; 540 | 541 | memcpy(output + *outputOffset, compressedData, compressedLength); 542 | *outputOffset += compressedLength; 543 | 544 | *(uint32_t *)(output + *outputOffset) = LONG_BITSWAP(crc32(0, (unsigned char *)output + *outputOffset - compressedLength - PNG_LENGTH_FIELD_SIZE, compressedLength + PNG_LENGTH_FIELD_SIZE)); 545 | *outputOffset += PNG_CRC_FIELD_SIZE; 546 | }); 547 | 548 | output = (char*)realloc(output, *outputOffset + chunkLength + PNG_CHUNK_HEADER_SIZE + PNG_CRC_FIELD_SIZE); 549 | 550 | // Copy the IEND chunk to our stego output 551 | *(uint32_t *)(output + *outputOffset) = LONG_BITSWAP(chunkLength); 552 | *outputOffset += PNG_LENGTH_FIELD_SIZE; 553 | 554 | *(uint32_t *)(output + *outputOffset) = IEND; 555 | *outputOffset += PNG_TYPE_FIELD_SIZE; 556 | 557 | memcpy(output + *outputOffset, chunkData, chunkLength); 558 | *outputOffset += chunkLength; 559 | 560 | *(uint32_t *)(output + *outputOffset) = LONG_BITSWAP(crc32(0, (unsigned char *)output + *outputOffset - chunkLength - PNG_LENGTH_FIELD_SIZE, chunkLength + PNG_LENGTH_FIELD_SIZE));; 561 | *outputOffset += PNG_CRC_FIELD_SIZE; 562 | 563 | // Update the offsets 564 | *inputOffset += PNG_CHUNK_HEADER_SIZE + chunkLength + PNG_CRC_FIELD_SIZE; 565 | 566 | return output; 567 | } 568 | 569 | // Processes a IDAT chunk 570 | // input - the input buffer containing PNG data 571 | // inputOffset - the offset into the input buffer where the IDAT chunk starts 572 | // output - the output buffer of our new image 573 | // outputOffset - the offset into the output buffer where we should start writing 574 | // returns a new output buffer as we most likely will need to realloc 575 | char* PNGStego::processIDAT(char *input, int *inputOffset, char *output, int *outputOffset) { 576 | 577 | char *chunkData = NULL; 578 | uint32_t chunkLength; 579 | uint32_t chunkType; 580 | uint32_t chunkCRC; 581 | 582 | chunkLength = LONG_BITSWAP(*(uint32_t *)(input + *inputOffset)); 583 | chunkType = LONG_BITSWAP(*(uint32_t *)((input + *inputOffset + PNG_LENGTH_FIELD_SIZE))); 584 | chunkData = input + *inputOffset + PNG_CHUNK_HEADER_SIZE; 585 | chunkCRC = LONG_BITSWAP(*(uint32_t *)((input + *inputOffset + PNG_CHUNK_HEADER_SIZE + chunkLength))); 586 | 587 | // Decompress the data 588 | this->compression->decompress((unsigned char *)chunkData, chunkLength, (this->width * this->height * this->bytesPerPixel) + this->height, [this](char *decompressedData, int decompressedLength) { 589 | 590 | // Copy the decompressed data for later processing 591 | printf("[*] Decompressed Length: %d bytes\n", decompressedLength); 592 | 593 | this->uncompressedData = (char *)realloc(this->uncompressedData, this->uncompressedDataLength + decompressedLength); 594 | memcpy(this->uncompressedData + this->uncompressedDataLength, decompressedData, decompressedLength); 595 | this->uncompressedDataLength += decompressedLength; 596 | 597 | }); 598 | 599 | *inputOffset += PNG_CHUNK_HEADER_SIZE + chunkLength + PNG_CRC_FIELD_SIZE; 600 | return output; 601 | } 602 | 603 | // Processes any unhandled chunks such as iTXT 604 | // input - the input buffer containing PNG data 605 | // inputOffset - the offset into the input buffer where the chunk starts 606 | // output - the output buffer of our new image 607 | // outputOffset - the offset into the output buffer where we should start writing 608 | // returns a new output buffer as we most likely will need to realloc 609 | char* PNGStego::processUnknownChunk(char *input, int *inputOffset, char *output, int *outputOffset) { 610 | 611 | char *chunkData = NULL; 612 | uint32_t chunkLength; 613 | uint32_t chunkType; 614 | uint32_t chunkCRC; 615 | 616 | chunkLength = LONG_BITSWAP(*(uint32_t *)(input + *inputOffset)); 617 | chunkType = LONG_BITSWAP(*(uint32_t *)((input + *inputOffset + PNG_LENGTH_FIELD_SIZE))); 618 | chunkData = input + *inputOffset + PNG_CHUNK_HEADER_SIZE; 619 | chunkCRC = LONG_BITSWAP(*(uint32_t *)((input + *inputOffset + PNG_CHUNK_HEADER_SIZE + chunkLength))); 620 | 621 | // If we are decoding, we return here 622 | if (this->mode == PNGStegoMode::DECODE) { 623 | *inputOffset += PNG_CHUNK_HEADER_SIZE + chunkLength + PNG_CRC_FIELD_SIZE; 624 | return output; 625 | } 626 | 627 | output = (char *)realloc(output, *outputOffset + PNG_CHUNK_HEADER_SIZE + chunkLength + PNG_CRC_FIELD_SIZE); 628 | if (output == NULL) { 629 | printf("[!] Error: realloc failed\n"); 630 | return NULL; 631 | } 632 | 633 | // Copy the unknown chunk to our stego output 634 | memcpy(output + *outputOffset, input + *inputOffset, PNG_CHUNK_HEADER_SIZE + chunkLength + PNG_CRC_FIELD_SIZE); 635 | 636 | // Update the offsets 637 | *inputOffset += PNG_CHUNK_HEADER_SIZE + chunkLength + PNG_CRC_FIELD_SIZE; 638 | *outputOffset += PNG_CHUNK_HEADER_SIZE + chunkLength + PNG_CRC_FIELD_SIZE; 639 | 640 | return output; 641 | } 642 | 643 | // Encodes the provided data into the PNG image 644 | // data - the data to encode 645 | // dataLen - the length of the data to encode 646 | // outLength - the length of the new encoded image generated 647 | // returns the new encoded image 648 | char* PNGStego::encode(char *data, int dataLen, int *outLength) { 649 | 650 | int inBufferOffset = 0; 651 | uint32_t pngChunkType = 0; 652 | int pngChunkLength = 0; 653 | char *inBuffer = NULL; 654 | char *outBuffer = NULL; 655 | int outBufferOffset = 0; 656 | 657 | if (this->mode != PNGStegoMode::ENCODE) { 658 | printf("[!] Error: encode() method called in decode mode\n"); 659 | return NULL; 660 | } 661 | 662 | this->inputData = data; 663 | this->inputDataLength = dataLen; 664 | this->bitIterator = new BitIterator(this->inputData, this->inputDataLength); 665 | 666 | inBuffer = this->fileContent; 667 | outBuffer = (char*)malloc(1024); 668 | 669 | // Check the magic 670 | if (*(uint64_t *)inBuffer != PNG_SIGNATURE) { 671 | return NULL; 672 | } 673 | 674 | // Copy the magic to our stego output 675 | memcpy(outBuffer, inBuffer, PNG_SIGNATURE_LENGTH); 676 | outBufferOffset += PNG_SIGNATURE_LENGTH; 677 | inBufferOffset += PNG_SIGNATURE_LENGTH; 678 | 679 | // Iterate through PNG chunks 680 | while (inBufferOffset < this->fileLength) { 681 | 682 | // Each chunk starts with a 4 byte length and 4 byte type 683 | // followed by the data (of length bytes) and a 4 byte CRC 684 | pngChunkLength = LONG_BITSWAP(*(uint32_t *)(inBuffer + inBufferOffset)); 685 | pngChunkType = *(uint32_t *)(inBuffer + inBufferOffset + PNG_LENGTH_FIELD_SIZE); 686 | 687 | switch(pngChunkType) { 688 | case IHDR: 689 | 690 | printf("[*] IHDR Chunk\n"); 691 | outBuffer = processIHDR(inBuffer, &inBufferOffset, outBuffer, &outBufferOffset); 692 | break; 693 | 694 | case IDAT: 695 | 696 | printf("[*] IDAT Chunk\n"); 697 | outBuffer = processIDAT(inBuffer, &inBufferOffset, outBuffer, &outBufferOffset); 698 | break; 699 | 700 | case IEND: 701 | 702 | printf("[*] IEND Chunk\n"); 703 | outBuffer = processIEND(inBuffer, &inBufferOffset, outBuffer, &outBufferOffset); 704 | break; 705 | 706 | default: 707 | 708 | printf("[*] Unknown Chunk: %c%c%c%c\n", (pngChunkType >> 24) & 0xFF, (pngChunkType >> 16) & 0xFF, (pngChunkType >> 8) & 0xFF, pngChunkType & 0xFF); 709 | outBuffer = processUnknownChunk(inBuffer, &inBufferOffset, outBuffer, &outBufferOffset); 710 | break; 711 | } 712 | } 713 | 714 | *outLength = outBufferOffset; 715 | return outBuffer; 716 | } 717 | 718 | // Decodes the data from the PNG image 719 | // returns the decoded data 720 | char* PNGStego::decode() { 721 | 722 | int inBufferOffset = 0; 723 | int pngChunkLength = 0; 724 | char *outBuffer = NULL; 725 | int outBufferOffset = 0; 726 | uint32_t pngChunkType = 0; 727 | 728 | if (this->mode != PNGStegoMode::DECODE) { 729 | printf("[!] Error: decode() method called in encode mode\n"); 730 | return NULL; 731 | } 732 | 733 | // Allocate the maximum amount of space likely to be taken up by the payload 734 | // which we can guess isn't going to be larger than the size of the file 735 | char *output = (char *)malloc(this->fileLength); 736 | memset(output, 0, this->fileLength); 737 | 738 | this->bitIterator = new BitIterator(output, this->fileLength); 739 | 740 | char *inBuffer = this->fileContent; 741 | 742 | // Check the magic 743 | if (*(uint64_t *)inBuffer != PNG_SIGNATURE) { 744 | return NULL; 745 | } 746 | 747 | inBufferOffset += PNG_SIGNATURE_LENGTH; 748 | 749 | while (inBufferOffset < this->fileLength) { 750 | 751 | // Each chunk starts with a 4 byte length and 4 byte type 752 | // followed by the data (of length bytes) and a 4 byte CRC 753 | pngChunkLength = LONG_BITSWAP(*(uint32_t *)(inBuffer + inBufferOffset)); 754 | pngChunkType = *(uint32_t *)(inBuffer + inBufferOffset + PNG_LENGTH_FIELD_SIZE); 755 | 756 | switch(pngChunkType) { 757 | 758 | case IHDR: 759 | 760 | printf("[*] IHDR Chunk\n"); 761 | outBuffer = processIHDR(inBuffer, &inBufferOffset, outBuffer, &outBufferOffset); 762 | break; 763 | 764 | case IDAT: 765 | 766 | printf("[*] IDAT Chunk\n"); 767 | outBuffer = processIDAT(inBuffer, &inBufferOffset, outBuffer, &outBufferOffset); 768 | break; 769 | 770 | case IEND: 771 | 772 | printf("[*] IEND Chunk\n"); 773 | outBuffer = processIEND(inBuffer, &inBufferOffset, outBuffer, &outBufferOffset); 774 | break; 775 | 776 | default: 777 | 778 | printf("[*] Unknown Chunk: %c%c%c%c\n", pngChunkType & 0xFF, (pngChunkType >> 8) & 0xFF, (pngChunkType >> 16) & 0xFF, (pngChunkType >> 24) & 0xFF); 779 | outBuffer = processUnknownChunk(inBuffer, &inBufferOffset, outBuffer, &outBufferOffset); 780 | break; 781 | 782 | } 783 | } 784 | 785 | return output; 786 | 787 | } -------------------------------------------------------------------------------- /src/decoder.cpp: -------------------------------------------------------------------------------- 1 | #include "pngstego.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | // Prints a hex dump of the input with each line prefixed with the line number 10 | // and each line being 16 bytes long 11 | // Thanks: https://gist.github.com/ccbrown/9722406 12 | void hexdump(const void* data, size_t size) { 13 | char ascii[17]; 14 | size_t i, j; 15 | ascii[16] = '\0'; 16 | for (i = 0; i < size; ++i) { 17 | printf("%02X ", ((unsigned char*)data)[i]); 18 | if (((unsigned char*)data)[i] >= ' ' && ((unsigned char*)data)[i] <= '~') { 19 | ascii[i % 16] = ((unsigned char*)data)[i]; 20 | } else { 21 | ascii[i % 16] = '.'; 22 | } 23 | if ((i+1) % 8 == 0 || i+1 == size) { 24 | printf(" "); 25 | if ((i+1) % 16 == 0) { 26 | printf("| %s \n", ascii); 27 | } else if (i+1 == size) { 28 | ascii[(i+1) % 16] = '\0'; 29 | if ((i+1) % 16 <= 8) { 30 | printf(" "); 31 | } 32 | for (j = (i+1) % 16; j < 16; ++j) { 33 | printf(" "); 34 | } 35 | printf("| %s \n", ascii); 36 | } 37 | } 38 | } 39 | } 40 | 41 | int main(int argc, char **argv) { 42 | 43 | printf("PNG Stego Decoder POC\n"); 44 | printf(" Created by @_xpn_ to support the blog post: https://blog.xpnsec.com/png-steganography/\n\n"); 45 | 46 | if (argc < 2) { 47 | printf("Usage: %s \n", argv[0]); 48 | exit(1); 49 | } 50 | 51 | // We're going to decode the payload from the image 52 | PNGStego *png = new PNGStego(argv[1], PNGStegoMode::DECODE); 53 | if (!png->load()) { 54 | printf("[!] Failed to load PNG file\n"); 55 | return 1; 56 | } 57 | 58 | char *output = png->decode(); 59 | if (output == NULL) { 60 | printf("[!] Error decoding PNG\n"); 61 | return 1; 62 | } 63 | 64 | // Usually we'd spawn te payload off the back of this, but for now we'll just print the output in hex 65 | printf("[*] 0x100 bytes of decoded Payload:\n"); 66 | hexdump(output, 0x100); 67 | 68 | free(output); 69 | delete png; 70 | } -------------------------------------------------------------------------------- /src/encoder.cpp: -------------------------------------------------------------------------------- 1 | #include "pngstego.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | int main(int argc, char **argv) { 11 | 12 | printf("PNG Stego Encoder POC\n"); 13 | printf(" Created by @_xpn_ to support the blog post: https://blog.xpnsec.com/png-steganography/\n\n"); 14 | 15 | if (argc < 3) { 16 | printf("Usage: %s \n", argv[0]); 17 | exit(1); 18 | } 19 | 20 | int outputLength = 0; 21 | int payloadLength = 0; 22 | char *payload = NULL; 23 | 24 | // Load our payload 25 | FILE *payloadFile = fopen(argv[2], "rb"); 26 | fseek(payloadFile, 0, SEEK_END); 27 | payloadLength = ftell(payloadFile); 28 | fseek(payloadFile, 0, SEEK_SET); 29 | payload = (char *)malloc(payloadLength); 30 | fread(payload, 1, payloadLength, payloadFile); 31 | fclose(payloadFile); 32 | 33 | // Load the file 34 | PNGStego *png = new PNGStego(argv[1], PNGStegoMode::ENCODE); 35 | if (!png->load()) { 36 | printf("[!] Failed to load PNG file\n"); 37 | return 1; 38 | } 39 | 40 | // Load and encode the payload to be hidden 41 | char *outData = png->encode(payload, payloadLength, &outputLength); 42 | 43 | // Write the output to a new file 44 | FILE *outputFile = fopen("output.png", "wb"); 45 | fwrite(outData, 1, outputLength, outputFile); 46 | fclose(outputFile); 47 | 48 | free(payload); 49 | delete png; 50 | } -------------------------------------------------------------------------------- /testdata/README.md: -------------------------------------------------------------------------------- 1 | ## Test Cases 2 | 3 | The following are the test cases used to verify that the encoder/decoder were working as expected. Output was run through `pngcheck` to verify that the PNGs were valid. 4 | 5 | ### Test Case 1 6 | 7 | #### imagetest.png 8 | 9 | ``` 10 | File: ./output1.png (30347 bytes) 11 | chunk IHDR at offset 0x0000c, length 13 12 | 1920 x 1080 image, 24-bit RGB, non-interlaced 13 | chunk pHYs at offset 0x00025, length 9: 2835x2835 pixels/meter (72 dpi) 14 | chunk cHRM at offset 0x0003a, length 32 15 | White x = 0.31269 y = 0.32899, Red x = 0.63999 y = 0.33001 16 | Green x = 0.3 y = 0.6, Blue x = 0.15 y = 0.05999 17 | chunk IDAT at offset 0x00066, length 30225 18 | zlib: deflated, 32K window, default compression 19 | chunk IEND at offset 0x07683, length 0 20 | No errors detected in ./output1.png (5 chunks, 99.5% compression). 21 | ``` 22 | 23 | #### imagetest2.png 24 | 25 | ``` 26 | File: ./output2.png (44631 bytes) 27 | chunk IHDR at offset 0x0000c, length 13 28 | 1920 x 1080 image, 32-bit RGB+alpha, non-interlaced 29 | chunk sRGB at offset 0x00025, length 1 30 | rendering intent = perceptual 31 | chunk gAMA at offset 0x00032, length 4: 0.45455 32 | chunk pHYs at offset 0x00042, length 9: 4724x4724 pixels/meter (120 dpi) 33 | chunk IDAT at offset 0x00057, length 44524 34 | zlib: deflated, 32K window, default compression 35 | chunk IEND at offset 0x0ae4f, length 0 36 | No errors detected in ./output2.png (6 chunks, 99.5% compression). 37 | ``` 38 | 39 | #### imagetest3.png 40 | 41 | ``` 42 | File: ./output3.png (85915 bytes) 43 | chunk IHDR at offset 0x0000c, length 13 44 | 1920 x 1080 image, 32-bit RGB+alpha, non-interlaced 45 | chunk sRGB at offset 0x00025, length 1 46 | rendering intent = perceptual 47 | chunk pHYs at offset 0x00032, length 9: 2835x2835 pixels/meter (72 dpi) 48 | chunk tIME at offset 0x00047, length 7: 30 May 2011 10:11:26 UTC 49 | chunk tEXt at offset 0x0005a, length 25, keyword: Comment 50 | chunk bKGD at offset 0x0007f, length 6 51 | red = 0x00ff, green = 0x00ff, blue = 0x00ff 52 | chunk IDAT at offset 0x00091, length 85750 53 | zlib: deflated, 32K window, default compression 54 | chunk IEND at offset 0x14f93, length 0 55 | No errors detected in ./output3.png (8 chunks, 99.0% compression). 56 | ``` 57 | 58 | #### imagetest4.png 59 | 60 | ``` 61 | File: ./output4.png (948125 bytes) 62 | chunk IHDR at offset 0x0000c, length 13 63 | 1920 x 1080 image, 32-bit RGB+alpha, non-interlaced 64 | chunk sRGB at offset 0x00025, length 1 65 | rendering intent = perceptual 66 | chunk eXIf at offset 0x00032, length 80: EXIF metadata, big-endian (MM) format 67 | chunk iTXt at offset 0x0008e, length 345, keyword: XML:com.adobe.xmp 68 | uncompressed, no language tag 69 | no translated keyword, 324 bytes of UTF-8 text 70 | chunk IDAT at offset 0x001f3, length 947606 71 | zlib: deflated, 32K window, default compression 72 | chunk IEND at offset 0xe7795, length 0 73 | No errors detected in ./output4.png (6 chunks, 88.6% compression). 74 | ``` 75 | 76 | #### imagetest5.png 77 | 78 | ``` 79 | File: ./output5.png (368360 bytes) 80 | chunk IHDR at offset 0x0000c, length 13 81 | 640 x 426 image, 32-bit RGB+alpha, non-interlaced 82 | chunk sRGB at offset 0x00025, length 1 83 | rendering intent = perceptual 84 | chunk IDAT at offset 0x00032, length 368290 85 | zlib: deflated, 32K window, default compression 86 | chunk IEND at offset 0x59ee0, length 0 87 | No errors detected in ./output5.png (4 chunks, 66.2% compression). 88 | ``` 89 | 90 | #### imagetest6.png 91 | 92 | ``` 93 | File: ./output6.png (2052441 bytes) 94 | chunk IHDR at offset 0x0000c, length 13 95 | 2399 x 1601 image, 16-bit grayscale+alpha, non-interlaced 96 | chunk pHYs at offset 0x00025, length 9: 2835x2835 pixels/meter (72 dpi) 97 | chunk bKGD at offset 0x0003a, length 2 98 | gray = 0x00ff 99 | chunk IDAT at offset 0x00048, length 2052349 100 | zlib: deflated, 32K window, default compression 101 | chunk IEND at offset 0x1f5151, length 0 102 | No errors detected in ./output6.png (5 chunks, 73.3% compression). 103 | ``` 104 | 105 | #### imagetest7.png 106 | 107 | ``` 108 | File: ./output7.png (52022 bytes) 109 | chunk IHDR at offset 0x0000c, length 13 110 | 256 x 256 image, 8-bit palette, non-interlaced 111 | chunk gAMA at offset 0x00025, length 4: 0.45455 112 | chunk sRGB at offset 0x00035, length 1 113 | rendering intent = perceptual 114 | chunk PLTE at offset 0x00042, length 363: 121 palette entries 115 | chunk IDAT at offset 0x001b9, length 51561 116 | zlib: deflated, 32K window, default compression 117 | chunk IEND at offset 0x0cb2e, length 0 118 | No errors detected in ./output7.png (6 chunks, 20.6% compression). 119 | ``` 120 | 121 | #### imagetest8.png 122 | 123 | ``` 124 | File: ./output8.png (226177 bytes) 125 | chunk IHDR at offset 0x0000c, length 13 126 | 768 x 512 image, 8-bit grayscale, non-interlaced 127 | chunk IDAT at offset 0x00025, length 226120 128 | zlib: deflated, 32K window, default compression 129 | chunk IEND at offset 0x37379, length 0 130 | No errors detected in ./output8.png (3 chunks, 42.5% compression). 131 | ``` -------------------------------------------------------------------------------- /testdata/imagetest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xpn/stego-playground/bd44cc2b56dea2699f246c3c8e69e24e5811158a/testdata/imagetest.png -------------------------------------------------------------------------------- /testdata/imagetest2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xpn/stego-playground/bd44cc2b56dea2699f246c3c8e69e24e5811158a/testdata/imagetest2.png -------------------------------------------------------------------------------- /testdata/imagetest3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xpn/stego-playground/bd44cc2b56dea2699f246c3c8e69e24e5811158a/testdata/imagetest3.png -------------------------------------------------------------------------------- /testdata/imagetest4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xpn/stego-playground/bd44cc2b56dea2699f246c3c8e69e24e5811158a/testdata/imagetest4.png -------------------------------------------------------------------------------- /testdata/imagetest5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xpn/stego-playground/bd44cc2b56dea2699f246c3c8e69e24e5811158a/testdata/imagetest5.png -------------------------------------------------------------------------------- /testdata/imagetest6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xpn/stego-playground/bd44cc2b56dea2699f246c3c8e69e24e5811158a/testdata/imagetest6.png -------------------------------------------------------------------------------- /testdata/imagetest7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xpn/stego-playground/bd44cc2b56dea2699f246c3c8e69e24e5811158a/testdata/imagetest7.png -------------------------------------------------------------------------------- /testdata/imagetest8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xpn/stego-playground/bd44cc2b56dea2699f246c3c8e69e24e5811158a/testdata/imagetest8.png --------------------------------------------------------------------------------