Hlavní navigace

Pohled pod kapotu JVM – úprava programových smyček s využitím nástroje Javassist

20. 8. 2013
Doba čtení: 26 minut

Sdílet

V dnešní části seriálu o jazyce Java si ukážeme, jak je možné s využitím Javassistu změnit sekvenci instrukcí použitou pro implementaci programových smyček. Tuto znalost využijeme k dalšímu způsobu „oháčkování“ třídy Login tak, aby se bylo možné přihlásit pomocí dvouznakového jména a hesla.

Obsah

1. Pohled pod kapotu JVM – úprava programových smyček s využitím nástroje Javassist

2. Instrukce používané pro implementaci programových smyček v bajtkódu JVM

3. Instrukce nepodmíněného skoku

4. Instrukce pro porovnání dvou operandů s podmíněným skokem

5. Demonstrační příklad – způsob překladu tří typů programových smyček

6. Třetí způsob změny bajtkódu třídy Login – úprava programové smyčky v metodě check()

7. Výpis bajtkódu metody Login.check()

8. Úprava bajtkódu těla metody Login.check()

9. Nástroj pro nalezení jména a hesla na základě prvního bajtu jeho otisku (výsledku hešovací funkce SHA-512)

10. Úplný zdrojový kód demonstračního příkladu ClassModification5

11. Výstup demonstračního příkladu ClassModification5

12. Výpis bajtkódu změněné třídy Login a porovnání s původním bajtkódem

13. Repositář se zdrojovými kódy dnešního demonstračního příkladu

14. Odkazy na Internetu

1. Pohled pod kapotu JVM – úprava programových smyček s využitím nástroje Javassist

V předchozích dvou částech seriálu o programovacím jazyku Java i o virtuálním stroji Javy jsme si ukázali, jakým způsobem je možné změnit bajtkód metody Login.login() tak, aby se nezávisle na zadaném jménu a heslu vždy vrátila pravdivostní hodnota true indikující, že uživatel může být přihlášen do systému. Změny chování metody Login.login() jsme docílili velmi jednoduše – záměnou instrukce sloužící pro uložení návratové hodnoty na zásobník operandů těsně před tím, než se provedla instrukce ireturn zajišťující návrat z metody s vrácením hodnoty uložené na TOS zásobníku operandů. Dnes si ukážeme alternativní způsob změny chování třídy Login. Namísto modifikace metody Login.login se změní programová smyčka v metodě Login.check() takovým způsobem, že se nebude kontrolovat celý otisk (hešovací kód) zadaného jména a hesla, ale pouze první bajt tohoto otisku (jak uvidíme dále, je tato změna méně patrná a pozměněná třída Login by s poměrně velkou pravděpodobností prošla běžnými testy).

Na první pohled se možná může zdát tato změna dosti nesmyslná, ovšem ve skutečnosti je mnohem jednodušší nalézt jméno a heslo, jehož první bajt otisku (hešovacího kódu) je shodný s celým otiskem uloženým ve třídě Login (viz též privátní pole NAME_SHA512_HASHPASSWORD_SHA512_HASH), než se pokoušet hledat obecné jméno a heslo, pro nějž nastane kýžená kolize. Připomeňme si, že při použití algoritmu SHA-512 je výsledný otisk jakýchkoli dat dlouhý 512 bitů, tj. 64 bajtů a navíc je hešovací funkce velmi kvalitní (se změnou jediného bitu na vstupu dojde ke změně mnoha bitů v otisku), takže hledání kolize pro dostatečně dlouhé jméno a heslo je – nadneseně řečeno – časově poněkud náročnější :-). Aby bylo možné změnit v kódu metody Login.check() programovou smyčku, musíme si nejdříve připomenout, jakým způsobem překladač Javy překládá programové konstrukce do, whilefor.

2. Instrukce používané pro implementaci programových smyček v bajtkódu JVM

Programové smyčky, které lze v programovacím jazyku Java zapisovat s využitím klíčových slov do, whilefor, se do bajtkódu překládají s využitím takzvaných řídicích instrukcí, zejména pomocí podmíněných a nepodmíněných skoků. K řídicím instrukcím můžeme připočíst i dvojici poměrně komplexních instrukcí nazvaných tableswitchlookupswitch. Tato dvojice instrukcí se používá pro implementaci rozvětvení, které je v programovacím jazyku Java zapisováno s využitím konstrukce switch. My se však dnes budeme zabývat pouze nepodmíněnými a podmíněnými skoky. Připomeňme si, že v bajtkódu lze skoky provádět pouze v rámci jedné metody, tj. nelze provést přímý skok do jiné metody – další metodu lze pouze volat například instrukcí invokestatic či invokevirtual. Díky tomu se ve skokových instrukcích nepoužívají absolutní adresy, ale indexy instrukcí číslovaných v každé metodě od nuly. Tyto indexy jsou většinou reprezentovány šestnáctibitovým bezznaménkovým číslem, což je pro naprostou většinu metod dostačující.

Podívejme se nyní na demonstrační příklad, při jehož překladu budou použity jak podmíněné, tak i nepodmíněné skoky (tento příklad jsme si uváděli již v dvacáté páté části tohoto seriálu):

class LoopTestMix {
 
    static void cmpInstr() {
        for (int y = 0; y < 10; y++) {
            if (y < 5) continue;
            for (int x = 0; x < 10; x++) {
                if (x == 5) break;
            }
        }
    }
 
}

Při překladu získáme následující bajktód, který je kvůli větší čitelnosti rozdělen do několika sekcí oddělených prázdným řádkem:

