
🔐 Kuvasteganografia PHP:ssä: tekstin piilottaminen pikseleihin LSB-menetelmällä
Klikkaat, ja encoded.png näyttää täysin samalta kuin container.jpg. Kukaan ei epäile, että sisällä on piilotettu viesti. Steganografia ei tee datasta lukukelvotonta kuten salaus, vaan se piilottaa koko tiedonsiirron tosiasian. PHP-kehittäjälle tämä on yksi niistä tekniikoista, jotka kannattaa ymmärtää bittien ja pikseleiden tasolla.
LSB-steganografia PHP:lla GD-moduulia käyttäen ei ole tuotantotyökalu vaan harjoituskenttä. Harjoituskenttä, joka opettaa binääriesityksen, bittimaskien ja häviöttömien formaattien käsittelyä paremmin kuin tusina CRUD-tehtävää. Kuljet polun "miten kuva rakentuu sisäisesti" aina komentoon php decrypt.php, joka poimii pikseleistä sanan hello, ja ymmärrät, miksi JPEG ei sovellu tähän.
💡 Pikakatsaus:
- Puretaan teoria: miten sinisen kanavan vähiten merkitsevä bitti piilottaa viestin ja miksi silmä ei sitä näe
- Kirjoitetaan kolme PHP-tiedostoa:
functions.php(apuvälineet),encrypt.php(koodaus),decrypt.php(purku) - Ajetaan se oikealla JPEG-säiliöllä ja saadaan visuaalisesti identtinen PNG-tulos
- Käydään läpi rajoitukset: pakkaus, havaittavuus, kapasiteetti ja yhdistelmä AES-salauksen kanssa
- Katsotaan valmiita PHP-kirjastoja ja päätetään, milloin GD riittää ja milloin on aika käyttää Pythonia
Miten LSB-steganografia toimii
LSB (Least Significant Bit) on menetelmä, jossa salainen viesti kirjoitetaan pikseleiden vähiten merkitseviin bitteihin. Nimi on kirjaimellinen: otamme värikanavan vähiten merkitsevän bitin ja korvaamme sen viestin bitillä.
Digitaalinen kuva on pikseliruudukko. Jokainen pikseli koodataan kolmella tavulla: punainen (R), vihreä (G) ja sininen (B). Kolmen arvon, 0-255, yhdistelmä määrittää lopullisen värin. Vähiten merkitsevän bitin muuttaminen siirtää kanavan arvoa korkeintaan yhdellä; esimerkiksi RGB(100, 50, 200) muuttuu muotoon RGB(100, 50, 201). Ihmissilmä ei erota eroa.
Otetaan oranssi pikseli:
- Punainen:
11001001(201) - Vihreä:
11111000(248) - Sininen:
00000011(3)
Sinisen kanavan vähiten merkitsevä bitti on yksi. Korvaa se salaisen viestin bitillä, ja väri pysyy visuaalisesti samana. Käsittelemällä pikseleitä peräkkäin kirjoitamme koko viestin bitti bitiltä.
Oletetaan, että meidän on piilotettava sekvenssi 1101. Otamme 4 pikseliä ja muutamme sinisen kanavan vähiten merkitsevää bittiä:
Pikseli | R | G | Alkuperäinen B | LSB korvattu | Uusi B |
|---|---|---|---|---|---|
1 |
|
|
| → |
|
2 |
|
|
| → |
|
3 |
|
|
| → |
|
4 |
|
|
| → |
|
Purku on käänteinen prosessi: keräämme sinisen kanavan vähiten merkitsevät bitit samoista neljästä pikselistä ja saamme 1101. Ei taikuutta, vain bittioperaatioita.
Käytännössä et ole rajoittunut pelkkään siniseen kanavaan: voit käyttää kaikkia kolmea (R, G, B), mikä kolminkertaistaa piilotetun datan määrän. Tässä oppaassa työskentelemme yksinkertaisuuden vuoksi vain sinisellä, jotta on helpompi nähdä, mitä kullekin bitille tapahtuu.
Vaiheittainen toteutus PHP:lla
Tarvitset PHP:n GD-moduulin kanssa, joka on esiasennettuna käytännössä kaikilla hosting-palveluntarjoajilla. Koodi on kirjoitettu PHP 8.0+:lle ja testattu versioon 8.4 asti. Luo kolme tiedostoa yhteen kansioon.
Vaihe 1: Apufunktiot, functions.php
Kaksi apuvälinettä: merkkijonon muuntaminen binääriesitykseksi ja takaisin.
1 <?php 2 3 function 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 20 function 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() muuntaa jokaisen merkkijonon merkin 8-bittiseksi esitykseksi decbin(ord(...))-funktiolla ja täyttää vasemmalta nollilla kokonaisen tavun muodostamiseksi. toString() tekee päinvastoin: se pilkkoo binäärimerkkijonon 8-bittisiksi paloiksi, muuntaa jokaisen palan desimaaliluvuksi ja hakee merkin chr()-funktiolla.
Lohkoittainen käsittely toString()-funktiossa ei ole oikku. Oppaissa suosittu lähestymistapa, jossa käytetään pack('H*', base_convert($binary, 2, 16)), hajoaa yli 31 bitin viesteillä: base_convert-funktio toimii kokonaisluvuilla, ja kun PHP_INT_MAX ylittyy, se palauttaa roskaa. Tavuittain pilkkominen ratkaisee ongelman minkä tahansa pituisille viesteille.
Vaihe 2: Koodaus, encrypt.php
Lukee JPEG-säiliön, upottaa viestin sinisen kanavan vähiten merkitseviin bitteihin ja tallentaa tuloksen PNG-muodossa. Miksi ei JPEG? Häviötön formaatti on pakollinen: JPEG-pakkaus tuhoaa piilotetut bitit, ja viesti muuttuu kohinaksi.
1 <?php 2 3 require_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 12 for ($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 28 imagepng($img, 'encoded.png'); 29 30 echo "Закодировано {$msgLen} бит — результат в encoded.png\n";
imagecolorat() hakee pikselin värin koordinaateista (x, y). Bittisiirrot >>16 ja >>8 erottavat R-, G- ja B-arvot. Sininen kanava laajennetaan 8-bittiseksi merkkijonoksi, vähiten merkitsevä bitti (indeksi 7) korvataan viestin seuraavalla bitillä, ja uusi väri kirjoitetaan imagesetpixel()-funktiolla.
Viesti koodataan diagonaalisesti (y = x), mikä helpottaa logiikan seuraamista pienessä esimerkissä. Oikeassa projektissa iteroi kaikki pikselit rivi riviltä ja lisää 32-bittinen otsikko viestin pituudella: ilman sitä purkaja ei tiedä, missä lopettaa.
Vaihe 3: Purku, decrypt.php
Purkaa piilotetun viestin: käy läpi samat pikselit ja kerää sinisen kanavan vähiten merkitsevät bitit.
1 <?php 2 3 require_once 'functions.php'; 4 5 $src = 'encoded.png'; 6 $img = imagecreatefrompng($src); 7 8 $bits = ''; 9 10 for ($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 19 echo toString($bits) . "\n";
Tässä 40 on kovakoodattu bittimäärä, tasan 5 tavua sanalle hello. Tuotantoversiossa korvaa tämä luku viestin otsikosta saadulla pituudella, jonka kirjoitit ensimmäisiin 32 bittiin koodauksen aikana.
Vaihe 4: Ajo ja varmennus
Sijoita container.jpg skriptien kanssa samaan kansioon. Toimintatapa:
php encrypt.phptulostaa konsoliin: "Encoded 40 bits, result in encoded.png".php decrypt.phptulostaa konsoliin:hello.- Avaa
container.jpgjaencoded.pngvierekkäin; eroa ei ole. Ne ovat visuaalisesti erottamattomat.
Voit vertailla tavu tavulta ImageMagickilla: magick compare container.jpg encoded.png diff.png korostaa erot vähiten merkitsevissä biteissä, jotka ovat käytännössä silmälle näkymättömiä. Mutta luotettavin tapa varmistaa on ajaa decrypt.php ja lukea viesti.
Menetelmän rajoitukset
LSB-steganografia on oppimisväline, ei kryptografista suojausta. Tässä on, mitä sinun on pidettävä mielessä ennen tämän lähestymistavan käyttöä oikeassa projektissa:
Pakkaus tuhoaa datan. JPEG laskee uudelleen 8×8 pikselin lohkot kvantisoinnilla, eivätkä vähiten merkitsevät bitit selviä. Siksi
encrypt.phptallentaa tuloksen ehdottomasti PNG-muodossa (häviötön). Yksi läpikulku JPEG-pakkauksen läpi, ja piilotetut bitit muuttuvat kohinaksi. Vaihtoehtoiset formaatit: BMP, pakkaamaton TIFF.Havaittavuus. LSB-täyttö jättää tilastollisia artefakteja: sinisen kanavan histogrammi muuttuu ennustettavalla tavalla. RS-analyysi ja χ²-testi havaitsevat piilotetun viestin sekunneissa. Ilman salausta LSB on kätkemistä, ei suojausta: viestin olemassaolo voidaan havaita, vaikka sisältöä ei voitaisikaan lukea.
Datan määrä. Jokainen pikseli antaa yhden bitin sinisessä kanavassa. 1000×1000 pikselin kuvaan mahtuu jopa 125 kt piilotettua tekstiä; tämä on matemaattinen yläraja, joka seuraa LSB-menetelmän määritelmästä. Käytännössä PNG-pakkauksen jälkeen hyödyllinen tilavuus on pienempi, ja lisäksi osa biteistä menee pituusotsikkoon.
Ei kryptografiaa. Steganografia ei korvaa salausta, vaan täydentää sitä. Oikea putki: salaa viesti (AES-256) ja piilota sitten salateksti kuvaan. Silloin edes piilokanavan havaitseminen ei paljasta sisältöä.
PHP GD on perustyökalu. GD tarjoaa matalan tason pääsyn pikseleihin, ja oppimiseen tämä on enemmän kuin tarpeeksi. Mutta Python-ekosysteemi steganografialle on huomattavasti rikkaampi:
stegano-kirjasto (LSB + salaus),Stegano(joukko menetelmiä) jasteghide(C++, mutta saatavilla on Python-sidoksia). Puhtaassa PHP:ssa on useita kirjastoja:kzykhys/Steganography(minimaalinen LSB-toteutus PNG:lle),seba/steganography(moderni kirjasto LSB-manipulaatioon) japateon/steganography(PHP 8.5+ GD- ja Imagick-tuella). Muttasteghide-tason tuotantokelpoisia ratkaisuja ei PHP:ssa ole vuonna 2026.
⁉️🤔 Usein kysytyt kysymykset
Voiko tekstiä piilottaa suoraan JPEG-kuvaan?
Ei. JPEG-pakkaus jakaa kuvan 8×8 lohkoihin ja soveltaa kvantisointia; vähiten merkitsevät bitit eivät selviä. Datan piilottamiseen JPEG-kuvaan käytetään erilaista lähestymistapaa: data piilotetaan diskreetin kosinimuunnoksen (DCT) kertoimiin, ei pikseleihin. Algoritmit: F5, JSteg, OutGuess. Valmiita PHP-kirjastoja tähän ei käytännössä ole; helpompaa on käyttää Pythonia.
Miten LSB eroaa steganografiasta metadatassa?
LSB muuttaa pikselidataa; viesti on piilotettu kuvan "sisään". Metadata (EXIF, XMP, PNG tEXt -lohkot) sijaitsee tiedoston otsikoissa, ja minkä tahansa ominaisuuksien katseluohjelman voi lukea sen. Metadata on helpompi purkaa, mutta se selviää JPEG-pakkauksesta, toisin kuin LSB.
Miten voin varmistaa, että encoded.png todella sisältää piilotetun viestin?
Visuaalisesti et mitenkään. Vertaa tiedostoja tavu tavulta:
magick compare container.jpg encoded.png diff.png(ImageMagick) näyttää erot vähiten merkitsevissä biteissä. Ne ovat käytännössä silmälle näkymättömiä, mutta apuohjelma korostaa muuttuneet pikselit. Luotettavin tapa on ajaaphp decrypt.phpja lukea viesti.
Kuinka turvallista tämä kaiken kaikkiaan on?
Ilman salausta se ei ole turvallista. LSB ilman edeltävää AES-salausta on kätkemistä, ei suojausta. Piilotetun tiedonsiirron tosiasia havaitaan tilastollisilla testeillä sekunneissa. Yhdistelmä "AES-salaus + LSB-kätkentä" tarjoaa hyväksyttävän tason harrasteprojekteille, mutta ei arkaluonteiselle datalle.
Onko olemassa valmiita PHP-kirjastoja steganografiaan?
Kyllä, mutta ne ovat kaikki opetuskäyttöön tai kevyitä.
kzykhys/Steganographyon minimaalinen LSB-toteutus, joka toimii vain PNG:n kanssa.seba/steganographyon tuoreempi, ja siinä on selkeä API tekstin koodaamiseen/purkamiseen kuviin.pateon/steganographyon PHP 8.5+:lle ja tukee sekä GD:tä että Imagickia. Vakaviin tehtäviin katso Pythonin suuntaan:stegano(LSB + salaus) jaStegano(joukko menetelmiä, mukaan lukien stego-kuvageneraattori).
Steganografia PHP:lla: oppimispohja vai valmis työkalu?
LSB-steganografia PHP:lla GD:n avulla on erinomainen harjoituskenttä sen ymmärtämiseen, miten dataa piilotetaan pikselitasolla. Kirjoitit toimivan koodaus/purku-syklin, selvitit bittioperaatiot ja näit käytännössä, miksi "näkymätön" muutos jättää jälkiä kuvan tilastoihin.
Oppimisdemoon, harrasteprojektiin tai CTF-haasteeseen tämä työkalupakki on enemmän kuin tarpeeksi. Oikeaan tietosuojaukseen lisää salaus ennen kätkentää ja tutustu Python-kirjastoihin, jotka ovat ominaisuuksiltaan ja havaitsemisen vastustuskyvyltään ylivoimaisia PHP-vastineisiin verrattuna.
Jos haluat kaivaa syvemmälle, toteuta koodaus kaikissa kolmessa värikanavassa kerralla ja lisää 32-bittinen otsikko viestin pituudella. Saat minityökalun, joka on 3 kertaa tehokkaampi kuin perusversio ja joka jo täyttää kurssiprojektin vaatimukset. Julkaise lähdekoodi GitHubissa, jotta muut voivat oppia esimerkistäsi.



