Programavimas

„Java“ baitų kodo šifravimas

2003 m. Gegužės 9 d

Klausimas: Jei užšifruoju savo .class failus ir naudoju pasirinktinį „classloader“, kad galėčiau juos įkelti ir iššifruoti skriejant, ar tai padės išvengti dekompiliavimo?

A: „Java“ baitų kodų dekompiliavimo prevencijos problema yra beveik tokia pati kaip ir pati kalba. Nepaisant daugybės rinkoje esančių apgaulės priemonių, pradedantieji „Java“ programuotojai ir toliau galvoja apie naujus ir sumanius būdus apsaugoti savo intelektinę nuosavybę. Šiame „Java“ klausimai ir atsakymai dalimi, aš išsklaidau keletą mitų dėl idėjos, dažnai perduodamos diskusijų forumuose.

Itin paprastas „Java“ pasirinkimas .klasė failus galima rekonstruoti į „Java“ šaltinius, kurie labai panašūs į originalus, daug susiję su „Java“ baito kodo projektavimo tikslais ir kompromisais. Be kitų dalykų, „Java“ baitų kodas buvo sukurtas kompaktiškumui, nepriklausomumui nuo platformos, tinklo mobilumui ir paprastam baitų kodų vertėjų ir „JIT (just-in-time) / HotSpot“ dinaminių kompiliatorių analizavimui. Galima teigti, kad sudaryta .klasė failai taip aiškiai išreiškia programuotojo ketinimus, kad juos būtų lengviau analizuoti nei pirminį šaltinio kodą.

Galima padaryti keletą dalykų, jei ne visiškai užkirsti kelią dekompiliavimui, bent jau tam, kad tai būtų dar sunkiau. Pavyzdžiui, atlikdami kompiliavimą, galite masažuoti .klasė duomenis, kad baito kodas būtų sunkiau įskaitomas išskaidant, arba sunkiau išskaidomas į galiojantį „Java“ kodą (arba abu). Tokie metodai, kaip ekstremalaus metodo pavadinimo perkrova, gerai veikia pirmąjį, o manipuliavimas valdymo srautu, norint sukurti valdymo struktūras, kurių neįmanoma pateikti per „Java“ sintaksę, gerai veikia antrąją. Sėkmingesni komerciniai obfuzatoriai naudoja šių ir kitų metodų derinį.

Deja, abu būdai iš tikrųjų turi pakeisti JVM paleistą kodą, ir daugelis vartotojų bijo (teisingai), kad ši transformacija gali pridėti naujų klaidų jų programose. Be to, pervadinus metodą ir lauką, refleksijos skambučiai gali nustoti veikti. Pakeitus tikrus klasių ir paketų pavadinimus, gali būti pažeistos kelios kitos „Java“ API (JNDI („Java“ pavadinimo ir katalogo sąsaja), URL teikėjai ir kt.). Be pakeistų pavadinimų, jei bus pakeistas ryšys tarp klasės baito kodo poslinkių ir šaltinio eilutės numerių, gali būti sunku atkurti pradinius išimties kamino pėdsakus.

Tada yra galimybė sutrukdyti originalų „Java“ šaltinio kodą. Bet iš esmės tai sukelia panašų problemų rinkinį.

Šifruoti, o ne gluminti?

Galbūt tai, kas išdėstyta pirmiau, privertė jus pagalvoti: „Na, o jei aš, užuot manipuliavęs baitų kodu, po kompiliavimo užšifruoju visas savo klases ir jas iššifruoju skrendant JVM viduje (tai galima padaryti naudojant pasirinktinį„ classloader “)? Tada JVM įvykdo mano originalus baito kodas ir vis dėlto nėra ko dekompiliuoti ar pakeisti inžinieriaus, tiesa? "

Deja, klystumėte ir galvodami, kad pirmieji sugalvojote šią idėją, ir galvodami, kad ji iš tikrųjų veikia. Priežastis neturi nieko bendra su jūsų šifravimo schemos stiprumu.

Paprastas klasės koduotojas

Norėdami iliustruoti šią idėją, įdiegiau pavyzdinę programą ir labai nereikšmingą pasirinktinį „classloader“, kad ją paleisčiau. Paraišką sudaro dvi trumpos klasės:

viešoji klasė Main {public static void main (final String [] args) {System.out.println ("secret result =" + MySecretClass.mySecretAlgorithm ()); }} // klasės paketo pabaiga my.secret.code; importuoti java.util.Random; public class MySecretClass {/ ** * Atspėk, slaptasis algoritmas tiesiog naudoja atsitiktinių skaičių generatorių ... * / public static int mySecretAlgorithm () {return (int) s_random.nextInt (); } private static final Random s_random = naujas atsitiktinis (System.currentTimeMillis ()); } // Klasės pabaiga 

Mano siekis yra paslėpti programos įgyvendinimą my.secret.code.MySecretClass užšifruojant atitinkamą .klasė failus ir juos iššifruoti vykdant. Tuo tikslu naudoju šį įrankį (kai kurios informacijos praleista; visą šaltinį galite atsisiųsti iš išteklių):

