JavaSvet - otvorena java zajednica

 
glavna stranica arr2javasvet  english version arr2java.net

Reverse Engineering: Disasembliranje Java koda

Igor Spasić
27 Dec 2004
Sadržaj:

Poznati pojam "Engineering" (ili "forward engineering") je proces s kojim se susrećemo u svakodnevnom radu, u toku koga se iz logičkog dizajna sistema koji je na visokom abstraktnom nivou stvara fizička implementacija sistema. Nasuprot njemu, "reverse engineering" je obrnut proces, u kome se od implementacije stvara logički dizajn i model sistema koji se posmatra. Proces reverse engineeringa se takođe često koristi, naročito kada iz nekog razloga nisu dostupne informacije o modelu, već su razvijaoci suočeni samo sa "sirovim" sorsom ili kodom sistema.

Reverse engineering je širok pojam i obuhvata veliku klasu postupaka i problema. Ovaj članak se bavi jednim oblikom reverse engineeringa: disasembliranjem Java koda. Disasembliranje, najprostije rečeno, čine niz postupaka kojima se dobija uvid u Java sors samo na osnovu izvršne verzije programa.

Proces diasembliranja je usko povezan sa domenima sigurnosti sistema i zaštite softvera, oblastima koje su neprekidno popularne i u žiži interesovanja. Disasmbliranje je ujedno i kontraverzan postupak, jer u nekim svojim oblicima dolazi u sukob sa pravnim zakonima pojedinih zemalja u kojima postoje propisi po kojima se pojedini vidovi reverse engineeringa smatraju krivičnim delima (na pr. Digital Millennium Copyright Act).

Treba uočiti da bavljenje reverse engineeringom i disasembliranjem samo po sebi nije zabranjeno. Ovo otvara mnoga druga moralna/pravna pitanja, o kojima ovde ipak neće biti reči.

Svrha članka

Serija članaka "Reverse Engineering":
1. Disasembliranje Java koda
2. Bytecode manipulacija

Članak upravo treba da pokaže kako danas izgleda jedna moderna zaštita Java programa i kako ju je moguće brzo prevazići upravo korišćenjem svakodnevnih alata. Svrha članka nije da pokaže kako se skida zaštita, već upravo da pruži informacije o sigurnosnim sistemima i da razbije eventualne iluzije o neprobojnim zaštitama.

Napomena

Iz razloga da se ovaj članak ne shvati drugačije, sledi autorova napomena/izjava:

Članak je napisan jedino i isključivo za edukativne potrebe i u cilju istraživanja sigurnosti Java sistema. Autor se ograđuje od bilo kakve zloupotrebe i štete koja je slučajno ili namerno nastala na osnovu članka i/ili korišćenjem izloženih tehnika.

Isto tako, autor neće odgovarati na pitanja koja su u suprotnosti sa gornjom napomenom.

Vrste zaštita

Pre primera vredi napraviti kratak pregled zaštita Java programa. Čini se da već svi znaju da je vrlo lako iz jedne kompajlirane klase dobiti njen sors. Najpopularniji alat za to je Jad, a koga vrlo praktično interno koristi DJ Java Decompiler (u daljem tekstu: DJ). Kada se u DJ GUI učita neki .class fajl, on ga dekompajlira uz pomoć Jad-a i prikaže dobijeni sors. Ovako dobijeni sors je moguće ponovo kompajlirati, bez ikakvih problema.

Očigledno je da je ovo potencijalni sigurnosni problem, jer niko ne sprečava da se napravi izmena na dekompajliranom java fajlu i da se tako izmenjena klasa vrati nazad u sistem. Svako ko bar malo poznaje Javu može relativno brzo naći mesto u dekompajliranom programu gde se izvršava kritični deo koda aplikacije i da napravi izmenu koja bi poremetila ili zloupotrebila normalni tok rada programa. U prilog ovome ide i činjenica da je dobra praksa u modelovanju sistema da se za imena klasa i metoda uzimaju nazivi koji bliže određuju biznis logiku koju nose, tako da se u sistemima često sreću klase, metodi i paketi nazvani LicenseValidator, isExpired() ili com.foo.registration i tome slično, pa se "iz aviona" vidi gde je smeštena kritična biznis logika i provera. Treba uočiti i da na udaru nisu samo stand-alone aplikacije, već i razne "3rd party" biblioteke koje mogu da se koriste i na server-side projektima.

Na svu sreću, postoji način kako je moguće prevazići ovaj nezgodan sigurnosni problem koji se nalazi u samoj srži Java okruženja.

