BAB 3 METODOLOGI RISET Penulis membuat beberapa test case dari yang sederhana (bukan program yang berguna) sampai menengah (program yang memiliki kegunaan bagi orang awam). Tiap test case akan diproses oleh masing-masing obfuscator, dan kemudian pada bytecode hasil obfuscation akan dicoba untuk dilakukan reverse engineering. Ada kalanya bytecode hasil obfuscation membutuhkan beberapa transformasi pembalik sederhana sebelum bisa di-decompile, untuk membuat pengukuran seragam maka transformasi pembalik selalu dilakukan. Setelah berhasil dihasilkan source code java yang bisa dicompile, metrik obfuscation diukur pada source code Java hasil dekompilasi. Compiler Source Code Obfuscator Transformasi pembalik / pra dekompilasi Distribusi ke user/client JVM bytecode (.class) Decompiler Peran pembuat software asli Measuring tool metrics Peran competitor atau hacker Gb. 3.1 Pelaksanaan pengukuran metrik obfuscation 3.1 Transformasi Pembalik Dalam usaha melakukan reverse engineering, diperlukan tool yang bisa melakukan transformasi untuk menghilangkan beberapa efek obfuscation yang mentargetkan representasi low-level bytecode (lawannya dari high-level program structure). Fakta 16 bahwa pembuatan transformasi ini cukup sederhana, menunjukkan obfuscation yang dimaksud tidaklah resilient [8] . Transformasi pembalik dibuat menggunakan pustaka manipulasi bytecode bernama ASM[20] . Pustaka ASM ini cukup cocok untuk melakukan transformasi dalam level low-level bytecode, dan merupakan memiliki waktu pemrosesan tercepat dibandingkan pustaka sejenisnya [13] . 3.1.1 Pemetaan ulang nama field dan nama kelas Java memiliki konvensi bahwa nama kelas diawali dengan huruf besar, nama package diawali huruf kecil, nama method dan field diawali huruf kecil (Java Language Specification[11] , butir 6.8.1-6.8.5). Obfuscator yang melakukan name obfuscation mengabaikan konvensi ini, dan karena penggunaan nama yang sama berulang-ulang, terjadi ambiguitas pada saat pembacaan source code. Pada kondisi 17 tertentu, tidaklah mungkin untuk memanggil sebuah static field yang dimiliki sebuah <<<<library class>>>> ClassAdapter RenamingPhase IdentifierEnumerationPhase visit visitField visitMethod visitInnerClass visitSource visit visitField visitMethod visitInnerClass visitSource transformMethodDesc transformFieldName transformClassName transformType newMethodName <<library class>> MethodAdapter <<create>> <<outer>> MethodAdp IdentifierData keywords addClassName addFieldName getClassBy visitTryCatchBlock visitFieldInsn visitMethodInsn visitMultiANewArrayInsn visitTypeInsn visitFrame * ClassNameRecord originalClassInternalName originalSuperInternalName newClassInternalName addFieldName getField putIn firstUpper * FieldNameRecord originalName originalDesc newName findNewUniqueName putInHashMap putInClassNameRecord Gb. 3.2 Class Diagram untuk transformasi pemetaan ulang nama kelas dan field kelas dari kelas lainnya, karena nama kelas tersebut sama dengan nama field yang dimiliki kelas lain. Source code hasil dekompilasi, meskipun benar, tetapi tidak bisa dicompile ulang karena alasan ini. Untuk menganggulangi masalah ini, maka nama kelas perlu diubah menjadi huruf besar. Dengan cara itu bisa dibedakan kelas B 18 dengan field b. Begitu pula dengan nama field, jika diawali dengan huruf besar maka akan diubah menjadi huruf kecil. Name obfuscation juga melakukan penggunaan ulang terhadap nama field yang tipenya berbeda. Source code tidak bisa dikompilasi ulang, karena selain adanya field bernama sama, tidak bisa dibedakan referensi ke field bertipe ‘boolean’ atau field bertipe ‘String’. Maka diperlukan pemetaan ulang nama field, sehingga semua field bernama unik. Implementasi transformasi ini dibagi menjadi dua fase, fase pertama ialah melakukan enumerasi atas identifier yang ada, dan dibuat pemetaan dari nama lama ke nama baru, dan fase kedua adalah fase di mana pemetaan tersebut diaplikasikan. Tiap fase dibuat menjadi sebuah kelas yang merupakan turunan dari kelas ClassAdapter, desain ini mengikuti pola desain Adapter (ref: Gamma [10] ). Class Diagram untuk implementasi ini terdapat pada Gambar 3.2, sedangkan listingnya terdapat di lampiran. Setelah melalui transformasi ini, obfuscated bytecode dapat didekompilasi dengan benar dan bisa dicompile ulang. 3.1.2 Menghilangkan Exception Handling palsu Java memiliki kemampuan exception handling terstruktur mempergunakan sintaks try-catch. Setelah dikompilasi, blok try-catch ini diterjemahkan menjadi tabel exception handling, yang tiap barisnya berisi 4 informasi: offset mulai, offset akhir, jenis exception, dan offset exception handler. Perekaan bentuk blok try-catch dari tabel expection handling cukup sulit dilakukan 3, dan beberapa teknik obfuscation memanfaatkan fakta ini untuk menghentikan decompiler, sehingga gagal menghasilkan source code yang bermakna. Transformasi balik yang dapat dilakukan ialah menghilangkan exception handler yang trivial. Sebuah exception handler yang trivial hanya berisi satu instruksi, yaitu instruksi ’athrow’, kerjanya hanya 3 Chapter 4, Exceptions, hal 56-82 dari tesis Miecznikowski [17] 19 melewatkan exception ke exception handler berikutnya. Exception handler seperti ini dimasukkan oleh Smokescreen ke dalam bytecode hasil obfuscation, dan dengan keberadaan handler seperti ini dapat merumitkan source code yang dihasilkan decompiler atau bahkan mencegah decompiler mengeluarkan source code. <<library class>> visitInsn(insn) [START] MethodAdapter insn = ATHROW? FakeThrowRemover h=findLabelInTable(curLabel) visitTryCatchBlock visitFieldInsn visitIincInsn visitInsn visitIntInsn visitJumpInsn visitLabel visitLdcInsn visitLookupSwitchInstruction visitMethodInsn visitMultiANewArrayInsn visitTableSwitchInsn visitTypeInsn visitVarInsn findLabelInTable h != NULL? handler.remove(h) return handler * TryCatchHandler start end handler type Gb. 3.3 Class Diagram dan Flowchart rutin penghilang exception handler palsu 20 3.1.3 Menghilangkan pemanggilan string encryption method Beberapa obfuscator melakukan string encryption menggunakan operasi bitwise XOR pada konstanta string yang berada di bytecode asli. Satu di antaranya menggunakan method yang sama untuk melakukan decryption, method ini dipanggil dari semua tempat di mana tadinya ada konstanta string. Dengan melakukan dekompilasi pada method string decryption, kemudian memasukkan method ini ke sebuah transformasi bytecode, maka bisa dilakukan decryption pada semua konstanta string terenkripsi yang ada dalam obfuscated bytecode. Hasil transformasi ini adalah bytecode yang berisi konstanta string yang asli, dan hilangnya pemanggilan rutin string decryption (konstanta string langsung dipanggil dari kode). Pada transformasi ini method untuk rutin string decryption diasumsikan bernama ”A_B_C_D”. Prinsip dasar transformasi pembalik ini ialah sebuah state machine yang memiliki dua state, dan input bagi mesin ini adalah event-event berupa instruksi JVM: • S0 ( state awal ) • Jika menerima LDC : simpan konstan, pindah ke SGotStrConst • Instruksi lainnya: langsung keluarkan bytecode instruksi yang bersangkutan ke output • SGotStrConst (state telah membaca konstanta string) • Jika menerima instruksi invokestatic (pemanggilan static method) dengan nama A_B_C_D: • Lakukan decrypt terhadap konstanta string yang tersimpan • Keluarkan bytecode instruksi LDC dengan operand konstanta string yg telah didecrypt • • Pindah ke state S0 Instruksi lainnya: Keluarkan bytecode LDC dengan konstanta yang tersimpan, pindah ke S0 Fakta bahwa sebuah state machine sederhana yang diimplementasikan dengan dua object state (mengikuti pola desain State[10] ) dapat melakukan transformasi pembalik 21 ini menunjukkan transformasi string encryption ini tidak resilient. Listing implementasi state machine ini bisa dilihat pada lampiran. State visitCode visitFieldInsn visitIincInsn visitInsn visiIntInsn visitLdcInsn visitLookupSwitchInsn visitMethodInsn visitMultiANewArrayInsn visitTableSwitchInsn visitTypeInsn visitVarInsn <<library class>> MethodAdapter <<delegate>>state MethAdapter constantMustBeLoaded nextState <<outer>> setState S0 <<outer>> SGotStrConst stateList[0] visitLdcInsn <<create>> stateList[1] StringDecrypter visitFieldInsn visitIincInsn visitInsn visitIntInsn visitJumpInsn visitLdcInsn visitLookupSwitchInsn visitMethodInsn visitMultiANewArrayInsn visitTableSwitchInsn <<library class>> ClassAdapter Gb. 3.4 Class Diagram transformasi penghilang enkripsi String 3.1.4 Menghilangkan predikat transparan Injeksi opaque predicates ke dalam program akan meningkatkan kompleksitas, namun jika predikat yang digunakan bisa dianalisa secara statis maka yang terjadi peningkatan semu, di mana predikat tersebut dapat dihapus secara otomatis menggunakan transformasi lain. Pada hasil obfuscation SmokeScreen ditemukan predikat if (null==null) dan if (null!=null), kedua predikat ini sudah jelas hasilnya, 22 sehingga tidak lagi disebut opaque predicates. Dengan melakukan pengenalan pola sederhana dapat dibuat transformasi untuk menghapus kedua predikat ini dan menggantikannya dengan goto jika diperlukan. Implementasi berbentuk sebuah kelas dengan state yang disimpan pada variabel boolean ’mustLoadConstNull’. Ditemukannya instruksi aconst_null akan membuat nilai variabel ini menjadi true, jika ditemukan instruksi ifnull maka akan diganti dengan goto, jika ditemukan instruksi ifnotnull maka kedua instruksi (aconst_null dan ifnotnull) akan dihapus. Di luar instruksi yang disebut, kelas visitor akan mengeluarkan aconst_null jika mustLoadConstNull berisi true (dan membuatnya jadi false setelahnya), dan juga mengeluarkan output sama dengan instruksi yang masuk. 3.2 Reverse Engineering Reverse Engineering dilakukan dengan bantuan tool-tool berikut : 1. JAD, Fast JAva Decompiler, tool ini dapat melakukan dekompilasi bytecode yang normal menjadi source code Java. Pada kondisi yang tidak biasa, JAD akan menghasilkan source code Java yang tidak valid, entah karena nama method yang dilakukan aggresive overloading, atau karena aliran loop tidak lagi dikenali sehingga JAD terpaksa menambahkan instruksi goto (yang sebenarnya tidak ada di Java). JAD memiliki kelemahan lain yaitu tidak bisa menangani instruksi JSR yang terjadi berkali-kali. 2. Modul Dava pada Soot. Soot sendiri merupakan framework untuk melakukan bytecode optimization[23] . Dava [17] merupakan decompiler yang mengambil masukan dari intermediate representation(IR) bernama Grimp, yang merupakan satu dari 3 macam IR yang didukung Soot. 3. Modul Grimp pada Soot. Ada kalanya Dava gagal mengeluarkan hasil, maka modul Grimp dapat menghasilkan source yang mendekati Java meskipun tidak melewati tahap loop structuring (bentuk loop menggunakan goto). 23 4. Modul CFG dump pada Soot yang telah diadaptasi untuk menghasilkan file GML. Modul ini dapat menampilkan code flow graph pada beberapa fase dalam pemrosesan bytecode. File GML dapat dibuka oleh yEd Graph Editor 5. yEd Graph Editor, freeware untuk menampilkan graph dan melakukan layouting. 6. Eclipse IDE, IDE ini memiliki refactoring tool yang sangat berguna, di samping itu ia menjadi host bagi beberapa tool lain. 3.3 Metrik Obfuscation Kualitas transformasi obfuscation menurut Collberg dan Thomborson[6] dinilai dari tiga faktor, yaitu potensi, resiliensi, dan cost. Perbandingan yang akan dilakukan dalam tesis ini menitikberatkan pada potensi, yaitu pengukuran seberapa jauh kompleksitas program bisa ditingkatkan oleh transformasi yang dilakukan. Faktor resiliensi dimasukkan sebagai filtering factor dalam mengukur kompleksitas, yaitu bahwa transformasi dengan resiliensi yang rendah akan dihilangkan dengan transformasi balik yang ada, dan kompleksitas yang diukur adalah kompleksitas program hasil transformasi balik ini. Pengukuran kompleksitas software object-oriented yang telah terobfuscate dilakukan menggunakan metrik yang diajukan oleh Naeem, Batchelder, dan Hendren dalam "Metrics for Measuring the Effectiveness of Decompilers and Obfuscators "[19] : 1. Ukuran Program, dalam banyaknya AST (Abstract Syntax Tree) node yang dibutuhkan dalam merepresentasikan program. Ukuran program dapat digunakan untuk membandingkan dua representasi berbeda dari satu program yang sama. Obfuscator menambahkan kode yang tidak berguna dalam program dalam rangka membuat program tersebut menjadi tambah rumit. 2. Number of Java Constructs, yang dihitung ialah banyaknya statement if-else, break/continue, blok terlabel dan variabel lokal. Break/continue dikelompokkan sebagai abrupt control flow. Abrupt control flow merupakan indikasi kompleksitas dalam program, keberadaan sebuah abrupt control flow 24 mengharuskan pembaca program melihat dalam dua scope sekaligus, di dalam loop dan di luar loop. Makin banyak instruksi abrupt control flow dalam satu program, makin banyak bagian dari program yang tidak berjalan urut. 3. Conditional Complexity, adalah ukuran kompleksitas total pada ekspresiekspresi boolean di dalam program. Operator relasional seperti > dan == diberi bobot 0.5 karena mudah dipahami, operator logika dinilai sebagai 1 karena membutuhkan penggabungan dua hasil operasi yang lain, variabel dinilai 1 karena tiap variabel menambahkan kompleksitas pada ekspresi. Makin banyak suku dan operator di dalam sebuah if condition atau while condition membuat makin sulitnya kondisional atau loop tersebut dipahami orang. Average Conditional Complexity adalah rata-rata nilai kompleksitas setiap ekspresi boolean dihitung dengan cara yang sama. 4. Identifier Complexity, adalah ukuran kompleksitas dari semua identifier yang ada di dalam program. Diberikan bobot yang berbeda untuk Method (4), Class (3), Field(2), Formal (1.5), Local (1), dan untuk tiap identifier dibuat fungsi kompleksitas yang merupakan jumlah kompleksitas karakter (rasio karakter kompleks dan non-kompleks/alfanumerik) dan kompleksitas kata (rasio kata yang bisa ditemukan dalam kamus terhadap kata yang tidak ada di dalam kamus). Ukuran kompleksitas ini pada tiap identifier menunjukkan usaha yang diperlukan pembaca untuk mengenali dan mengingat masingmasing identifier. Dalam melakukan perbandingan, faktor ke 4 (Identifier Complexity) ini akan diabaikan kecuali pada dua buah obfuscator generasi pertama4. Dalam menghadapi source code yang telah mengalami name obfuscation sehingga Identifier Complexity meningkat, pelaku reverse engineer dapat memanfaatkan refactoring tool untuk merubah nama-nama terobfuscate menjadi nama yang mudah dibaca. Sedikit demi sedikit pelaku reverse engineer mendeduksi nama method, kemudian field, kemudian nama kelas, tiap nama yang diberikan membantu untuk deduksi nama 4 Obfuscator generasi pertama tidak mengakibatkan perubahan pada metrik apapun kecuali Identifier Complexity 25 berikutnya. Cara ini membutuhkan usaha berbanding lurus dengan banyaknya identifier di dalam program, dan tidak terpengaruh oleh ukuran identifier complexity. Argumen lain untuk mendukung pengabaian ukuran identifier complexity adalah bahwa Mike Melanson[16] membuat sebuah alat sederhana yang dinamakan olehnya automated deobfuscation tool, yang memetakan nama-nama method, field dan class menjadi nama yang berasal dari kamus. Meskipun hasilnya adalah nama yang acak tetapi cara ini bisa merubah metrik identifier complexity. Maka disimpulkan identifier complexity bukan faktor yang tepat untuk dijadikan bahan perbandingan. 3.4 Prosedur perbandingan Pelaksanaan perbandingan adalah sebagai berikut : 1. Source code dicompile, dibuat JAR (Java Archive) jika diperlukan 2. Dihitung AST metrics menggunakan Soot Framework (parameter pemrosesan: ast-metrics –src-prec java) 3. Obfuscator dijalankan, menghasilkan kumpulan file .class yang terobfuscate 4. Dilakukan reverse engineering pada hasil obfuscator menggunakan : - JAD - Dava - Disassembler DeJasmin - Grimp dumping utility milik Soot Toolkit Penelitian lebih lanjut pada flow graph dilakukan dengan menjalankan CFG dump utility milik Soot Framework pada kelas-kelas yang menjadi perhatian 5. Source hasil reverse engineering diubah sehingga bisa dicompile ulang 6. Jika tidak bisa didapat source yang bisa dicompile, dilakukan transformasi khusus sehingga langkah no 4 menghasilkan output yang bisa dicompile lagi 7. Dihitung AST metrics menggunakan Soot Framework (parameter pemrosesan: ast-metrics –src-prec java) 26 3.5 Pekerjaan lain yang terkait dengan pengukuran hasil obfuscation Collberg et al[7] melakukan pengukuran kompleksitas menggunakan metrik panjang program dari Halstead yang mengukur banyaknya operator dan operand, metrik cyclomatic complexity McCabe yang mengukur banyaknya predikat dalam program, metrik nesting complexity Harrison yang mengukur nesting level dari kondisional, metrik data flow complexity Oviedo yang mengukur referensi variabel antar-blok, metrik data structure complexity yang mengukur kompleksitas struktur data, dan metrik Object Oriented Chidamber. Ukuran Halstead serupa dengan konsep AST node, konsep nesting complexity serupa dengan metrik conditional complexity, metrik abrupt control flow akan berbanding lurus dengan banyaknya predikat dalam program, dan banyaknya referensi variabel serupa dengan banyaknya variabel lokal yang diukur dalam metrik Java Constructs. Perbandingan antara obfuscator-obfuscator pernah dilakukan Hongying Lai pada tahun 20015, tanpa analisa terhadap CFG, mempergunakan ukuran-ukuran heuristik yang jauh dari kuantitatif. Laporan Hongying Lai ini membandingkan 13 macam obfuscator, hasilnya menunjukkan Zelix KlassMaster sebagai obfuscator terbaik saat itu, dan tampaknya fakta ini dimanfaatkan pembuat ZKM dengan membuat link ke report ini sebagai iklan di google. Perbandingan yang lebih terstruktur dilakukan oleh Naeem, Batchelder, dan Hendren dalam Metrics for Measuring Effectiveness of Decompilers and Obfuscators [19] yang metriknya diadopsi dalam riset ini. Meskipun lebih terstruktur, obfuscator yang dibandingkan hanyalah versi awal dari JBCO[1] dan Zelix KlassMaster. 5 Hongying Lai, A comparative survey of Java obfuscators available on the Internet, Project Report, 2001 27