static void cmpInstr();
  Code:
   0:   iconst_0        // inicializace počitadla vnější smyčky
   1:   istore_0        // jedná se o první lokální proměnnou (s viditelností jen uvnitř smyčky)
 
   2:   iload_0         // podmínka ukončení vnější smyčky
   3:   bipush  10      // konstanta představující hodnotu počitadla, při jejímž dosažení se smyčka ukončí
   5:   if_icmpge   44  // počitadlo dosáhlo mezní hodnoty - skok ZA konec vnější smyčky
 
   8:   iload_0         // implementace podmínky "if (y < 5) continue;"
   9:   iconst_5        // konstanta, s níž je hodnota počitadla srovnávána
   10:  if_icmpge   16
   13:  goto    38      // skok ZA konec vnitřní smyčky
 
   16:  iconst_0        // inicializace počitadla vnitřní smyčky
   17:  istore_1        // jedná se o druhou lokální proměnnou (s viditelností jen uvnitř smyčky)
 
   18:  iload_1         // podmínka ukončení vnitřní smyčky
   19:  bipush  10      // konstanta představující hodnotu počitadla, při jejímž dosažení se smyčka ukončí
   21:  if_icmpge   38  // počitadlo dosáhlo mezní hodnoty - skok ZA konec vnitřní smyčky 
 
   24:  iload_1         // implementace podmínky "if (x == 5) break;"
   25:  iconst_5        // konstanta, s níž je hodnota počitadla srovnávána
   26:  if_icmpne   32
   29:  goto    38      // skok ZA konec vnitřní smyčky
 
   32:  iinc    1, 1    // zvýšení počitadla vnitřní smyčky
   35:  goto    18      // další iterace vnitřní smyčky
 
   38:  iinc    0, 1    // zvýšení počitadla vnější smyčky
   41:  goto    2       // další iterace vnější smyčky
 
   44:  return

Při pohledu na bajtkód je patrné, že se v něm využilo hned několik typů podmíněných skoků a několikrát zde nalezneme i instrukci goto pro nepodmíněný skok. Pro samotnou implementaci počítané smyčky for je nutné použít dva skoky – podmíněný skok if_icmpge, který zajišťuje test na koncovou podmínku a nepodmíněný skok goto, jenž na konci smyčky zajistí skok na její začátek. Další dvojice if_icmpge+goto je použita pro implementaci konstrukcí breakcontinue.

Vše bude možná přehlednější při doplnění výpisu bajtkódu o šipky s cíli skoků:

static void cmpInstr();
             Code:
              0:   iconst_0
              1:   istore_0
+-----------> 2:   iload_0
|             3:   bipush  10
|             5:   if_icmpge   44 ------------+
|             8:   iload_0                    |
|             9:   iconst_5                   |
|             10:  if_icmpge   16 ----+       |
|   +-------- 13:  goto    38         |       |
|   |         16:  iconst_0      <----+       |
|   |         17:  istore_1                   |
|   |         18:  iload_1       <............|.....
|   |         19:  bipush  10                 |    :
|   |         21:  if_icmpge   38 --------+   |    :
|   |         24:  iload_1                |   |    :
|   |         25:  iconst_5               |   |    :
|   |         26:  if_icmpne   32 ----+   |   |    :
|   |   +---- 29:  goto    38         |   |   |    :
|   |   |     32:  iinc    1, 1  <----+   |   |    :
|   |   |     35:  goto    18    .........|...|....:
|   +---\===> 38:  iinc    0, 1  <--------+   |
+------------ 41:  goto    2                  |
              44:  return        <------------+

3. Instrukce nepodmíněného skoku

V této kapitole se budeme zabývat pouze jedinou instrukcí. Jedná se o instrukci nepodmíněného skoku na jinou instrukci, jejíž jméno je goto. Podobně jako podmíněné skoky popsané v následujících kapitolách, má i instrukce goto několik podstatných omezení – skok lze totiž provést pouze v rámci těla jedné metody, není tedy možné skočit na libovolné místo v bajtkódu. Toto omezení bylo zavedeno ze dvou důvodů – zajišťuje se tím mnohem větší bezpečnost (při kontrole bajtkódu se výrazně omezuje stavový prostor) a taktéž se tím zjednodušuje práce JIT překladače, který při optimalizacích generovaného nativního binárního kódu může pracovat s izolovaným stavovým prostorem (má totiž jistotu, že když danou metodu celou přeloží, není nutné vyhledávat, z jakých dalších metod jsou do právě přeložené metody prováděny skoky – jednoduše to není možné, což je zajištěno výše zmíněnou kontrolou bajtkódu).

Instrukce goto existuje ve dvou variantách – „krátké“ a „dlouhé“. Tyto varianty se od sebe odlišují pouze počtem bajtů, které se v bajtkódu použijí pro uložení adresy cíle skoku. Buď je možné použít 16bitovou adresu (vyhovuje prakticky všem rozumně dlouhým metodám) nebo adresu 32bitovou (to se obecně příliš často nepoužívá, protože existují další omezení na maximální počet 65536 instrukcí v jedné metodě):

# Instrukce Opkód Operandy Popis
1 goto 0×A7 highbyte, lowbyte přímý skok na adresu uloženou v dvojici operandů: highbyte*256+lowbyte
2 goto_w 0×C8 byte1,byte2,byte3 byte4 přímý skok na adresu uloženou ve čtveřici operandů: byte1*224+byte2*216+byte3*28+by­te4

Podívejme se na velmi jednoduchý demonstrační příklad s trojicí statických metod, v nichž je použita nekonečná smyčka:

public class LoopTest_Goto {
 
    static void loop1() {
        while (true) {
        }
    }
 
    static void loop2(int x) {
        while (true) {
            x++;
        }
    }
 
    static void loop3(float x) {
        do {
            x++;
        } while (true);
    }
 
}

Ve všech třech případech se nekonečná smyčka v těle demonstračních metod přeloží s využitím instrukce goto (povšimněte si, že adresa skoku je skutečně lokální v rámci dané metody):

static void loop1();
  Code:
   0:   goto    0      // nekonečná smyčka bez těla - je pouze proveden skok na tu samou instrukci
static void loop2(int);
  Code:
   0:   iinc    0, 1   // tělo nekonečné smyčky
   3:   goto    0      // skok na začátek nekonečné smyčky
static void loop3(float);
  Code:
   0:   fload_0        // začátek těla nekonečné smyčky
   1:   fconst_1
   2:   fadd
   3:   fstore_0
   4:   goto    0      // skok na začátek nekonečné smyčky

4. Instrukce pro porovnání dvou operandů s podmíněným skokem

