Cheating Game Android yang dibuat dengan Unity

Setelah tulisan saya mengenai Cheat dan AntiCheat pada game, banyak yang bertanya ke saya tentang cheating game. Kebanyakan adalah pemula yang tidak tahu programming, dan hanya sekedar memakai tool yang sudah jadi. Biasanya mereka mentok jika gamenya crash, atau jika modifikasinya tidak berhasil.

https://twitter.com/bambenek/status/1365800872786157571

Tulisan ini bertujuan untuk menginspirasi: ada begitu banyak hal yang bisa dipelajari dari sekedar keisengan untuk mencurangi game. Tapi tentunya jangan sekedar cuma bisa mengikuti tutorial dan mentok di situ. Dari cheating game ini ada banyak hal tingkat lanjut dalam bidang programming dan security yang bisa dipelajari.

Di tulisan lalu, saya sudah memberi tahu bahwa ada begitu banyak game engine, dan ada banyak tool, banyak proteksi pada sebuah game. Di tulisan sebelumnya saya membahas game secara umum, jadi sekarang saya ingin mengambil contoh yang lebih konkrit: game yang dibuat dengan game engine Unity dan hanya pada Android.

Kenapa saya mengambil contoh Unity? karena banyak sekali game populer yang ditulis dengan Unity, misalnya Genshin Impact, Pokemon Go, Super Mario Run, Call Of Duty: Mobile, dan masih banyak lagi. Unity bisa dipakai untuk membuat game untuk Android, iOS, PC, dan bahkan juga semua console saat ini. Jadi kenapa dibatasi hanya Android saja? karena kebanyakan orang yang bertanya hanya memiliki Android (bahkan banyak yang tidak punya PC). Selain itu: setiap platform memiliki kesulitan dan masalahnya sendiri, jadi akan lebih mudah jika saya bahas satu saja: Android.

Tulisan ini tidak akan membahas sangat dalam tentang cheating itu sendiri, lebih untuk menjelaskan bahwa:

  • Hanya satu framework (Unity) dan satu target saja (Android) sudah ada banyak hal yang perlu diketahui
  • Memahami framework untuk membuat game akan memudahkan kita memahami bagaimana membuat cheat (dan bagaimana melindungi game buatan kita)

Unity Engine: dulu dan sekarang

Dulu hampir semua game Unity memakai runtime mono dan memakai bytecode .NET (MSIL) sehingga mudah dibongkar dengan decompiler .NET (contoh tutorialnya ini). Tapi sekarang pemakai framework versi baru hampir semuanya sudah memakai il2cpp (hanya target Windows .NET yang masih memakai .NET). Dengan il2cpp, kode .NET diterjemahkan menjadi kode C++ dan dikompilasi dengan compiler C++. Artinya sekarang lebih sulit membongkar kode-nya, butuh decompiler seperti Ghidra atau IDA Pro.

Di masa depan, teknologi yang dipakai Unity bisa saja berubah lagi, jadi isi artikel ini mungkin tidak akan valid lagi. Hal yang ingin saya sampaikan adalah: carilah informasi terbaru, sesuai dengan game yang menjadi target. Sebagian game akan menampilkan (di logcat) versi Unity yang dipakai.

Metadata

Karena kode Unity tetap ditulis dalam C# dan butuh fitur seperti Reflection (untuk mengakses field/method berdasarkan string namanya), maka kode C++ yang dihasilkan memiliki metadata. Metadata ini berisi informasi kelas, nama method (termasuk return value, dan tipe parameternya), nama dan tipe field, dan aneka informasi lainnya. Nama file metadata ini biasanya adalah: global-metadata.dat

Untuk aplikasi yang tidak diproteksi sama sekali, metadata ini (bersama dengan file il2cpp.so) bisa didump dengan tool il2cppdumper atau il2cppinspector . Saat ini fitur il2cppdumper akan digabung ke il2cppinspector dan saat il2cppinspector ini bahkan bisa melakukan bypass proteksi game tertentu (hanya bypass proteksi sehingga bisa didump, bukan di bagian modifikasi gamenya).

il2cppinspector

