5. týždeň

Týždeň 5 – Cvičenie: Zatemni vlastný kód a potom cudzí prečítaj

Cvičenie

Ciele

  1. Priprav si program, ktorý budeš upravovať
  2. Premenuj a zavádzaj
  3. Skry konštanty v dátach
  4. Splošti tok riadenia
  5. Pridaj nepriehľadné podmienky
  6. Odmeraj, čo ťa to stálo
  7. 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?