V instrukčním souboru virtuálního stroje Javy existuje hned několik typů skoků s podmínkou. Zejména se jedná o instrukce ifeq, ifne, iflt, ifge, ifgtifle, které provedou popř. naopak neprovedou skok na základě porovnání jednoho operandu typu int s konstantou nula (označení TOS v následující tabulce znamená hodnotu uloženou na vrcholu zásobníku operandů):

# Instrukce Opkód Operandy Podmínka Operace
1 ifeq 0×99 highbyte, lowbyte TOS=0 skok na lokální adresu highbyte*256+lowbyte při splnění podmínky
2 ifne 0×9A highbyte, lowbyte TOS≠0 skok na lokální adresu highbyte*256+lowbyte při splnění podmínky
3 iflt 0×9B highbyte, lowbyte TOS<0 skok na lokální adresu highbyte*256+lowbyte při splnění podmínky
4 ifge 0×9C highbyte, lowbyte TOS≥0 skok na lokální adresu highbyte*256+lowbyte při splnění podmínky
5 ifgt 0×9D highbyte, lowbyte TOS>0 skok na lokální adresu highbyte*256+lowbyte při splnění podmínky
6 ifle 0×9E highbyte, lowbyte TOS≤0 skok na lokální adresu highbyte*256+lowbyte při splnění podmínky

My se však dnes budeme zabývat instrukcemi pro porovnání dvou operandů s následným skokem na základě splněné podmínky. V praxi – zejména při implementaci počítaných programových smyček (což je téma, které nás dnes nejvíc zajímá) – je totiž vhodné umět efektivně provést podmíněný skok na základě porovnání dvou operandů, nikoli na základě porovnání jednoho operandu vůči nule. Samozřejmě je možné nejdříve oba operandy od sebe odečíst a poté provést skok na základě výsledku tohoto rozdílu (což se podobá systému používanému u mnohých typů mikroprocesorů), to však vyžaduje zbytečně dlouhou sekvenci instrukcí a tím i nárůst velikosti bajtkódu. Z tohoto důvodu se v instrukčním souboru JVM nachází i instrukce, které porovnají dvojici operandů typu int uloženou na nejvrchnějších dvou pozicích zásobníku operandů a skok vykonají na základě toho, zda je první operand větší, menší či roven operandu druhému (oba operandy jsou navíc ze zásobníku odstraněny, nezávisle na tom, zda se skok provede či nikoli):

# Instrukce Opkód Operandy Podmínka Operace
1 if_icmpeq 0×9F highbyte, lowbyte value1=value2 skok na adresu highbyte*256+lowbyte při splnění podmínky
2 if_icmpne 0×A0 highbyte, lowbyte value1≠value2 skok na adresu highbyte*256+lowbyte při splnění podmínky
3 if_icmplt 0×A1 highbyte, lowbyte value1<value2 skok na adresu highbyte*256+lowbyte při splnění podmínky
4 if_icmpge 0×A2 highbyte, lowbyte value1≥value2 skok na adresu highbyte*256+lowbyte při splnění podmínky
5 if_icmpgt 0×A3 highbyte, lowbyte value1>value2 skok na adresu highbyte*256+lowbyte při splnění podmínky
6 if_icmple 0×A4 highbyte, lowbyte value1≤value2 skok na adresu highbyte*256+lowbyte při splnění podmínky

5. Demonstrační příklad – způsob překladu třech typů programových smyček

Nyní se podívejme na to, jakým způsobem dokáže překladač Javy vygenerovat bajtkód pro trojici programových smyček, v nichž se postupně zvyšuje hodnota počitadla (lokální proměnné) od nuly do desíti. Pro tento účel lze využít všechny tři typy programových smyček Javy, tj. smyčku while, do-while i smyčku for. Důležité je, že se překladač Javy nesnaží provádět žádné složité optimalizace; maximálně může celou smyčku z bajtkódu odstranit ve chvíli, kdy je jasné, že se neprovede ani jedna iterace:

/**
 * Testovaci trida ukazujici zpusob prekladu tri typu programovych smycek.
 */
public class LoopTest {
 
    private static void printMessage1() {
        System.out.println("loopTest1");
    }
 
    private static void printMessage2() {
        System.out.println("loopTest2");
    }
 
    private static void printMessage3() {
        System.out.println("loopTest3");
    }
 
    /**
     * Prvni typ programove smycky
     */
    private static void loopTest1() {
        int i = 0;
        while (i < 10) {
            printMessage1();
            i++;
        }
    }
 
    /**
     * Druhy typ programove smycky
     */
    private static void loopTest2() {
        int i = 0;
        do {
            printMessage2();
            i++;
        } while (i < 10);
    }
 
    /**
     * Treti typ programove smycky
     */
    private static void loopTest3() {
        for (int i = 0; i < 10; i++) {
            printMessage3();
        }
    }
 
    /**
     * Test funkcnosti tridy.
     */
    public static void main(String[] args) {
        loopTest1();
        loopTest2();
        loopTest3();
    }
 
}

Překlad první metody loopTest1. Z výpisu je patrné, že se nejdříve provede skok na konec smyčky, kde je implementována podmínka jejího ukončení:

private static void loopTest1();
  Code:
   0:           iconst_0
   1:           istore_0
   2:           goto            11   // ukoncujici podminka smycky se testuje ihned jeste pred zacatkem tela smycky
   5:           invokestatic    #36; // Method printMessage1:()V
   8:           iinc            0, 1 // zvyseni pocitadla - lokalni promenne "i"
   11:          iload_0              // ulozeni pocitadla na zasobnik kvuli porovnani
   12:          bipush          10   // hodnota pocitadla se bude porovnavat s konstantou 10
   14:          if_icmplt       5    // porovnani a podmineny zpetny skok na index 5
   17:          return

Překlad druhé metody loopTest2. Zde se nepoužil nepodmíněný skok, který je u smyčky s podmínkou na konci zbytečný:

private static void loopTest2();
  Code:
   0:           iconst_0
   1:           istore_0
   2:           invokestatic    #40; // Method printMessage2:()V
   5:           iinc            0, 1 // zvyseni pocitadla - lokalni promenne "i"
   8:           iload_0              // ulozeni pocitadla na zasobnik kvuli porovnani
   9:           bipush          10   // hodnota pocitadla se bude porovnavat s konstantou 10
   11:          if_icmplt       2    // porovnani a podmineny zpetny skok na index 2
   14:          return