Dengan dump metadata ini, kita akan mendapatkan banyak informasi yang cukup untuk mencurangi game. Misalnya kita jadi tahu kelas mana yang memproses senjata milik user dan bisa diutak-atik (misalnya menambahkan damage).

Perlu dicatat: walau kita bisa memodifikasi metadata, tapi yang dipatch biasanya adalah file il2cpp.so (di Android, di OS lain bisa berbeda, misalnya il2cpp.dll di Windows, UnityEngine.framework di iOS). Lokasi yang harus dipatch didapatkan dari metadata hasil dump.

Contoh parsial output dari il2cppinspector

Selain mendapatkan nama kelas, il2cppdumper dan il2cppinspector juga bisa menghasilkan script untuk IDA dan Ghidra yang akan otomatis merename setiap fungsi dan mengeset parameter dan return valuenya. Bahkan untuk IDA, berbagai struktur kelas juga bisa diimpor, hasilnya: hasil dekompilasi bisa sangat baik jika kode tidak diobfuscate.

Contoh output IDA dengan tambahan informasi dari il2cppdumper: nama method, parameter, dan return value jelas terbaca

Proteksi Metadata

Sebagai developer Unity, kita diberi source code runtime il2cpp dan bisa kita modifikasi sendiri. Banyak developer game yang menambahkan layer enkripsi sebelum proses loading metadatanya.

Sudah ada yang memberikan penjelasan lengkap mengenai proses loading metadata dan bisa dibaca di sini, jadi tidak perlu saya ulangi. Developer juga bisa menambahkan kode anti tamper, jadi jika metadata dimodifikasi, maka bisa crash.

Selain enkripsi, developer juga bisa menambahkan name/identifier obfuscation pada nama kelas/method/fieldnya. Misalnya tadi namanya adalah “WeaponSystem” bisa menjadi “AABBCC”, jadi akan mempersulit untuk mencari tahu kelas mana yang perlu dimodifikasi. Nama yang sudah diobfuscate ini tidak bisa dikembalikan secara otomatis, kecuali kita punya mappingnya. Developer bisa melakukan obfuscation dan punya mappingnya (AABBCC=WeaponSystem), tapi yang membongkar tidak akan memiliki ini.

Kadang hanya nama method/class yang diobfuscate, tapi berbagai string tidak diobfuscate, kadang isi sebuah enum juga tidak diobfuscate. Dari berbagai nama ini kita bisa menebak method/kelasnya. Misalnya jika ketemu string yang mengandung kata “weapon” kemungkinan itu adalah kelas yang berhubungan dengan weapon.

Beberapa nama bawaan dari Unity juga biasanya tidak diobfuscate (misalnya: MonoBehaviour) dan bisa jadi titik awal untuk mencari tahu informasi tentang sebuah kelas. Di titik ini: anggap saja melakukan reverse engineering sebuah malware yang tidak memiliki nama fungsi sama sekali, kita harus membaca kodenya untuk memahami fungsinya.

Sebelum memulai

Hal pertama yang perlu dilakukan sebelum melakukan modifikasi adalah: melakukan unpack APK target, repack, dan jalankan lagi. Jika bisa berjalan (sampai masuk ke gamenya), maka tidak ada proteksi di level APK. Proteksi yang bisa dilakukan adalah:

  • Verifikasi signer: jika tidak disign dengan certificate tertentu, maka akan langsung exit
  • Verifikasi dengan safetynet, jika hash apk berubah maka akan langsung ditolakm, jika HP diroot juga akan ditolak

Hanya dengan unpack dan repack saja (tanpa modifikasi), sebagian game akan:

  • Crash
  • Menampilkan blank screen
  • Menampilkan pesan error
  • Tidak menampilkan error tapi tidak bisa login

Game yang APK-nya diproteksi seperti ini biasanya tetap bisa dibypass dengan menggunakan Frida (dan kadang perlu Magisk). Cerita mengenai frida ini bisa panjang, saya pernah menuliskan perkenalan Frida di sini.

Perlu dicatat juga: sebagian game akan berjalan normal, tapi akan melaporkan ke server bahwa hash APK-nya tidak valid/tidak sama dengan dari appstore. Server kemudian bisa mem-ban Anda, entah langsung, atau seminggu kemudian.