Obfuskatori

Prvo i najjednostavnije rešenje koje pada na pamet je da ako je već nemoguće sprečiti da se klasa dekompajlira, onda bar da se proba da se učini da dobijeni sors bude što je moguće više nečitljiv. Nečitljiv kod je, naravno, daleko teže analizirati. Tako, na primer, ako se pomenuta metoda isExpired() nazove aaaAb() ime metode prestaje da donosi informaciju o logici koju izvršava.

Kako je glupo dozvoliti da se sistem od početka modeluje s nečitljivim i nesuvislim imenima paketa, klasa i metoda, nastali su obfuskatori (engl.: obfuscators). Obfuskator je program koji se poziva posle bildovanja aplikacije, a koji ima zadatak da prođe kroz sve kreirane klase sistema i da modifikacijom bajtkoda promeni imena paketa, klasa i metoda u neka koja ništa ne znače, ili čak, još više otežavaju analizu dekompajliranog koda. U praksi nije moguće zameniti baš sva imena (main() na primer mora da ostane), ali se pokazuje da je broj takvih imena jako mali i da ne narušava smisao obfuskacije.

Treba uočiti da je obfuskatoru za rad dovoljan samo izvršni kod (bajtkod), a ne i sors.

Nova, izmenjena imena mogu biti baš svakojaka i zavise samo od maštovitosti obfuskatora. Tako neki menjaju imena metoda u dugačke nizove istih slova, različite dužine. Pri tome još mogu da se koriste nasumična mala i velika slova kako bi razlikovala imena iste dužine. Ili se pak koriste samo minimalna dužina imena, pa je većina imena sačinjena od jednog slova, koja se često ponavlja u drugim klasama.

Ovakva obfuskacija zaista radi posao - značajno otežava analizu dekompajliranog koda. Kada je zaštita sakrivena negde duboko u sistemu i po dubini poziva, teško je jednostavnim praćenjem sorsa doći do tih mesta.

Iako dobro zamišljena, ovakva zaštita je još daleko od savršene. Danas mnogi IDE mogu lako da rade refaktorisanje promene imena, i pomažu u analizi sorsa omogućavajući lakšu navigaciju.

Napredniji obfuskatori

Činjenica da je Java virtuelna mašina manje restriktivna od Java kompajlera je ono što stoji iza ideje naprednijih oblika obfuskacije. Ovo znači da je Java kompajler taj koji pazi o raznim nedozvoljenim upotrebama Jave: sprečava da se bilo šta poziva pre konstruktora, ne dozvoljava da se ključne reči koriste za imena metoda i promenljivih itd. Sama virtuelna mašina nema takvih ograničenja i izmenom samog bajtkoda i class fajla je moguće koristiti nedozvoljene konstrukcije Java jezika.

Napredniji obfuskatori se, dakle, služe upravo ovom činjenicom. Njihov cilj je ne samo otežati analizu dekompajlirane klase, već sprečiti da se ona ispravno dekompajlira, čime njeno ponovno kompajliranje postaje nemoguće!

Najprostiji primer je upravo korišćenje Javinih ključnih reči za imena paketa, klasa i metoda. Java kompajler ne dozvoljava, na primer, da se metoda zove for(). Zato napredniji obfuskator posle kompajliranja menja imena u class fajloviima. Javinoj VM to ne smeta, pošto ona ionako radi samo sa bajtkodom, a sama imena joj ništa ne znače. Dekompajleri uglavnom mogu da prozivedu Java sors od ovakvo obfuskovane klase, ali njeno ponovo kompajliranje postaje nemoguće!

Kao da to sve nije dovoljno, obfuskatori se služe mnogim drugim prljavim trikovima na nivou bajtkoda. Najjednostavnije rečeno, svi ti trikovi se svode na činjenicu da je moguće bajtkod instrukcije rasporediti ili napisati ih na drugačiji način, tako da se sama logika izvršavanja ne menja, ali da postanu neprepoznatljive za dekompajlere. U takvim slučajevima dekompajleri se "zbune" i dekompajliran Java sors kod je često pun komentara sa napomenama i bajtkodom VM instrukcija koje nije uspeo da prevede u neki smisleni oblik Java jezika.

Dakle, obfuskacija je danas zaista dostigla svoj cilj: klase nije lako dekompajlirati, a nemoguće je ponovo ih kompajlirati.

Enkripcija

Paralelno sa razvitkom obfuskatora, razvijala se i svest o enkripciji podataka, mada od kako su obfuskatori postali napredniji i dostigli svoj cilj, ona se ne koristi toliko koliko pre.

