PwnKnight: Dungeon of Darkness

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 ก็ได้ ดูว่าอันไหนคือแอพที่เราต้องการทดสอบ
# สำหรับ adbadb shell pm list packages
# Output Example สำหรับ adbpackage:com.android.companiondevicemanager.auto_generated_characteristics_rropackage:com.android.systemui.auto_generated_rro_vendor__package:com.google.android.providers.media.modulepackage:com.google.android.overlay.permissioncontrollerpackage:com.google.android.overlay.googlewebviewpackage:com.android.calllogbackuppackage:com.android.carrierconfig.auto_generated_rro_vendor__package:com.android.systemui.accessibility.accessibilitymenupackage:com.google.android.nfc
# สำหรับ frida-psfrida-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 ดูแล้วเข้าเกมใหม่

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


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

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


Act II: The Nameless Market

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

รู้จักกับ Frida Flutter Proxy
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
การใช้งานพื้นฐาน:
frida -U -f com.example.app -l frida-flutterproxy.jsเครื่องมือนี้เป็นตัวช่วยสำคัญสำหรับการทำ security testing กับ Flutter apps เพราะช่วยแก้ปัญหาที่ Flutter มักจะทำ SSL Pinning และยากต่อการดัก traffic ด้วยวิธีปกติ
นั้นแหละนะเราต้องพึ่งพา script นี้เพื่อที่จะดัก api ให้ได้โดยใช้ frida
frida -U -f com.mayaseven.mobile_pwn_adventure -l flutterproxy.js
แต่เดี๋ยวก่อนโดยเช็ค root สะงั้น งั้นก็ลองหา script frida bypass ตามเน็ตดู ก็ไปเจอ https://codeshare.frida.re/@sdcampbell/unified-android-root-and-debugger-bypass/ ก็ลองเอามาใช้ดู ก็จะ bypass root สำเร็จ


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


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


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




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

Act III: The Truth of PwnKnight

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 ออกไปผจญภัยได้แล้ว แต่ไปไม่รอด

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

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

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

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

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

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

แล้วก็นำไป rename ใน ida ซึ่งวิธีก็จะตามบทความนี้เลย Reverse Engineering an iOS Flutter Game: Bypassing FreeFall’s Scoring System
ต่อจากนี้ก็จะกลายเป็นหน้าที่ของ ai ในการวิเคราะห์ละว่า function ที่ใช้ซื้อของอยู่ตรงไหน และ ai ก็เจออยู่ที่ 0x3F1D48

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

// 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 loadfunction 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 แล้ว



