Programmēšana

Java baitu kodu šifrēšanas uzlaušana

2003. gada 9. maijs

J: Ja es šifrēju savus .class failus un izmantoju pielāgotu classloader, lai tos ielādētu un atšifrētu lidojumā, vai tas novērsīs dekompilāciju?

A: Java baitu kodu dekompilācijas novēršanas problēma ir gandrīz tikpat veca kā pati valoda. Neskatoties uz tirgū pieejamo neskaidru rīku klāstu, iesācēji Java programmētāji turpina domāt par jauniem un gudriem veidiem, kā aizsargāt savu intelektuālo īpašumu. Šajā Java jautājumi un atbildes pa daļām, es kliedēju dažus mītus par ideju, kas bieži tiek pārstrādāta diskusiju forumos.

Ļoti vienkāršs, ar kuru Java .klase failus var rekonstruēt par Java avotiem, kas ļoti līdzinās oriģināliem, ir daudz sakara ar Java baitu kodu noformēšanas mērķiem un kompromisiem. Cita starpā Java baitu kods tika paredzēts kompaktumam, platformas neatkarībai, tīkla mobilitātei un baitu kodu tulku un JIT (tieši laikā) / HotSpot dinamisko kompilatoru analīzei. Var teikt, ka sastādīts .klase faili tik skaidri pauž programmētāja nodomu, ka tos varētu būt vieglāk analizēt nekā sākotnējo pirmkodu.

Var darīt vairākas lietas, ja ne, lai pilnībā novērstu dekompilāciju, vismaz lai to apgrūtinātu. Piemēram, kā pēckompilācijas soli jūs varētu masēt .klase datus, lai padarītu baita kodu dekompilēta vai nu grūtāk nolasāmu, vai grūtāk dekompilējamu derīgā Java kodā (vai abos). Pirmie labi darbojas tādas metodes kā ekstremālas metodes nosaukuma pārslodzes veikšana, un manipulēšana ar vadības plūsmu, lai izveidotu vadības struktūras, kuras nav iespējams attēlot, izmantojot Java sintaksi, labi darbojas otrajā. Veiksmīgāki komerciālie obfusatori izmanto šo un citu paņēmienu kombināciju.

Diemžēl abām pieejām faktiski jāmaina JVM palaistais kods, un daudzi lietotāji baidās (pamatoti), ka šī pārveidošana var pievienot jaunas kļūdas viņu lietojumprogrammām. Turklāt metodi un lauku pārdēvēšana var izraisīt pārdomu zvanu pārtraukšanu. Mainot faktiskos klases un pakotņu nosaukumus, var tikt izjaukti vairāki citi Java API (JNDI (Java Naming and Directory Interface), URL nodrošinātāji utt.). Papildus mainītiem nosaukumiem, ja tiek mainīta saistība starp klases baitu kodu nobīdēm un avota rindu numuriem, sākotnējo izņēmumu kaudzes pēdu atgūšana var kļūt sarežģīta.

Tad ir iespēja sākotnējo Java avota kodu apmulsināt. Bet būtībā tas rada līdzīgu problēmu kopumu.

Šifrēt, nevis apmulsināt?

Varbūt iepriekšminētais ir licis domāt: "Nu, ja nu es nevis manipulētu ar baitu kodu, bet pēc sastādīšanas es šifrēju visas savas klases un tos atšifrēju lidojumā JVM iekšienē (ko var izdarīt ar pielāgotu classloader)? Tad JVM izpilda manu oriģināls baitu kods, un tomēr nekas nav dekompilējams vai pārveidojams, vai ne? "

Diemžēl jūs kļūdāties gan domājot, ka esat pirmais, kurš nāca klajā ar šo ideju, gan domājot, ka tā faktiski darbojas. Un tam nav nekāda sakara ar jūsu šifrēšanas shēmas stiprumu.

Vienkāršs klases kodētājs

Lai ilustrētu šo ideju, es to izpildīju, ieviešot lietojumprogrammas paraugu un ļoti niecīgu pielāgotu classloader. Pieteikums sastāv no divām īsām klasēm:

public class Main {public static void main (final String [] args) {System.out.println ("slepenais rezultāts =" + MySecretClass.mySecretAlgorithm ()); }} // klases paketes beigas my.secret.code; importēt java.util.Random; public class MySecretClass {/ ** * Uzmini, slepenais algoritms izmanto tikai nejaušu skaitļu ģeneratoru ... * / public static int mySecretAlgorithm () {return (int) s_random.nextInt (); } private static final Random s_random = new Random (System.currentTimeMillis ()); } // Nodarbības beigas 

Mans mērķis ir slēpt programmas īstenošanu my.secret.code.MySecretClass šifrējot attiecīgo .klase failus un tos atšifrējot lidojuma laikā izpildlaikā. Šajā nolūkā es izmantoju šādu rīku (daži dati ir izlaisti; pilnu avotu varat lejupielādēt no resursiem):

