2
\$\begingroup\$

After learning the concept of steganography, I've decided to code an application for it in PHP out of all languages, using the Least Significant Bit technique.

A pixel of a colored image generally has 3 channels, one for Red, one for Green and one for Blue. 8 bits are usually used to encode the value for each channel, which gives a range of 0 to 255 in decimal. The idea is to put the bits of my payload into the least significant bits of the color channel values.

Theoretically, you could hide text or any type of file in an image as long as that image is large enough. The length of that text or file in bits must be smaller than the width * height * 3.

From a visual perspective, the difference is not noticeable to the naked eye, which is why "steganalysis" tools are used to detect it.

What I'm looking for is ways to

  • maximize the performance
  • improve the logic

Converting the contents of my payload to binary form (inside a string) each time and processing all those 1's and 0's is probably a lot of work, or so I've noticed. The code that follows hides a simple, short text, but I've tried hiding the contents of an entire image inside another image and I either screwed up, or that really takes a long time since I set my max execution limit to 5 minutes and it exceeded even that. Maybe I'm just used to things running fast in this day and age.

functions.php

<?php

function toBinary(string $message) {
    $result = '';
    for($i = 0; $i < strlen($message); $i++) {
        $result .= str_pad(decbin(ord($message[$i])), 8, "0", STR_PAD_LEFT);
    }
    return $result;
}

?>

encode.php

<?php

include('functions.php');

// Number of bytes in an integer.
$INTEGER_BYTES = 4;
$BYTE_BITS = 8;

$message = "Hello from outer space, it's your commander speaking.";
$binaryMessage = toBinary($message);
// The number of bits contained in the message, aka the size of the payload as an integer.
$messageLength = strlen($binaryMessage);
// Convert the length to binary as well and make sure to pad it with 32 0's.
$binaryMessageLength = str_pad(decbin($messageLength), $INTEGER_BYTES * $BYTE_BITS, "0", STR_PAD_LEFT);


// The payload will incorporate the length and the message.
$payload = $binaryMessageLength.$binaryMessage;

$src = 'wilderness.jpg';
$image = imagecreatefromjpeg($src);

$size = getimagesize($src);
$width = $size[0];
$height = $size[1];

function encodePayload(string $payload, $image, $width, $height) {
    $payloadLength = strlen($payload);
    // We are able to store 3 bits per pixel (1 LSB for each color channel) times the width, times the height.
    if($payloadLength > $width * $height * 3) {
        echo "Image not big enough to hold data.";
        return false;
    }
    $bitIndex = 0;
    for($y = 0; $y < $height; $y++) {
        for($x = 0; $x < $width; $x++) {
            $rgb = imagecolorat($image, $x, $y);
            // Each color channel's value is extracted from the original integer.
            $r = ($rgb >> 16) & 0xFF;
            $g = ($rgb >> 8) & 0xFF;
            $b = $rgb & 0xFF;

            // LSB's are cleared by ANDing with 0xFE and filled by ORing with the current payload bit, as long as the payload length isn't hit.
            $r = ($bitIndex < $payloadLength) ? (($r & 0xFE) | $payload[$bitIndex++]) : $r;
            $g = ($bitIndex < $payloadLength) ? (($g & 0xFE) | $payload[$bitIndex++]) : $g;
            $b = ($bitIndex < $payloadLength) ? (($b & 0xFE) | $payload[$bitIndex++]) : $b;

            $color = imagecolorallocate($image, $r, $g, $b);
            imagesetpixel($image, $x, $y, $color);

            if($bitIndex >= $payloadLength) {
                return true;
            }
        }
    }
}

if(encodePayload($payload, $image, $width, $height)) {
    echo 'Payload delivered.'.'<br>';
} else {
    echo 'Something went wrong.'.'<br>';
}

imagepng($image, 'encoded.png');
imagedestroy($image);

?>

decode.php

<?php

$INTEGER_BITS = 32;
$src = 'encoded.png';
$image = imagecreatefrompng($src);

$size = getimagesize($src);
$width = $size[0];
$height = $size[1];

// Returns the message length in bits as an integer.
function decodeMessageLength($image, $width, $height) {
    // We need to process the first 32 LSB's of the image to retrieve the int.
    $numOfBits = 32;
    $bitIndex = 0;
    $binaryMessageLength = 0;
    for($y = 0; $y < $height; $y++) {
        for($x = 0; $x < $width; $x++) {
            $rgb = imagecolorat($image, $x, $y);
            // We extract each component's LSB by simply ANDing with 1.
            $r = ($rgb >> 16) & 1;
            $g = ($rgb >> 8) & 1;
            $b = $rgb & 1;

            $binaryMessageLength = ($bitIndex++ < $numOfBits) ? (($binaryMessageLength << 1) | $r) : $binaryMessageLength;
            $binaryMessageLength = ($bitIndex++ < $numOfBits) ? (($binaryMessageLength << 1) | $g) : $binaryMessageLength;
            $binaryMessageLength = ($bitIndex++ < $numOfBits) ? (($binaryMessageLength << 1) | $b) : $binaryMessageLength;

            if($bitIndex >= $numOfBits) {
                return $binaryMessageLength;
            }
        }
    }
}

function decodeBinaryMessage($image, $width, $height, $offset, $messageLength) {
    $offsetRemainder = $offset % 3;
    // We get 3 bits for each pixel, so the offset needs to be divided by 3.
    $offset /= 3;
    // Instead of looping through all the pixels, an offset is used for the starting indices.
    $line = $offset / $width;
    $col = $offset % $width;
    $binaryMessage = '';
    $bitIndex = 0;
    for($y = $line; $y < $height; $y++) {
        for($x = $col; $x < $width; $x++) {
            $rgb = imagecolorat($image, $x, $y);
            // We extract each component's LSB by simply ANDing with 1.
            $r = ($rgb >> 16) & 1;
            $g = ($rgb >> 8) & 1;
            $b = $rgb & 1;

            // Depending on the remainder, we will start with a different LSB.
            if($offsetRemainder == 1) {
                $binaryMessage .= $g;
                $binaryMessage .= $b;
                $offsetRemainder = 0;
                $bitIndex += 2;
            } else if($offsetRemainder == 2) {
                $binaryMessage .= $b;
                $offsetRemainder = 0;
                $bitIndex++;
            } else {
                // As long as the bit index is lower than the length of the message, concatenate each component's LSB to the message.
                $binaryMessage = ($bitIndex++ < $messageLength) ? ($binaryMessage.$r) : $binaryMessage;
                $binaryMessage = ($bitIndex++ < $messageLength) ? ($binaryMessage.$g) : $binaryMessage;
                $binaryMessage = ($bitIndex++ < $messageLength) ? ($binaryMessage.$b) : $binaryMessage;

                if($bitIndex >= $messageLength) {
                    return $binaryMessage;
                }
            }
        }
    }
}

$decodedMessageLength = decodeMessageLength($image, $width, $height);
$decodedBinaryMessage = decodeBinaryMessage($image, $width, $height, $INTEGER_BITS, $decodedMessageLength);

$decodedMessage = implode(array_map('chr', array_map('bindec', str_split($decodedBinaryMessage, 8))));
echo 'The hidden message was:'.'<br>'.$decodedMessage.'<br>';

imagedestroy($image);

?>
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.