1182 words
6 minutes
Archa CTF 2026 - Mobile Game Hacking

PwnKnight: Dungeon of Darkness#

image.png

PwnKnight: Dungeon of Darkness The Abyssal Kingdom was an empire of flawless logic until the Demon of Corruption arrived. Using “Forbidden Logic,” it distorted the world’s rules, creating dungeons that were actually fractures in the world’s system. The Knight Who Believed in Flaws In the darkness, only PwnKnight believed that “Every system has a flaw.” He relied on intellect and observation to question the laws of the world. One day, he was summoned by the keeper of rules, Archmage Rootwell. Act I: Hidden in Plain Sight Rootwell revealed that the Princess was taken by the Demon to rewrite the world itself. PwnKnight’s first mission was not battle, but to find a hidden path. Rootwell warned: “Some truths are not hidden behind doors, but behind conditions. The answer may be closer than you expect.”

Hint: Local storage

APK : PWNKnight.apk

เริ่มกันเลย#

ข้อมูลมี hint บอกว่า Local Storage ก็เข้าไปเช็คใน /data/data/com.appname ดูว่ามีอะไรน่าสนใจไหม

ก่อนอื่นก็ต้องดูว่าชื่อ package คืออะไรโดยใช้คคำสั่ง adb หรือ frida-ps ก็ได้ ดูว่าอันไหนคือแอพที่เราต้องการทดสอบ

Terminal window
# สำหรับ adb
adb shell pm list packages
# Output Example สำหรับ adb
package:com.android.companiondevicemanager.auto_generated_characteristics_rro
package:com.android.systemui.auto_generated_rro_vendor__
package:com.google.android.providers.media.module
package:com.google.android.overlay.permissioncontroller
package:com.google.android.overlay.googlewebview
package:com.android.calllogbackup
package:com.android.carrierconfig.auto_generated_rro_vendor__
package:com.android.systemui.accessibility.accessibilitymenu
package:com.google.android.nfc
# สำหรับ frida-ps
frida-ps -Uai
# Output Example สำหรับ frida-ps
PID Name Identifier
----- ------------------- ---------------------------------------
8605
Camera com.android.camera2
9186
Chrome com.android.chrome
8186
Clock com.google.android.deskclock
9118
Contacts com.google.android.contacts
7729
Google com.google.android.googlequicksearchbox

เข้าไปดูก็จะเจอไฟล์ config.json อยู่ที่ /data/data/com.mayaseven.mobile_pwn_adventure/app_flutter/config.json มี **secret_shop_enabled** เป็น false อยู่ลองเปลี่ยนให้เป็น true ดูแล้วเข้าเกมใหม่

image.png

จะเห็นว่าเมนู Secret Shop สามารถกดเข้าไปได้แล้ว แต่ให้ใส่ key แล้ว key คืออะไรกันละ จริงๆมีคำใบ้อยู่ในโจทย์ตรง “Some truths are not hidden behind doors, but behind conditions. The answer may be closer than you expect.” นั้นก็คือเราต้องไปคุยกับ พ่อมด แล้วดูที่ logcat

image.png

image.png

ใช้คำสั่งนี้ แล้วกดคุยกับพ่อมดจะเห็นบางอย่างที่เกี่ยวกับ rsa encrypt อะไรสักอย่างโยนให้ ai ถอดมาให้ก็จบได้ key ที่ถูกต้องนั้นก็คือ 83721910

Terminal window
adb logcat

image.png

image.png

พอใส่ key ที่ถูกต้องก็จะมีอะไรให้ซื้อหลังจากซื้อเสร็จไปดูที่ กระเป๋าก็จะเจอ flag เรียบร้อยจบข้อแรก

image.png

image.png

Act II: The Nameless Market#

image.png