Třetí metoda loopTest3 se přeložila stejně, jako metoda loopTest1:

private static void loopTest3();
  Code:
   0:           iconst_0
   1:           istore_0
   2:           goto            11   // ukoncujici podminka smycky se testuje ihned jeste pred zacatkem tela smycky
   5:           invokestatic    #42; // Method printMessage3:()V
   8:           iinc            0, 1 // zvyseni pocitadla - lokalni promenne "i"
   11:          iload_0              // ulozeni pocitadla na zasobnik kvuli porovnani
   12:          bipush          10   // hodnota pocitadla se bude porovnavat s konstantou 10
   14:          if_icmplt       5    // porovnani a podmineny zpetny skok na index 5
   17:          return

Pro nás je v tuto chvíli důležitý fakt, že se hodnota počitadla testuje na dosažení koncové hodnoty takovým způsobem, že se na zásobník operandů uloží instrukcí bipush celé malé číslo (bajt), s nímž se hodnota počitadla následně porovná. Přesně tuto instrukci budeme v dalších krocích měnit: pokud totiž změníme operand této instrukce, efektivně se změní i počet iterací programové smyčky!

6. Třetí způsob změny bajtkódu třídy Login – úprava programové smyčky v metodě check()

Jak jsme si již řekli v úvodní kapitole, bude náš dnešní úkol následující – změnit chování metody Login.check() tak, aby se v ní neporovnávalo všech 64 bajtů otisku uloženého v poli NAME_SHA512_HASH či PASSWORD_SHA512_HASH s vypočteným otiskem, ale aby se namísto toho kontroloval jen první bajt otisku. To s sebou přináší hned několik předností, především to, že změna není tak viditelná, protože původní jméno a heslo bude stále fungovat a náhodně zvolené jméno a heslo velmi pravděpodobně k přihlášení nepovede. S poměrně velkou pravděpodobností by tedy i změněná třída Login prošla běžným testováním :-)

Připomeňme si, jak vlastně metoda Login.check() vypadá. Je vlastně velmi jednoduchá – nejprve se vypočte otisk (hešovací kód) zadaného řetězce a následně se otisk porovná (bajt po bajtu) s předaným polem obsahujícím šedesát čtyři konstant:

    /**
     * Kontrola jmena a/nebo hesla na zaklade jeho hashe.
     */
    private static boolean check(String str, short[]hash) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-512");
            md.update(str.getBytes());
            byte[] digest = md.digest();
            // pro SHA-512 se kontroluje 512/8 = 64 bajtu
            for (int i = 0; i < 64; i++) {
                if (digest[i] != (byte)hash[i]) {
                    return false;
                }
            }
        }
        catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return true;
    }

7. Výpis bajtkódu metody Login.check()

Z výpisu bajtkódu metody Login.check(), který lze získat příkazem javap -c -private Login je patrné, jakým způsobem je implementována podmínka pro ukončení programové smyčky použité pro porovnání všech 64 bajtů otisku (hešovací hodnoty). Nejprve se zvýší hodnota počitadla instrukcí iinc, posléze se počitadlo uloží na zásobník operandů a porovná se s hodnotou 64, která je taktéž uložena na zásobník operandů instrukcí bipush. Skok na začátek smyčky je proveden jedině tehdy, pokud je aktuální hodnota počitadla menší než 64:

private static boolean check(java.lang.String, short[]);
  Code:
   0:           ldc             #25; //String SHA-512
   2:           invokestatic    #31; //Method java/security/MessageDigest.getInstance:(Ljava/lang/String;)Ljava/security/MessageDigest;
   5:           astore_2
   6:           aload_2
   7:           aload_0
   8:           invokevirtual   #37; //Method java/lang/String.getBytes:()[B
   11:          invokevirtual   #41; //Method java/security/MessageDigest.update:([B)V
   14:          aload_2
   15:          invokevirtual   #44; //Method java/security/MessageDigest.digest:()[B
   18:          astore_3
   19:          iconst_0
   20:          istore          4
   22:          goto            42
   25:          aload_3              // začátek těla programové smyčky
   26:          iload           4
   28:          baload
   29:          aload_1
   30:          iload           4
   32:          saload
   33:          i2b
   34:          if_icmpeq       39
   37:          iconst_0
   38:          ireturn
   39:          iinc            4, 1 // zvýšení hodnoty počitadla o jedničku
   42:          iload           4    // uloženi počitadla na zásobník
   44:          bipush          64   // hodnota počitadla se bude porovnávat s touto konstantou
   46:          if_icmplt       25   // porovnání a zpětný skok na začátek těla smyčky
   49:          goto            57
   52:          astore_2
   53:          aload_2
   54:          invokevirtual   #49; //Method java/security/NoSuchAlgorithmException.printStackTrace:()V
   57:          iconst_1
   58:          ireturn
  Exception table:
   from   to  target type
     0    52    52   Class java/security/NoSuchAlgorithmException

8. Úprava bajtkódu těla metody Login.check()

Změna bajtkódu metody Login.check() se provede v uživatelské metodě nazvané modifyMethodCheck(). Budeme postupovat stejným způsobem, jako v případě modifikace bajtkódu metody Login.login(), tj. postupně budeme procházet jednotlivými instrukcemi a budeme kontrolovat, zda dvojice za sebou jdoucích instrukcí neobsahuje operační kódy bipush+if_icmplt. Pokud na tuto dvojici instrukcí narazíme, provede se ještě další kontrola na operand instrukce bipush. V případě, že je tento operand roven konstantě 64, je tato konstanta nahrazena číslem 1, tj. novým kýženým počtem iterací. To je vše – žádné další modifikace bajtkódu ve skutečnosti není zapotřebí provádět.

