2026/08/28

PNG autopsy III

A következőkben a Filterezés támadását tovább gondoljuk és saját filtereket találunk ki. Érdemes az előző bejegyzéssel alapozni, ha nem onnan jöttél. 

Tehát azt már tudni véljük, hogy a PNG 5 filterétől eltérő (más irányokból is építkező) filterek alkalmazásának nincs pragmatikus vonatkozása, de ez nem jelenti azt, hogy nem kísérletezhetünk vele.
Már van olyan eszközünk (köszi Ai, vagy ti mindannyian, akik drágább memória árakkal fizetitek ezt helyettem, vagy az utókornak, akinek ezzel éljük fel a jövőjét), amelyik képes olyan PNG-ket menteni (csak RGB8 semmia alfa és semmi interlace), ahol a scanlineokra kötelezően általunk választott filtert alkalmaz, csak annyit kell hozzáprogramozni, hogy saját algoritmusokat is tudjon kezelni. Ezeknek mondjuk a filter byte-ját 5-6-7 és így tovább, írná bele a képbe, amitől egy normális képnézegető egyből hülyét kap, például a Photoshop, hiszen nem ismert Filtert nem tud használni. De például az Irfan (sőt a windows file explorer nézete) nem esik kétségbe, látszólag az összes nem értelmezhető filterű scanlinet None-nak tekint és megjeleníti. De van olyan eszközünk is, ami adott scanline típus-byteokat el tud hazudni. Az ilyen Custom filterezett képeinknek itt tudunk valid filter-byteot beállítani, ezután már minden képnézegető meg fogja nyitani a képünket a kiválasztott algoritmus szerint, ami természetesen a glitchet fogja okozni.

Három Custom filterrel próbálkoztunk, remélve a szebbnél szebb glitch mintázatokat. Amit ha nem is sikerült elérni, de tapasztalatokat azért szereztünk. Fontos, hogy ezeknél a teszteknél az előző bejegyzésben is említett kis segédprogramunkat használjuk, ami minden scanlinra ráerőlteti ugyanazt a filtert.

Custom1 -- Körkörös prediktor (mind a 8 szomszéd átlaga) - ami körbe minden pixelt figyel, vagyis 4 már elkódoltat a szokásos irányból (balsó, felső irányok), és négy pixelt a jobbra és alul elterülő részről, amit  kikódolásnál azért nem ismerhetünk, mert még nem kódoltuk ki (ha nem világos, tényleg menj vissza az előző posztra). Nyilván emiatt egy ilyen filterrel elkódolt kép nem invertálható (dekódolható) egyszerű szekvenciális dekódolással, de most ez nem is cél, hiszen a Glitch okozására hajtunk, ezért csak ENCODE-ra használjuk.

Custom2 -- RGB-Average (csatornánként MÁSHOVA néző prediktor) - vagyis
- R csatorna <- bal szomszéd R csatornája
- G csatorna <- balsó-fenti szomszéd G csatornája
- B csatorna <- fenti szomszéd B csatornája
Ez az algoritmus elvileg ENCODE-ra és DECODE-ra is használható, hiszen csak valid irányokból veszi az adatot.

Custom3 -- InverseAverage (jobbra / lent szomszédok átlaga) - itt minden hivatkozás jövőbeli, tehát későbbi feldolgozású pixelre mutat, ENCODE-ra tökéletesen használható, de DECODE-ot csak fordított irányban lehetne végrehajtani rajta, tehát ha a jobb alsó pixeltől indulva visszafelé zajlana a dekódolás.

És így néznek ki a Custom Filterekkel elkódolt, de valós Filtereknek (azokkal kikódolt) hazudva:



Emlékeztetésnek és összehasonlításnak (az előző bejegyzésből átemelve),  a valós filterek egymás között elhazudva így néztek ki:

Kis felbontású és JPG-nek mentett nézet, az eredeti jóval gazdagabb.


Az eredmény érdekes, de sajnos nem a reményeink szerint való, azt vártuk volna ugyanis, hogy a merőben eltérő encoder filtereink merőben eltérő hatásokat fognak produkálni, rosszabb esetben annyira összeborzolják a képet, hogy az felismerhetetlenné válik. Ezzel szemben a kikódolásra használt filter hatása a domináns, tehát például egy UP függőlegesen vonalas, egy Average átlósan, stb, és egyáltalán nem felismerhetetlen a kép sem. 