Act II: The Nameless Market During his journey, PwnKnight encountered a small shop tucked away in a forgotten corner of the kingdom. No sign. No history. No one ever spoke of it. The merchant guarding the shop said only: “Some things were never meant to be seen by everyone.” PwnKnight did not know where the shop came from. He only knew that it still existed, even as the world around it changed. He stepped inside, uncertain whether what awaited him was merely merchandise— or another fragment of the kingdom’s hidden truth.

Hint: API

เริ่มกันเลย#

ข้อนี้เป็นข้อที่ผมหา flag เจอก่อนข้อแรกสะอีก เนื่องจาก งง ที่ข้อแรกตั้งนาน แต่ดูจาก Hint แล้วก็ง่ายๆ ดัก api นั้นแหละ ก็เริ่มจาก หาวิธีที่จะดัก api ให้ได้ก่อน ถ้าไปดูที่ jadx จะเห็นว่าแอพนี้ถูกเขียนด้วย flutter ดูที่ไฟล์ lib ก็จะมี

  • libflutter.so อยู่จะเป็น native library (shared object) ที่ใช้รัน Flutter Engine บน Android และ ไฟล์
  • libapp.so ก็คือ library ที่มี Dart code ที่ถูก compile แล้วของแอป Flutter

เท่านี้ก็ยืนยันได้ว่าถูกเขียนด้วย flutter

image.png

รู้จักกับ Frida Flutter Proxy#

hackcatml
/
frida-flutterproxy
Waiting for api.github.com...
00K
0K
0K
Waiting...

Frida Flutter Proxy คือเครื่องมือสำหรับ intercept และดัก traffic จากแอป Flutter ผ่าน proxy โดยอัตโนมัติ

ความสามารถหลัก:#

ทำอะไรได้:

  • ดักจับ HTTP/HTTPS requests/responses จากแอป Flutter
  • Bypass SSL Pinning โดยอัตโนมัติ
  • ส่ง traffic ผ่าน proxy tools เช่น Burp Suite, Charles, mitmproxy
  • ไม่ต้อง repackage หรือ patch แอป

ข้อดี:

  • ใช้งานง่าย ไม่ต้อง modify APK
  • รองรับทั้ง Android และ iOS
  • ทำงานกับ Flutter apps ได้ทันที
  • เหมาะสำหรับ penetration testing และ security analysis

การใช้งานพื้นฐาน:

Terminal window
frida -U -f com.example.app -l frida-flutterproxy.js

เครื่องมือนี้เป็นตัวช่วยสำคัญสำหรับการทำ security testing กับ Flutter apps เพราะช่วยแก้ปัญหาที่ Flutter มักจะทำ SSL Pinning และยากต่อการดัก traffic ด้วยวิธีปกติ

นั้นแหละนะเราต้องพึ่งพา script นี้เพื่อที่จะดัก api ให้ได้โดยใช้ frida

Terminal window
frida -U -f com.mayaseven.mobile_pwn_adventure -l flutterproxy.js

image.png

แต่เดี๋ยวก่อนโดยเช็ค root สะงั้น งั้นก็ลองหา script frida bypass ตามเน็ตดู ก็ไปเจอ https://codeshare.frida.re/@sdcampbell/unified-android-root-and-debugger-bypass/ ก็ลองเอามาใช้ดู ก็จะ bypass root สำเร็จ

image.png

image.png

ทีนี้ก็ไปดูที่เมนู shop ที่คุยกับพ่อมด เมนูนี้จะมีการเรียก api ไปที่ /api/shop/list?max_price=2000

image.png

image.png

ถ้าทดสอบ api เส้นนี้ดูก็จะเจอช่องโหว่ sql injection ใช้ SQLMap เทสต่อเลย ก็จะเจอ item_id ที่ดูเหมือนจะเป็น flag แต่ก็ไม่ใช้ ถ้าดูที่ชื่อ column มันก็คือ item_id และ ตัว api มันก็ response item_id เหมือนกันเป็นรายการ item ให้เราซื้อ

image.png

image.png