Současně je však nutné kontrolovat, zda následující instrukce skutečně existuje, tj. zdali nám metoda lookAhead() nevrátila index neexistující instrukce. Kontrola je jednoduchá, protože přes metodu getCodeLength() lze přečíst celkovou velikost bajtkódu zvolené metody:

    /**
     * Modifikace metody Login.check() - zmena tela metody takovym zpusobem,
     * aby se smycka zredukovala na pouhou jednu iteraci.
     *
     * @param testClass
     *            testovaci (modifikovana) trida.
     * @throws NotFoundException
     *             vyvolana v pripade, ze metoda neni nalezena.
     * @throws CannotCompileException 
     * @throws BadBytecode 
     *             vyhozena, pokud se nalezne neplatna instrukce v bytekodu
     */
    private static void modifyMethodCheck(CtClass testClass) throws NotFoundException, CannotCompileException, BadBytecode {
        CtMethod method = testClass.getDeclaredMethod("check");
        MethodInfo methodInfo = method.getMethodInfo();
 
        // ziskat atribut "CODE" prirazeny k metode
        CodeAttribute ca = methodInfo.getCodeAttribute();
 
        // ziskat iterator pouzity pro prochazeni bajtkodem
        CodeIterator iterator = ca.iterator();
 
        // projit vsemi instrukcemi
        while (iterator.hasNext()) {
            // precist instrukci
            int currentIndex = iterator.next();
            int currentOpcode = iterator.byteAt(currentIndex);
 
            // precist NASLEDUJICI instrukci
            int nextIndex = iterator.lookAhead();
            // kontrola, zda se nepokousime cist ZA posledni instrukci
            if (nextIndex >= iterator.getCodeLength()) {
                break;
            }
            int nextOpcode = iterator.byteAt(nextIndex);
 
            // nahrada instrukce BIPUSH 64 za instrukci BIPUSH 1
            // v pripade, ze se tato instrukce nachazi tesne pred
            // instrukci IF_ICMPLT
            if (currentOpcode == Opcode.BIPUSH && nextOpcode == Opcode.IF_ICMPLT) {
                // nyni jsme v situaci, kdy po sobe nasleduji instrukce
                // BIPUSH xxx
                // IF_CMPLT
                // zbyva tedy otestovat, zda xxx == 64 a provest nahradu
                int dataByte = iterator.byteAt(currentIndex + 1);
                if (dataByte == 64) {
                    iterator.writeByte(1, currentIndex + 1);
                }
            }
        }
 
        // zmena atributu "CODE" prirazeneho k metode
        methodInfo.setCodeAttribute(ca);
    }

9. Nástroj pro nalezení jména a hesla na základě prvního bajtu jeho otisku (výsledku hešovací funkce SHA-512)

Ve chvíli, kdy máme metodu Login.check() úspěšně „oháčkovanou“, nám již vlastně zbývá jediný úkol. Musíme nalézt takové jméno a takové heslo, jehož první bajt otisku bude odpovídat prvnímu bajtu v poli NAME_SHA512_HASHPASSWORD_SHA512_HASH. Pro jednoduchost budeme hledat dvouznaková jména a hesla, přičemž množinu znaků omezíme pouze na písmena malé abecedy. Pomocí dvou písmen ‚a‘ až ‚z‘ lze vyjádřit 26×26=676 kombinací, takže skutečně při procházení všemi kombinacemi dokážeme nalézt vhodný otisk. O nalezení prvního vhodného jména a hesla se postará následující velmi jednoduchý program, který však nemusí být příliš efektivní ve chvíli, kdybychom hledali delší hesla (skládání znaků do řetězce je velmi náročné):

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
 
 
 
/**
 * Jednoduchy nastroj, ktery se pokusi najit takovy dvouznakovy retezec, jehoz
 * SHA-512 otisk zacina zadanym bajtem. Tento nalezeny dvouznakovy retezec bude
 * pouzit jako jmeno a heslo predavane do "ohackovane" tridy Login.
 * 
 * @author Pavel Tisnovsky
 */
public class HashFinder {
 
    /** Otisky, pro ktere hledame vzor. */
    private static final byte NAME_SHA512_FIRST_HASH_BYTE = 0x53;
    private static final byte PASSWORD_SHA512_FIRST_HASH_BYTE = 0x46;
 
    /**
     * Vypocet prvniho bajtu SHA-512 otisku pro zadany vstup.
     */
    private static byte computeFirstHashByte(String str) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-512");
            md.update(str.getBytes());
            byte[] digest = md.digest();
            // ziskame prvni z 518/8 = 64 bajtu
            return digest[0];
        }
        catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return -1;
    }
 
    /**
     * Najde dvouznakovy text, jehoz otisk zacina zadanym bajtem.
     */
    private static void findOrigForGivenHashByte(byte firstHashByte) {
        // vyzkousime vsechny kombinace dvou malych pismen
        for (char c1 = 'a'; c1 <= 'z'; c1++) {
            for (char c2 = 'a'; c2 <= 'z'; c2++) {
                // neefektivni - pri hledani delsich kombinaci nutno optimalizovat!
                String orig = "" + c1 + c2;
                if (computeFirstHashByte(orig) == firstHashByte) {
                    System.out.println(orig);
                    return;
                }
            }
        }
    }
 
    /**
     * Vstupni bod nastroje.
     */
    public static void main(String[] args) {
        System.out.print("Name: ");
        findOrigForGivenHashByte(HashFinder.NAME_SHA512_FIRST_HASH_BYTE);
        System.out.print("Password: ");
        findOrigForGivenHashByte(HashFinder.PASSWORD_SHA512_FIRST_HASH_BYTE);
    }
}

A jaké jméno a heslo bylo tímto jednoduchým nástrojem nalezeno?

  1. Jméno: „ko“
  2. Heslo: „aq“

10. Úplný zdrojový kód demonstračního příkladu ClassModification5

V této kapitole bude uveden výpis úplného zdrojového kódu demonstračního příkladu pojmenovaného ClassModification5, který je založen na minule a předminule popsaných příkladech ClassModification3ClassModification4. Tento příklad načte původní bajtkód třídy Login, vypíše strukturu této třídy, provede jednoduchou modifikaci těla metody Login.check(), opět vypíše strukturu třídy a následně otestuje, zda nová metoda Login.check() skutečně vrací pravdivostní hodnotu true pro zadané dvouznakové jméno a heslo:

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
 
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Modifier;
import javassist.NotFoundException;
import javassist.bytecode.BadBytecode;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.CodeIterator;
import javassist.bytecode.MethodInfo;
import javassist.bytecode.Mnemonic;
import javassist.bytecode.Opcode;
 
 
 