Ezen a ponton végleg el kellett fogadnunk, hogy a végső, meg-glitchet kép zajmintázatát, a glitch geometriáját leginkább nem az határozza meg, hogy eredetileg milyen filterrel volt elkódolva, hanem jellemzően a kikódoló algoritmus. Tehát hiába találunk ki bármilyen irányú, csillag alakú, hatszög, stb. saját-filtert, a végeredmény mégis a kikódoló filter jellegzetességeit fogja mutatni. 

De hogyan lehetne megnézni egy (bármilyen filterezésű) képet a saját filterrel kikódolva, amikor nincs a földön olyan képszerkesztő, amelyik ismerné a mi saját filterünk algoritmusát?  Na erre is írattunk egy python progit a mesterséges intelligenciával, ami bármilyen filterezésű bemeneti képet egy saját filterrel próbál értelmezni. A saját filternek azt választottuk, amelyik a pixel összes (körben mind a 8) szomszédjának átlagával dolgozik, és ugyanúgy, balról jobbra, fentről lefelé járja be a képet. Ilyenkor értelemszerűen az adott pixel kikódolásakor a 8 szomszédból csak négynek (balra és felül található) az értéke ismert, a többi négynek (jobbra és alul található pixeleknek) az értéke még nincs kikódolva. Ez fogja okozni  a glitchet. Ezt egy kis parancssoros pythonnal értük el, ami annyit csinál, hogy bármilyen filterezésű bemeneti képet a mi saját filterünkkel kódol ki, majd az egyszerűség kedvéért sima bmp-nek ment.


Azt vártuk, hogy olyan zaj-katyvaszt kapunk, hogy a fal adja a másikat, ezzel szemben meglepődtünk. Egyrészt igazolódott, hogy mindig a kikódoló filter karakterisztikája lesz a domináns a glitchelt képen. Vagyis közelről megnézve pontszerű (pointilista-szerű) zaj keletkezik, ami nem meglepő hiszen egyes pixelek értéke minden irányból származik, nincs kitüntetett irány. Ugyanakkor az is látszik, hogy az elkódoló filterek is rányomják, bár sokkal csekélyebb mértékben, a kézjegyüket a végleges glitchre. Érdemes megfigyelni, hogy az eredetileg Sub-nak kódolt, majd Customnak kikódolt képen a nagyobb felületek élei dominánsan függőlegesek, míg az Up-nak elkódolt és Customnak kikódoltak esetében vízszintesek. Az Average filterrel elkódolt és Custommal kikódolt képeknél, az összefüggő felületek alsó-jobboldali élei dominánsak. Külön meglepett, hogy a Paeth elkódolás, ami mindig borzalmas kristályszerű szín-glitcheket okozott, itt egyáltalán nem mutatta a szivárványos geometriáit. 

A további 3 kép, amikor az elkódolás során három saját filtert használtunk, külön érdekes. A bejegyzés elején felsoroltuk ezeket a saját filtereket, a C1 az maga a körkörös, amit a kikódolásra is használunk (C1 - C1). Természetesen nem tudja visszaadni az eredeti képet glitchmentesen, hiszen említettük, hogy a 8 szomszédos pixel értékéből, amiből építkezik, 4 értéke nem ismert. Egyfajta edge-detection hatást vélünk felfedezni itt. A C2-C1 esetében az elkódolás során a második saját filterünket próbáljuk kikódolni az első saját filterünkkel, egyszerűsítve, a C2 az RGB értékeinek eltérését három szomszédjától veszi, a pirosat a baltól, a zöldet a bal felsőtől, a kéket pedig a felsőtől.)
A C3-C1 meg hasonló, de itt a C3 egy inverz Average filter, vagyis jobboldali és alsó szomszédaiból építkezik (gyakorlatilag mindkét szomszédja ismeretlen a kikódolás balról és fentről bejárása miatt), ezért látszik úgy, mintha a zajban az edge-detection fordított irányú lenne az Average-C1-hez képest. 

Na így. A programokat, szkipteket elküldöm annak aki kéri, de nem látom értelmét ide felpakolni a blogra, mert valószínű, hogy az elkövetkező 10 évben nem lesz több érdeklődő mint 0.

És végezetül így néz ki a normál RGB8 PNG (vegyes scanline filterrel), Custom 1 filterrel kikódolva:




PNG autopsy II.