เราสามารถเอา item_id ที่ได้มาจาก sql injection ไปใส่ใน response ได้โดยใช้ Burp Suite เพื่อให้เราสามารถซื้อของชิ้นนี้ที่ซ้อนอยู่ได้ เนื่องจากเกมเช็คเรื่องการซื้อของที่ฝั่ง client ทำให้เราสามารถหลอก client ได้

โดยทำการ Intercept Request และทำการแก้ไข Response เสร็จแล้วกลับไปดูที่ตัวเกมก็จะเห็นว่า item ที่เราแก้ไขไปถูกเปลี่ยนไปตามที่เราแก้ไข response

image.png

image.png

image.png

image.png

เมื่อกดซื้อแล้วก็จะได้ flag เรียบร้อยในกระเป๋า แถมเงินไม่ลดด้วยนะ เพราะเราแก้ไข price เป็น 0

image.png

Act III: The Truth of PwnKnight#

image.png

Act III: The Truth of PwnKnight As the Demon’s castle finally came into view, PwnKnight uncovered a truth that brought him to a halt. He was not a knight born of fate or legend. He was the result of an experiment— a fusion of code and magic. This world might not be real at all. It might be nothing more than a massive sandbox. The princess.The Demon. Even PwnKnight himself. All of them could be nothing more than components of a system. And so the final question was no longer: “Can the Demon be defeated?” But instead: “When we have the power to control the system— should we use it… or let it remain untouched?”

เริ่มกันเลย#

แน่นอนว่าข้อนี้ผมก็ทำได้ก่อนข้อแรกอีกแล้ว แสดงว่าข้อแรกน่าจะ hard สำหรับผม 555

ในข้อที่ 2 เราสามารถซื้อ item ได้โดยแก้ไขค่าเงินเป็น 0 ก็สามารถซื้อ item ออกไปผจญภัยได้แล้ว แต่ไปไม่รอด

image.png

งั้นกลับมาดูที่ api มี query param ที่ชื่อ max_price อยู่แปลกไหมทำไมต้องมีด้วยลองเพิ่มเป็น 999999999 ดูสิมีอะไรซ่อนอยู่ไหม และแล้วเราก็จะ item ที่แสนจะแพง อยู่ 2 ชิ้น ไหนลองปรับราคาเป็น 0 เหมือนเดิมดูสิ

image.png

ขอขั้นนิดนึ่ง เราสามารถใช้ HTTP match and replace rules ได้ จะได้ไม่ต้องไปกด Intercept บ่อยๆ ตัวนี้จะจัดการให้เรา

image.png

อ้าววว ซื้อไม่ได้สะงั้น งั้นก็มีอีกวิธีคือ ปรับค่าเงินของเราให้เป็น ค่าที่เท่ากับหรือมากว่าราคา item ที่เราจะซื้อได้ มี 2 วิธีคือใช้ Game Guardian กับ Reverse Engineering ไฟล์ libapp.so ซึ่งตอนผมเล่นอยู่ลองใช้ Game Guarddian แล้วหาไม่เจอ เลยเลือกวิธี Reverse libapp.so แทน

image.png

Reverse Engineering

worawit
/
blutter
Waiting for api.github.com...
00K
0K
0K
Waiting...

ลองใช้ blutter decomplie ดูสิได้ไหม สรุปไม่ได้

image.png

Impact-I
/
reFlutter
Waiting for api.github.com...
00K
0K
0K
Waiting...

งั้นไปลอง reFlutter ดูสิได้รึป่าว

image.png

เปิดเกมแล้วไปดูไฟล์ dump.dart ที่ /data/data/com.mayaseven.mobile_pwn_adventure

image.png

แล้วก็นำไป rename ใน ida ซึ่งวิธีก็จะตามบทความนี้เลย Reverse Engineering an iOS Flutter Game: Bypassing FreeFall’s Scoring System

ต่อจากนี้ก็จะกลายเป็นหน้าที่ของ ai ในการวิเคราะห์ละว่า function ที่ใช้ซื้อของอยู่ตรงไหน และ ai ก็เจออยู่ที่ 0x3F1D48

