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()
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
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_HASH a PASSWORD_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, while a for.
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, while a for, 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 tableswitch a lookupswitch. 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í break a continue.
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+byte4 |
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, ifgt a ifle, 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_HASH a PASSWORD_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?
- Jméno: „ko“
- 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 ClassModification3 a ClassModification4. 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:
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.org/people/ptisnovs/jvm-tools/. V následující tabulce najdete odkazy na prozatím nejnovější verze těchto zdrojových kódů:
# | Zdrojový soubor/skript | Umístění souboru v repositáři |
---|---|---|
1 | ClassModification5.java | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/d0981157da08/javassist/ClassModification5/ClassModification5.java |
2 | LoopTest.java | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/d0981157da08/javassist/ClassModification5/LoopTest.java |
3 | HashFinder.java | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/d0981157da08/javassist/ClassModification5/HashFinder.java |
14. Odkazy na Internetu
- Open Source ByteCode Libraries in Java
http://java-source.net/open-source/bytecode-libraries - ASM Home page
http://asm.ow2.org/ - Seznam nástrojů využívajících projekt ASM
http://asm.ow2.org/users.html - ObjectWeb ASM (Wikipedia)
http://en.wikipedia.org/wiki/ObjectWeb_ASM - Java Bytecode BCEL vs ASM
http://james.onegoodcookie.com/2005/10/26/java-bytecode-bcel-vs-asm/ - BCEL Home page
http://commons.apache.org/bcel/ - Byte Code Engineering Library (před verzí 5.0)
http://bcel.sourceforge.net/ - Byte Code Engineering Library (verze >= 5.0)
http://commons.apache.org/proper/commons-bcel/ - BCEL Manual
http://commons.apache.org/bcel/manual.html - Byte Code Engineering Library (Wikipedia)
http://en.wikipedia.org/wiki/BCEL - BCEL Tutorial
http://www.smfsupport.com/support/java/bcel-tutorial!/ - Bytecode Engineering
http://book.chinaunix.net/special/ebook/Core_Java2_Volume2AF/0131118269/ch13lev1sec6.html - Bytecode Outline plugin for Eclipse (screenshoty + info)
http://asm.ow2.org/eclipse/index.html - Javassist
http://www.jboss.org/javassist/ - Byteman
http://www.jboss.org/byteman - Java programming dynamics, Part 7: Bytecode engineering with BCEL
http://www.ibm.com/developerworks/java/library/j-dyn0414/ - The JavaTM Virtual Machine Specification, Second Edition
http://java.sun.com/docs/books/jvms/second_edition/html/VMSpecTOC.doc.html - The class File Format
http://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html - javap – The Java Class File Disassembler
http://docs.oracle.com/javase/1.4.2/docs/tooldocs/windows/javap.html - javap-java-1.6.0-openjdk(1) – Linux man page
http://linux.die.net/man/1/javap-java-1.6.0-openjdk - Using javap
http://www.idevelopment.info/data/Programming/java/miscellaneous_java/Using_javap.html - Examine class files with the javap command
http://www.techrepublic.com/article/examine-class-files-with-the-javap-command/5815354 - aspectj (Eclipse)
http://www.eclipse.org/aspectj/ - Aspect-oriented programming (Wikipedia)
http://en.wikipedia.org/wiki/Aspect_oriented_programming - AspectJ (Wikipedia)
http://en.wikipedia.org/wiki/AspectJ - EMMA: a free Java code coverage tool
http://emma.sourceforge.net/ - Cobertura
http://cobertura.sourceforge.net/ - jclasslib bytecode viewer
http://www.ej-technologies.com/products/jclasslib/overview.html