Reverse Engineering Game Android: Membongkar PES Club Manager — dari APK sampai Stub Server

Bisa nggak kita membongkar isi sebuah game Android — melihat bagaimana data pemain disimpan, format file-nya seperti apa, dan server-nya diajak bicara pakai protokol apa? Bisa, dan itulah yang disebut reverse engineering (RE). Artikel ini membedah prosesnya langkah demi langkah berdasarkan proyek nyata: membongkar game PES Club Manager (Konami, versi 4.5.1) sampai ke database pemain dan protokol jaringannya.

Catatan etika dulu: proyek ini murni riset dan belajar — analisis read-only pada salinan game milik sendiri, tanpa mengganggu server produksi, tanpa bypass anti-cheat, dan tanpa dipakai untuk curang di permainan live. RE dengan batas seperti ini adalah cara sah untuk belajar bagaimana software bekerja.

1. Alat Perang: yang Perlu Disiapkan

Semua alat di bawah gratis dan berjalan di PC biasa (Linux/Windows/macOS):

ALAT RE GAME ANDROID
--------------------
apktool        : bongkar APK jadi smali + resource
                 (java -jar apktool.jar d game.apk)
jadx / Bytecode-Viewer
               : decompile DEX jadi kode Java terbaca
Ghidra         : disassembler/decompiler native (gratis, dari NSA)
radare2/Cutter : analisis binary ARM64 via CLI/GUI
Python 3       : scripting, parser format file, stub server
Frida          : instrumentasi runtime (hook fungsi native
                 saat game berjalan di HP root)
adb            : jembatan ke HP (push/pull file, port reverse)

Sasarannya: file APK game + dua file OBB (main dan patch). Di Android, aset besar game disimpan di OBB, bukan di APK — jadi membongkar APK saja tidak cukup.

2. Langkah 1: Identifikasi Engine & Container

Langkah pertama selalu sama: kenali dulu kamu berhadapan dengan apa. Cek magic bytes dan isi OBB:

# OBB main ternyata ZIP biasa — langsung bisa dibuka
$ file main.104050101.jp.konami.pesclubmanager.obb
main....obb: Zip archive data

$ unzip -l main.104050101.jp.konami.pesclubmanager.obb | head
  PesMobile-Android_ETC1.pak      <-- ketemu file .pak

# OBB patch beda: header-nya bertuliskan CPK
$ xxd patch.104050101.jp.konami.pesclubmanager.obb | head -2
00000000: 4350 4b00 ...            "CPK"  <-- container CRIWARE

Dari sini peta jalannya jelas:

TEMUAN IDENTIFIKASI
-------------------
Engine        : Unreal Engine 4.22.3
                (dari string di libUE4.so:
                 "++UE4+Release-4.22")
OBB main      : ZIP berisi .pak (format aset Unreal)
                -> 49.556 path aset berhasil diinventarisasi
OBB patch     : CPK (CRIWARE) — format umum game Jepang,
                berisi animasi & definisi data
Library native: libUE4.so (engine), libafp-core.so,
                libavs2-core.so (middleware audio/video)

Pelajaran pertama RE: jangan menebak format — baca header-nya. Hampir semua container punya magic bytes dan tabel isi. CPK misalnya, punya tabel @UTF plus indeks ITOC/ETOC/GTOC yang memetakan semua file di dalamnya; dari tabel itu saja kita dapat daftar lengkap file tanpa mengekstrak satu pun.

3. Langkah 2: Memetakan Skema Database Pemain dari Simbol

Game bola pasti punya database pemain. Pertanyaannya: di mana, dan formatnya apa? Karena binary-nya tidak di-strip total, nama-nama C++ masih terbaca di tabel simbol. Namespace onlinemode::ParameterPlayerDefine adalah tambang emas: setiap field pemain punya fungsi getter/setter sendiri.

Tekniknya: disassemble fungsi getter kecil itu di Ghidra — isinya hampir selalu pola yang sama, load dari this + offset tetap. Offset itulah posisi field di struktur datanya:

