Hlavní navigace

Pohled pod kapotu JVM – další modifikace bajtkódu Javy s využitím nástroje Javassist

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

Sdílet

V dnešní části seriálu o jazyce Java i o virtuálním stroji Javy budeme pokračovat v popisu cílené modifikace bajtkódu javovských tříd s využitím nástroje Javassist. Ukážeme si, jak vylepšit demonstrační příklad popsaný minule tak, aby pracoval korektně i za předpokladu, že se změní kód třídy Login.

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

12. Odkazy na Internetu

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í iconst0iconst1 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()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.

CS24_early

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.or­g/people/ptisnovs/jvm-tools/. V následující tabulce najdete odkazy na prozatím nejnovější verze těchto zdrojových kódů:

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 tableswitchlookupswitch. 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

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

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

Autor článku

Vystudoval VUT FIT a v současné době pracuje na projektech vytvářených v jazycích Python a Go.