Ohjelmointi

Java-tavukoodin salaus

9. toukokuuta 2003

K: Jos salaan .class-tiedostoni ja käytän mukautettua classloader-ohjelmaa niiden lataamiseen ja salauksen purkamiseen lennossa, estäisikö tämä salauksen purkamisen?

A: Java-tavukoodien purkamisen estämisen ongelma on melkein yhtä vanha kuin itse kieli. Huolimatta monista markkinoilla olevista hämärtymistyökaluista, aloittelevat Java-ohjelmoijat suunnittelevat edelleen uusia ja älykkäitä tapoja suojata henkistä omaisuuttaan. Tässä Java-kysymykset ja vastaukset erässä, hajotan joitain myyttejä ajatuksesta, joka toistetaan usein keskustelufoorumeilla.

Javan äärimmäinen helppous .luokka tiedostot voidaan rekonstruoida Java-lähteiksi, jotka muistuttavat läheisesti alkuperäisiä, on paljon tekemistä Java-tavukoodin suunnittelutavoitteiden ja kompromissien kanssa. Muun muassa Java-tavukoodi on suunniteltu pienikokoisuuteen, alustan riippumattomuuteen, verkon liikkuvuuteen ja tavujen kooditulkkien ja dynaamisten JIT (just-in-time) / HotSpot-kääntäjien analyysien helppouteen. Väitetysti koottu .luokka tiedostot ilmaisevat ohjelmoijan aikomuksen niin selvästi, että niitä on helpompi analysoida kuin alkuperäistä lähdekoodia.

Useita asioita voidaan tehdä, ellei dekompilaation estämiseksi kokonaan, ainakin sen vaikeuttamiseksi. Esimerkiksi kokoamisen jälkeisenä vaiheena voit hieroa .luokka tietoja, jotta tavukoodi olisi joko vaikeampaa lukea purettaessa tai vaikeampaa hajottaa kelvolliseksi Java-koodiksi (tai molemmiksi). Tekniikat, kuten äärimmäisen menetelmän nimen ylikuormitus, toimivat hyvin edelliselle, ja ohjausvirran manipulointi ohjausrakenteiden luomiseksi, joita ei voida edustaa Java-syntaksin kautta, toimivat hyvin jälkimmäiselle. Menestyneemmät kaupalliset hämärtimet käyttävät näiden ja muiden tekniikoiden yhdistelmää.

Valitettavasti molempien lähestymistapojen on itse asiassa muutettava JVM: n suorittama koodi, ja monet käyttäjät pelkäävät (oikeutetusti), että tämä muutos voi lisätä uusia vikoja sovelluksiinsa. Lisäksi menetelmän ja kentän uudelleennimeäminen voi aiheuttaa heijastuspyyntöjen lakkaamisen. Todellisten luokkien ja pakettien nimien muuttaminen voi rikkoa useita muita Java-sovellusliittymiä (JNDI (Java Naming and Directory Interface), URL-palveluntarjoajat jne.). Muutettujen nimien lisäksi, jos luokkatavukoodien siirtymien ja lähderivinumeroiden välistä yhteyttä muutetaan, alkuperäisten poikkeuspinojälkien palauttaminen voi tulla vaikeaksi.

Sitten on mahdollisuus peittää alkuperäinen Java-lähdekoodi. Mutta pohjimmiltaan tämä aiheuttaa samanlaisia ​​ongelmia.

Salataan, eikö hämärtää?

Ehkä yllä oleva on saanut sinut ajattelemaan: "No, entä jos tavukoodin manipuloinnin sijaan salaan kaikki luokkani kokoamisen jälkeen ja purkaa ne lennossa JVM: n sisällä (mikä voidaan tehdä mukautetulla classloaderilla)? Sitten JVM suorittaa minun alkuperäinen tavukoodi, mutta silti ei ole mitään dekompiloitavaa tai käänteistä insinööriä, eikö? "

Valitettavasti olisit väärässä sekä ajattellessasi, että keksit ensimmäisenä tämän idean, että ajattelemalla, että se todella toimii. Ja syyllä ei ole mitään tekemistä salausjärjestelmän vahvuuden kanssa.

Yksinkertainen luokan kooderi

Tämän idean havainnollistamiseksi otin käyttöön esimerkkisovelluksen ja erittäin triviaalin mukautetun luokan latausohjelman sen suorittamiseksi. Hakemus koostuu kahdesta lyhyestä luokasta:

public class Main {public static void main (final String [] args) {System.out.println ("salainen tulos =" + MySecretClass.mySecretAlgorithm ()); }} // luokan loppupaketti my.secret.code; tuo java.util.Random; public class MySecretClass {/ ** * Arvaa mitä, salainen algoritmi käyttää vain satunnaislukugeneraattoria ... * / public static int mySecretAlgorithm () {return (int) s_random.nextInt (); } yksityinen staattinen lopullinen Satunnainen s_random = uusi satunnainen (System.currentTimeMillis ()); } // Luokan loppu 

