Obsah
1. Pohled pod kapotu JVM – další modifikace bajtkódu Javy s využitím nástroje Javassist
2. Zdrojový kód upravené třídy Login
3. Bajtkód třídy Login před a po modifikaci programem ClassModification3
4. Pokus o použití upravené třídy Login
5. Využití metody CodeIterator.lookAhead() pro přečtení operačního kódu další instrukce
6. Opravená varianta uživatelské metody modifyMethodLogin()
7. Úplný zdrojový kód demonstračního příkladu ClassModification4
8. Výstup demonstračního příkladu ClassModification4
9. Výpis bajtkódu změněné třídy Login a porovnání s původním bajtkódem
10. Repositář se zdrojovými kódy dnešního demonstračního příkladu
11. Obsah následující části seriálu
1. Pohled pod kapotu JVM – další modifikace bajtkódu Javy s využitím nástroje Javassist
V předchozí části seriálu o programovacím jazyce Java i o virtuálním stroji Javy jsme si ukázali, jak je možné s využitím nástroje Javassist provést cílenou modifikaci bajtkódu javovských tříd takovým způsobem, aby se změnilo chování některých vybraných metod, tj. aby tyto metody prováděly jinou sekvenci instrukcí. Konkrétně jsme si popsali způsob změny chování statické metody Login.login() takovým způsobem, aby bylo možné se přihlásit i bez znalosti správného uživatelského jména a hesla. Připomeňme si, že výše zmíněná statická metoda Login.login() slouží ke kontrole uživatelského jména a hesla, přičemž jméno ani heslo není nikde uloženo v otevřené podobě – máme k dispozici jen otisk obou řetězců získaný hešovací funkcí SHA-512. Minule jsme se nesnažili o útok na jméno a heslo hrubou silou, ale pouze jsme pozměnili bajtkód metody Login.login() takovým způsobem, aby se vždy vracela pravdivostní hodnota true, nezávisle na tom, jak dopadlo porovnání otisku předaného jména a hesla s otisky uloženými ve třídě Login.
Pro připomenutí si ukažme, jak vypadal původní zdrojový kód třídy Login:
import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** * Testovaci trida, ktera bude modifikovana nastrojem Javassist. */ public class Login { /** * Hash jmena uzivatele (musi byt short[] kvuli hodnotam vetsim nez 0x80). */ private static final short[] NAME_SHA512_HASH = { 0x53,0x07,0xd0,0xbe,0xf6,0x35,0x32,0x10, 0xa9,0x8a,0x22,0xed,0xd7,0xa7,0x7d,0x07, 0xb3,0x70,0xa1,0xe3,0x48,0xb4,0xe8,0xf3, 0x4e,0x2f,0x50,0x95,0xef,0x18,0x67,0x39, 0x31,0x0b,0x5b,0x9c,0xa4,0x0c,0xb0,0x79, 0xfe,0x38,0x89,0x45,0xa0,0xd4,0x13,0xcd, 0x67,0x42,0x34,0x50,0x29,0x52,0xb8,0x4a, 0xc5,0xc1,0xf8,0x8f,0x66,0x27,0x78,0x31, }; /** * Hash hesla uzivatele. */ private static final short[] PASSWORD_SHA512_HASH = { 0x46,0xab,0xe2,0x83,0x21,0x83,0x92,0xe5, 0x6d,0x4c,0xdf,0xad,0x6e,0xe3,0x68,0xc8, 0x35,0x95,0x33,0x9b,0xd0,0x9b,0x4d,0x43, 0xb2,0x0f,0x89,0xb4,0x2c,0x15,0xfb,0x4a, 0x8a,0x64,0xe2,0x20,0xa6,0xd0,0x02,0xfa, 0xfb,0x09,0x23,0x02,0x30,0xdb,0x38,0x55, 0xb4,0x18,0xbf,0xe0,0x79,0x36,0x79,0xc9, 0xa7,0x08,0x6e,0x05,0x99,0x51,0x95,0xce, }; /** * 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; } /** * Kontrola jmena a hesla. */ public static boolean login(String name, String password) { boolean nameOk = check(name, NAME_SHA512_HASH); boolean passwordOk = check(password, PASSWORD_SHA512_HASH); return nameOk && passwordOk; } /** * Test funkcnosti tridy. */ public static void main(String[] args) { System.out.println(login("x","y")); System.out.println(login("fakt","nevim")); System.out.println(login("administrator","nbusr123")); } }
2. Zdrojový kód upravené třídy Login
Změna chování metody Login.login() s využitím nástroje Javassist byla, alespoň zdánlivě, poměrně jednoduchá, protože postačilo nahradit instrukci iconst0 za instrukci iconst1. Tyto instrukce byly použity pro uložení pravdivostní hodnoty true či false na zásobník operandů, kde byly následně použity jako návratová hodnota předaná zpět volající metodě instrukcí ireturn (připomeňme si, že pravdivostní hodnoty jsou ve virtuálním stroji Javy představovány celočíselnými hodnotami 0 a 1, což ovšem neplatí pro samotný programovací jazyk Java, kde jsou pravdivostní a celočíselné datové typy striktně odděleny). Náhrada sekvence instrukcí:
iconst_0 ireturn
za sekvenci instrukcí:
iconst_1 ireturn
tedy vedla k tomu, že se z metody Login.login() vždy vracela pravdivostní hodnota true, nezávisle na tom, jaká hodnota byla získána voláním metody Login.check().
Mohlo by se tedy zdát, že demonstrační příklad ClassModification3.java popsaný minule je již korektní a je možné ho použít pro změnu libovolné varianty metody Login.login(). Předpokládejme, že hlavička této metody je popsána v nějakém rozhraní, takže se bude měnit maximálně její implementace, která však vždy musí dodržovat návratový typ boolean. Ve skutečnosti však náš demonstrační „hackovací“ nástroj nebude pracovat ve všech případech korektně! Je tomu tak z toho důvodu, že provede nahrazení všech instrukcí iconst0 a iconst1 bez ohledu na kontext, ve kterém se tato instrukce v bajtkódu používá. Postačuje, aby někdo nepatrně změnil kód metody Login.login() a po modifikaci bajtkódu dojde k běhové chybě!
Podívejme se nyní, jak může k takovému problému dojít. Postačuje skutečně jen malá úprava třídy Login, která dokonce vůbec nesouvisí s vlastní kontrolou jména a hesla:
import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; /** * Testovaci trida, ktera bude modifikovana nastrojem Javassist. */ public class Login { /** * Hash jmena uzivatele. */ private static final short[] NAME_SHA512_HASH = { 0x53,0x07,0xd0,0xbe,0xf6,0x35,0x32,0x10, 0xa9,0x8a,0x22,0xed,0xd7,0xa7,0x7d,0x07, 0xb3,0x70,0xa1,0xe3,0x48,0xb4,0xe8,0xf3, 0x4e,0x2f,0x50,0x95,0xef,0x18,0x67,0x39, 0x31,0x0b,0x5b,0x9c,0xa4,0x0c,0xb0,0x79, 0xfe,0x38,0x89,0x45,0xa0,0xd4,0x13,0xcd, 0x67,0x42,0x34,0x50,0x29,0x52,0xb8,0x4a, 0xc5,0xc1,0xf8,0x8f,0x66,0x27,0x78,0x31, }; /** * Hash hesla uzivatele. */ private static final short[] PASSWORD_SHA512_HASH = { 0x46,0xab,0xe2,0x83,0x21,0x83,0x92,0xe5, 0x6d,0x4c,0xdf,0xad,0x6e,0xe3,0x68,0xc8, 0x35,0x95,0x33,0x9b,0xd0,0x9b,0x4d,0x43, 0xb2,0x0f,0x89,0xb4,0x2c,0x15,0xfb,0x4a, 0x8a,0x64,0xe2,0x20,0xa6,0xd0,0x02,0xfa, 0xfb,0x09,0x23,0x02,0x30,0xdb,0x38,0x55, 0xb4,0x18,0xbf,0xe0,0x79,0x36,0x79,0xc9, 0xa7,0x08,0x6e,0x05,0x99,0x51,0x95,0xce, }; /** * 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; } /** * Kontrola jmena a hesla. */ public static boolean login(String name, String password) { System.out.format("Trying to log in user: %s\n", name); boolean nameOk = check(name, NAME_SHA512_HASH); boolean passwordOk = check(password, PASSWORD_SHA512_HASH); return nameOk && passwordOk; } /** * Test funkcnosti tridy. */ public static void main(String[] args) { System.out.println(login("x","y")); System.out.println(login("fakt","nevim")); System.out.println(login("administrator","nbusr123")); } }
3. Bajtkód třídy Login před a po modifikaci programem ClassModification3
Jedinou změnou provedenou mezi původní a novou verzí třídy Login je přidání jednoho řádku na začátek metody Login.login():
System.out.format("Trying to log in user: %s\n", name);
Tento na první pohled nenápadný řádek však vede k poměrně velkým změnám ve vygenerovaném bajtkódu, a to z toho důvodu, že se do metody System.out.format() předává proměnný počet argumentů představovaný polem. A právě při předávání argumentů do této metody je mj. použita i instrukce iconst0, což je patrné i z výpisu bajtkódu (zmíněná instrukce je zvýrazněna):
public static boolean login(java.lang.String, java.lang.String); Code: 0: getstatic #67; //Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #69; //String Trying to log in user: %s\n 5: iconst_1 6: anewarray #4; //class java/lang/Object 9: dup 10: iconst_0 11: aload_0 12: aastore 13: invokevirtual #75; //Method java/io/PrintStream.format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream; 16: pop 17: aload_0 18: getstatic #12; //Field NAME_SHA512_HASH:[S 21: invokestatic #77; //Method check:(Ljava/lang/String;[S)Z 24: istore_2 25: aload_1 26: getstatic #14; //Field PASSWORD_SHA512_HASH:[S 29: invokestatic #77; //Method check:(Ljava/lang/String;[S)Z 32: istore_3 33: iload_2 34: ifeq 43 37: iload_3 38: ifeq 43 41: iconst_1 42: ireturn 43: iconst_0 44: ireturn
Demonstrační příklad ClassModification3 popsaný minule změní bajtkód metody Login.login() takovým způsobem, že nahradí všechny výskyty instrukce iconst0 za instrukci iconst1, bez ohledu na kontext:
public static boolean login(java.lang.String, java.lang.String); Code: 0: getstatic #67; //Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #69; //String Trying to log in user: %s\n 5: iconst_1 6: anewarray #4; //class java/lang/Object 9: dup 10: iconst_1 11: aload_0 12: aastore 13: invokevirtual #75; //Method java/io/PrintStream.format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream; 16: pop 17: aload_0 18: getstatic #12; //Field NAME_SHA512_HASH:[S 21: invokestatic #77; //Method check:(Ljava/lang/String;[S)Z 24: istore_2 25: aload_1 26: getstatic #14; //Field PASSWORD_SHA512_HASH:[S 29: invokestatic #77; //Method check:(Ljava/lang/String;[S)Z 32: istore_3 33: iload_2 34: ifeq 43 37: iload_3 38: ifeq 43 41: iconst_1 42: ireturn 43: iconst_1 44: ireturn
Pro větší přehled si můžeme rozdíly mezi oběma bajtkódy zvýraznit vizuálně:
Změna instrukce ležící na indexu 43 je korektní, ovšem instrukce na indexu 10 měla být ve skutečnosti zachována, tj. měl zde zůstat operační kód iconst0.
4. Pokus o použití upravené třídy Login
Co se stane ve chvíli, kdy se budeme snažit spustit modifikovanou variantu třídy Login? V metodě Login.login() dojde nejprve k vytvoření jednoprvkového pole typu Object[] a posléze k pokusu o zápis nové hodnoty (reference na řetězec) do druhého prvku tohoto pole, což je prvek s indexem 1. A právě v tomto okamžiku dojde k běhové chybě, konkrétně k vyhození výjimky typu ArrayIndexOutOfBoundsException. O tom se ostatně můžeme jednoduše přesvědčit:
~$ java Login Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 1 at Login.login(Login.java:62) at Login.main(Login.java:72)
5. Využití metody CodeIterator.lookAhead() pro přečtení operačního kódu další instrukce
Aby k běhové chybě popsané v předchozí kapitole nedošlo, musí se bajtkód metody Login.login() změnit poněkud sofistikovanějším způsobem, než prostou globální náhradou všech instrukcí iconst0 za iconst1. Budeme se muset dívat i na instrukci, která následuje ihned po instrukci iconst0 a pokud bude následující instrukce ireturn, teprve poté se bude jednat o tu část kódu, kterou je možné modifikovat. Jak je však při použití nástroje Javassist možné získat operační kód následující instrukce, když jednotlivými instrukcemi procházíme sekvenčně s využitím objektu typu CodeIterator? Víme již, že ve třídě CodeIterator máme k dispozici metody hasNext() a next() určené pro sekvenční procházení jednotlivými instrukcemi bajtkódu. Metoda next() vrátí index právě zpracovávané instrukce, přičemž operační kód této instrukce lze přečíst metodou byteAt(index).
Kromě toho však mají vývojáři k dispozici i metodu nazvanou lookAhead(), která vrátí index následující instrukce, a to nezávisle na tom, kolik bajtů má instrukce právě zpracovávaná (operační kódy instrukcí jsou sice vždy dlouhé jen jeden bajt, ovšem parametry mohou mít různou délku, v závislosti na typu instrukce). A právě metodu lookAhead() můžeme společně s metodou byteAt(index) použít pro přečtení operačního kódu instrukce, která se nachází ihned za instrukcí iconst0. Konkrétní způsob implementace je ukázán v navazující kapitole.
6. Opravená varianta uživatelské metody modifyMethodLogin()
S využitím výše popsané metody lookAhead() je již oprava uživatelské metody modifyMethodLogin() poměrně jednoduchá – budeme kontrolovat, zda dvojice za sebou jdoucích instrukcí neobsahuje operační kódy iconst0+ireturn a v případě shody se první instrukce z této dvojice nahradí za iconst1. 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.login() - zmena tela metody takovym zpusobem, * aby se vzdy vracela hodnota true. * * @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 modifyMethodLogin(CtClass testClass) throws NotFoundException, CannotCompileException, BadBytecode { CtMethod method = testClass.getDeclaredMethod("login"); 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 ICONST_0 za instrukci ICONST_1 // v pripade, ze se tato instrukce nachazi tesne pred // instrukci IRETURN if (currentOpcode == Opcode.ICONST_0 && nextOpcode == Opcode.IRETURN) { iterator.writeByte(Opcode.ICONST_1, currentIndex); } } // zmena atributu "CODE" prirazeneho k metode methodInfo.setCodeAttribute(ca); }
7. Úplný zdrojový kód demonstračního příkladu ClassModification4
V této kapitole bude uveden výpis úplného zdrojového kódu demonstračního příkladu ClassModification4 založeného na minule popsaném příkladu ClassModification3. Tento příklad načte původní bajtkód třídy Login, vypíše strukturu této třídy, provede náhradu těla metody Login.login(), opět vypíše strukturu třídy a následně otestuje, zda nová metoda Login.login() skutečně vrací pravdivostní hodnotu true pro jakékoli jméno a heslo (odlišné od NULL):
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 ClassModification4 { /** * 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.login() - zmena tela metody takovym zpusobem, * aby se vzdy vracela hodnota true. * * @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 modifyMethodLogin(CtClass testClass) throws NotFoundException, CannotCompileException, BadBytecode { CtMethod method = testClass.getDeclaredMethod("login"); 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 ICONST_0 za instrukci ICONST_1 // v pripade, ze se tato instrukce nachazi tesne pred // instrukci IRETURN if (currentOpcode == Opcode.ICONST_0 && nextOpcode == Opcode.IRETURN) { iterator.writeByte(Opcode.ICONST_1, currentIndex); } } // 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")); } /** * 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 login modifyMethodLogin(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(); } } }
8. Výstup demonstračního příkladu ClassModification4
Podívejme se nyní na výstup demonstračního příkladu ClassModification4, z něhož poznáme původní i novou strukturu třídy Login. Trojice řádků obsahujících pouze text „true“ vznikla v metodě checkMethodLogin() jako důkaz toho, že tato metoda byla skutečně úspěšně „oháčkována“, a to i při použití příkazu System.out.format() na začátku této metody:
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 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_1 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 return Trying to log in user: x true Trying to log in user: fakt true Trying to log in user: administrator true
9. 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 skutečně změněn korektně. Podobu bajtkódu získáme nám již známým příkazem javap -c Login, přičemž pod tímto odstavcem je ukázána pouze nejzajímavější část bajtkódu – metoda Login.login():
public static boolean login(java.lang.String, java.lang.String); Code: 0: getstatic #67; //Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #69; //String Trying to log in user: %s\n 5: iconst_1 6: anewarray #4; //class java/lang/Object 9: dup 10: iconst_0 11: aload_0 12: aastore 13: invokevirtual #75; //Method java/io/PrintStream.format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream; 16: pop 17: aload_0 18: getstatic #12; //Field NAME_SHA512_HASH:[S 21: invokestatic #77; //Method check:(Ljava/lang/String;[S)Z 24: istore_2 25: aload_1 26: getstatic #14; //Field PASSWORD_SHA512_HASH:[S 29: invokestatic #77; //Method check:(Ljava/lang/String;[S)Z 32: istore_3 33: iload_2 34: ifeq 43 37: iload_3 38: ifeq 43 41: iconst_1 42: ireturn 43: iconst_1 44: ireturn
Rozdíl mezi původním bajtkódem a bajtkódem modifikovaným si opět můžeme znázornit vizuálně:
Z tohoto obrázku je patrné, že se změnila jen jediná instrukce, a to konkrétně instrukce na indexu 43.
10. 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 je společně s novou variantou testovací třídy Login 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 | ClassModification4.java | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/e144eb90fcf3/javassist/ClassModification4/ClassModification4.java |
2 | Login.java | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/e144eb90fcf3/javassist/ClassModification4/Login.java |
11. Obsah následující části seriálu
V následující části tohoto seriálu se již budeme zabývat poněkud složitější problematikou, konkrétně způsoby implementace popř. modifikace programových smyček a větvení v javovském bajtkódu, samozřejmě opět s využitím prostředků nabízených nástrojem Javassist. Tato problematika je poměrně složitá a rozsáhlá zejména kvůli tomu, že ve virtuálním stroji Javy jsou skoky realizovány velkým množstvím instrukcí, včetně komplikovaných instrukcí typu tableswitch a lookupswitch. Pro efektivní práci s těmito instrukcemi nám nástroj Javassist nabízí několik tříd a metod, které mj. dokážou automaticky přepočítat indexy cílů skoků atd.
12. 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