Lalu bagaimana jika program crash setelah dipack, dan bahkan belum dimodifikasi? Pertama pastikan bahwa cara packagingnya sudah benar, apakah semua resource dan .so sudah dipack dengan benar. Kedua: cari pesan crashnya dengan melihat output dari logcat. Penting sekali untuk bisa membaca pesan error ini:

  • Kadang pesan errornya sangat jelas: misalnya “invalid hash”
  • Kadang ada pesan exception yang muncul dan terlihat di sisi Java (smali) di mana crashnya dari stack tracenya
  • Kadang yang muncul adalah pesan crash dari native code. Biasanya akan muncul isi register. Untuk ARM/ARM64 kita bisa melihat isi register PC (Program Counter) untuk melihat di titik mana program crash, atau kadang perlu melihat LR (Link Register) untuk melihat dari mana fungsi itu dipanggil (kadang PC tidak valid, tapi biasanya LR valid)

Tidak ada satu cara mudah untuk mengetahui apa penyebab crashnya. Pengecekan tiap game berbeda-beda, harus dipelajari kodenya. Kalau sudah paham assembly dan sistem operasi, akan mudah mengetahui penyebab crash dengan melihat kodenya. Contoh: SIGSEGV terjadi ketika kita berusaha mengakses memori yang permissionnya tidak valid (misalnya menulis ke page memori yang permissionnya read-only, atau membaca yang proteksinya none), dan biasanya akan terjadi di instruksi LDR/STR.

Jika program tidak crash jika belum dimodifikasi, tapi ketika dimodifikasi langsung crash ketika dibuka maka periksalah mulai dari hal dasar. Pertama: modifikasi 1 byte di kode yang tidak dipanggil, misalnya sebuah string yang kita tahu tidak akan muncul di awal game. Jika sudah crash, maka ada kemungkinan game memeriksa hash kodenya, dan akan crash jika dimodifikasi.

Jika sudah berhasil memodifikasi 1 byte, tapi crash ketika dimodifikasi lebih banyak lagi, biasanya modifikasinya salah. Beberapa kesalahan yang umum:

  • salah mengedit smali, coba buka kembali APK hasil modifikasi dengan jadx-gui atau program sejenis untuk memastikan bahwa modifikasinya sudah benar
  • lokasi patch salah atau isi patchnya salah. Lakukan disassembly/dekompilasi untuk memastikan patchnya sudah benar

Mencari titik untuk modifikasi

Ini sangat bergantung pada gamenya, jadi tidak bisa dijelaskan dengan detail. Intinya adalah kita perlu mengetahui:

  • apakah aksi dicek di server atau client. Contoh: jumlah koin biasanya dicek di server, jadi percuma dimodifikasi di sisi client (di sisi game)
  • di fungsi mana aksi tersebut dilakukan. Ini biasanya dilakukan dengan memperkirakan dari nama fungsi (jika nama fungsi tidak obfuscated)
  • modifikasi apa yang dibutuhkan

Beberapa contoh modifikasi yang mungkin:

  • mengubah nilai damage
  • membuat objek (misalnya dinding) jadi transparan (untuk melihat musuh di belakang dinding)
  • Menampilkan informasi yang seharusnya tidak muncul di game normal
  • Mengotomasi sesuatu

Kadang proses modifikasi bisa sangat sederhana sekali, misalnya mengubah pointer agar getCurrentPower jadi getMaxPower sehingga jika getCurrentPower dipanggil, yang didapatkan adalah selalu nilai maksimum. Di sini kita tidak mengubah kode sama sekali, hanya sekedar menimpa pointer sebuah fungsi dengan pointer lain.

Modifikasi lain adalah mengubah kode assemblynya, misalnya sekedar mengembalikan false atau true (di ARM64 ini hanya perlu mengeset register x0 dengan 0 atau 1 dan melakukan ret). Atau sekedar mengubah perbandingan/jump (misalnya CBZ menjadi CBNZ).

Modifikasi yang lebih lanjut juga bisa dilakukan: kita bisa menimpa fungsi agar memanggil fungsi kita. Kita harus mengimplementasikan fungsi baru, mungkin mengubah beberapa parameter, lalu memanggil fungsi aslinya.