POLA GETTER (ARM64)
-------------------
GetContractSalary:
    LDR  W0, [X0, #0x1F0]   ; ambil u32 di offset 0x1F0
    RET                     ; -> ContractSalary @ 0x1F0

CONTOH HASIL PEMETAAN (73+ field, confidence tinggi)
----------------------------------------------------
AppearanceId    @ 0x1B0  u32
GameSum         @ 0x168  u32   (total pertandingan)
GoalSum         @ 0x170  u32   (total gol)
AssistSum       @ 0x178  u32   (total assist)
ContractPeriod  @ 0x1E0  u32
ContractSalary  @ 0x1F0  u32   (gaji kontrak)
FamePoint       @ 0x318  u32

Dengan cara ini, tanpa membuka file data satu pun, skema pemain sudah terbaca dari kode programnya sendiri. Ini pola umum di RE: kode adalah dokumentasi terbaik dari format datanya.

4. Langkah 3: Player.bin — Record 312 Byte & Parser-nya

Database pemainnya sendiri bernama Player.bin (Table ID 15 di sistem database internal game, disebut pesdb). Dari fungsi loader di binary (GetVectorOfPesdbPlayerData) ketahuan fakta kuncinya: setiap pemain disimpan sebagai record tetap 312 byte (0x138), dan 64 byte pertamanya terkemas sebagai bitfield.

Artinya parser-nya sederhana: potong file per 312 byte, lalu baca field dari offset yang sudah dipetakan. Ini parser Python lengkapnya:

#!/usr/bin/env python3
"""Parser Player.bin PES Club Manager.
Record tetap 312 byte/pemain (Table ID 15, pesdb::PlayerData).
Jalankan: python3 parse_player.py Player.bin
"""
import struct
import sys

RECORD_SIZE = 312  # 0x138, dari GetVectorOfPesdbPlayerData

# Field header record (offset dalam byte, little-endian)
HEADER_FIELDS = [
    ("field_0x08", 0x08, "<I"),   # u32
    ("field_0x20", 0x20, "<I"),
    ("field_0x38", 0x38, "<Q"),   # u64 (dipakai loader sbg cek ukuran)
    ("field_0x40", 0x40, "<I"),
]

def parse(path):
    data = open(path, "rb").read()
    assert len(data) % RECORD_SIZE == 0, \
        f"ukuran {len(data)} bukan kelipatan {RECORD_SIZE}"
    n = len(data) // RECORD_SIZE
    print(f"{path}: {len(data)} byte = {n} record pemain")
    for i in range(n):
        rec = data[i * RECORD_SIZE:(i + 1) * RECORD_SIZE]
        vals = []
        for name, off, fmt in HEADER_FIELDS:
            v = struct.unpack_from(fmt, rec, off)[0]
            vals.append(f"{name}={v}")
        print(f"[{i:5d}] " + "  ".join(vals))
        print("        hex[0x00:0x20]:", rec[:0x20].hex(" "))

if __name__ == "__main__":
    parse(sys.argv[1] if len(sys.argv) > 1 else "Player.bin")

Kenapa tidak semua 312 byte langsung terbaca sebagai field bernama? Karena fungsi import game (SetWorkFromPesdb) hanya membaca ~72 byte pertama untuk manajemen tim — sisa ~240 byte dipakai konsumen lain (match engine) atau struktur yang belum terpetakan. RE yang jujur membedakan mana yang sudah terbukti dan mana yang masih dugaan.

Satu masalah besar: Player.bin aslinya terkunci di dalam CPK yang terenkripsi. Membuka enkripsinya dari file saja butuh memulihkan kunci CRIWARE — pekerjaan berhari-hari. Jalan pintas yang sah: ambil datanya setelah game sendiri mendekripsinya di memori (langkah 6).

5. Langkah 4: Protokol Jaringan & Stub Server

Game online seperti ini bicara ke server lewat HTTP API. Dari analisis statis string dan kode jaringan, terkumpul 113 endpoint yang dipakai client. Dengan daftar itu kita bisa membangun stub server: server lokal tiruan yang menjawab seolah-olah dia server resmi, supaya client bisa dipelajari perilakunya secara offline.

Stub paling sederhana cukup beberapa baris Python:

#!/usr/bin/env python3
"""Stub server minimal: jawab endpoint login dengan JSON contoh.
Semua endpoint lain dijawab {} agar client tidak crash.
"""
from http.server import BaseHTTPRequestHandler, HTTPServer
import json

RESPONSES = {
    "/api/login": {"result": 0, "user_id": 1001,
                   "session": "stub-session"},
}

class Stub(BaseHTTPRequestHandler):
    def _reply(self):
        body = json.dumps(
            RESPONSES.get(self.path, {"result": 0})).encode()
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)
        print("HIT:", self.command, self.path)

    do_GET = _reply
    do_POST = _reply

    def log_message(self, *a):
        pass