Pod enkripcijom podataka se podrazumeva simetrično kriptovanje poruka (stringova) kako se ne bi lako uočile pregledom sadržaja klasa sistema.

Neka, na primer, neki program ima hiljade klase koje su obfuskovane jednostavnim postupkom zamena imena nekim teškim za praćenje. Čini se da je ovakav vid zaštite dobar, jer ko će da sedi i da među hiljadama klasa i metoda sličnog nerazumnog imena traži onu koja je ključna? Međutim, čest je slučaj da program ispisuje poruku o pogrešnom unetom registracinom kodu ili o isteku trial vremena. Sve što treba uraditi je izvršiti pretragu po sadržaju fajlova klasa i locirati onaj fajl koji sadrži pomenuti tekst. Ovo "hvatanje" značajno ubrzava pronalaženje ključne klase, koja mora biti u bližoj okolini nađene (posmatrano sa strane izvršavanja).

Ako se, pak, poruka kriptuje čak i najprostijim specifičnim algoritmom, ovakvo "hvatanje" pretragom postaje praktično nemoguće.

Ostalo

Ostale metode su univerzalne i mogu se primeniti na bilo koji drugi sistem ili jezik. O njima ovaj put neće biti mnogo reči, jer se ne tiču samih postupaka vezanih za Javu.

Pokazni zadatak

Kao zadatak je uzet program koji je srednje obfuskovan i koji koristi finu enkripciju. Da stvari budu teže, program sam za sebe nije stand-alone aplikacija, već plugin koji se integriše u poznato IDE okruženje Idea, tako da izvršavanje plugina direktno zavisi od okruženja, a ne od korisnika. Reč je o UML pluginu "Tereza" (u imenu treba da stoji 's' umesto 'z', ali ono namerno nije i neće biti ispravno pisano! Isto važi i za sva imena koja asociraju na prozivod! :) Uz plugin se dobija trial licenca na mesec dana, predstavljena kao jako kriptovan fajl od 11 KB.

Plan

Svaka zaštita se praktično može prevazići u dva koraka. Prvi je "hvatanje" ili pronalaženje ključnih mesta u aplikaciji koja se tiču zaštite. Drugi deo je razumevanje logike nađenog mesta da bi se napravila odgovarajuća izmena. Vrlo prosto:)

U svakom sistem zaštite, prvi problem "hvatanja" ili pronalaženja slabih tačaka sistema je obično onaj teži. Tako je i ovde slučaj, jer među 1200 obfuskovanih klasa koje čine glavne Terezine jar-ove treba pronaći one ključne za zaštitu.

Ipak, ako se problemu pristupi na pravi način, za ovaku, komercijalno dobru zaštitu treba utrošiti samo 10-tak efektivnih minuta (ako se poseduje skromno iskustvo u disasembliranju)!

Hvatanje

Kada se otvori Terezin jar, primećuje se da su klase obfuskovane. Prva stvar u takvom slučaju je pokušaj da se "uhvati" ključno mesto pretragom za nekim specifičnim stringom. Ako se promeni sistemsko vreme tako da licenca više ne važi, ili se skroz ukloni licenca, dobija se odgovarajuća poruka. Međutim, pretraga po rečenici sadržanoj u poruci ili po njenim delovima ne daje rezultate. Znači, Tereza koristi enkripciju za stringove.

Ovo predstavlja praktično skoro najgori mogući slučaj: obfuskovane klase + enkripcija + plugin. Sva sreća pa je nešto ostalo u glavi sa prvog sastanka JavaSveta kada je Nemanja držao odlično predavanje o aspektima!:)

AspectJ kalauz

AspectJ se pokazao kao jako korisan alat za ovakve stvari jer dopušta elegantnu direktnu manipulaciju bajtkoda. Ali zašta bi on konkretno služio?

Kao i većina programa, plugin Tereza nudi period od 30 dana za slobodnu upotrebu pune verzije programa. Kada period istekne, pojavljuje se poruka. To znači da se na nekom mestu u pluginu proverava tekuće sistemsko vreme sa onim zapisanim u licencnom fajlu. Na tom mestu mora da postoji neko granjanje (magični if:) koje tok programa plugina preusmerava na jednu ili na drugu stranu, zavisno od trenutnog sistemskog vremena. Pošto je praktično nemoguće naći to mesto prostim gledanjem dekompajliranih obfuskovanih klasa, bilo bi lepo kada bi se to granjanje moglo naći na brži način.