Perlu dipikirkan juga sebelum melakukan hal yang terlalu sulit: apakah ada cara sederhana lain untuk mengakali gamenya? contoh: saya pernah ketemu game dimana kita akan diberi koin jika menonton video. Dengan menggunakan burp suite, saya mengubah nilai jumlah koin reward dari server, dan hasilnya saya punya jutaan koin yang bisa dipakai membeli item in-game apa saja. Atau dalam kasus lain ternyata file data aplikasi bisa dibackup dan ternyata tidak dienkripsi sama sekali. Di kasus lain, save game ternyata disimpan ke server dalam bentuk POST dan bisa dimodifikasi karena tidak ada hash/signaturenya.

Aneka Cara Modifikasi

Ada berbagai cara untuk memodifikasi game:

  1. mengedit file il2cpp.so di disk, dan dipack ulang
  2. memodifikasi smali agar meload file .so yang nantinya akan mempatch il2cpp di memori (jadi di disk tidak berubah)
  3. memodifikasi langsung di memori (misalnya dengan Frida)

Contoh library yang bisa dipakai pendekatan kedua adalah xHook, libinjector, KittyMemory, dsb. Sebagai catatan: library-library ini tidak spesifik untuk Unity, bisa dipakai untuk patching apa saja. Tapi sebenarnya membuat library sejenis ini tidak sulit: untuk membuat memori menjadi writable kita cuma perlu memakai mprotect, untuk mendapatkan akses library dan offset ASLR kita bisa memakai dl_iterate_phdr.

Frida sangat cocok untuk eksperimen, tapi agak repot untuk membuat sebuah Game APK yang dimodifikasi dengan Frida (tidak mustahil, tapi perlu memodifikasi kode Frida).

Proteksi kode

Saat ini ada banyak produk yang memproteksi kode yang ditulis dalam Unity (misalnya iXGuard/DexGuard, Obfuscator, Anti-Cheat-Toolkit, dsb), jadi kode tetap tidak bisa dimengerti ketika dibuka dengan Ghida/IDA Pro. Seperti telah dijelaskan sebelumnya, kode C++ bisa dikompilasi dengan apa saja, termasuk juga dengan obfuscating compiler (ada banyak produk yang berdasarkan pada OLLVM ini).

Kode yang sudah diproteksi kadang tidak bisa langsung diload dengan disassembler/decompiler, perlu didump dari memori. Kadang kode bisa diload tapi bagian data section akan dalam kondisi terenkripsi (semua string tidak terbaca), dalam kasus ini bagian data section bisa didump dari game yang berjalan.

Jika kode diproteksi dengan jenis proteksi RASP (Runtime Application Self Protection), maka berbagai modifikasi dan hook runtime bisa dideteksi. Diperlukan pemahaman yang cukup dalam untuk bisa melakukan bypass terhadap proteksi seperti ini.

Kebanyakan orang yang mengerti bagaimana melakukannya tidak mempublikasikan secara umum, karena jika dipublikasikan, maka proteksinya akan diubah lagi di versi berikutnya, dan akan merepotkan diri sendiri. Jadi inilah alasan kenapa Anda akan jarang menemukan tutorial lengkap anti-anti-cheat, dan kalaupun ketemu, kebanyakan sudah tidak berlaku lagi karena sudah berubah.

Asalkan kita mengerti apa yang dilakukan oleh kode anti cheatnya, maka ada banyak alternatif bypassnya. Contoh: ada yang namanya anti time cheat, dengan mengecek waktu saat ini ke internet. Tapi ini bisa dibypass dengan banyak cara, misalnya: mengarahkan time ke server sendiri, mengubah nilai kembalian dari server, menskip kode anti cheatnya.

Di sinilah titik di mana banyak orang mentok: tidak mau belajar sampai ke level assembly atau level decompiler untuk memahami kodenya. Jadi hanya bergantung tutorial saja. Saya tidak bisa membantu mereka yang tidak mau belajar lebih dalam.

