Skip to content

Allt om WordPress, webbutveckling — och mer därtill

🔐 Bildsteganografi i PHP: dölja text i pixlar med LSB

🔐 Bildsteganografi i PHP: dölja text i pixlar med LSB

Klicka, så ser din encoded.png identisk ut med container.jpg. Ingen kommer att misstänka att det finns ett dolt meddelande inuti. Steganografi gör inte data oläslig som kryptering gör; det döljer själva faktumet att överföring sker. För en PHP-utvecklare är detta en av de tekniker som är värd att förstå på bit- och pixelnivå.

LSB-steganografi i PHP med GD-modulen är inget produktionsverktyg utan en träningsarena. En träningsarena som lär dig att arbeta med binär representation, bitmasker och förlustfria format bättre än ett dussin CRUD-uppgifter. Du går från "hur en bild är uppbyggd inuti" till raden php decrypt.php som extraherar hello ur pixlar, och du förstår varför JPEG inte lämpar sig för detta.

💡 Snabb översikt:

  • Vi bryter ner teorin: hur den minst signifikanta biten i den blå kanalen gömmer ett meddelande och varför ögat inte ser det
  • Vi skriver tre PHP-filer: functions.php (hjälpfunktioner), encrypt.php (kodning), decrypt.php (extrahering)
  • Vi kör det på en riktig JPEG-behållare och får ett visuellt identiskt PNG-resultat
  • Vi går igenom begränsningarna: komprimering, upptäckbarhet, kapacitet och kombinationen med AES-kryptering
  • Vi tittar på färdiga PHP-bibliotek och avgör när GD räcker och när det är dags att använda Python

Hur LSB-steganografi fungerar

LSB (Least Significant Bit) är en metod där det hemliga meddelandet skrivs in i de minst signifikanta bitarna i pixlar. Namnet är bokstavligt: vi tar den minst signifikanta biten i en färgkanal och ersätter den med en bit från meddelandet.

En digital bild är ett rutnät av pixlar. Varje pixel kodas med tre byte: röd (R), grön (G) och blå (B). Kombinationen av tre värden från 0 till 255 bestämmer den slutliga färgen. Att ändra den minst signifikanta biten förskjuter kanalvärdet med högst 1; till exempel blir RGB(100, 50, 200) RGB(100, 50, 201). Det mänskliga ögat kan inte urskilja skillnaden.

Låt oss ta en orange pixel:

  • Röd: 11001001 (201)
  • Grön: 11111000 (248)
  • Blå: 00000011 (3)

Den minst signifikanta biten i den blå kanalen är ett. Ersätt den med en bit från det hemliga meddelandet, så förblir färgen visuellt densamma. Genom att bearbeta pixlar sekventiellt skriver vi hela meddelandet bit för bit.

Anta att vi behöver gömma sekvensen 1101. Vi tar 4 pixlar och ändrar den minst signifikanta biten i den blå kanalen:

Pixel

R

G

Ursprunglig B

LSB ersatt

Ny B

1

11001100

10010001

00101011

1

00101011

2

00011000

11110000

11111110

1

11111111

3

11100010

00100101

01010101

0

01010100

4

11111101

00001010

01000011

1

01000011

Avkodning är den omvända processen: vi samlar in de minst signifikanta bitarna från den blå kanalen från samma fyra pixlar och får 1101. Ingen magi, bara bitvisa operationer.

I praktiken är du inte begränsad till enbart den blå kanalen: du kan använda alla tre (R, G, B), vilket tredubblar volymen av dold data. I denna handledning arbetar vi bara med blå för enkelhetens skull, så att det är lättare att se vad som händer med varje bit.

Steg-för-steg-implementering i PHP

Du behöver PHP med GD-modulen, som är förinstallerad på praktiskt taget alla webbhotell. Koden är skriven för PHP 8.0+ och testad upp till 8.4. Skapa tre filer i en mapp.

Steg 1: Hjälpfunktioner, functions.php

Två verktyg: konvertera en sträng till binär representation och tillbaka.

1<?php
2
3function toBin(string $str): string
4{
5 $result = '';
6 $len = strlen($str);
7
8 while ($len--) {
9 $result = str_pad(
10 decbin(ord($str[$len])),
11 8,
12 '0',
13 STR_PAD_LEFT
14 ) . $result;
15 }
16
17 return $result;
18}
19
20function toString(string $binary): string
21{
22 $result = '';
23 foreach (str_split($binary, 8) as $byte) {
24 $result .= chr((int) base_convert($byte, 2, 10));
25 }
26
27 return $result;
28}

toBin() konverterar varje tecken i strängen till en 8-bitars representation via decbin(ord(...)) och fyller på med nollor till vänster för att få en full byte. toString() gör det motsatta: den delar upp den binära strängen i 8-bitars block, konverterar varje block till ett decimaltal och hämtar tecknet via chr().