Ideja je užasno prosta: kada bi bilo moguće pratiti tok izvršavanja programa plugina u jednom i u drugom slučaju, lako bi se uočilo mesto od kojeg tok programa u oba ova slučaja ide na različite strane. Kada bi samo postojao nešto kao trejsing u javi, koji bi ispisivao imena paketa/klasa/metoda koje se izvršavaju...

Jedno rešenje je korišćenje relativno novog profajling interfejsa Javine VM. Problem je što su mogućnosti takvog rešenja male u odnosu na AspectJ. AspectJ upravo nudi mogućnost da se jednostavno i brzo uradi trejsing! Bez dalje priče, evo sorsa TraceAspect aspekta:

import java.io.*;
import org.aspectj.lang.*;

public aspect TraceAspect {

   
pointcut traceScope() : execution(* com.betasoftware..*.*(..)) && !within(TraceAspect);

   
static private int count = 0;

   
static public void doTrace(String data) {
       
try {
           
RandomAccessFile raf = new RandomAccessFile("c:\\trace", "rw");
            raf.seek
(raf.length());
           
for (int i = 1; i < count; i++) {
               
raf.writeBytes("\t");
           
}
           
raf.writeBytes(data);
            raf.writeBytes
("\n");
            raf.close
();
       
} catch (IOException ioex) {
        }
    }

   
before() : traceScope() {
       
count++;
        String host = thisJoinPointStaticPart.toString
();
        doTrace
(host);
   
}

   
after() : traceScope() {
       
count--;
   
}
}

Ovaj aspekt hvata sva izvršavanja svih metoda iz Terezinog paketa i ispisuje ime u trejs log. Zavisno od dubine pozivanja, metode se tabuliraju na desno, radi lakše vizuelne identifikacije. Ovaj aspekt treba primenti na glavni Terezin jar, a zatim rezultujući izmenjeni jar staviti namesto originalnog. Ostaje još ubaciti AspectJ-ov runtime jar među ostale Terezine i "hvatanje" može da počne.

Mala digresija: AspectJ je ovde primenjen u najjednostavnjem obliku. Ono što je lepo je to što su njegove mogućnosti u analizi izvršnog koda dosta veće, tako da je moguće analizirati konkretnu metodu, vrednost prosleđenih parametara itd.

Rezultat

Prvi slučaj je kada postoji validan licence fajl koji još nije istekao. Startuje se Idea, proveri se da li je tu Tereza, i izađe se iz programa. Gornji apsekt je napravio trejs fajla, koga treba preimenovati i sačuvati.

Za drugi slučaj treba pomeriti sistemsko vreme tako da licenca više ne važi, i ponoviti prethodnu proceduru. Tereza javlja da je licenca istekla.