Pyrin piilottamaan my.secret.code.MySecretClass salaamalla asiaankuuluva .luokka tiedostoja ja purkaa ne lennossa ajon aikana. Tätä varten käytän seuraavaa työkalua (joitain yksityiskohtia jätetty pois; voit ladata koko lähteen Resursseista):

public class EncryptedClassLoader laajentaa URLClassLoader {public static void main (viimeinen merkkijono [] args) heittää poikkeuksen {if ("-run" .equals (args [0]) && (args.length> = 3)) {// Luo mukautettu lataaja, joka käyttää nykyistä latainta // valtuutuksen vanhempana: final ClassLoader appLoader = new EncryptedClassLoader (EncryptedClassLoader.class.getClassLoader (), new File (args [1])); // Myös langan kontekstikuormaajaa on säädettävä: Thread.currentThread () .setContextClassLoader (appLoader); viimeinen luokan sovellus = appLoader.loadClass (argumentit [2]); lopullinen menetelmä appmain = app.getMethod ("main", uusi luokka [] {String [] .luokka}); viimeinen merkkijono [] appargs = uusi merkkijono [arg.pituus - 3]; System.arraycopy (args, 3, appargs, 0, appargs.length); appmain.invoke (null, uusi objekti [] {appargs}); } else if ("-encrypt" .equals (args [0]) && (args.pituus> = 3)) {... salaa määritetyt luokat ...} else heittää uuden IllegalArgumentException (USAGE); } / ** * Ohittaa java.lang.ClassLoader.loadClass (), jos haluat muuttaa tavanomaisia ​​vanhemman ja lapsen * delegointisääntöjä vain niin paljon, että pystyt "nappaamaan" sovellusluokkia * järjestelmän luokkakuormaajan nenästä. * / public Class loadClass (lopullinen merkkijonon nimi, lopullinen boolen resoluutio) heittää ClassNotFoundException {if (TRACE) System.out.println ("loadClass (" + nimi + "," + ratkaisu + ")"); Luokka c = nolla; // Tarkista ensin, onko tämä classloader jo määrittänyt tämän luokan // ilmentymä: c = findLoadedClass (nimi); if (c == null) {Luokan vanhemmatVersion = null; kokeile {// Tämä on hieman epätavallista: tee kokeilulataus // vanhemman lataimen kautta ja huomaa, onko vanhempi delegoinut vai ei; // mitä tämä saavuttaa, on asianmukainen delegointi kaikille ydin //- ja laajennusluokille ilman, että minun on suodatettava luokan nimeä: vanhemmatVersion = getParent () .loadClass (nimi); if (vanhemmatVersion.getClassLoader ()! = getParent ()) c = vanhemmatVersion; } catch (ClassNotFoundException ignore) {} catch (ClassFormatError ignore) {} if (c == null) {yritä {// OK, joko 'c' ladattiin järjestelmästä (ei bootstrap // tai laajennus). missä tapauksessa haluan jättää huomioimatta // määritelmän) tai vanhempi epäonnistui kokonaan; Joko niin // yritän määritellä oman versioni: c = findClass (nimi); } catch (ClassNotFoundException ignore) {// Jos tämä epäonnistui, palaa takaisin vanhemman versioon // [joka voi olla tässä vaiheessa tyhjä]: c = vanhemmatVersion; }}} if (c == null) heittää uusi ClassNotFoundException (nimi); jos (ratkaise) resoluutio (c); paluu c; } / ** * Ohittaa java.new.URLClassLoader.defineClass () voidakseen kutsua * crypt () ennen luokan määrittelyä. * / suojattu Class findClass (lopullinen merkkijono) heittää ClassNotFoundException {if (TRACE) System.out.println ("findClass (" + nimi + ")"); // .class-tiedostojen ei voida taata olevan resursseina ladattavissa; // mutta jos Sunin koodi tekee sen, niin ehkä voi minun ... final String classResource = name.replace ('.', '/') + ".class"; lopullinen URL classURL = getResource (classResource); if (classURL == null) heittää uusi ClassNotFoundException (nimi); else {InputStream sisään = tyhjä; kokeile {in = classURL.openStream (); viimeinen tavu [] classBytes = readFully (in); // "purkaa": salaus (classBytes); if (TRACE) System.out.println ("purettu [" + nimi + "]"); return defineClass (nimi, classBytes, 0, classBytes.length); } catch (IOException ioe) {heitä uusi ClassNotFoundException (nimi); } lopuksi {if (in! = null) kokeile {in.close (); } catch (Exception ignore) {}}}} / ** * Tämä classloader pystyy mukautettuun lataamiseen vain yhdestä hakemistosta. * / private EncryptedClassLoader (viimeinen ClassLoader-vanhempi, lopullinen File-luokan polku) heittää MalformedURLException {super (uusi URL [] {classpath.toURL ()}, vanhempi); if (vanhempi == null) heittää uusi IllegalArgumentException ("EncryptedClassLoader" + "vaatii ei-nolla delegoinnin vanhemman"); } / ** * Poistaa / salaa binääridatan tietyssä tavujärjestelmässä. Menetelmän kutsuminen uudelleen * muuttaa salauksen. * / private static void crypt (viimeisen tavun [] data) {for (int i = 8; i <data.length; ++ i) data [i] ^ = 0x5A; } ... lisää auttajamenetelmiä ...} // luokan loppu 

EncryptedClassLoader on kaksi perustoimintoa: tietyn luokkaryhmän salaus tietyssä luokan polun hakemistossa ja aiemmin salatun sovelluksen suorittaminen. Salaus on hyvin suoraviivaista: se koostuu periaatteessa joidenkin bittien kääntämisestä jokaisesta binaariluokan sisällön tavusta. (Kyllä, vanha hyvä XOR (yksinomainen TAI) ei ole lainkaan salausta, mutta kannattaa minua. Tämä on vain esimerkki.)

Luokan lataaminen EncryptedClassLoader ansaitsee hieman enemmän huomiota. Toteutukseni alaluokat java.net.URLClassLoader ja ohittaa molemmat loadClass () ja defineClass () saavuttaa kaksi tavoitetta. Yksi on taivuttaa tavalliset Java 2 -luokanlataimen delegointisäännöt ja saada mahdollisuus ladata salattu luokka ennen kuin järjestelmän luokkaohjelma tekee sen, ja toinen on kutsua krypta () juuri ennen puhelua defineClass () joka muuten tapahtuu sisällä URLClassLoader.findClass ().

Kun olet koonnut kaiken bin hakemisto:

> javac -d bin src / *. java src / my / secret / code / *. java 

"Salaan" molemmat Main ja MySecretClass luokat:

> java -cp bin EncryptedClassLoader -salaa bin Main my.secret.code.MySecretClass encrypted [Main.class] encrypted [my \ secret \ code \ MySecretClass.class] 

Nämä kaksi luokkaa bin on nyt korvattu salatuilla versioilla, ja minun on ajettava sovellus läpi, jotta voin suorittaa alkuperäisen sovelluksen EncryptedClassLoader:

> java -cp bin Pääpoikkeus ketjussa "main" java.lang.ClassFormatError: Main (laiton vakioallastyyppi) osoitteessa java.lang.ClassLoader.defineClass0 (Native Method) osoitteessa java.lang.ClassLoader.defineClass (ClassLoader.java: 502) osoitteessa java.security.SecureClassLoader.defineClass (SecureClassLoader.java:123) osoitteessa java.net.URLClassLoader.defineClass (URLClassLoader.java:250) osoitteessa java.net.URLClassLoader.access00 (URLClassLoader.ja net.URLClassLoader.run (URLClassLoader.java:193) osoitteessa java.security.AccessController.doPrivileged (alkuperäinen menetelmä) osoitteessa java.net.URLClassLoader.findClass (URLClassLoader.java:186) osoitteessa java.lang.ClassLoader.loadClass. java: 299) osoitteessa sun.misc.Launcher $ AppClassLoader.loadClass (Launcher.java:265) osoitteessa java.lang.ClassLoader.loadClass (ClassLoader.java:255) osoitteessa java.lang.ClassLoader.loadClassInternal (ClassLoader.java:315) )> java -cp bin EncryptedClassLoader -run bin Pääsalaatus purettu [Main] salattu [my.secret.code.MySecretClass] salainen tulos = 1362768201 

Tosiaan, minkä tahansa dekompilaattorin (kuten Jad) suorittaminen salatuissa luokissa ei toimi.

Aika lisätä hienostunut salasanasuojausjärjestelmä, sisällyttää tämä alkuperäiseen suoritettavaan tiedostoon ja veloittaa satoja dollareita "ohjelmistosuojausratkaisusta", eikö? Ei tietenkään.

ClassLoader.defineClass (): väistämätön sieppauspiste

Kaikki ClassLoaderNiiden on toimitettava luokkamäärittelyt JVM: lle yhden hyvin määritetyn API-pisteen kautta: java.lang.ClassLoader.defineClass () menetelmä. ClassLoader API: lla on useita tämän menetelmän ylikuormituksia, mutta ne kaikki kutsuvat defineClass (merkkijono, tavu [], int, int, ProtectionDomain) menetelmä. Se on lopullinen menetelmä, joka kutsuu JVM: n natiivikoodiksi muutaman tarkistuksen jälkeen. On tärkeää ymmärtää se yksikään classloader ei voi välttää kutsumasta tätä menetelmää, jos se haluaa luoda uuden Luokka.

defineClass () menetelmä on ainoa paikka, jossa a Luokka objekti litteästä tavujärjestelmästä voi tapahtua. Ja arvaa mitä, tavutaulukon on sisällettävä salaamaton luokan määritelmä hyvin dokumentoidussa muodossa (katso luokan tiedostomuodon määrittely). Salausjärjestelmän rikkominen on nyt yksinkertainen tapa siepata kaikki tähän menetelmään kutsutut puhelut ja purkaa kaikki mielenkiintoiset luokat sydämesi halun mukaan (mainitsen toisen vaihtoehdon, myöhemmin JVM Profiler Interface (JVMPI)).