Bytecode (Smali) Patching: Modifikasi Source Code Aplikasi Android

Bayangkan skenario ini. Aplikasi gold exchange favorit Anda tersedia di marketplace pihak ketiga (atau bahkan Google Play Store), terlihat sah, berjalan normal tanpa ada masalah, namun diam-diam mengeksekusi kode berbahaya tanpa Anda sadari. Tanpa adanya file integrity check, hal ini bisa terjadi melalui teknik yang disebut bytecode patching.
Teknik ini telah menjadi salah satu metode paling umum yang digunakan oleh malicious actors untuk mendistribusikan aplikasi berbahaya yang terlihat “legit”. Biasanya, aplikasi-aplikasi ini dipasarkan dengan label seperti “MODDED” atau “Premium Unlocked”, yang dirancang untuk menarik korban agar menginstalnya ke perangkat mereka. Korban mungkin mengira aplikasi dengan fitur “modded” tersebut sah, padahal sebenarnya data pribadi mereka sedang dikirim ke remote endpoint yang dikendalikan oleh penyerang, sehingga kredensial maupun informasi akun mereka bocor dan bisa dimanfaatkan untuk aksi berbahaya yang lebih serius.
Namun, perlu dicatat bahwa teknik ini tidak hanya digunakan untuk tujuan berbahaya (dan jelas Anda tidak boleh menggunakannya untuk itu!). Dalam konteks lain, bytecode patching juga bisa dipakai untuk melewati mekanisme proteksi tertentu atau mengubah logika aplikasi. Pada artikel ini, kita akan membahas beberapa skenario penggunaan teknik ini: mulai dari mendistribusikan aplikasi Trojan, melewati proteksi aplikasi, hingga bagaimana teknik ini membantu saya menyelesaikan masalah saat melakukan proyek penetration testing.
Implementasi Bytecode (Smali) Patching
1. Root Detection Bypass
Mari kita mulai dengan salah satu skenario paling umum di mana teknik ini bisa sangat berguna ketika melakukan assessment pada sebuah aplikasi Android: bypass root detection! Saya akan mencontohkan dari salah satu aplikasi yang baru-baru ini saya uji. Setelah aplikasi di-install dan dijalankan pada perangkat yang sudah di-root, saya langsung mendapat alert dialog yang memberi tahu bahwa perangkat terdeteksi rooted, sehingga saya tidak bisa melanjutkan akses ke aplikasi tersebut.
Hal pertama yang terlintas di pikiran saya adalah menggunakan teknik runtime process injection dengan memanfaatkan Frida script yang tersedia secara publik (https://codeshare.frida.re/) untuk mencoba melewati implementasi root detection tersebut. Teknik ini juga bisa dilakukan secara manual dengan menjalankan Frida script buatan sendiri, hooking ke fungsi-fungsi yang kemungkinan besar bertanggung jawab mendeteksi apakah perangkat rooted atau tidak, lalu memanipulasi nilai parameter atau nilai balik dari fungsi tersebut saat aplikasi berjalan. (Mungkin saya akan mendemonstrasikan cara manual ini di artikel berikutnya, jadi stay tuned!).
Namun, saya pikir teknik ini terlalu ribet dan membuang banyak waktu, apalagi saat itu saya sering berpindah-pindah antara aplikasi dan perangkat. Saya kurang suka untuk menjalankan ulang Frida script yang sama setiap kali berganti perangkat/aplikasi. Karena alasan itu, saya memutuskan untuk mengambil pendekatan lain: menganalisis decompiled source code untuk mencari implementasi root detection dan menguji apakah ada bagian yang bisa saya ubah agar lolos dari deteksi.
Saya mendekompilasi file APK target dengan JADX (https://github.com/skylot/jadx) dan mencari class serta fungsi yang kemungkinan bertanggung jawab terhadap root detection. Tidak butuh waktu lama sampai saya menemukan class com.scottyab.rootbeer.RootBeer yang langsung menarik perhatian. RootBeer (https://github.com/scottyab/rootbeer) adalah sebuah root checker library yang dikembangkan oleh Scott Alexander-Brown dan Matthew Rollings. Saya kemudian menganalisis metode-metode yang ada di dalam kelas tersebut, dan menemukan beberapa metode yang tampaknya digunakan untuk mendeteksi root, yaitu isRooted() dan isRootedWithBusyBoxCheck(), yang keduanya mengembalikan nilai boolean.
Setelah menemukan metode yang digunakan untuk pengecekan root, saya menganalisis smali bytecode dari kedua metode tersebut dan mencoba mengubah nilai baliknya agar selalu mengembalikan “false”. Apa itu smali bytecode? Singkatnya, smali adalah interpretasi dari Dalvik bytecode dalam format yang lebih mudah dibaca manusia, dibuat agar analisis Dalvik bytecode jadi lebih sederhana.
Untuk melakukannya, langkah pertama adalah mendapatkan smali bytecode agar bisa diutak-atik. Saya menggunakan APKTool (https://github.com/iBotPeaches/Apktool) untuk mendekompilasi file APK aplikasi tersebut sehingga mendapatkan decompiled source code dalam format smali. Setelah itu saya masuk ke kelas “RootBeer”, lalu menambahkan instruksi berikut di awal metode isRooted() dan isRootedWithBusyBoxCheck().
Apa fungsi instruksi tersebut? Pada dasarnya, kita menetapkan sebuah konstanta 4-bit (const/4) ke variabel lokal v0 dengan nilai 0, lalu mengembalikan nilai dari variabel v0. Jika Anda familiar dengan pemrograman (dan saya yakin sebagian besar pembaca di sini paham), Anda tahu bahwa nilai boolean “false” direpresentasikan dengan angka 0. Jadi, singkatnya, apa yang kita lakukan dengan instruksi tersebut adalah mengembalikan nilai boolean false untuk kedua metode tadi.
Jika kita lihat bagaimana patch yang kita implementasikan diinterpretasikan oleh decompiler, kedua metode tersebut langsung mengembalikan “false” boolean value.
Oke, semuanya berjalan sesuai harapan. Langkah selanjutnya adalah membangun ulang aplikasi dengan patch yang sudah kita terapkan menggunakan APKTool, menyelaraskan semua file yang tidak dikompresi di dalam APK dengan zipalign, lalu menandatangani aplikasi dengan debug key menggunakan apksigner. Setelah aplikasi yang sudah ditandatangani di-install dan dijalankan di perangkat yang sudah rooted, hasilnya: alert dialog tidak lagi muncul, dan saya bisa mengakses semua fitur aplikasi tanpa hambatan. Mantap! Tidak perlu lagi repot dengan Frida instances!
2. Trojan Application Distribution
Sejauh ini saya sudah menunjukkan bagaimana teknik ini bisa digunakan untuk melewati protection mechanisms seperti root checking. Sekarang, mari kita coba sesuatu yang lebih menarik. Saya akan mengambil sebuah production-build exchange application dari salah satu aplikasi yang pernah saya lakukan assessment.
Kalau Anda belum terlalu familiar dengan Android application components, sebuah activity bisa ditetapkan sebagai launcher activity dengan menambahkan elemen <intent-filter> yang berisi dua komponen berikut:
- <action android:name="android.intent.action.MAIN" />, yang menunjukkan bahwa activity tersebut adalah main entry point dari aplikasi,
- <category android:name="android.intent.category.LAUNCHER" />, yang memungkinkan activity tersebut dijalankan langsung dari system app launcher.
Agar malicious patch code bisa langsung dieksekusi begitu aplikasi dijalankan, maka bytecode dari activity ini menjadi target dari malicious patching yang saya lakukan.
Analisis lebih lanjut pada aplikasi menunjukkan adanya layout file berikut, yang menambahkan elemen WebView layout untuk menampilkan sebuah halaman web sebagai bagian dari client application.
Sebuah ide tiba-tiba muncul: bagaimana jika kita memanfaatkan WebView layout sebagai content view dari launcher activity, lalu mengonfigurasi WebView secara tidak aman, dan menampilkan malicious web page dari direktori asset aplikasi untuk memfasilitasi pencurian file internal yang berada di sandbox? Menarik sekali.. Saya pun memutuskan untuk mencobanya.
Jika Anda familiar dengan Android activity lifecycle, smali bytecode patch dilakukan pada onCreate() lifecycle callback, seperti ditunjukkan pada gambar di bawah. Saya tidak akan menjelaskan setiap instruksi bytecode karena itu akan membuat blog ini super panjang :). Code dari malicious web page juga tidak akan ditampilkan di tulisan ini karena bukan fokus utama, tapi mungkin saya bisa membuat artikel terpisah kalau Anda tertarik.
Setelah itu, kita lihat bagaimana patch yang sudah kita implementasikan diinterpretasikan oleh decompiler. Dari hasilnya terlihat bahwa launcher activity mengatur activity content dari WebView layout resource, yang memuat halaman web dari asset aplikasi menggunakan skema file://.
Langkah berikutnya adalah melakukan re-build, re-sign, dan re-distribute aplikasi yang sudah di-patch tadi ke perangkat Android kita. Setelah aplikasi dijalankan, dari gambar di bawah terlihat bahwa malicious web page berhasil dimuat, dan kita berhasil melakukan pencurian file internal berupa file SQLite database yang berada di dalam sandbox, lalu mendump isinya ke sebuah remote endpoint. Keren!
Bonus: Troubleshooting ProxyDroid dan Kompatibilitas dengan Versi Terbaru Magisk
Sedikit section bonus di sini. Pada sebuah project baru-baru ini, saya melakukan update Magisk ke versi terbaru (v29.0), hanya sekadar untuk mencobanya.
Namun, saya segera menyadari bahwa ProxyDroid tiba-tiba mengalami masalah dalam mendeteksi apakah perangkat saya sudah rooted, meskipun permintaan root access berjalan dengan sangat baik pada versi Magisk sebelumnya. Tentu saja saya memeriksa apakah Magisk sudah terinstal dengan benar, dan memang semua aplikasi lain yang membutuhkan root access bekerja dengan sempurna—kecuali ProxyDroid.
“Mengapa repot-repot? Kan bisa langsung atur proxy di pengaturan Wi-Fi,” mungkin Anda bertanya. Nah, pada aplikasi yang proxy-unaware seperti aplikasi berbasis Flutter, mengatur proxy di pengaturan Wi-Fi tidak akan berpengaruh. Selain itu, mempercayai sertifikat di pengaturan sistem juga tidak akan memvalidasi sertifikat HTTPS karena aplikasi tersebut menggunakan certificate store-nya sendiri.
Ide untuk memperbaiki masalah proxy ini sebenarnya adalah dengan menggunakan iptables untuk melakukan redirect trafik, yang memang menjadi mekanisme di balik ProxyDroid. Namun, bukankah akan merepotkan jika kita harus melakukan manual traffic redirection setiap kali ingin mengonfigurasi proxy? Saya pun berpikir demikian :). Jadi, saya memutuskan untuk meluangkan waktu untuk melakukan troubleshooting masalah ini.
Langkah pertama, seperti biasa: decompiling ProxyDroid. Static analysis kemudian dilakukan, dan tidak lama kemudian saya menemukan sebuah array of strings global variable bernama DEFAULT_ROOTS, yang berisi path menuju binary “su”, terletak pada kelas org.proxydroid.utils.Utils.
Setelah itu, saya melacak penggunaan variabel global DEFAULT_ROOTS, dan ternyata variabel ini digunakan oleh metode isRoot() dalam kelas yang sama untuk memeriksa apakah perangkat sudah rooted. Cukup jelas dan sederhana.
Oke, static analysis beres. Berikutnya, saya mengakses Android device’s shell dan mencari binary “su” dengan perintah which. Output dari perintah tersebut menunjukkan path:
/apex/com.android.runtime/bin/su yang ternyata tidak ada dalam array DEFAULT_ROOTS yang disebutkan sebelumnya.
Berdasarkan informasi yang sudah kita dapatkan dari analisis tersebut, kita kemudian masuk ke fase patching. Saya menambahkan panjang array DEFAULT_ROOTS sebanyak satu elemen, lalu menambahkan nilai path /apex/com.android.runtime/bin/su ke bagian akhir array. Patches yang sudah diimplementasikan ditunjukkan pada gambar di bawah.
Jika kita lihat bagaimana patch yang sudah diterapkan diinterpretasikan oleh decompiler, terlihat bahwa path ke su binary kita sudah ditambahkan ke array DEFAULT_ROOTS. Mantap!
Aplikasi ProxyDroid yang sudah dipatch kemudian dibangun ulang (rebuild) dan diinstal kembali pada perangkat Android yang rooted. Akhirnya, ProxyDroid berhasil meminta root access di perangkat kita. Masalah selesai! Tidak ada lagi mimpi buruk dengan aturan iptables!
Kalau Anda tertarik, rencananya saya akan membagikan ProxyDroid hasil patch di tautan berikut: … Yah, sayangnya Google Drive tidak mengizinkan saya membagikan file ini. Jadi, saya sangat menyarankan Anda untuk langsung praktik sendiri, coba patch, dan rasakan pengalamannya!
Kesimpulan
Oke, saya rasa cukup banyak “ngobrol” di postingan kali ini. Kita sudah membahas dan mendemonstrasikan bagaimana bytecode (smali) patching bisa dilakukan untuk melewati teknik deteksi tertentu, menambahkan kode berbahaya ke dalam aplikasi Trojan, hingga menyelesaikan masalah kecil dan melakukan troubleshooting pada penetration testing tools, semuanya dalam lingkungan aplikasi Android production. Untuk menutup tulisan ini, ada beberapa poin penting yang bisa diambil, dan saya akan membaginya berdasarkan kelompok pembaca yang mungkin membaca artikel ini.
1. Application Owners
Hal ini semakin menegaskan betapa pentingnya melakukan integrity check pada source code aplikasi Anda, agar aplikasi tidak dimanfaatkan dan didistribusikan untuk tujuan berbahaya. Kegagalan dalam menerapkan hal ini bisa membahayakan pengguna, membuat mereka lebih rentan terhadap serangan siber melalui aplikasi Trojan, yang pada akhirnya bisa menyebabkan kerugian reputasi secara signifikan.
Sebagai langkah mitigasi, Anda dapat menerapkan CRC checks sebagai lapisan proteksi tambahan untuk app bytecode, native libraries, dan important data files. Dengan cara ini, aplikasi hanya akan berjalan dengan benar dalam kondisi unmodified, meskipun code signature-nya valid. Selain itu, berinvestasi pada produk security-hardening seperti DexGuard untuk Android dan SecNeo Packer juga bisa dipertimbangkan. Keduanya dapat meng-obfuscate dan mengenkripsi kode hasil dekompilasi di level bytecode, sehingga meningkatkan security posture aplikasi Anda dan membuatnya jauh lebih sulit disalahgunakan oleh pihak jahat.
Anda juga bisa memanfaatkan Play Integrity API dari Google untuk memverifikasi apakah aksi dan permintaan server benar-benar berasal dari aplikasi asli Anda, yang dipasang melalui Google Play, dan berjalan di perangkat Android asli.
2. Security Practitioners
Bagi Anda para praktisi keamanan, ini bisa jadi pengingat untuk tidak mengabaikan jenis pemeriksaan seperti ini dalam prosedur security assessment, khususnya pada aplikasi-aplikasi sensitif yang melakukan transaksi atau memiliki basis data pengguna besar, seperti aplikasi mobile banking, capital market exchange apps, dan sebagainya.
Selain itu, teknik ini bisa menjadi skill yang sangat berguna untuk dikembangkan, terutama untuk melewati logic atau detection mechanisms tertentu. Hal ini dapat membuat pekerjaan Anda jadi sedikit lebih mudah, tidak terlalu merepotkan, sekaligus memberi kesempatan untuk bereksperimen, have fun, dan melakukan troubleshooting terhadap masalah yang mungkin muncul di toolset aplikasi Android Anda. Intinya: jangan berhenti bereksperimen, selalu cari kemungkinan skenario baru yang bisa memperkuat argumen mengapa pemeriksaan seperti ini penting diterapkan.
3. People in General
Hei, pembaca non-teknis, ini juga buat kalian! Kalau Anda tidak memahami detail teknisnya, tidak masalah. Yang penting ada sesuatu yang bisa dipetik. Pesan utamanya sederhana: jangan install aplikasi pihak ketiga dari untrusted marketplace, dan selalu terapkan prinsip zero-trust. Jangan percaya, selalu verifikasi.
Kalau Anda menerima file APK mencurigakan dari orang asing, atau bahkan dari teman atau keluarga, tetapi Anda tidak memiliki kemampuan untuk memverifikasi apakah file tersebut benar-benar legitimate, jangan pernah menginstalnya di perangkat Anda! Dengan memegang teguh satu prinsip sederhana ini, Anda sudah melindungi diri dari berbagai serangan siber.
Untuk mendapatkan berbagai insight menarik seputar keamanan siber dan penetration testing di Indonesia, terus ikuti artikel-artikel berikutnya dari tim ahli kami!
Referensi
https://developer.android.com/guide/components/activities/activity-lifecycle
https://sallam.gitbook.io/sec-88/android-appsec/smali/smali-cheat-sheet
https://busk3r.medium.com/intercept-traffic-of-proxy-unaware-applications-in-burpsuite-eeb1ac329a87
https://mas.owasp.org/MASTG/techniques/android/MASTG-TECH-0109/
https://androidcracking.blogspot.com/2011/06/anti-tampering-with-crc-check.html
https://developer.android.com/google/play/integrity/overview
Copyright (c) 2025 Penetration-Test.id. All rights reserved.