Upoređivanjem ova dva trejs fajla uočava se mesto odakle tok izvršavanja kreće svaki na svoju stranu. To ne mora nužno biti baš prva razlika. Ovde se može se desiti da Tereza u jednom slučaju pozove 1-2 metode koje u drugom ne zove, pre kritične tačke. Ipak, takve stvari se lako uočavaju, pošto se posle takvih neznačajnih razlika program istovetno nastavlja. Posle ključne tačke, tok programa je drastično različit! Evo tog mesta:

  • trejs kada je licenca validna:
  • ...
    execution(i com.betasoftware.b.c.k())
    	execution(Date com.betasoftware.a.a.h.j())
    	execution(int com.betasoftware.tereza.g.j())
    		execution(void com.betasoftware.tereza.g.n())
    	execution(int com.betasoftware.a.a.h.h())
    ...
  • trejs kada je licenca istekla:
  • ...
    execution(i com.betasoftware.b.c.k())
    	execution(Date com.betasoftware.a.a.h.j())
    execution(boolean com.betasoftware.tereza.plugin.ApplicationPlugin.a(ApplicationPlugin, boolean))
    execution(void com.betasoftware.tereza.plugin.ApplicationPlugin.c(ApplicationPlugin, File))
    	execution(void com.betasoftware.tereza.plugin.ApplicationPlugin.a(File))
    ...
    

    Očigledno da se u drugom slučaju izvršavanje metode com.betasoftware.b.c.k() prekida baš posle poziva metode com.betasoftware.a.a.h.j() koja vraća Date. Ovo je potencijalno mesto provere te ga vredi dekompajlirati.

    Dekompajliranje

    Kada se iz originalnog Terezinog jara izvuče klasa com.betasoftware.b.c, dekompajlira uz pomoć Jad-a, dobija se sledeći sors za metodu k():

    public i k() {
       
    i j;
    label0:
       
    {
           
    boolean flag1 = b.l;
           
    try {
               
    Date date = j();
               
    if (date != null && date.before(new Date())) {
                   
    j = i.b;
                   
    if (!flag1)
                       
    break label0;
               
    }
               
    boolean flag = g.j() > h() || g.j() == h() && g.k() > i();
               
    if (flag) {
                   
    Calendar calendar = Calendar.getInstance(Locale.UK);
                    calendar.setTime
    (c());
                    calendar.add
    (2, 12);
                    Date date1 = calendar.getTime
    ();
                    j = date1.before
    (g.f()) ? i.c : i.a;
                   
    if (!flag1)
                       
    break label0;
               
    }
               
    j = i.a;
           
    }
           
    catch (com.betasoftware.a.a.c c1) {
               
    j = i.c;
           
    }
        }
       
    return j;
    }

    Metod može da vrati vrednosti: i.a ili i.b ili i.c (šta god one bile). Posmatranjem koda i trejsa, može se doći do pretpostavke da ako ova metoda vraća vrednost i.a onda je sve u redu sa licencom, naročito kada se uoči linija:

    j = date1.before(g.f()) ? i.c : i.a;

    Dakle, radi testa umesto postojećeg celog tela metode treba staviti samo jedno return i.a;.

    Ponovno kompajliranje

    Sledeći problem je taj što klasa ne može da se ponovo kompajlira. Greška koja se prijavljuje se tiče konstruktora, jer je obavezno prvo pozvati super() pre bilo čega drugog. Naravno, Java VM nema to ograničenje, pa Terezin obfuskator to koristi.

    Ispravkom konstruktora se i ovaj, poslednji, problem rešava. Izmenjeni sors se uspešno kompajlira, nova klasa se stavlja namesto stare klase u originalnom Terezinom jaru i ...

    Rezultat

    ...sve radi kako treba i nakon isteka trial perioda:) Napomena: članak se bavi samo proverom vremena licence. Nije isključeno da postoje još neki slojevi zaštite, tako da ostaje isprobati plugin na neko vreme da se vidi da li je zaista sve kako treba.

    Kako sa naprednijim obfuskatorima?

    Slučaj je hteo da je Terezin obfuskator ne spada u one najnaprednije. Znači, lako je moglo da se desi da čak iako se nađe ključno mesto i ustanovi šta treba izmeniti, to jednostavno nije moguće pošto dekompajlirani sors ne može nikako da se ponovo kompajlira, zbog obfuskatorove jake manipulacije originalnog bajtkoda. Šta raditi u tom slučaju?

    Odgovor je (kao i uvek:) jednostavan: rešenje je raditi samo sa bajtkodom. Za to je, dakle, potrebno naći dobar dekompajler koji će od klase tj. class fajla generisati neki mnemonički tekstualni fajl sa bajtkod instrukcijama. Druga stvar koju je neophodno imati je dobar bajtkod kompajler koji može da kompajlira takav bajtkod sors u class fajl. Tu već prestaje direktno programiranje u Javi, a počinje programiranje sa instrukcijama Java VM. Instrukcija nema puno, i dobro su dokumentovane na mreži u okviru opisa Javine VM.

    Samo programiranje u bajtkodu nije nešto naročito popularno na mreži, ali se mogu naći par besplatnih projekata kojima se to može raditi. Najpoznatiji primer je par programa Jasper & Jasmin, mada su oni napravljeni više za potrebe knjige i ima slučajeva kada ne rade kako treba (na pr.: dužina linije sorsa prelazi neku vrednosti itd.). Autor članka je našao da KJC biblioteka odlično radi posao. Isto tako, pri radu sa bajtkodom ne treba odbaciti DJ - on i tada može da pruži dragocene informacije koje značajno mogu da uštede vreme i olakšaju izmenu klase.

    Iz skromnog iskustva autora, čini se da jednom kada se savladaju Java VM instrukcija, nije teško napraviti željenu izmenu u bilo kojoj klasi ma kako je ona obfuskovana. O ovome može biti više reči u nekom drugom članku, ukoliko bude postojalo interesovanje.

    Zaključak

    Java zbog svoje prirode postavlja nešto drugačiji problem zaštite koda u odnosu na dosadašnje jezike i programska okruženja, pogotovo što je u značajnom broju slučajeve to dosta lakše ostvariti u Javi nego na nekoj drugoj platformi. Iako postoje načini koji značajno otežavaju probijanje zaštite, nemoguće je biti 100% uspešan u tome. Nema zaštite koja se ne može prevazići:)