image.png

จากนั้นก็ให้ ai วิเคราะห์ function นี้ต่อว่าจะเปลี่ยนแปลง เงินในเกมได้ยังไง แล้วให้เขียน frida script ให้ เท่านี้ก็เสร็จเรียบร้อย

image.png

// Frida script to find and modify coins in PWNKnight game
// Initial coin value: 20
console.log("[*] Coin Modifier for PWNKnight");
console.log("[*] Initial coin value: 20 (encoded as 40 in Dart Smi)");
const moduleName = "libapp.so";
// Wait for module to load
function waitForModule() {
return new Promise((resolve) => {
const check = setInterval(() => {
const mod = Process.findModuleByName(moduleName);
if (mod) {
clearInterval(check);
resolve(mod.base);
}
}, 5000);
});
}
waitForModule().then((base) => {
console.log("[+] libapp.so loaded at: " + base);
// Strategy 1: Hook the coin read function
console.log("\n[*] Strategy 1: Hooking coin read at 0x3f1d80");
const coinReadAddr = base.add(0x3f1d80);
let gameStatePtr = null;
let coinAddr = null;
Interceptor.attach(coinReadAddr, {
onEnter: function (args) {
// At this point, v6 register (x26) contains game state pointer
const v6 = this.context.x26;
try {
// Calculate: *(_QWORD *)(v6 + 120) + 6440
const ptr1 = v6.add(120).readPointer();
coinAddr = ptr1.add(6440);
// Read current coin value
const coinSmi = coinAddr.readU64();
const actualCoins = coinSmi.toInt32() >> 1;
console.log("\n[*] Coin read detected!");
console.log(" Game state (x26): " + v6);
console.log(" Coin address: " + coinAddr);
console.log(" Coin value (Smi): " + coinSmi);
console.log(" Actual coins: " + actualCoins);
// Save for later modification
gameStatePtr = v6;
} catch (e) {
// Ignore errors during initial attempts
}
},
});
// Strategy 2: Hook shop purchase to intercept price check
console.log("\n[*] Strategy 2: Hooking price check at 0x3f1d94");
const priceCheckAddr = base.add(0x3f1d94);
Interceptor.attach(priceCheckAddr, {
onEnter: function (args) {
// At this point:
// x2 = current coins (Smi encoded)
// x0 = item price (Smi encoded)
// Comparison: if (coins < price) return;
const currentCoins = this.context.x2.toInt32() >> 1; // Decode Smi
const itemPrice = this.context.x0.toInt32() >> 1; // Decode Smi
console.log("\n[*] Purchase check:");
console.log(" Current coins: " + currentCoins);
console.log(" Item price: " + itemPrice);
try {
const newCoins = 999999999; // จำนวน coins ที่ต้องการ
const smiValue = newCoins * 2; // Encode เป็น Smi
Memory.protect(coinAddr, 8, "rwx");
coinAddr.writeU64(smiValue);
console.log("[+] Modified coins in memory to: " + newCoins);
console.log(" (Smi value: " + smiValue + ")");
// อัพเดท register ด้วย
this.context.x2 = ptr(smiValue);
} catch (e) {
console.log("[-] Error modifying coins: " + e);
}
},
onLeave: function (retval) {
// ตรวจสอบว่าการ purchase ผ่านไหม
// บาง implementation อาจ return boolean หรือ error code
console.log(" Return value: " + retval);
},
});
});

ได้เงินพอที่จะซื้อไอเทมสุดโหดแล้วววววว ได้เวลาไปลุยบอสส ฆ่าบอสได้ก็ได้ flag แล้ว

image.png

image.png

image.png

image.png

Archa CTF 2026 - Mobile Game Hacking
https://blog.0x01code.me/posts/archa-ctf-2026-mobile-game-hacking/
Author
0x01code
Published at
2026-01-06
License
CC BY-NC-SA 4.0