Ciele
- Priprav si program, ktorý budeš upravovať
- Premenuj a zavádzaj
- Skry konštanty v dátach
- Splošti tok riadenia
- Pridaj nepriehľadné podmienky
- Odmeraj, čo ťa to stálo
- Prečítaj cudzí obfuskovaný kód
Úvod
Na prednáške sme hovorili o štyroch kategóriách obfuskácie a o štyroch kritériách, podľa ktorých sa posudzuje — účinnosť, odolnosť, nenápadnosť a cena.
Dnes si tri z tých kategórií prejdeš na vlastnom programe a cenu si odmeriaš. Potom sa postavíš na druhú stranu a skúsiš prečítať kód, ktorý upravil niekto iný.
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 program, ktorý budeš upravovať
Potrebuješ niečo krátke, čo sa dá ľahko overiť, že stále funguje. Použijeme výpočet zľavy na vstupenku podľa veku.
# original.py
HRANICA_DOSPELOSTI = 18
def kategoria(vek):
if vek < HRANICA_DOSPELOSTI:
return "detske"
if vek >= 65:
return "seniorske"
return "dospele"
if __name__ == "__main__":
for v in [10, 18, 40, 70]:
print(v, "->", kategoria(v))
Úloha 1.1
Spusti program a zapíš si všetky štyri výstupy. Toto je tvoj etalón — po každej úprave musí program dávať presne tie isté výsledky.
Poznámka: Táto podmienka je celé cvičenie v skratke. Obfuskácia mení formu, nie funkciu. Keď sa výstup zmení, nie je to zatemnený program, ale pokazený.
Krok 2: Premenuj a zavádzaj
Prvá kategória, syntaktická. Urob dve verzie a porovnaj ich.
Úloha 2.1
Vytvor súbor obf1_nic.py, kde premenuješ všetky názvy na nič nehovoriace
krátke identifikátory. Funkcia nech sa volá _f, premenná a.
Úloha 2.2
Vytvor súbor obf1_zavadza.py, kde urobíš opak — necháš názvy zmysluplné,
ale podsunieš nesprávny význam. Funkciu nazvi napríklad vypocet_rychlosti,
premennú vzdialenost, hranicu MAX_RYCHLOST.
Úloha 2.3
Daj obe verzie prečítať niekomu, kto pôvodný kód nevidel, a zapíš si, ako dlho mu trvalo pochopiť, čo program robí. Potom tie dva časy porovnaj.
Poznámka: Ktorá verzia zdržala viac? Toto je rozdiel medzi odstránením významu a podsunutím nesprávneho. Druhá technika cieli na človeka a jeho očakávania, nie na nástroj — a pri rovnakej cene býva účinnejšia.
Krok 3: Skry konštanty v dátach
Druhá kategória, dátová. Najprv si ukáž, že konštanty sú v súbore vidieť.
grep -a -n "18\|65" original.py
Úloha 3.1
Zapíš si, ako rýchlo sa dajú obe hranice v súbore nájsť.
Teraz ich nahraď výpočtom, ktorý dá rovnaký výsledok, ale v kóde ako holé číslo nevystupuje.
# namiesto 18 napriklad:
HRANICA_DOSPELOSTI = (3 * 3) + (3 * 3) # = 18
# namiesto 65 napriklad:
HRANICA_SENIOR = (13 * 5) # = 65
Úloha 3.2
Vytvor obf2.py s týmito výpočtami a over, že všetky štyri výstupy sú rovnaké.
Potom spusti ten istý grep na novom súbore a zapíš si výsledok.
Úloha 3.3
Teraz spusti program tak, aby sa zastavil, a pozri sa na skutočnú hodnotu:
python3 -i obf2.py
>>> HRANICA_DOSPELOSTI
Poznámka: Hodnota je za behu znova k dispozícii, lebo inak by program nefungoval. Toto zdrží toho, kto prehľadáva súbor, nie toho, kto sleduje bežiaci program. Je to presne ten rozdiel medzi statickou a dynamickou analýzou z prednášky.
Krok 4: Splošti tok riadenia
Tretia kategória, kontrolno-toková. Toto je jadro cvičenia.
Úloha 4.1
Rozdeľ funkciu kategoria na očíslované bloky. Každá podmienka a každý návrat
je samostatný blok. Zapíš si zoznam blokov a to, ktorý blok po ktorom môže
nasledovať.
Úloha 4.2
Prepíš funkciu do tvaru s jednou slučkou a prepínačom, presne ako na prednáške.
Výsledok nech je v obf3.py. Kostra vyzerá takto:
def kategoria(vek):
stav = 1
vysledok = None
while stav != 0:
if stav == 1:
stav = 2 if vek < 18 else 3
elif stav == 2:
vysledok = "detske"
stav = 0
elif stav == 3:
stav = 4 if vek >= 65 else 5
elif stav == 4:
vysledok = "seniorske"
stav = 0
elif stav == 5:
vysledok = "dospele"
stav = 0
return vysledok
Úloha 4.3
Over, že všetky štyri výstupy sú rovnaké. Potom prehádž poradie vetiev v prepínači — napríklad daj blok 5 úplne hore — a over znova.
Poznámka: Práve si videl to podstatné. Poradie blokov v zdrojovom kóde už nehovorí nič o poradí vykonávania. Tok je uložený v hodnotách premennej, nie v štruktúre programu.
Krok 5: Pridaj nepriehľadné podmienky
Úloha 5.1
Vlož do obf3.py aspoň dve nepriehľadné podmienky. Použi tvrdenie, ktoré platí
vždy, ale z kódu sa to priamo nevidí:
if (vek * (vek + 1)) % 2 == 0: # vzdy pravda: dve za sebou iduce cisla
skutocny_blok()
else:
# sem daj lubovolny nezmysel, nikdy sa nevykona
vysledok = "nic"
stav = 0
Úloha 5.2
Over, že výstupy sú stále rovnaké. Potom daj obf3.py prečítať spolužiakovi
a zapíš si, ako dlho mu trvalo pochopiť, čo funkcia robí, oproti originálu.
Poznámka: Nástroj na analýzu nedokáže dokázať, že druhá vetva je mŕtva, takže ju musí považovať za možnú. Tým sa počet ciest programom násobí.
Krok 6: Odmeraj, čo ťa to stálo
Obfuskácia sa posudzuje aj cenou. Tú vieš zmerať presne.
import timeit
for nazov in ["original", "obf3"]:
t = timeit.timeit(
f"m.kategoria(40)",
setup=f"import {nazov} as m", number=1_000_000)
print(nazov, round(t, 3), "s na milion volani")
Úloha 6.1
Zapíš si oba časy a pomer medzi nimi. Potom porovnaj aj veľkosť oboch súborov
príkazom wc -c.
Úloha 6.2
Odpovedz si na otázku, pri akom programe by ti toto spomalenie prekážalo a pri akom nie. To je dôvod, prečo sa obfuskuje len malá kritická časť, nie celý program.
Krok 7: Prečítaj cudzí obfuskovaný kód
Teraz z druhej strany. Vymeň si obf3.py so spolužiakom a pokús sa zistiť,
čo jeho funkcia robí — bez toho, aby ti to povedal.
Úloha 7.1
Najprv skús statickú cestu. Prečítaj kód a pokús sa pochopiť tok len z textu. Zapíš si, ako ďaleko si sa dostal a kde si sa zasekol.
Úloha 7.2
Potom skús dynamickú cestu. Spusti funkciu na rôznych vstupoch a pozoruj výstupy:
import obf3
for v in range(0, 100, 5):
print(v, obf3.kategoria(v))
Úloha 7.3
Z výstupov urči obe hranice, ktoré boli v kóde skryté. Zapíš si ich a porovnaj s tým, čo si videl v texte.
Poznámka: Toto je presne ten dôvod, prečo obfuskácia nie je ochrana. Kód musí byť nakoniec vykonaný, takže ho stačí nechať bežať a sledovať, čo robí. Statická cesta ťa zdržala, dynamická nie.
Záver
Čo budeš mať po tomto cvičení pripravené pre mentora:
- Štyri výstupy originálu, ktoré sa po každej úprave museli zhodovať.
- Dve syntaktické verzie a porovnanie, ktorá čitateľa zdržala viac.
- Výsledok vyhľadávania konštánt pred úpravou dátovej vrstvy a po nej.
- Sploštenú verziu funkcie a potvrdenie, že prehádzanie poradia vetiev nič nezmenilo.
- Sploštenú verziu s nepriehľadnými podmienkami.
- Oba namerané časy, ich pomer a veľkosti súborov.
- Obe hranice, ktoré si z cudzieho kódu zistil dynamickou cestou.
A odpovede na tieto otázky:
- Ktorá syntaktická technika zdržala viac a prečo?
- Pred kým ochránilo skrytie konštánt v dátach a pred kým nie?
- Prečo poradie blokov v sploštenej funkcii už nič neprezrádza?
- Prečo ťa pri čítaní cudzieho kódu dynamická cesta dostala k výsledku rýchlejšie než statická?
Doplňujúce úlohy
Úloha A.1
Skús na originál pustiť nástroj na zmenšovanie alebo preformátovanie kódu a porovnaj výsledok so svojou syntaktickou verziou. Vysvetli, prečo zmenšovanie nie je obfuskácia, hoci výsledok vyzerá podobne.
Úloha A.2
Zamysli sa, ako by sa dala sploštená funkcia z kroku 4 automaticky zjednodušiť späť. Ktorý údaj by na to útočníkovi stačil a prečo je práve ten slabým miestom celej techniky?