Régóta dédelgetett, parkoltatott stb. vágyam, hogy megberheljem a PNG-t, de úgy igazán mélyen.  S bár az AI azt állítja, hogy a PNG-be nem lehet hexeditorban csak úgy belepiszkítani, nekünk régebb mégis sikerült (gondolom az Irfan megengedőbb a sérült CRC-vel).

Kezdjük ezzel. UCNV japán művész, nagyon érdekes munkái vannak, de most csak a PNG-glitchjeire koncentráljunk. Amiről régebb nem találtunk futtatható programot (Rubyhoz még annyira se értünk mint a Pythonhoz), de most a  Claude olyan python-progit ír nekünk, amilyet csak szeretnénk. 

A PNG feldolgozási lánca (hol érdemes megtámadi):

  1. Raw Data (nyers pixelek byte értékei RGB vagy RGBA stb.) — ezt piszkálni valójában BMP glitch, nem PNG-specifikus
  2. Filtered Data — ide lehet nyúlni  - erről fog szólni a bejegyzés
  3. Compressed Data — ide is lehet nyúlni
  4. Formatted PNG (checksummal) — ide NEM tanácsos, mert a sérült CRC miatt érvénytelennek látják a képszerkesztő programok, de  - ilyet csináltunk már hexeditorban és van progi amivel megnyitható.
A nyers megnyitott kép pixeleit, ami mondjuk RGB, RGBA, soronként (ez a scanline) filterezi a képszerkesztő program. RGB8 esetén a pixelek három byteal vannak reprezentálva
<Filter-byte><Pixel1-Red><Pixel1-Green><Pixel1-Blue><Pixel2-Red><Pixel2-Green><Pixel2-Blue>  ... <Pixeln-Red><Pixeln-Green> <Pixeln-Blue> 
RGBA esetén, nyilván 4 byte ír le egy pixelt, RGB16 esetén meg 6 byte. A tovébbiakban RGB8 képekkel dolgozunk, és az Interlace-et is messzire elkerüljük, mert az csak tovább bonyolítaná az egészet.

Az 5 filter típus (amiből választ a képmentő program):

  • None — nincs filter, minden pixel önálló, saját RGB értéket tartalmaz, egy bitcsere csak lokális zajt okoz
  • Sub — az előző (bal oldali) pixel három bytejához viszonyít, és az eltérést tartalmazza → a pixel bytejaiban okozott hiba ezért jobbra "csorog", hiszen a következő pixel bytejai már ebből a sérültből fogja venni az adatot. 
  • Up — a fölötte lévő sorhoz viszonyít, az eltérést tartalmazza → függőlegesen csoroghat a hiba lefelé
  • Average — a Sub és Up átlaga, a balra és fölötte levő szomszédainak átlagára támaszkodik  → átlós, lágy, "üstökös-farok" jellegű, kevésbé "glitches", inkább szép színátmenet
  • Paeth — a legkomplexebb algoritmus → a leglátványosabb, legkiterjedtebb glitch, miközben a kép formája még felismerhető marad 
Ez nagyjából annyit jelent, hogy legtöbbször nem konkrét RGB értékeket tárol a fileban, hanem az adott filter algoritmusa szerint más, szomszédos pixelektől való eltérését, például a Sub esetén az előtte (feldolgozási időben előtte, amúgy balra tőle) levő pixel értékétől való eltérést, Up esetén a fölötte levő pixeltől. Az Average és a Paeth algoritmusai ennél komplexebbek, de a logika ugyanaz. Nagy egybefüggő színfelületek így nagyon sok 0 és más kis értékkel leírhatóak, amik majd jól tömöríthetőek.

A PNG filterezésének logikája az, hogy ha egy irányból bejárjuk a képet, mondjuk bal felső saroktól soronként jobbra, majd lefelé, akkor majd az RGB adat visszanyerésénél is ugyanígy végig tudjuk járni az összes pixelt, ugyanazzal az algoritmussal és mindig valid RGB adatokat találjunk azokon a pozíciókon, amikre támaszkodva vissza kell állítanunk egy pixel értékét. 

Például, a legfelső sor mondjuk Up filterrel kerül elkódolásra. Balról jobbra, fentről lefelé. Az első pixel értéke a felette levő pixeltől való eltérése lenne, de fölötte nincs sor. Ilyenkor a fölötte levő pixel értékét 0-nak veszi a program, így az eltérés gyakorlatilag ugyanaz lesz, mint maga az RGB érték. És így tovább jobbra. A következő sornál, ha történetesen megint Up filterezés van, akkor már tud építkezni a fölötte levő pixelekből. Ugyanez a logika érvényesül a Sub-ra is, csak  vízszintesen. Sőt az Averagnél is, csak itt mind a két, előtte és fölötte levő szomszédját átlagolja, ahol mindig valid (az adott pixel kiszámításánál már visszaállított) adatok vannak. A Paeth esetében szintén, csak itt még bele keveri a bal-felső szomszédot is és nem átlaggal dolgozik. 