public class EncryptedClassLoader paplašina URLClassLoader {public static void main (final String [] args) izmet izņēmumu {if ("-run" .equals (args [0]) && (args.length> = 3)) {// Izveidot pielāgotu iekrāvējs, kas pašreizējo iekrāvēju izmantos kā // deleģēšanas vecāku: final ClassLoader appLoader = new EncryptedClassLoader (EncryptedClassLoader.class.getClassLoader (), jauns fails (args [1])); // Ir jāpielāgo arī pavedienu konteksta ielāde: Thread.currentThread () .setContextClassLoader (appLoader); final Class app = appLoader.loadClass (argumenti [2]); galīgā metode appmain = app.getMethod ("main", jaunā klase [] {String [] .class}); galīgā virkne [] appargs = jauna virkne [arg.length - 3]; System.arraycopy (args, 3, appargs, 0, appargs.length); appmain.invoke (null, jauns objekts [] {appargs}); } else if ("-encrypt" .equals (args [0]) && (args.length> = 3)) {... šifrēt norādītās klases ...} else thrown new IllegalArgumentException (USAGE); } / ** * ignorē java.lang.ClassLoader.loadClass (), lai mainītu parastos vecāku un bērnu * deleģēšanas noteikumus tikai tik daudz, lai varētu "izgrābt" lietojumprogrammu klases * no sistēmas klases ielādētāja deguna. * / public Class loadClass (pēdējās virknes nosaukums, gala būla izšķirtspēja) izmet ClassNotFoundException {if (TRACE) System.out.println ("loadClass (" + nosaukums + "," + atrisināt + ")"); C klase = nulle; // Pirmkārt, pārbaudiet, vai šo klasi jau ir definējis šis classloader // gadījums: c = findLoadedClass (nosaukums); if (c == null) {Klases vecākiVersion = null; mēģiniet {// Tas ir nedaudz neparasti: veiciet izmēģinājuma ielādi, izmantojot vecāku iekrāvēju //, un atzīmējiet, vai vecāks ir deleģējis vai nē; // ko tas panāk, ir pareiza deleģēšana visām galvenajām // un paplašinājumu klasēm, man nav jāfiltrē klases nosaukums: vecākiemVersion = getParent () .loadClass (nosaukums); if (vecākiemVersion.getClassLoader ()! = getParent ()) c = vecākiemVersion; } catch (ClassNotFoundException ignore) {} catch (ClassFormatError ignore) {} if (c == null) {mēģiniet {// Labi, vai nu 'c' ielādēja sistēma (nevis bootstrap // vai paplašinājums) iekrāvējs ( kurā gadījumā es gribu ignorēt šo // definīciju) vai arī vecāks vispār izgāzās; katrā ziņā es // mēģinu definēt savu versiju: ​​c = findClass (nosaukums); } catch (ClassNotFoundException ignore) {// Ja tas neizdevās, atgriezieties vecāku versijā // [kas šajā brīdī varētu būt nulle]: c = vecākiemVersion; }}} if (c == null) mest jaunu ClassNotFoundException (nosaukums); if (atrisināt) atrisinātClass (c); atgriešanās c; } / ** * ignorē java.new.URLClassLoader.defineClass (), lai pirms klases definēšanas varētu izsaukt * crypt (). * / aizsargāta Class findClass (galīgais virknes nosaukums) met ClassNotFoundException {if (TRACE) System.out.println ("findClass (" + nosaukums + ")"); // netiek garantēts, ka .class faili būs ielādējami kā resursi; // bet, ja to dara Saules kods, tad varbūt var arī manu ... final String classResource = name.replace ('.', '/') + ".class"; gala URL classURL = getResource (classResource); if (classURL == null) mest jaunu ClassNotFoundException (nosaukums); else {InputStream in = null; mēģiniet {in = classURL.openStream (); galīgais baits [] classBytes = readFully (in); // "atšifrēt": kriptēšana (classBytes); if (TRACE) System.out.println ("atšifrēts [" + nosaukums + "]"); atgriezties defineClass (nosaukums, classBytes, 0, classBytes.length); } catch (IOException ioe) {mest jaunu ClassNotFoundException (nosaukums); } visbeidzot {if (in! = null) mēģiniet {in.close (); } catch (Izņēmuma ignorēšana) {}}}} / ** * Šis classloader spēj pielāgoti ielādēt tikai no viena direktorija. * / privāts EncryptedClassLoader (pēdējais ClassLoader vecāks, galīgais File classpath) met MalformedURLException {super (jauns URL [] {classpath.toURL ()}, vecāks); if (parent == null) mest jaunu IllegalArgumentException ("EncryptedClassLoader" + "nepieciešama nullei nedeleģēta vecāka"); } / ** * Atšifrē bināros datus dotajā baitu masīvā. Atkārtoti izsaucot metodi *, šifrēšana tiek mainīta. * / private static void crypt (final byte [] data) {for (int i = 8; i <data.length; ++ i) datiem [i] ^ = 0x5A; } ... citas palīgu metodes ...} // Nodarbības beigas 

EncryptedClassLoader ir divas pamatdarbības: noteiktā klašu kopas šifrēšana noteiktā classpath direktorijā un iepriekš šifrētas lietojumprogrammas palaišana. Šifrēšana ir ļoti vienkārša: tā galvenokārt sastāv no katra bita bināro klases satura bitu apgriešanas. (Jā, vecā labā XOR (ekskluzīvā VAI) gandrīz nav nekāda šifrēšana, bet izturieties pret mani. Šī ir tikai ilustrācija.)

Klases komplektēšana pēc EncryptedClassLoader ir pelnījis nedaudz lielāku uzmanību. Manas ieviešanas apakšklases java.net.URLClassLoader un ignorē abus loadClass () un defineClass () lai sasniegtu divus mērķus. Viens no tiem ir saliekt parastos Java 2 classloader deleģēšanas noteikumus un iegūt iespēju ielādēt šifrētu klasi pirms sistēmas classloader to dara, un otrs ir izsaukt kripta () tieši pirms zvana uz defineClass () kas citādi notiek iekšā URLClassLoader.findClass ().

Pēc visu apkopošanas atkritumu tvertne direktorijs:

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

Es "šifrēju" abus Galvenais un MySecretClass klases:

> java -cp bin EncryptedClassLoader -šifrēt bin Galvenā my.secret.code.MySecretClass šifrēta [Main.class] šifrēta [my \ secret \ code \ MySecretClass.class] 

Šīs divas klases atkritumu tvertne tagad ir aizstāti ar šifrētām versijām, un, lai palaistu sākotnējo lietojumprogrammu, man tā ir jāpalaiž EncryptedClassLoader:

> java -cp bin Galvenais izņēmums pavedienā "main" java.lang.ClassFormatError: Main (nelegāli pastāvīga kopas tips) vietnē java.lang.ClassLoader.defineClass0 (vietējā metode) vietnē java.lang.ClassLoader.defineClass (ClassLoader.java: 502) vietnē java.security.SecureClassLoader.defineClass (SecureClassLoader.java:123) vietnē java.net.URLClassLoader.defineClass (URLClassLoader.java:250) vietnē java.net.URLClassLoader.access00 (URLClassLoader.ja. net.URLClassLoader.run (URLClassLoader.java:193) vietnē java.security.AccessController.doPrivileged (vietējā metode) vietnē java.net.URLClassLoader.findClass (URLClassLoader.java:186) vietnē java.lang.ClassLoader.loadClass ( java: 299) vietnē sun.misc.Launcher $ AppClassLoader.loadClass (Launcher.java:265) vietnē java.lang.ClassLoader.loadClass (ClassLoader.java:255) vietnē java.lang.ClassLoader.loadClassInternal (ClassLoader.java:315 )> java -cp bin EncryptedClassLoader -run bin Galvenais atšifrēts [Main] atšifrēts [my.secret.code.MySecretClass] slepenais rezultāts = 1362768201 

Protams, jebkura dekompilatora (piemēram, Jad) palaišana šifrētās klasēs nedarbojas.

Laiks pievienot sarežģītu paroles aizsardzības shēmu, ietīt to vietējā izpildāmajā failā un iekasēt simtiem dolāru par "programmatūras aizsardzības risinājumu", vai ne? Protams, nē.

ClassLoader.defineClass (): neizbēgams pārtveršanas punkts

Viss ClassLoaderS klases definīcijas ir jānogādā JVM, izmantojot vienu precīzi definētu API punktu: java.lang.ClassLoader.defineClass () metodi. The ClassLoader API ir vairākas šīs metodes pārslodzes, taču tās visas izsauc defineClass (virkne, baits [], int, int, ProtectionDomain) metodi. Tas ir galīgais metode, kas pēc pāris pārbaudēm izsauc JVM vietējo kodu. Ir svarīgi to saprast Neviens classloader nevar izvairīties no šīs metodes izsaukšanas, ja vēlas izveidot jaunu Klase.

The defineClass () metode ir vienīgā vieta, kur a Klase var notikt objekts no plakana baita masīva. Un uzmini, baitu masīvā ir jāiekļauj nešifrēta klases definīcija labi dokumentētā formātā (skat. Klases faila formāta specifikāciju). Šifrēšanas shēmas laušana tagad ir vienkārša visu zvanu pārtveršana uz šo metodi un visu interesanto klašu dekompilēšana pēc sirds patikas (es minēju vēl vienu iespēju - JVM Profiler Interface (JVMPI), vēlāk).

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