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