public class EncryptedClassLoader pratęsia URLClassLoader {public static void main (final String [] args) meta išimtį {if ("-run" .equals (args [0]) && (args.length> = 3)) {// Sukurti pasirinktinį krautuvas, kuris naudos dabartinį krautuvą kaip // delegacijos tėvą: final ClassLoader appLoader = new EncryptedClassLoader (EncryptedClassLoader.class.getClassLoader (), new File (args [1])); // Taip pat reikia pakoreguoti gijų konteksto krautuvą: Thread.currentThread () .setContextClassLoader (appLoader); final Class app = appLoader.loadClass (argumentai [2]); galutinis metodas appmain = app.getMethod ("main", nauja klasė [] {String [] .class}); galutinė eilutė [] papildo = nauja eilutė [argumento ilgis - 3]; System.arraycopy (args, 3, appargs, 0, appargs.length); appmain.invoke (null, naujas objektas [] {appargs}); } else if ("-šifruoti" .equals (args [0]) && (args.length> = 3)) {... užšifruoti nurodytas klases ...} else thrown new IllegalArgumentException (USAGE); } / ** * Nepaiso java.lang.ClassLoader.loadClass (), kad pakeistumėte įprastas tėvų ir vaikų * delegavimo taisykles tiek, kad būtų galima „išplėšti“ programų klases * iš sistemos klasės pakrovėjo nosies. * / public class loadClass (galutinis eilutės pavadinimas, galutinis loginis sprendimas) išmeta ClassNotFoundException {if (TRACE) System.out.println ("loadClass (" + vardas + "," + išspręsti + ")"); C klasė = nulis; // Pirmiausia patikrinkite, ar šią klasę jau apibrėžė šis „classloader“ // egzempliorius: c = findLoadedClass (vardas); if (c == null) {Klasės tėvaiVersija = null; pabandykite {// Tai šiek tiek netradiciškai: atlikite bandomąją apkrovą per // pagrindinį krautuvą ir atkreipkite dėmesį, ar tėvas delegavo, ar ne; // tai pasiekia, tai yra tinkamas visų pagrindinių // ir plėtinių klasių delegavimas, man nereikia filtruoti klasės pavadinimo: tėveliaiVersija = getParent () .loadClass (vardas); if (tėvaiVersion.getClassLoader ()! = getParent ()) c = tėvaiVersija; } catch (ClassNotFoundException ignore) {} catch (ClassFormatError ignore) {} if (c == null) {bandykite {// Gerai, sistemos „(ne įkrovos juostos // ar plėtinio) krautuvas įkėlė„ c “(in kuriuo atveju noriu nepaisyti to // apibrėžimo) arba tėvas iš viso nepavyko; bet kuriuo atveju aš // bandau apibrėžti savo versiją: c = findClass (vardas); } catch (ClassNotFoundException ignore) {// Jei tai nepavyko, grįžkite į tėvų versiją // [kuri šiuo metu gali būti niekinė]: c = tėvaiVersija; }}} if (c == null) mesti naują „ClassNotFoundException“ (vardas); if (spręsti) išspręstiClass (c); grįžimas c; } / ** * Nepaiso java.new.URLClassLoader.defineClass (), kad galėtumėte iškviesti * crypt () prieš apibrėždami klasę. * / protected Class findClass (galutinis eilutės pavadinimas) meta ClassNotFoundException {if (TRACE) System.out.println ("findClass (" + name + ")"); // negarantuojama, kad .class failus galima įkelti kaip išteklius; // bet jei tai daro „Sun“ kodas, tai galbūt gali ir mano ... final String classResource = name.replace ('.', '/') + ".class"; galutinis URL classURL = getResource (classResource); if (classURL == null) mesti naują „ClassNotFoundException“ (vardas); else {InputStream in = nulis; pabandykite {in = classURL.openStream (); galutinis baitas [] classBytes = readFully (in); // "iššifruoti": crypt (classBytes); if (TRACE) System.out.println ("iššifruotas [" + vardas + "]"); return defineClass (vardas, classBytes, 0, classBytes.length); } gaudyti (IOException ioe) {mesti naują ClassNotFoundException (vardas); } pagaliau {if (in! = null) pabandykite {in.close (); } catch (Išimties ignoravimas) {}}}} / ** * Šis „classloader“ gali atlikti pasirinktinį įkėlimą tik iš vieno katalogo. * / privatus EncryptedClassLoader (galutinis „ClassLoader“ tėvas, galutinis failo „classpath“) meta „MalformedURLException“ {super (naujas URL [] {classpath.toURL ()}, pagrindinis); if (parent == null) mesti naują „IllegalArgumentException“ („EncryptedClassLoader“ + “reikalingas ne nulinis delegavimo tėvas“); } / ** * De / šifruoja dvejetainius duomenis nurodytame baitų masyve. Pakvietus metodą dar kartą *, šifravimas pasikeis. * / private static void crypt (final byte [] data) {for (int i = 8; i <duomenys.length; ++ i) duomenys [i] ^ = 0x5A; } ... daugiau pagalbininkų metodų ...} // Pamokos pabaiga 

„EncryptedClassLoader“ turi dvi pagrindines operacijas: tam tikro klasių rinkinio šifravimas tam tikrame „classpath“ kataloge ir anksčiau užšifruotos programos paleidimas. Šifravimas yra labai paprastas: jis iš esmės apverčia kai kuriuos kiekvieno baito bitus dvejetainės klasės turinyje. (Taip, senas geras XOR (išskirtinis ARBA) beveik nėra šifravimas, bet laikykis manęs. Tai tik iliustracija.)

Klasių perkėlimas pagal „EncryptedClassLoader“ nusipelno šiek tiek daugiau dėmesio. Mano įgyvendinimo poklasiai java.net.URLClassLoader ir nepaiso abiejų „loadClass“ () ir defineClass () pasiekti du tikslus. Vienas iš jų yra sulenkti įprastas „Java 2 classloader“ delegavimo taisykles ir gauti galimybę įkelti užšifruotą klasę prieš tai darant sistemos „classloader“, o kitas - tai kripta () prieš pat skambutį defineClass () kad kitaip nutinka viduje URLClassLoader.findClass ().

Sudėjus viską į šiukšliadėžė katalogas:

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

„Užšifruoju“ abu Pagrindinis ir „MySecretClass“ klasės:

> java -cp bin EncryptedClassLoader -šifruoti šiukšliadėžę Pagrindinis my.secret.code.MySecretClass užšifruotas [Main.class] užšifruotas [my \ secret \ code \ MySecretClass.class] 

Šios dvi klasės šiukšliadėžė dabar buvo pakeisti šifruotomis versijomis, o norėdamas paleisti pradinę programą, turiu paleisti programą „EncryptedClassLoader“:

> java -cp bin Pagrindinė gijos „pagrindinis“ išimtis java.lang.ClassFormatError: Main (neteisėtas pastovaus telkinio tipas) adresu java.lang.ClassLoader.defineClass0 (vietinis metodas) adresu java.lang.ClassLoader.defineClass (ClassLoader.java: 502) java.security.SecureClassLoader.defineClass (SecureClassLoader.java:123) ne java.net.URLClassLoader.defineClass (URLClassLoader.java:250) adresu java.net.URLClassLoader.access00 (URLClass4oader.ja. net.URLClassLoader.run (URLClassLoader.java:193) adresu java.security.AccessController.doPrivileged (vietinis metodas) adresu java.net.URLClassLoader.findClass (URLClassLoader.java:186) adresu java.lang.ClassLoader.loadClass ( java: 299) at sun.misc.Launcher $ AppClassLoader.loadClass (Launcher.java:265) at java.lang.ClassLoader.loadClass (ClassLoader.java:255) at java.lang.ClassLoader.loadClassInternal (ClassLoader.java:315) )> java -cp bin EncryptedClassLoader -run bin Pagrindinė iššifruota [Main] iššifruota [my.secret.code.MySecretClass] slaptas rezultatas = 1362768201 

Tikrai, bet kokio dekompiliatoriaus (pvz., „Jad“) paleidimas šifruotose klasėse neveikia.

Laikas pridėti sudėtingą slaptažodžių apsaugos schemą, supakuoti ją į vietinį vykdomąjį failą ir imti šimtus dolerių už „programinės įrangos apsaugos sprendimą“, tiesa? Žinoma ne.

ClassLoader.defineClass (): neišvengiamas perėmimo taškas

Viskas „ClassLoader“Jie turi pateikti savo klasės apibrėžimus JVM per vieną gerai apibrėžtą API tašką: java.lang.ClassLoader.defineClass () metodas. „ClassLoader“ API turi kelias šio metodo perkrovas, tačiau visos jos kreipiasi į defineClass (eilutė, baitas [], int, int, ProtectionDomain) metodas. Tai yra galutinis metodas, iškviečiantis JVM gimtąjį kodą atlikus kelis patikrinimus. Svarbu tai suprasti joks klasių perkėlėjas negali išvengti šio metodo iškvietimo, jei nori sukurti naują Klasė.

defineClass () metodas yra vienintelė vieta, kur sukuriama magija a Klasė objektas iš plokščio baitų masyvo gali vykti. Ir spėkite ką, baitų masyve turi būti neužšifruotas klasės apibrėžimas gerai dokumentuotu formatu (žr. Klasės failo formato specifikaciją). Šifravimo schemos sulaužymas dabar yra paprastas dalykas - perimti visus iškvietimus į šį metodą ir išskaidyti visas įdomias klases pagal savo širdies norą (vėliau pamenu dar vieną variantą - „JVM Profiler Interface“ (JVMPI)).

$config[zx-auto] not found$config[zx-overlay] not found