Tehát soha nem próbál meg a jobboldalról, vagy alábbi sorokból építkezni, mert amikor vissza kell állítani a képet, mondjuk egy tetszőleges pixelen, akkor mindig csak tőle balra és fölötte találhatóak a már visszaállított értékek, jobbra, és alatta a még kikódolatlan eltéréseket találja, a nemtudni-mihez-viszonyítva. Ezért nincsenek mondjuk körkörös filterek, amik a pixel összes szomszédját használják az algoritmusban, vagy nincsenek olyanok sem, amelyek balról és jobbról is, vagy alulról és felülről is  építkeznének. Mert ezeket egyszerű módon nem is lehetne visszaállítani (bár biztos ennek is meg lehetne találni a matematikáját, de ehhez kicsik vagyunk). 

Minden scanline előtt egy plusz byte jelzi, az adott sor melyik filterrel van képezve (0=None, 1=Sub, 2=Up, 3=Average, 4=Paeth), innen tudja majd a kikódoló program, hogy milyen algoritmust használjon. De egy képszerkesztő programban, felhasználóként, nincs hatásunk arra, hogy melyik sorra milyen filtert használ mentésnél (talán csak nagyon közvetve, amikor a lassúság-minőséget beállítjuk). De az nagyon valószínű, hogy nagy vízszintes hasonló színű felületek ugyanolyan filtert kapnak. Ez is lehetne egy tesztelendő irány, de ragaszkodjunk amennyire lehet az eredeti irányhoz.

A megnyitott kép összes sorát az öt hivatalos és több saját filterrel lehet elmenteni

Mivel minden sor mindenféle filterű lehet, ez nagyon sok változót visz a rendszerbe, ami egyrészt a Glitch-véletlenségének jót tesz, de ha meg akarjuk valamennyire érteni a véletlent, akkor meg pont nem segít. Ezért spéci python-programmal (amit az AI írt nekünk) úgy mentettük el tehát a PNG-ket, hogy minden scanline-ra ugyanazt a filtert erőszakoltuk, majd ezeknek a filtereit szerre elhazudtuk (átállítottuk a scanline elején a filter-byteokat és aszerint kódoltuk ki). Így például egy SUB-nak mentett és SUB-ként megjelenített kép tökéletesen visszaadja a kiindulási képet, de az összes többi filterrel kikódolva a két filter interakciójára jellemző glithet eredményez.


A következő tesztképeket a fenti program 3. pontjánál generáltuk, vagyis benyitottuk szerre az összes None, Sub, Up, Average, Paeth-filterre normalizált - az előbbi programmal generált - képünket és szerre mindegyiknek elhazudtuk a filterét az összes többi lehetséges filterre. Ezért van az, hogy például egy Up-ra normalizált kép Up-ként megnyitva tökéletesen visszaadja az eredetit, de már Average-nek nyitva sérül, és ez mind az ötre igaz:

Kis felbontású és JPG-nek mentett nézet, az eredeti jóval gazdagabb.

Ezzel nagyjából látjuk, hogy milyen hatásra számíthatunk az egyes filterek esetén (ha a PNG minden sora ugyanazzal a filterrel lenne elkódolva). És már az is megsejthető, ami nekünk egyből nem esett le, hogy mindegy mivel volt elkódolva a kép, úgyis a kikódolás / interpretálás filtere fogja rányomni a bélyegét a glitchelt képre. Így a None oszlopban a képek csak zajosak, a Sub oszlopban inkább vízszintes vonalas, Up-nál függőleges struktúrák, Average-nél átlós, Paethnél meg színkristály-zajos. 

Mint az elején láttuk, két helyen érdemes a PNG-t feszegetni, a filterezett adatnál, illetve a tömörített adatnál. Most már érthető, hogy miért van a PNG-glitcheknek ilyen sajátos megjelenése.