Blockvis bearbetning i toString() är inget infall. Den populära metoden i handledningar som använder pack('H*', base_convert($binary, 2, 16)) fallerar på meddelanden längre än 31 bitar: funktionen base_convert arbetar på heltal, och när PHP_INT_MAX överskrids returnerar den skräp. Uppdelning i byte löser problemet för meddelanden av godtycklig längd.

Steg 2: Kodning, encrypt.php

Läser en JPEG-behållare, bäddar in meddelandet i de minst signifikanta bitarna i den blå kanalen och sparar resultatet som PNG. Varför inte JPEG? Ett förlustfritt format är obligatoriskt: JPEG-komprimering förstör de dolda bitarna, och meddelandet förvandlas till brus.

1<?php
2
3require_once 'functions.php';
4
5$message = 'hello';
6$binary = toBin($message);
7$msgLen = strlen($binary);
8
9$src = 'container.jpg';
10$img = imagecreatefromjpeg($src);
11
12for ($x = 0; $x < $msgLen; $x++) {
13 $y = $x;
14 $rgb = imagecolorat($img, $x, $y);
15
16 $r = ($rgb >> 16) & 0xFF;
17 $g = ($rgb >> 8) & 0xFF;
18 $b = $rgb & 0xFF;
19
20 $blueBits = str_pad(decbin($b), 8, '0', STR_PAD_LEFT);
21 $blueBits[7] = $binary[$x];
22 $newB = bindec($blueBits);
23
24 $newColor = imagecolorallocate($img, $r, $g, $newB);
25 imagesetpixel($img, $x, $y, $newColor);
26}
27
28imagepng($img, 'encoded.png');
29
30echo "Закодировано {$msgLen} бит — результат в encoded.png\n";

imagecolorat() hämtar pixelfärgen vid koordinaterna (x, y). Bitskiften >>16 och >>8 extraherar R, G, B. Den blå kanalen expanderas till en 8-bitars sträng, den minst signifikanta biten (index 7) ersätts med nästa bit av meddelandet, och den nya färgen skrivs via imagesetpixel().

Meddelandet kodas diagonalt (y = x), vilket gör det lättare att följa logiken i ett litet exempel. I ett riktigt projekt itererar du genom alla pixlar rad för rad och lägger till ett 32-bitars huvud med meddelandelängden: utan det vet avkodaren inte var den ska sluta.

Steg 3: Avkodning, decrypt.php

Extraherar det dolda meddelandet: går igenom samma pixlar och samlar in de minst signifikanta bitarna i den blå kanalen.

1<?php
2
3require_once 'functions.php';
4
5$src = 'encoded.png';
6$img = imagecreatefrompng($src);
7
8$bits = '';
9
10for ($x = 0; $x < 40; $x++) {
11 $y = $x;
12 $rgb = imagecolorat($img, $x, $y);
13
14 $b = $rgb & 0xFF;
15 $blueBits = str_pad(decbin($b), 8, '0', STR_PAD_LEFT);
16 $bits .= $blueBits[7];
17}
18
19echo toString($bits) . "\n";

Här är 40 ett hårdkodat antal bitar, exakt 5 byte för ordet hello. I en produktionsversion ersätter du detta nummer med längden från meddelandehuvudet som du skrev i de första 32 bitarna under kodningen.

Steg 4: Körning och verifiering

Placera container.jpg i mappen med skripten. Proceduren:

  • php encrypt.php skriver ut i konsolen: "Encoded 40 bits, result in encoded.png".
  • php decrypt.php skriver ut i konsolen: hello.
  • Öppna container.jpg och encoded.png sida vid sida; det finns ingen skillnad. De är visuellt identiska.

Du kan jämföra byte för byte med ImageMagick: magick compare container.jpg encoded.png diff.png kommer att markera skillnader i de minst signifikanta bitarna, praktiskt taget osynliga för ögat. Men det mest pålitliga sättet att verifiera är att köra decrypt.php och läsa meddelandet.

Metodens begränsningar