HTTPServer(("127.0.0.1", 8080), Stub).serve_forever()

Di proyek aslinya stub ini tumbuh jadi 113 endpoint dengan tes integrasi otomatis — 9 dari 9 tes lolos: client tiruan (mock client) bisa login, minta data, dan menerima jawaban yang strukturnya sama seperti server asli.

6. Langkah 5: Patch APK & Frida — Menjalankan Client Asli

Agar APK asli mau bicara ke stub lokal, dua hal dilakukan. Pertama, patch APK: alamat server di konfigurasi client diganti ke localhost, APK di-build ulang dengan apktool lalu di-sign dengan kunci sendiri. Kedua, adb reverse meneruskan port HP ke port stub di PC:

$ adb reverse tcp:8080 tcp:8080
# sekarang localhost:8080 di HP = stub server di PC

Dan untuk masalah Player.bin terenkripsi tadi, solusinya Frida: hook fungsi loader native tepat setelah game mendekripsi data ke memori, lalu salin buffer-nya keluar. Pola hook-nya generik — pasang interceptor di fungsi, baca argumen/buffer saat fungsi selesai:

// Pola generik hook Frida untuk dump buffer hasil dekripsi
const target = Module.findExportByName(null, "nama_fungsi_loader");
Interceptor.attach(target, {
  onEnter(args) {
    this.buf = args[1];              // pointer buffer data
    this.len = args[2].toUInt32();   // panjang data
  },
  onLeave(retval) {
    const bytes = Memory.readByteArray(this.buf, this.len);
    const f = new File("/sdcard/dump.bin", "wb");
    f.write(bytes);
    f.close();
    console.log("[dump] tersimpan:", this.len, "byte");
  }
});

Hasil gabungan patch + stub + Frida ini nyata: client asli berhasil jalan melawan stub lokal dan nyaris sampai ke menu home — satu blocker terakhir masih misteri dan sedang dicari penyebabnya. Beginilah RE di dunia nyata: maju selangkah demi selangkah, setiap tahap terverifikasi.

Ringkasan: Alur RE Game Android

Identifikasi (engine & container dari magic bytes) → inventarisasi (tabel isi CPK/PAK) → petakan skema (offset field dari getter/setter di binary) → tulis parser (record 312 byte Player.bin) → analisis protokol (113 endpoint) → stub server (tes integrasi 9/9) → patch + Frida (client asli jalan lokal, dump data terdekripsi dari memori).

Pelajaran terbesarnya: reverse engineering itu 90% membaca dengan sabar dan mencatat bukti, 10% menulis kode. Setiap klaim harus punya dasar — alamat fungsi, offset, atau output nyata — dan bagian yang belum terbukti diakui sebagai belum terbukti. Dengan disiplin itu, game yang tertutup rapat pun bisa dipahami lapis demi lapis. Proyek ini masih berlanjut; artikel lain di blog ini membahas fondasi yang sama dari sisi berbeda, misalnya cara membaca protokol data dan troubleshooting format data biner.

Komentar