Ha a filterezett adatba piszkálunk, az akkor még eléggé pixelspecifikus, konkrét RGB-értéket ugyan nem (a None filtert leszámítva), de pixel-pozíciót, pixel blokkokat, még precízen tudunk támadni x,y koordináta alapján. A filterezett adatfolyamot képmentés során tömörítik (deflate - zlib) ez lesz a compressed data, amit chunkokba szervez, minden chunknak CRC-t számol, ezután már nem tudunk célzottan és közvetlenül támadni egyes pixeleket, vagy kép-zónákat a deflate algoritmus miatt. Elvileg a Compressed-adat támadás után az új IDAT miatt új CRC-t kell számoltatni, hogy a képnéző programok meg tudják jeleníteni. Mivel az Irfan elég megengedő, és mivel régebb Hex-editorban compressed adatot már hajlítgattunk PNG-ben, ezért ezt a vonalat most nem fejlesztjük tovább. Nem kell hozzá semmi, csak egy Hex-editor és szerencsés találat, hogy ne pont az életfontosságú adatba rondítsunk bele. 

A filterezett adatok támadása,
- leginkább pozíciót (x,y koordinátát) lehet támadni. A None filteren kívül, az összes többinél ezek már nem konkrét RGB adatok, hanem valamely szomszédjától való eltérések. 
- de lehet byte mintázatot  (eltérés-mértéket) is támadni, ekkor a képen a támadott pixel koordinátáját egyáltalán nem célozzuk, csak a tárolt értéket cseréljük, bárhol is talál olyat a képen.
- filter-byte támadása, vagyis a scanlinera használt filter típusának elhazudása, ami miatt megnyitáskor nem a megfelelő algoritmussal fogja interpretálni a képnéző. Pl, Sub volt de Average-re változtatjuk, vagy minden sornál random választunk a lehetséges 5-ből, vagyis az összes permutáció kipróbálható. Ezt láttuk fennebb.
- vagy lehetőség van teljesen saját filter-algoritmus létrehozására is, azzal a korláttal, hogy a képnéző programok csak a fenti 5 filtert ismerik és tudják használni. Tehát, ha egy saját algoritmussal filtereztünk, akkor is egy valid filtert kell megnevezzünk a scanline előtt, és a képnéző ezzel fogja visszaalakítani a képet, vagyis ez az eltérés fogja a torzítást vinni a képbe. 

Ezeknek a támadásoknak a végrehajtására az AI írt nekünk egy cukor kis Python programot, ami lehetővé teszi pixel/pixelblokkok értékeinek a megváltoztatását. Illetve byte-mintázatok támadását és filterbyteok meghamisítását is. (Ezeket akár kombinálva.)

1. Pixelpozíció támadása
A következőben a spéci programunkkal létrehozott egységes filterű képeinket felhasználva (hogy az effektusok ne keveredjenek átláthatatlan módon) a 100,85  (bal felső barna folt közepén) pozíciójú pixelt támadjuk meg 10,100,200-as értékkel (R-ben 10-es eltérést, G-ben 100-as eltérést, B-ben 200-as eltérést okozva)
None filternél a 10,100,200 konkrét RGB értéket jelent, nem eltérést. Egyetlen pixelt átszínezett a barna mezőben, szinte nem is látszik.. 

Sub esetén a hiba jobbra végigcsorog.

Up esetén lefele csorog a hiba.

Average esetén átlósan, üstökösen


Paeth esetén meg a pozíciótól jobbra is, lefele is kumulálódik a hiba.

2. Byteok-mintázatok támadása
Ha visszatekintünk a legelső ábrára, az látszik, hogy az Average-filterezett kép az, ami a leglátványosabban támadható (eléggé roncsol, a Sub vagy Up-hoz visszafogott hatásáshoz képest, de nem annyira, mint a Paeth). Ezért az Average-filterre normalizált képünkkel játszunk most tovább. Mivel a képünk sok azonos/nagyon hasonló színű pixelből áll (színpaletta, ez a dolga) és  PNG mentés törekszik a kis értékek halmozására a scanlineban, ezért ha nagyobb byte értékeket (nagyobb eltéréseket reprezentáló) akarunk megtámadni (lecserélni), szinte alig lesz sérülés a képen, mert kevés pixel fog nagyon eltérni a szomszédaitól. A következőben a 2-3-5-7-10 byteértékeket (eltéréseket) cseréljük le egységesen 150-es eltérésre (egységesen 150-et kap az R, G és a B is).  10-től 2 felé nő a roncsolás mértéke.
10 - 150

7-150

5-150

3-150

2-150


A továbbiakban saját szűrőket fogunk kipróbálni.