/**
 * Test moznosti nastroje Javassist - zmena tela jedne metody
 * ve tride Login tak, aby tato metoda vzdy vratila hodnotu true
 * nezavisle na zadanem jmenu a heslu.
 *
 * @author Pavel Tisnovsky
 */
public class ClassModification5 {
 
    /**
     * Jmeno testovaci tridy.
     */
    private static final String TEST_CLASS_NAME = "Login";
 
    /**
     * Vypis struktury vybrane metody.
     * 
     * @param modifiedClass
     *            predstavuje vytvarenou ci modifikovanou tridu
     * @param methodName
     *            jmeno metody, jejiz struktura se ma vypsat
     * @throws NotFoundException
     *             vyhozena, pokud metoda nebyla nalezena
     * @throws BadBytecode
     *             vyhozena, pokud se nalezne neplatna instrukce v bytekodu
     */
    private static void printMethodStructure(CtClass modifiedClass, String methodName) throws NotFoundException, BadBytecode {
        System.out.println("Method '" + methodName + "' structure:");
        CtMethod method = modifiedClass.getDeclaredMethod(methodName);
        if (method == null) {
            System.out.println("   not found!");
            return;
        }
        MethodInfo methodInfo = method.getMethodInfo();
        System.out.println("    real name:    " + methodInfo.getName());
        System.out.println("    descriptor:   " + methodInfo.getDescriptor());
        System.out.println("    access flags: " + Modifier.toString(methodInfo.getAccessFlags()));
        System.out.println("    method body:");
        printMethodBody(methodInfo);
        System.out.println();
    }
 
    /**
     * Vypis instrukci tvoricich telo vybrane metody.
     *
     * @throws NotFoundException
     *             vyhozena, pokud metoda nebyla nalezena
     * @throws BadBytecode
     *             vyhozena, pokud se nalezne neplatna instrukce v bytekodu
     */
    private static void printMethodBody(MethodInfo methodInfo) throws BadBytecode {
        CodeAttribute ca = methodInfo.getCodeAttribute();
        CodeIterator iterator = ca.iterator();
        while (iterator.hasNext()) {
            int index = iterator.next();
            int opcode = iterator.byteAt(index);
            System.out.println("        " + Mnemonic.OPCODE[opcode]);
        }
    }
 
    /**
     * Vypis struktury vybranych metod z modifikovane tridy.
     * 
     * @param modifiedClass
     *            predstavuje vytvarenou ci modifikovanou tridu
     * @throws NotFoundException
     *             vyhozena, pokud metoda nebyla nalezena
     * @throws BadBytecode 
     *             vyhozena, pokud se nalezne neplatna instrukce v bytekodu
     */
    private static void printMethodStructures(CtClass modifiedClass) throws NotFoundException, BadBytecode {
        printMethodStructure(modifiedClass, "check");
        printMethodStructure(modifiedClass, "login");
        printMethodStructure(modifiedClass, "main");
    }
 
    /**
     * Modifikace metody Login.check() - zmena tela metody takovym zpusobem,
     * aby se smycka zredukovala na pouhou jednu iteraci.
     *
     * @param testClass
     *            testovaci (modifikovana) trida.
     * @throws NotFoundException
     *             vyvolana v pripade, ze metoda neni nalezena.
     * @throws CannotCompileException 
     * @throws BadBytecode 
     *             vyhozena, pokud se nalezne neplatna instrukce v bytekodu
     */
    private static void modifyMethodCheck(CtClass testClass) throws NotFoundException, CannotCompileException, BadBytecode {
        CtMethod method = testClass.getDeclaredMethod("check");
        MethodInfo methodInfo = method.getMethodInfo();
 
        // ziskat atribut "CODE" prirazeny k metode
        CodeAttribute ca = methodInfo.getCodeAttribute();
 
        // ziskat iterator pouzity pro prochazeni bajtkodem
        CodeIterator iterator = ca.iterator();
 
        // projit vsemi instrukcemi
        while (iterator.hasNext()) {
            // precist instrukci
            int currentIndex = iterator.next();
            int currentOpcode = iterator.byteAt(currentIndex);
 
            // precist NASLEDUJICI instrukci
            int nextIndex = iterator.lookAhead();
            // kontrola, zda se nepokousime cist ZA posledni instrukci
            if (nextIndex >= iterator.getCodeLength()) {
                break;
            }
            int nextOpcode = iterator.byteAt(nextIndex);
 
            // nahrada instrukce BIPUSH 64 za instrukci BIPUSH 1
            // v pripade, ze se tato instrukce nachazi tesne pred
            // instrukci IF_ICMPLT
            if (currentOpcode == Opcode.BIPUSH && nextOpcode == Opcode.IF_ICMPLT) {
                // nyni jsme v situaci, kdy po sobe nasleduji instrukce
                // BIPUSH xxx
                // IF_CMPLT
                // zbyva tedy otestovat, zda xxx == 64 a provest nahradu
                int dataByte = iterator.byteAt(currentIndex + 1);
                if (dataByte == 64) {
                    iterator.writeByte(1, currentIndex + 1);
                }
            }
        }
 
        // zmena atributu "CODE" prirazeneho k metode
        methodInfo.setCodeAttribute(ca);
    }
 
    /**
     * Zjisteni funkcnosti metody Login.login().
     * 
     * @param testClass
     *            testovaci (modifikovana) trida.
     * @throws CannotCompileException
     *             muze byt vyhozena v prubehu prevodu CtClass na Class
     */
    @SuppressWarnings("unchecked")
    private static void checkMethodLogin(CtClass testClass) throws CannotCompileException {
        Class testClassKlass = testClass.toClass();
 
        // otestovani metody Login.login()
        System.out.println(invokeStaticMethod(testClassKlass, "login", "x", "y"));
        System.out.println(invokeStaticMethod(testClassKlass, "login", "fakt", "nevim"));
        System.out.println(invokeStaticMethod(testClassKlass, "login", "administrator", "nbusr123"));
        System.out.println(invokeStaticMethod(testClassKlass, "login", "ko", "aq"));
    }
 
