Code Obfuscation adalah salah satu bentuk proteksi agar kode sulit dibongkar orang lain. Inti dari obfuscation adalah menyamarkan/membuat kode sulit dibaca. Obfuscation bisa dilakukan manual atau dengan tool yang disebut “obfuscator”. Sementara itu dari sisi reverse engineering, proses mengembalikan dari bentuk samar ini disebut “deobfuscation”.
Meskipun kata “samar” sepertinya cukup berpadanan dengan “obfuscated” saya akan tetap menggunakan istilah inggrisnya di posting ini.
Di posting ini saya hanya ingin memberikan beberapa ilustrasi nyata seperti apa obfuscation ini. Untuk para programmer, ini bisa membantu memproteksi program, dan untuk para reverse engineer bisa berusaha memahami bagaimana obfuscation dilakukan.
Obfuscation
Jika kita punya dua fungsi sederhana seperti ini:
int search(int element, int *data, int count) { for (int i =0; i < count; i++) { if (data[i] == count) return i; } return -1; } int replace_first(int element, int replacement, int *data, int count) { int pos = search(element, data, count); if (pos !=-1) { data[pos] = replacement; return 1; } return 0; }
Bentuk obfuscation pertama adalah dengan mengganti nama menjadi nama lain. Ini bisa jadi nama yang sangat singkat misalnya “a”. Pada kode yang dikompilasi menjadi bahasa mesin, nama ini bahkan tidak ada lagi karena tidak diperlukan. Pada bahasa lain yang tidak dicompile jadi bahasa mesin (seperti Java dan Python), nama ini akan tetap ada.
Tanpa membaca kodenya, sudah sulit menebak apa fungsinya ini
int a(int a1, int *a2, int a3); int b(int a1, int a2, int *a2, int a3)
Bahasa tertentu (seperti Java/C++) mendukung overloading. Beberapa fungsi bisa memiliki nama sama asalkan parameternya berbeda:
int a(int a1, int *a2, int a3); int a(int a1, int a2, int *a2, int a3)
Cara lain adalah dengan mengubah namanya menjadi menyesatkan, misalnya kedua nama seperti di atas diubah menjadi seperti ini (perhatikan bahwa sengaja count-nya dipindah ke depan untuk menyesatkan pembacanya).
int clear(int count, int *data, int position); int remove_range(int start, int end, int *data, int element);
Selain penggunaan nama yang menyesatkan, cara lain adalah dengan mengubah control flow program. Contoh sederhananya seperti ini, kita memanggil beberapa fungsi berurutan:
verify_license(); initialize_printer(); initialize_camera(); connect_to_database();
Jika sebuah bahasa mendukung goto
maka bentuk obfuscationnya bisa seperti ini:
verify_license(); goto label2; label1: initialize_camera(); connect_to_database(); goto label4; label2: initialize_printer(); goto label1; label4:
Jika suatu bahasa tidak mendukung goto
urutan operasi bisa disamarkan dengan loop dan switch:
int order[] = {3,1,2,0}; for (int i =0; i < 4; i++) { switch (order[i]) { case 0: connect_to_database(); break; case 1: initialize_printer(); break; case 2: initialize_camera(); break; case 3: verify_license(); break; } }
Obfuscation kecil seperti contoh di atas masih mudah dimengerti untuk kode yang pendek. Untuk kode yang besar, jumlah “case”-nya bisa puluhan, dan masing-masing nama fungsinya tidak jelas.
Beberapa obfuscation bisa mudah dilihat jika data atau string terlihat jelas, misalnya jika ada kode seperti ini, meskipun kita tidak tahu apa itu kelas x, tapi terlihat bahwa fungsi saat ini berhubungan dengan enkripsi AES:
throw new x("AES decrypt error");
Jadi bentuk obfuscation berikutnya adalah: string encryption supaya tidak mudah mencari string di dalam program dan mempersulit pemahaman. Tentunya string ini harus bisa didekrip ketika program berjalan, hanya mempersulit pemahaman program.
throw new x(decrypt(ConstString.ERR1));
Teknik-teknik lain juga bisa ada banyak, saya tidak akan memberikan contoh kode satu persatu. Beberapa yang bisa dilakukan misalnya
- Obfuscation di level bahasa mesin/bytecode
- Menggunakan exception untuk control flow obfuscation
- Menyisipkan kode sampah, misalnya mengurutkan elemen, lalu mencari elemen tengah, menjumlahkan semua elemen, lalu hasilnya tidak dipakai
- Menggunakan thread untuk memecah algoritma menjadi beberapa bagian sehingga lebih sulit di mengerti
- Mengubah logika program, misalnya menambahkan angka 2312312 di awal, lalu di akhir dikurangi lagi 2312312
- Menggunakan enkripsi dan/atau kompresi untuk sebagian kode program yang diload secara dinamis
- Memakai custom virtual machine
Perlu diperhatikan bahwa obfuscation tertentu bisa membuat program jadi lebih lambat. Sekedar mengganti nama method tidak akan membuat lebih lambat (bahkan biasanya malah membuat sedikit lebih cepat), tapi mengganti control flow biasanya membuat kode menjadi lebih lambat. Obfuscation juga membuat debugging menjadi lebih sulit (karena memang itu tujuannya). Jadi sebaiknya obfuscation hanya dilakukan di akhir development.
Saya tidak bisa menyarankan tool obfuscator tertentu karena memang jarang melakukan obfuscation pada kode saya, silakan search “obfuscator” dan nama bahasa yang Anda pakai di search engine. Tool obfuscator yang banyak saya temui adalah Proguard (gratis) yang dipakai untuk kode Java/Android.
Di dunia Javascript ada istilah minifier, yaitu tool untuk membuat kode Javascript mejadi lebih kecil. Ini dilakukan dengan mengganti nama variabel, menghapus spasi, komentar dsb. Secara umum ini juga berfungsi sebagai obfuscator sederhana.
Deobfuscation
Secara umum tidak ada cara generik yang membuat obfuscated code bisa dibaca dengan mudah. Tapi ada beberapa tool yang bisa membantu proses deobfuscation spesifik untuk bahasa/teknologi tertentu.
Contohnya jika bertemu dengan kode JavaScript yang sudah minified, maka kita bisa memakai JavaScript beautifier. Ini tidak bisa mengembalikan nama variabel, hanya membuat teks yang sulit dibaca menjadi lebih mudah dibaca (tool beautifier ini sekarang sudah built in di Developer Tools-nya Google Chrome).
Contoh lain: ada yang membuat tool untuk mendekrip String untuk APK yang ditulis dalam Java. Tapi tool ini tidak selalu jalan untuk semua protektor. Setiap kali ada yang membuat tool untuk otomasi sesuatu, pembuat obfuscator menambahkan satu hal kecil sehingga toolnya harus diupdate (atau bahkan ditulis ulang).
Cara yang pasti berhasil adalah kombinasi manual dengan debugger dan sedikit programming. Hal utama adalah memahami obfuscation apa yang dilakukan: apakah stringnya dienkripsi, apakah nama methodnya diubah, apakah control flow-nya berubah, dsb.
Jika sekedar nama methodnya diubah, maka kita harus membaca kodenya dan melakukan renaming untuk mendapatkan nama yang benar. Ini bisa dilakukan berdasarkan beberapa hal, misalnya:
- string yang muncul (misalnya “AES Error” mengindikasikan AES)
- konstanta yang dipakai (misalnya 0x9E3779B9 mengindikasikan penggunaan enkripsi XTEA)
- fungsi yang memanggil. Misalnya jika suatu fungsi dipanggil dari fungsi AES, maka kemungkinan itu hanyalah subrutin AES
- fungsi yang dipanggil, misalnya jika aplikasi C memanggil “system” maka kemungkinan ini fungsi menjalankan command line lain
- algoritma yang dipakai. Beberapa algoritma sederhana (search, sort, traversal) mudah diidentifikasi
Untuk mendapatkan gambaran sebuah program seperti menyusun sebuah puzzle. Kita bisa mulai dari bagian-bagian yang jelas. Untuk puzzle bagian yang jelas adalah pinggiran puzzle, dan bagian-bagian yang unik. Untuk program kita bisa mulai dari titik awal program (main di sistem POSIX, Activity di Android, dsb), dan titik di mana program memanggil fungsi eksternal.
Jika string dienkripsi, kita bisa membuat breakpoint di method dekrip-nya agar bisa mendapatkan hasil string-nya. Jika control flow-nya obfuscated, kita bisa membuat beberapa breakpoint untuk berhenti di tiap titik, jadi kita bisa mengetahui urutan yang sebenarnya. Selain menggunakan breakpoint dan debugger, kita juga bisa menggunakan Frida atau tool sejenis.
Penutup
Walaupun obfuscator bisa membantu melindungi program, tapi faktor keamanan lain harus tetap diperhatikan. Attacker yang gigih akan bisa membuka segala jenis obfuscation, hanya akan memperlama saja. Di kasus tertentu kombinasi obfuscator dan pentester yang kurang berpengalaman justru bisa membuat aplikasi kurang aman karena testing aplikasi kurang optimal.
Contohnya begini: aplikasi memakai enkripsi, lalu kodenya diobfuscate. Aplikasi ini ditest oleh pentester yang tidak bisa melakukan reverse engineering terhadap kode tersebut, dan pentester menganggap aplikasi tersebut aman karena dia tidak dapat melakukan tampering terhadap nilai yang dikirimkan. Ketika aplikasi dirilis dan dibongkar oleh seorang reverse engineer yang berpengalaman, dia dapat mengubah nilai yang dikirimkan dan ternyata tidak dicek di sisi server (aplikasinya jebol).
Jadi sebaiknya: pentester diberi akses pada kode yang belum obfuscated dan kode final yang sudah. Ini akan lebih optimal karena kerja pentester lebih cepat, tidak perlu membongkar obfuscation dan lebih aman (seluruh fungsi bisa ditest dengan baik). Ini dengan asumsi bahwa pentester memiliki keahlian untuk membaca kode dengan baik.
Semoga artikel singkat ini bisa memberikan gambaran mengenai apa itu code obfuscation dan berbagai batasannya.