Ciele
- Priprav si prostredie a nosič
- Vlož vodoznak podľa kľúča
- Vyber ho späť a over, že bez kľúča to nejde
- Odmeraj, čo s ním urobí kompresia
- Vykonaj útok spolčením
- Porovnaj to s krehkým vodoznakom
- Vlož vodoznak do vlastného kódu
Úvod
Na prednáške sme hovorili o tom, že watermarking má prevrátené priority oproti steganografii — nejde o utajenie existencie značky, ale o jej odolnosť.
Dnes si to odmeriaš. Vložíš vodoznak, vyberieš ho, a potom budeš postupne skúšať, čím sa dá oslabiť. Pri každom útoku dostaneš číslo, nie dojem.
Všetko robíš vo vlastnom Pythone. Celé to trvá zhruba hodinu a pol. Nie je to za body, mentori to s tebou preberú.
Postup
Krok 1: Priprav si prostredie a nosič
pip install pillow numpy
Vyrob si nosič. Použi vlastnú fotku, alebo si ho vygeneruj — pre meranie je dôležité len to, aby mal jemný šum, tak ako každá skutočná fotografia.
import numpy as np
from PIL import Image
W, H = 960, 640
rng = np.random.default_rng(1)
yy, xx = np.mgrid[0:H, 0:W].astype(float)
nosic = np.clip(np.dstack([
120 + 80 * np.sin(xx / 400),
130 + 70 * np.cos(xx / 290 + 1),
150 + 60 * np.sin((xx + yy) / 470),
]) + rng.normal(0, 2, (H, W, 3)), 0, 255)
Image.fromarray(nosic.astype(np.uint8)).save("nosic.png")
Poznámka: Ak použiješ vlastnú fotku, musí byť vo formáte PNG. Prečo, uvidíš v šiestom kroku.
Krok 2: Vlož vodoznak podľa kľúča
Značka sa nevkladá na jedno miesto. Rozptýli sa po celom obrázku podľa pseudonáhodného vzoru odvodeného z kľúča, s veľmi malou amplitúdou.
from PIL import ImageDraw, ImageFont
AMP = 6.0
def znacka(text):
m = Image.new("L", (W, H), 0)
f = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 150)
ImageDraw.Draw(m).text((W // 2, H // 2), text, font=f, fill=255, anchor="mm")
return np.where(np.array(m) > 128, 1.0, -1.0)
def carrier(kluc):
return np.random.default_rng(kluc).choice([-1.0, 1.0], (H, W))
def vloz(vzor, kluc):
return np.clip(nosic + (carrier(kluc) * vzor * AMP)[:, :, None], 0, 255)
kopiaA = vloz(znacka("KOPIA A"), 1001)
Image.fromarray(kopiaA.astype(np.uint8)).save("kopia_a.png")
print("najvacsia zmena hodnoty bodu:", int(np.abs(kopiaA - nosic).max()), "z 255")
Úloha 2.1
Zapíš si, o koľko najviac sa zmenila hodnota jedného bodu. Potom otvor nosic.png
a kopia_a.png vedľa seba a skús nájsť rozdiel voľným okom.
Ak použiješ vlastnú fotku, nahraď prvý blok načítaním a cestu k písmu uprav podľa svojho systému.
Krok 3: Vyber ho späť a over, že bez kľúča to nejde
Vyberá sa korelačne — rozdiel voči originálu sa vynásobí tým istým vzorom.
def extrahuj(obrazok, kluc, nazov):
rozdiel = obrazok.mean(axis=2) - nosic.mean(axis=2)
odhad = rozdiel * carrier(kluc)
Image.fromarray(np.clip(128 + odhad * 14, 0, 255).astype(np.uint8)).save(nazov)
return rozdiel
def sila(rozdiel, kluc, vzor):
return float((rozdiel * carrier(kluc) * vzor).mean() / AMP)
rA = extrahuj(kopiaA, 1001, "extrakcia_spravnym_klucom.png")
_ = extrahuj(kopiaA, 2002, "extrakcia_cudzim_klucom.png")
print("spravny kluc:", round(sila(rA, 1001, znacka("KOPIA A")), 2))
print("cudzi kluc: ", round(sila(rA, 2002, znacka("KOPIA A")), 2))
Úloha 3.1
Otvor oba výstupné obrázky a zapíš si obe čísla. Jedno z nich má byť blízko jednotky, druhé blízko nuly.
Poznámka: Útočník vie, že vodoznak v obrázku je. Nevie však, kde je rozptýlený, a nedokáže odlíšiť vodoznak od pôvodného šumu. Práve preto ho nemôže odstrániť cielene — môže ho len oslabiť poškodením celého obrázka.
Krok 4: Odmeraj, čo s ním urobí kompresia
for q in (95, 85, 70, 60, 40):
Image.fromarray(kopiaA.astype(np.uint8)).save("t.jpg", quality=q)
po = np.array(Image.open("t.jpg").convert("RGB")).astype(float)
r = extrahuj(po, 1001, f"extrakcia_jpeg_{q}.png")
print(f"kvalita {q}: sila {sila(r, 1001, znacka('KOPIA A')):.2f}")
Úloha 4.1
Zapíš si päť čísel a prezri si k nim obrázky. Nájdi tú kvalitu, pri ktorej už značku nepovažuješ za čitateľnú.
Úloha 4.2
Teraz sa pozri na samotný obrázok pri tej kvalite. Odpovedz si na otázku, či je ešte použiteľný.
Poznámka: O toto pri watermarkingu ide. Nie o to, aby sa značka nedala odstrániť, ale aby jej odstránenie stálo kvalitu obsahu.
Krok 5: Vykonaj útok spolčením
Vyrob druhú kópiu s iným vodoznakom a iným kľúčom, tak ako by ju dostal druhý odberateľ. Potom obe spriemeruj.
kopiaB = vloz(znacka("KOPIA B"), 3003)
Image.fromarray(kopiaB.astype(np.uint8)).save("kopia_b.png")
priemer = (kopiaA + kopiaB) / 2
Image.fromarray(priemer.astype(np.uint8)).save("priemer.png")
rP = extrahuj(priemer, 1001, "extrakcia_priemer.png")
print("sila znacky A v priemere dvoch kopii:", round(sila(rP, 1001, znacka("KOPIA A")), 2))
Úloha 5.1
Zapíš si číslo a prezri si výsledný obrázok. Potom si odpovedz, prečo vyšlo práve takto.
Úloha 5.2
Zisti, koľko kópií by sa muselo spolčiť, aby bola značka nerozpoznateľná. Vyrob si ďalšie kópie s ďalšími kľúčmi a meraj.
kopie = [kopiaA] + [vloz(znacka(f"KOPIA {i}"), 4000 + i) for i in range(2, 9)]
for k in (2, 4, 8):
p = sum(kopie[:k]) / k
r = extrahuj(p, 1001, f"priemer_{k}.png")
print(f"{k} kopii: sila {sila(r, 1001, znacka('KOPIA A')):.2f}")
Poznámka: Práve toto je najväčší problém forenzného vodoznaku. Rieši sa tým, že identifikátory nie sú ľubovoľné, ale priradené podľa kódov navrhnutých tak, aby sa z priemeru dal zistiť aspoň jeden z tých, ktorí sa spolčili.
Krok 6: Porovnaj to s krehkým vodoznakom
Krehký vodoznak má opačnú požiadavku — má sa zničiť pri prvom zásahu. Vkladá sa preto do najmenej významného bitu.
bity = (znacka("NEPORUSENE") > 0).astype(np.uint8)
krehky = nosic.astype(np.uint8).copy()
krehky[:, :, 2] = (krehky[:, :, 2] & 0xFE) | bity
Image.fromarray(krehky).save("krehky.png")
Image.fromarray(((krehky[:, :, 2] & 1) * 255).astype(np.uint8)).save("krehky_overenie.png")
Image.fromarray(krehky).save("t.jpg", quality=90)
upr = np.array(Image.open("t.jpg").convert("RGB"))
Image.fromarray(((upr[:, :, 2] & 1) * 255).astype(np.uint8)).save("krehky_po_uprave.png")
print("po ulozeni do JPEG sedi", round(100 * ((upr[:, :, 2] & 1) == bity).mean(), 1), "% bitov")
Úloha 6.1
Porovnaj krehky_overenie.png a krehky_po_uprave.png a zapíš si to číslo.
Úloha 6.2
Odpovedz si na otázku, či je ten výsledok zlyhaním alebo splnenou úlohou. Odpoveď závisí od toho, na čo je vodoznak určený.
Krok 7: Vlož vodoznak do vlastného kódu
Posledná časť je príprava na druhé zadanie. Vodoznak sa vkladá aj do softvéru, a to voľbou medzi funkčne rovnocennými zápismi.
Úloha 7.1
Napíš krátku funkciu, ktorá spočíta súčet prvkov poľa. Potom vytvor štyri miesta, kde sa dá vybrať medzi dvoma rovnocennými zápismi, a dohodni si, ktorý znamená nulu a ktorý jednotku. Zakóduj do nich štvorbitovú značku.
Príklady rovnocenných volieb, z ktorých si môžeš vybrať:
i += 1 proti i = i + 1
for cyklus proti while cyklus
a = f(); b = g() proti b = g(); a = f() (len ak su nezavisle)
range(0, n) proti range(n)
Úloha 7.2
Napíš k tomu rozpoznávač — funkciu, ktorá prečíta zdrojový súbor a z tých štyroch miest značku prečíta späť.
Úloha 7.3
A teraz to podstatné. Pusti na svoj kód automatický formátovač alebo linter, a potom rozpoznávač spusti znova. Zapíš si, či značku ešte našiel.
Poznámka: Toto je najčastejší spôsob, akým softvérový vodoznak zlyhá. Zničí ho nástroj, ktorý o žiadnom vodoznaku nevie a nikoho nenapadá — len zjednotí to, čo nemá vplyv na význam programu. A vodoznak vložený výberom medzi rovnocennými zápismi je presne taká vec.
Záver
Čo budeš mať po tomto cvičení pripravené pre mentora:
- Najväčšiu zmenu hodnoty bodu po vložení vodoznaku.
- Silu značky pri správnom a pri cudzom kľúči.
- Päť hodnôt sily pri klesajúcej kvalite kompresie a tú hranicu, kde značka prestáva byť čitateľná.
- Sily značky pri spolčení dvoch, štyroch a ôsmich kópií.
- Podiel správnych bitov krehkého vodoznaku po uložení do JPEG.
- Svoju funkciu so štyrmi miestami kódovania a k nej rozpoznávač.
A odpovede na tieto otázky:
- Prečo útočník, ktorý vie o existencii vodoznaku, ho aj tak nedokáže odstrániť cielene?
- Prečo sila značky pri spolčení klesá práve takým spôsobom, aký si odmeral?
- Pri ktorom type vodoznaku je zničenie kompresiou zlyhanie a pri ktorom splnená úloha?
- Čo zničilo vodoznak v tvojom kóde a prečo sa to dá očakávať aj bez útočníka?
Doplňujúce úlohy
Úloha A.1
Vyskúšaj geometrický útok. Otoč obrázok o jeden stupeň alebo mu orež okraj, potom ho vráť do pôvodných rozmerov a skús značku vybrať. Zapíš si silu a vysvetli, prečo vyšla takto, hoci v obrázku je vodoznak stále prítomný.
Úloha A.2
Zmeň amplitúdu vloženia na dvojnásobok a na polovicu. Pre každú hodnotu odmeraj silu po kompresii a zároveň posúď, či je zmena v obrázku viditeľná. Nájdi hodnotu, ktorá je podľa teba najlepším kompromisom.