LSB-steganografi är ett inlärningsverktyg, inte kryptografiskt skydd. Här är vad du behöver ha i åtanke innan du använder denna metod i ett riktigt projekt:

  • Komprimering förstör data. JPEG räknar om 8×8 pixelblock med kvantisering, och de minst signifikanta bitarna överlever inte. Det är därför encrypt.php sparar resultatet strikt som PNG (förlustfritt). En passage genom JPEG-komprimering, och de dolda bitarna förvandlas till brus. Alternativa format: BMP, TIFF utan komprimering.

  • Upptäckbarhet. LSB-fyllning lämnar statistiska artefakter: histogrammet för den blå kanalen förändras på ett förutsägbart sätt. RS-analys och χ²-testet upptäcker ett dolt meddelande på sekunder. Utan kryptering är LSB döljande, inte skydd: närvaron av ett meddelande kan upptäckas även om innehållet inte kan läsas.

  • Datavolym. Varje pixel ger upp en bit i den blå kanalen. En 1000×1000 pixelbild kan rymma upp till 125 KB dold text; detta är den matematiska gränsen som följer av definitionen av LSB-metoden. I praktiken, efter PNG-komprimering, är den användbara volymen mindre, plus att några bitar går till huvudet med längden.

  • Inte kryptografi. Steganografi ersätter inte kryptering; det kompletterar den. Den korrekta pipelinen: kryptera meddelandet (AES-256), göm sedan chiffertexten i bilden. Då avslöjar inte ens upptäckt av den dolda kanalen innehållet.

  • PHP GD är ett grundläggande verktyg. GD ger lågnivååtkomst till pixlar, och för inlärning är detta mer än tillräckligt. Men Python-ekosystemet för steganografi är märkbart rikare: stegano-biblioteket (LSB + kryptering), Stegano (en uppsättning metoder) och steghide (C++, men det finns Python-bindningar). I ren PHP finns det flera bibliotek: kzykhys/Steganography (en minimal LSB-implementation för PNG), seba/steganography (ett modernt bibliotek med LSB-manipulation) och pateon/steganography (PHP 8.5+ med GD- och Imagick-stöd). Men det finns inga produktionsklassade lösningar på steghide-nivå i PHP från och med 2026.

⁉️🤔 Vanliga frågor

Kan man gömma text direkt i JPEG?

Nej. JPEG-komprimering delar upp bilden i 8×8-block och tillämpar kvantisering; de minst signifikanta bitarna överlever inte. För att gömma data i JPEG används en annan metod: data göms i diskreta cosinustransform-koefficienter (DCT), inte i pixlar. Algoritmer: F5, JSteg, OutGuess. Det finns praktiskt taget inga färdiga PHP-bibliotek för detta; det är enklare att använda Python.

Hur skiljer sig LSB från steganografi i metadata?

LSB ändrar pixeldata; meddelandet göms "inuti" bilden. Metadata (EXIF, XMP, PNG tEXt-chunks) finns i filhuvuden och kan läsas av vilken egenskapsvisare som helst. Metadata är lättare att extrahera, men det överlever JPEG-komprimering, vilket LSB inte kan.

Hur kan jag verifiera att encoded.png faktiskt innehåller ett dolt meddelande?

Visuellt kan du inte det. Jämför filerna byte för byte: magick compare container.jpg encoded.png diff.png (ImageMagick) kommer att visa skillnader i de minst signifikanta bitarna. De är praktiskt taget osynliga för ögat, men verktyget markerar de ändrade pixlarna. Det mest pålitliga sättet är att köra php decrypt.php och läsa meddelandet.

Hur säkert är detta totalt sett?

Utan kryptering är det inte säkert. LSB utan föregående AES-kryptering är döljande, inte skydd. Faktumet att dold dataöverföring sker upptäcks av statistiska tester på sekunder. Kombinationen "AES-kryptering + LSB-döljande" ger en acceptabel nivå för pet-projekt, men inte för känslig data.

Finns det färdiga PHP-bibliotek för steganografi?

Ja, men de är alla pedagogiska eller lätta. kzykhys/Steganography är en minimal LSB-implementation som bara fungerar med PNG. seba/steganography är nyare, med ett rent API för kodning/avkodning av text i bilder. pateon/steganography är PHP 8.5+ och stöder både GD och Imagick. För seriösa uppgifter, titta mot Python: stegano (LSB + kryptering) och Stegano (en uppsättning metoder, inklusive en stego-bildgenerator).

Steganografi i PHP: en inlärningsbas eller ett färdigt verktyg?

LSB-steganografi i PHP med GD är en utmärkt träningsarena för att förstå hur data göms på pixelnivå. Du skrev en fungerande kodnings-/avkodningscykel, redde ut bitvisa operationer och såg i praktiken varför en "osynlig" förändring lämnar spår i bildstatistiken.

För en inlärningsdemonstration, ett pet-projekt eller en CTF-utmaning är denna verktygslåda mer än tillräcklig. För verkligt dataskydd, lägg till kryptering före döljandet och titta på Python-bibliotek, som är huvud och axlar över PHP-motsvarigheter i kapacitet och motståndskraft mot upptäckt.

Om du vill gräva djupare, implementera kodning i alla tre färgkanaler samtidigt och lägg till ett 32-bitars huvud med meddelandelängden. Du får ett miniverktyg som är 3 gånger effektivare än grundversionen och redan kvalificerar som ett kursprojekt. Lägg upp källkoden på GitHub så att andra kan lära sig av ditt exempel.