Obsah
1. Pohled pod kapotu JVM – jednoduchý hacking bajtkódu Javy s využitím nástroje Javassist
2. Testovací příklad – třída Login ověřující jméno a heslo
3. Výpis originálního bajtkódu třídy Login
4. Úplná změna těla metody Login.login()
5. Otestování funkce změněné metody Login.login()
6. Úplný zdrojový kód demonstračního příkladu ClassModification2
7. Výstup demonstračního příkladu ClassModification2
8. Výpis bajtkódu změněné třídy Login a porovnání s původním bajtkódem
9. Cílená modifikace těla metody Login.login() – změna návratové hodnoty
10. Úplný zdrojový kód demonstračního příkladu ClassModification3
11. Výstup demonstračního příkladu ClassModification3
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. Jaké je tedy skutečné jméno a heslo použité ve třídě Login?
1. Pohled pod kapotu JVM – jednoduchý hacking bajtkódu Javy s využitím nástroje Javassist
V dnešní části seriálu o programovacím jazyce Java i o virtuálním stroji Javy si ukážeme použití nástroje Javassist pro 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í. Navážeme tak na část předchozí, v níž jsme si vysvětlili, jak je možné změnit přístupová práva k metodám a/nebo k atributům tříd.
Aby byl dnešní článek poněkud zábavnější, budeme se snažit změnit 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. Aby vše nebylo tak jednoduché, není jméno ani heslo uloženo v otevřené podobě, ale ve formě hashe, takže bude skutečně jednodušší změnit bajtkód metody Login.login() a nesnažit se najít buď původní jméno+heslo, nebo takové řetězce, které budou mít stejný hash (otisk), který bude v našem případě dlouhý 512 bitů (je použita hešovací funkce SHA-512).
2. Testovací příklad – třída Login ověřující jméno a heslo
Podívejme se nyní na třídu Login, na jejíž metodu login() budeme „útočit“. Tato třída je pro účely tohoto článku velmi jednoduchá a nevyužívá například systém Java Authentication and Authorization Service (JAAS). Zjednodušeně řečeno je možné říci, že metoda Login.login() získá dva řetězce představující uživatelské jméno a heslo. Následně jsou k oběma řetězcům vypočteny otisky (hashe) s využitím hešovací funkce SHA-512. Nezávisle na délce jména a hesla tak v každém případě dostaneme pole bajtů o velikosti 64 bajtů neboli 512 bitů. Následně je každé vypočtené pole porovnáno s obsahem statického pole NAME_SHA512_HASH popř. PASSWORD_SHA512_HASH a pouze v případě, že v obou případech dojde ke shodě, vrátí metoda Login.login() pravdivostní hodnotu true. Pokud ke shodě nedojde v jednom nebo obou případech, vrátí se samozřejmě hodnota false. Jak je patrné, jedná se o dosti primitivní způsob ověření uživatele:
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")); } }
3. Výpis originálního bajtkódu třídy Login
Pro další práci budeme potřebovat znát strukturu bajtkódu třídy Login. Tu získáme již známým standardním nástrojem javap, kterému předáme přepínač -c -v a v případě potřeby i přepínač -private:
Compiled from "Login.java" public class Login extends java.lang.Object{ private static final short[] NAME_SHA512_HASH; private static final short[] PASSWORD_SHA512_HASH; public Login(); Code: 0: aload_0 1: invokespecial #19; //Method java/lang/Object."<init>":()V 4: return 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 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 42: iload 4 44: bipush 64 46: if_icmplt 25 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 public static boolean login(java.lang.String, java.lang.String); Code: 0: aload_0 1: getstatic #12; //Field NAME_SHA512_HASH:[S 4: invokestatic #63; //Method check:(Ljava/lang/String;[S)Z 7: istore_2 8: aload_1 9: getstatic #14; //Field PASSWORD_SHA512_HASH:[S 12: invokestatic #63; //Method check:(Ljava/lang/String;[S)Z 15: istore_3 16: iload_2 17: ifeq 26 20: iload_3 21: ifeq 26 24: iconst_1 25: ireturn 26: iconst_0 27: ireturn public static void main(java.lang.String[]); Code: 0: getstatic #76; //Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #78; //String x 5: ldc #80; //String y 7: invokestatic #82; //Method login:(Ljava/lang/String;Ljava/lang/String;)Z 10: invokevirtual #88; //Method java/io/PrintStream.println:(Z)V 13: getstatic #76; //Field java/lang/System.out:Ljava/io/PrintStream; 16: ldc #90; //String fakt 18: ldc #92; //String nevim 20: invokestatic #82; //Method login:(Ljava/lang/String;Ljava/lang/String;)Z 23: invokevirtual #88; //Method java/io/PrintStream.println:(Z)V 26: getstatic #76; //Field java/lang/System.out:Ljava/io/PrintStream; 29: ldc #94; //String administrator 31: ldc #96; //String nbusr123 33: invokestatic #82; //Method login:(Ljava/lang/String;Ljava/lang/String;)Z 36: invokevirtual #88; //Method java/io/PrintStream.println:(Z)V 39: return }
4. Úplná změna těla metody Login.login()
Nejjednodušší, ovšem ne vždy nejlepší možností změny těla metody Login.login() je náhrada celého algoritmu pro ověřování jména a hesla za jediný příkaz return true;. To lze provést velmi jednoduchým způsobem naznačeným v následujícím kódu:
/** * Modifikace metody Login.login() - zmena tela metody. * * @param testClass * testovaci (modifikovana) trida. * @throws NotFoundException * vyvolana v pripade, ze metoda neni nalezena. * @throws CannotCompileException */ private static void modifyMethodLogin(CtClass testClass) throws NotFoundException, CannotCompileException { CtMethod method = testClass.getDeclaredMethod("login"); method.setBody("return true;"); }
/** * 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(); } }
5. Otestování funkce změněné metody Login.login()
Chování změněné metody Login.login() je samozřejmě vhodné ihned otestovat, a to bez opuštění virtuálního stroje Javy, v němž byl spuštěn nástroj Javassist. Pro spuštění modifikované metody opět použijeme reflexi, podobně jako tomu bylo i v předchozí části tohoto seriálu:
/** * 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")); }
Vlastní zavolání testované metody:
/** * 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")); }
6. Úplný zdrojový kód demonstračního příkladu ClassModification2
V této kapitole bude uveden výpis úplného zdrojového kódu demonstračního příkladu ClassModification2. Tento příklad načte původní bajtkód třídy Login, vypíše strukturu této třídy, provede náhradu těla 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; /** * 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 ClassModification2 { /** * 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 */ 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. * * @param testClass * testovaci (modifikovana) trida. * @throws NotFoundException * vyvolana v pripade, ze metoda neni nalezena. * @throws CannotCompileException */ private static void modifyMethodLogin(CtClass testClass) throws NotFoundException, CannotCompileException { CtMethod method = testClass.getDeclaredMethod("login"); method.setBody("return true;"); } /** * 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(); } } }
7. Výstup demonstračního příkladu ClassModification2
Podívejme se nyní na výstup demonstračního příkladu ClassModification2, 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():
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: 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: 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 true true true
8. Výpis bajtkódu změněné třídy Login a porovnání s původním bajtkódem
O tom, že poměrně drastická změna bajtkódu metody Login.login() může mít vliv i na další části bajtkódu celé třídy, se můžeme snadno přesvědčit dnes již jednou zmíněným nástrojem javap, tentokrát ovšem spuštěným nad změněným souborem Login.class:
Compiled from "Login.java" public class Login extends java.lang.Object{ public Login(); Code: 0: aload_0 1: invokespecial #21; //Method java/lang/Object."<init>":()V 4: return public static boolean login(java.lang.String, java.lang.String); Code: 0: iconst_1 1: ireturn public static void main(java.lang.String[]); Code: 0: getstatic #71; //Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #73; //String x 5: ldc #75; //String y 7: invokestatic #77; //Method login:(Ljava/lang/String;Ljava/lang/String;)Z 10: invokevirtual #83; //Method java/io/PrintStream.println:(Z)V 13: getstatic #71; //Field java/lang/System.out:Ljava/io/PrintStream; 16: ldc #85; //String fakt 18: ldc #87; //String nevim 20: invokestatic #77; //Method login:(Ljava/lang/String;Ljava/lang/String;)Z 23: invokevirtual #83; //Method java/io/PrintStream.println:(Z)V 26: getstatic #71; //Field java/lang/System.out:Ljava/io/PrintStream; 29: ldc #89; //String administrator 31: ldc #91; //String nbusr123 33: invokestatic #77; //Method login:(Ljava/lang/String;Ljava/lang/String;)Z 36: invokevirtual #83; //Method java/io/PrintStream.println:(Z)V 39: return }
Na následujícím obrázku se zvýrazněnými rozdíly mezi původní a novou podobou bajtkódu je patrné, že kvůli zjednodušení těla metody Login.login() došlo i ke změnám v constant poolu a tím pádem i ke změnám v ostatních metodách – to vše samozřejmě provede nástroj Javassist automaticky:
9. Cílená modifikace těla metody Login.login() – změna návratové hodnoty
Aplikací předchozího příkladu ClassModification2 se nám sice skutečně podařilo „oháčkovat“ třídu Login takovým způsobem, že není nutné znát přihlašovací jméno ani heslo, ovšem výsledek není v žádném případě elegantní. Je tomu tak z toho důvodu, že se faktickým smazáním původního těla třídy Login bajtkód tak zjednodušil, že byly odstraněny některé položky z constant poolu, což vedlo k tomu, že se změnily indexy do constant poolu i v dalších metodách. Navíc by se v případě, že by metoda login() prováděla i nějakou inicializaci tato část zcela přeskočila.
My ovšem můžeme bajtkód metody login() změnit i chytřeji. Stačí když si uvědomíme, že nám záleží pouze na změně návratové hodnoty této metody. Připomeňme si, že pravdivostní hodnota true je v JVM představována hodnotou 1 a false pak hodnotou 0. Při pohledu na disassemblovaný bajtkód pak můžeme vidět způsob překladu příkazu:
return nameOk && passwordOk;
sekvencí instrukcí:
0: aload_0 ; false ... 16: iload_2 ; nameOk 17: ifeq 26 ; if nameOk == false then goto 26 20: iload_3 ; passwordOk 21: ifeq 26 ; if passwordOk == false then goto 26 24: iconst_1 25: ireturn ; return true 26: iconst_0 27: ireturn ; return false
Nám by mohlo postačovat pouze změnit jednu instrukci iconst0 na iconst1, čímž by bylo zajištěno, že se ve všech případech bude vracet pravdivostní hodnota true:
0: aload_0 ; false ... 16: iload_2 ; nameOk 17: ifeq 26 ; if nameOk == false then goto 26 20: iload_3 ; passwordOk 21: ifeq 26 ; if passwordOk == false then goto 26 24: iconst_1 25: ireturn ; return true 26: iconst_1 27: ireturn ; return true
Tuto změnu lze kupodivu provést velmi jednoduše. Využijeme zde všechny znalosti, které již o nástroji Javassist máme, takže by následující kód neměl být příliš překvapivý:
/** * Modifikace metody Login.login() - zmena tela metody. * * @param testClass * testovaci (modifikovana) trida. * @throws NotFoundException * vyvolana v pripade, ze metoda neni nalezena. * @throws CannotCompileException * @throws BadBytecode */ private static void modifyMethodLogin(CtClass testClass) throws NotFoundException, CannotCompileException, BadBytecode { // potrebujeme instanci MethodInfo pro metodu "login" CtMethod method = testClass.getDeclaredMethod("login"); MethodInfo methodInfo = method.getMethodInfo(); // ziskat atribut "Code" prirazeny k metode - to je vlastni bajtkod CodeAttribute ca = methodInfo.getCodeAttribute(); CodeIterator iterator = ca.iterator(); // projit vsemi instrukcemi while (iterator.hasNext()) { int index = iterator.next(); int opcode = iterator.byteAt(index); // nahrada instrukce ICONST_0 za instrukci ICONST_1 if (opcode == Opcode.ICONST_0) { iterator.writeByte(Opcode.ICONST_1, index); } } methodInfo.setCodeAttribute(ca); }
To je vše!
10. Úplný zdrojový kód demonstračního příkladu ClassModification3
Úplný výpis zdrojového kódu dnešního druhého demonstračního příkladu nazvaného ClassModification3 vypadá následovně:
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 ClassModification3 { /** * 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 */ 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. * * @param testClass * testovaci (modifikovana) trida. * @throws NotFoundException * vyvolana v pripade, ze metoda neni nalezena. * @throws CannotCompileException * @throws BadBytecode */ private static void modifyMethodLogin(CtClass testClass) throws NotFoundException, CannotCompileException, BadBytecode { CtMethod method = testClass.getDeclaredMethod("login"); MethodInfo methodInfo = method.getMethodInfo(); CodeAttribute ca = methodInfo.getCodeAttribute(); CodeIterator iterator = ca.iterator(); // projit vsemi instrukcemi while (iterator.hasNext()) { int index = iterator.next(); int opcode = iterator.byteAt(index); // nahrada instrukce ICONST_0 za instrukci ICONST_1 if (opcode == Opcode.ICONST_0) { iterator.writeByte(Opcode.ICONST_1, index); } } 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(); } } }
11. Výstup demonstračního příkladu ClassModification3
Po spuštění dnešního druhého demonstračního příkladu ClassModification3 by se měly na standardním výstupu objevit následující zprávy obsahující strukturu původního i nového bajtkódu i výsledek volání checkMethodLogin():
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: 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: 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 true true true
12. Výpis bajtkódu změněné třídy Login a porovnání s původním bajtkódem
Vzhledem k tomu, že jsme provedli jen minimální zásahy do bajtkódu metody Login.login(), neměly by se změny promítnout na globální úrovni, tj. například by nemělo dojít k úpravám constant poolu. Nicméně je dobré si tuto domněnku ověřit, a to opět s využitím nástroje javap:
Compiled from "Login.java" public class Login extends java.lang.Object{ public Login(); Code: 0: aload_0 1: invokespecial #19; //Method java/lang/Object."<init>":()V 4: return public static boolean login(java.lang.String, java.lang.String); Code: 0: aload_0 1: getstatic #12; //Field NAME_SHA512_HASH:[S 4: invokestatic #63; //Method check:(Ljava/lang/String;[S)Z 7: istore_2 8: aload_1 9: getstatic #14; //Field PASSWORD_SHA512_HASH:[S 12: invokestatic #63; //Method check:(Ljava/lang/String;[S)Z 15: istore_3 16: iload_2 17: ifeq 26 20: iload_3 21: ifeq 26 24: iconst_1 25: ireturn 26: iconst_1 27: ireturn public static void main(java.lang.String[]); Code: 0: getstatic #76; //Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #78; //String x 5: ldc #80; //String y 7: invokestatic #82; //Method login:(Ljava/lang/String;Ljava/lang/String;)Z 10: invokevirtual #88; //Method java/io/PrintStream.println:(Z)V 13: getstatic #76; //Field java/lang/System.out:Ljava/io/PrintStream; 16: ldc #90; //String fakt 18: ldc #92; //String nevim 20: invokestatic #82; //Method login:(Ljava/lang/String;Ljava/lang/String;)Z 23: invokevirtual #88; //Method java/io/PrintStream.println:(Z)V 26: getstatic #76; //Field java/lang/System.out:Ljava/io/PrintStream; 29: ldc #94; //String administrator 31: ldc #96; //String nbusr123 33: invokestatic #82; //Method login:(Ljava/lang/String;Ljava/lang/String;)Z 36: invokevirtual #88; //Method java/io/PrintStream.println:(Z)V 39: return }
Zajímavější bude asi pohled na diff provedený mezi originálním dekompilovaným bajtkódem a bajtkódem modifikovaným. Při pohledu na následující obrázek je patrné, že změny jsou skutečně minimální:
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á dvojice demonstračních příkladů je společně s testovací třídou Login uložena 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 | ClassModification2.java | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/0b52fd623eb5/javassist/ClassModification2/ClassModification2.java |
2 | ClassModification3.java | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/0b52fd623eb5/javassist/ClassModification3/ClassModification3.java |
3 | Login.java | http://icedtea.classpath.org/people/ptisnovs/jvm-tools/file/0b52fd623eb5/javassist/ClassModification2/Login.java |
14. Jaké je tedy skutečné jméno a heslo použité ve třídě Login?
Pozorný čtenář se teď asi ptá, jaké přihlašovací jméno a heslo je tedy správné. Prozradím jen fakt, že jak jméno, tak i heslo se skládá pouze ze znaků malé abecedy (regexp: [a-z]+), délka přihlašovacího jména je šest znaků a hesla pět znaků. Dokáže někdo získat oba řetězce hrubou silou na základě znalosti polí NAME_SHA512_HASH a PASSWORD_SHA512_HASH? :-)
15. 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