Perlu ditekankan juga: sebagian proteksi sangat sulit dibypass. Para programmer ini dibayar mahal untuk memproteksi gamenya, jadi mereka sudah memikirkan berbagai macam hal dengan matang. Ibaratnya begini: kalau Anda diminta menyembunyikan sesuatu, kemungkinan Anda sembunyikan di sekitar rumah, tapi kalau Anda dibayar sekian milyar tiap tahun untuk menyembunyikan sesuatu, maka mungkin Anda akan menyewa helikopter untuk menyembunyikan di tempat terpencil.

Native Code

Kode il2cpp juga bisa memanggil kode plugin dalam native code yang ditulis dalam C/C++. Biasanya ini terlihat dari adanya library .so tambahan. Beberapa plugin sudah cukup jelas terlihat dari namanya (misalnya untuk signin google, untuk firebase, dsb yang merupakan library standard dan akan kenal nama-nama librarynya jika sudah biasa memprogram Android).

Sisanya adalah library custom buatan pembuat game. Library-library ini biasanya diproteksi ekstra, misalnya dengan enkripsi, obfuscation, dan kode anti-debug. Reverse engineering kode native seperti ini sama seperti reverse engineering kode lain , butuh disassembler, debugger, dsb (jadi butuh ilmu reverse engineering secara umum, tidak spesifik untuk game engine tertentu).

Beberapa game bahkan memiliki kode yang dicampur dengan bahasa lain, misalnya yang cukup populer adalah Lua. Jadi tidak semua logic game ada di dalam il2cpp, tapi dipindahkan ke dalam file lua. Tentunya file lua-nya sudah dicompile menjadi bytecode, lalu dienkrip dan sering kali bytecodenya sudah dibuat tidak standard (dari mulai sekedar ditukar posisinya atau dibuat opcode baru). Interpreter Lua ini sangat mudah dimodifikasi, jadi proteksi yang bisa dilakukan tergantung kreativitas programmernya. Saya pernah menulis tentang Lua di tulisan ini.

API il2cpp

Jika kita memahami IL2CPP, maka reverse engineering aplikasi Unity akan lebih mudah. Informasi mengenai internal il2cpp bisa dibaca di blog Unity. Library il2cpp.so memiliki banyak fungsi untuk mengakses langsung berbagai struktur il2cpp di memori (nama fungsinya dimulai dengan il2cpp_.

Meskipun metadata dienkripsi, fungsi-fungsi ini bisa dipanggil untuk men-dump semua kelas dalam memori. Saya menemukan contoh skrip Frida yang memanggil fungsi-fungsi il2cpp untuk melakukan dumping dengan memanggil fungsi il2cpp secara langsung. Kita juga bisa memanggil API il2cpp ini misalnya untuk membuat string baru.

Belajarlah membuat game kecil

Cara paling baik untuk belajar adalah membuat game kecil, mengcompile gamenya, lalu berusaha mencurangi game itu sendiri. Saat ini ada banyak sekali tutorial di Youtube mengenai bagaimana membuat berbagai jenis game Unity (dan bahkan sudah ada tutorial built in juga).

Setelah itu coba pakai protektor tertentu, dan lihat apakah Anda masih bisa mencurangi gamenya. Setelah itu pikirkan: apakah Anda bisa menambahkan sendiri encryptor untuk metadata? apa hasilnya jika dibongkar dengan Ghidra/IDA Pro?

Penutup

Reverse engineering game hanyalah salah satu dari keisengan saya, tidak saya lakukan dengan serius. Tapi membypass berbagai teknik proteksi dalam game ini sangat berguna untuk melakukan testing pada aplikasi serius yang saya lakukan.

Saran saya buat yang mentok: belajarlah dari dasarnya, karena tidak ada cara lain yang lebih baik selain memahami dasarnya. Seperti gambar tweet di awal artikel ini: jika kita serius, maka pengetahuan tentang assembly, tentang dynamic patching, memahami proteksi program, dsb bisa jadi modal yang besar untuk masuk ke bidang cybersecurity. Kalau mentok tidak lebih dari bisa mengikuti tutorial orang lain, ya tidak akan berguna di masa depan.

Tinggalkan Balasan

Situs ini menggunakan Akismet untuk mengurangi spam. Pelajari bagaimana data komentar Anda diproses.