Pohled pod kapotu JVM – jednoduchý hacking bajtkódu Javy s využitím nástroje Javassist

6. 8. 2013
Doba čtení: 24 minut

Sdílet

V dnešní části seriálu o jazyce Java i o virtuálním stroji si řekneme, jak se s využitím nástroje Javassist může cíleně modifikovat bajtkód javovských tříd tak, aby se změnilo chování některých metod. Konkrétně si ukážeme „útok“ na metodu sloužící pro přihlašování, resp. pro ověření uživatele.

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?

15. Odkazy na Internetu

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í:

bitcoin_skoleni

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

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

  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

Autor článku

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