    /**
     * Zavolani vybrane staticke metody Login.login().
     * 
     * @param anyClass
     *            trida, v niz je staticka metoda deklarovana
     * @param methodName
     *            jmeno staticke metody, ktera se ma spustit
     * @param name
     *            jmeno predavane do metody Login.login()
     * @param password
     *            heslo predavane do metody Login.login()
     */
    @SuppressWarnings("unchecked")
    private static boolean invokeStaticMethod(Class anyClass, String methodName, String name, String password) {
        try {
            Method method = anyClass.getMethod(methodName, String.class, String.class);
            Object result = method.invoke(null, name, password);
            return (Boolean)result;
        }
        catch (SecurityException e) {
            e.printStackTrace();
        }
        catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        catch (IllegalArgumentException e) {
            e.printStackTrace();
        }
        catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return false;
    }
 
    /**
     * Spusteni modifikatoru tridy.
     *
     * @param args nevyuzito
     */
    public static void main(String[] args) {
        // ziskat vychozi class pool
        ClassPool pool = ClassPool.getDefault();
 
        // objekt predstavujici menenou tridu
        CtClass testClass;
 
        try {
            // ziskat objekt predstavujici tridu Test
            testClass = pool.get(TEST_CLASS_NAME);
 
            // vypis puvodni struktury tridy Test
            System.out.println("Original class structure:\n");
            printMethodStructures(testClass);
 
            // modifikace tela metody check()
            modifyMethodCheck(testClass);
 
            // vypis zmenene struktury tridy Test
            System.out.println("Modified class structure:\n");
            printMethodStructures(testClass);
 
            // ulozeni bajtkodu tridy na disk
            testClass.writeFile();
 
            // a otestovani, zda mame skutecne pristup ke vsem atributum
            checkMethodLogin(testClass);
        }
        catch (NotFoundException e) {
            e.printStackTrace();
        }
        catch (BadBytecode e) {
            e.printStackTrace();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
        catch (CannotCompileException e) {
            e.printStackTrace();
        }
        catch (SecurityException e) {
            e.printStackTrace();
        }
        catch (IllegalArgumentException e) {
            e.printStackTrace();
        }
    }
 
}

11. Výstup demonstračního příkladu ClassModification5

Podívejme se nyní na výstup dnešního demonstračního příkladu ClassModification5, z něhož poznáme původní i novou strukturu třídy Login. Posledních osm řádků obsahujících informace ze čtyř pokusů o přihlášení vzniklo v metodě checkMethodLogin() jako důkaz toho, že tato metoda byla skutečně úspěšně „oháčkována“. Pro jméno „ko“ a heslo „aq“ se totiž přihlášení skutečně podařilo, i když se nejedná o správné jméno a heslo:

Original class structure:
 
Method 'check' structure:
    real name:    check
    descriptor:   (Ljava/lang/String;[S)Z
    access flags: private static
    method body:
        ldc
        invokestatic
        astore_2
        aload_2
        aload_0
        invokevirtual
        invokevirtual
        aload_2
        invokevirtual
        astore_3
        iconst_0
        istore
        goto
        aload_3
        iload
        baload
        aload_1
        iload
        saload
        i2b
        if_icmpeq
        iconst_0
        ireturn
        iinc
        iload
        bipush
        if_icmplt
        goto
        astore_2
        aload_2
        invokevirtual
        iconst_1
        ireturn
 
Method 'login' structure:
    real name:    login
    descriptor:   (Ljava/lang/String;Ljava/lang/String;)Z
    access flags: public static
    method body:
        getstatic
        ldc
        iconst_1
        anewarray
        dup
        iconst_0
        aload_0
        aastore
        invokevirtual
        pop
        aload_0
        getstatic
        invokestatic
        istore_2
        aload_1
        getstatic
        invokestatic
        istore_3
        iload_2
        ifeq
        iload_3
        ifeq
        iconst_1
        ireturn
        iconst_0
        ireturn
 
Method 'main' structure:
    real name:    main
    descriptor:   ([Ljava/lang/String;)V
    access flags: public static
    method body:
        getstatic
        ldc
        ldc
        invokestatic
        invokevirtual
        getstatic
        ldc
        ldc
        invokestatic
        invokevirtual
        getstatic
        ldc
        ldc
        invokestatic
        invokevirtual
        getstatic
        ldc
        ldc
        invokestatic
        invokevirtual
        return
 
Modified class structure:
 
Method 'check' structure:
    real name:    check
    descriptor:   (Ljava/lang/String;[S)Z
    access flags: private static
    method body:
        ldc
        invokestatic
        astore_2
        aload_2
        aload_0
        invokevirtual
        invokevirtual
        aload_2
        invokevirtual
        astore_3
        iconst_0
        istore
        goto
        aload_3
        iload
        baload
        aload_1
        iload
        saload
        i2b
        if_icmpeq
        iconst_0
        ireturn
        iinc
        iload
        bipush
        if_icmplt
        goto
        astore_2
        aload_2
        invokevirtual
        iconst_1
        ireturn
 
Method 'login' structure:
    real name:    login
    descriptor:   (Ljava/lang/String;Ljava/lang/String;)Z
    access flags: public static
    method body:
        getstatic
        ldc
        iconst_1
        anewarray
        dup
        iconst_0
        aload_0
        aastore
        invokevirtual
        pop
        aload_0
        getstatic
        invokestatic
        istore_2
        aload_1
        getstatic
        invokestatic
        istore_3
        iload_2
        ifeq
        iload_3
        ifeq
        iconst_1
        ireturn
        iconst_0
        ireturn
 
Method 'main' structure:
    real name:    main
    descriptor:   ([Ljava/lang/String;)V
    access flags: public static
    method body:
        getstatic
        ldc
        ldc
        invokestatic
        invokevirtual
        getstatic
        ldc
        ldc
        invokestatic
        invokevirtual
        getstatic
        ldc
        ldc
        invokestatic
        invokevirtual
        getstatic
        ldc
        ldc
        invokestatic
        invokevirtual
        return
 
Trying to log in user: x
false
Trying to log in user: fakt
false
Trying to log in user: administrator
false
Trying to log in user: ko
true

12. Výpis bajtkódu změněné třídy Login a porovnání s původním bajtkódem

Pro jistotu ještě zkontrolujeme, zda je bajtkód třídy Login, resp. přesněji řečeno bajtkód metody Login.check() skutečně změněn korektně. Podobu bajtkódu získáme nám již známým příkazem javap -c -private Login (přepínač -private je důležitý pro výpis bajtkódu privátních metod), přičemž pod tímto odstavcem je ukázána pouze nejzajímavější část bajtkódu – metoda Login.check():

private static boolean check(java.lang.String, short[]);
  Code:
   0:           ldc             #25; //String SHA-512
   2:           invokestatic    #31; //Method java/security/MessageDigest.getInstance:(Ljava/lang/String;)Ljava/security/MessageDigest;
   5:           astore_2
   6:           aload_2
   7:           aload_0
   8:           invokevirtual   #37; //Method java/lang/String.getBytes:()[B
   11:          invokevirtual   #41; //Method java/security/MessageDigest.update:([B)V
   14:          aload_2
   15:          invokevirtual   #44; //Method java/security/MessageDigest.digest:()[B
   18:          astore_3
   19:          iconst_0
   20:          istore          4
   22:          goto            42
   25:          aload_3              // začátek těla programové smyčky
   26:          iload           4
   28:          baload
   29:          aload_1
   30:          iload           4
   32:          saload
   33:          i2b
   34:          if_icmpeq       39
   37:          iconst_0
   38:          ireturn
   39:          iinc            4, 1 // zvýšení hodnoty počitadla o jedničku
   42:          iload           4    // uloženi počitadla na zásobník
   44:          bipush          1    // hodnota počitadla se bude porovnávat s touto konstantou
   46:          if_icmplt       25   // porovnání a zpětný skok na začátek těla smyčky
   49:          goto            57
   52:          astore_2
   53:          aload_2
   54:          invokevirtual   #49; //Method java/security/NoSuchAlgorithmException.printStackTrace:()V
   57:          iconst_1
   58:          ireturn
  Exception table:
   from   to  target type
     0    52    52   Class java/security/NoSuchAlgorithmException

Rozdíl mezi původním bajtkódem metody Login.check() a bajtkódem modifikovaným si opět můžeme znázornit vizuálně, podobně jako v předchozích dvou částech tohoto seriálu:

root_podpora

Z tohoto obrázku je patrné, že se změnila jen jediná instrukce, a to konkrétně operand instrukce bipush na indexu 44.

13. Repositář se zdrojovými kódy dnešního demonstračního příkladu

Následuje – v tomto seriálu již tradiční – kapitola s odkazy na zdrojové kódy. Dnes popsaný demonstrační příklad ClassModification5 je společně s nástrojem HashFinder a testovací třídou LoopTest uložen do Mercurial repositáře dostupného na adrese http://icedtea.classpath.or­g/people/ptisnovs/jvm-tools/. V následující tabulce najdete odkazy na prozatím nejnovější verze těchto zdrojových kódů:

14. Odkazy na Internetu

  1. Open Source ByteCode Libraries in Java
    http://java-source.net/open-source/bytecode-libraries
  2. ASM Home page
    http://asm.ow2.org/
  3. Seznam nástrojů využívajících projekt ASM
    http://asm.ow2.org/users.html
  4. ObjectWeb ASM (Wikipedia)
    http://en.wikipedia.org/wi­ki/ObjectWeb_ASM
  5. Java Bytecode BCEL vs ASM
    http://james.onegoodcooki­e.com/2005/10/26/java-bytecode-bcel-vs-asm/
  6. BCEL Home page
    http://commons.apache.org/bcel/
  7. Byte Code Engineering Library (před verzí 5.0)
    http://bcel.sourceforge.net/
  8. Byte Code Engineering Library (verze >= 5.0)
    http://commons.apache.org/pro­per/commons-bcel/
  9. BCEL Manual
    http://commons.apache.org/bcel/ma­nual.html
  10. Byte Code Engineering Library (Wikipedia)
    http://en.wikipedia.org/wiki/BCEL
  11. BCEL Tutorial
    http://www.smfsupport.com/sup­port/java/bcel-tutorial!/
  12. Bytecode Engineering
    http://book.chinaunix.net/spe­cial/ebook/Core_Java2_Volu­me2AF/0131118269/ch13lev1sec6­.html
  13. Bytecode Outline plugin for Eclipse (screenshoty + info)
    http://asm.ow2.org/eclipse/index.html
  14. Javassist
    http://www.jboss.org/javassist/
  15. Byteman
    http://www.jboss.org/byteman
  16. Java programming dynamics, Part 7: Bytecode engineering with BCEL
    http://www.ibm.com/develo­perworks/java/library/j-dyn0414/
  17. The JavaTM Virtual Machine Specification, Second Edition
    http://java.sun.com/docs/bo­oks/jvms/second_edition/html/VMSpec­TOC.doc.html
  18. The class File Format
    http://java.sun.com/docs/bo­oks/jvms/second_edition/html/Clas­sFile.doc.html
  19. javap – The Java Class File Disassembler
    http://docs.oracle.com/ja­vase/1.4.2/docs/tooldocs/win­dows/javap.html
  20. javap-java-1.6.0-openjdk(1) – Linux man page
    http://linux.die.net/man/1/javap-java-1.6.0-openjdk
  21. Using javap
    http://www.idevelopment.in­fo/data/Programming/java/mis­cellaneous_java/Using_javap­.html
  22. Examine class files with the javap command
    http://www.techrepublic.com/ar­ticle/examine-class-files-with-the-javap-command/5815354
  23. aspectj (Eclipse)
    http://www.eclipse.org/aspectj/
  24. Aspect-oriented programming (Wikipedia)
    http://en.wikipedia.org/wi­ki/Aspect_oriented_program­ming
  25. AspectJ (Wikipedia)
    http://en.wikipedia.org/wiki/AspectJ
  26. EMMA: a free Java code coverage tool
    http://emma.sourceforge.net/
  27. Cobertura
    http://cobertura.sourceforge.net/
  28. jclasslib bytecode viewer
    http://www.ej-technologies.com/products/jclas­slib/overview.html

Byl pro vás článek přínosný?