ดีบักแอปที่ดีบักไม่ได้
(bryce.co)- การทำ reverse engineering แอป iOS จำเป็นต้องสามารถสังเกตและปรับเปลี่ยนแอปขณะกำลังรันได้ แต่แอปวิดเจ็ตนี้ใช้ทั้ง การบล็อกดีบักเกอร์·การบล็อกการฉีดโค้ด·การตรวจจับ jailbreak ร่วมกัน
- กลไกบล็อกหลักอาจถูกทำด้วย PT_DENY_ATTACH ของ
ptraceหรือ system call โดยตรง (svc #0x80) ที่ให้ผลแบบเดียวกัน ทำให้จับไม่ได้ด้วย breakpoint ที่ptraceแบบง่าย ๆ เท่านั้น - การเรียก system call โดยตรงถูกเลี่ยงด้วยการค้นหาแพตเทิร์น
mov w16, #26ในไบนารี ตั้ง breakpoint ที่ตำแหน่งsvcแล้วใช้lldb jumpข้ามคำสั่งนั้น - พฤติกรรมที่ทำให้โทรศัพท์ soft-reboot/respring หลังตรวจจับ jailbreak เชื่อมโยงกับฟังก์ชันที่เรียก
snapshotViewAfterScreenUpdates:ใน ลูปไม่สิ้นสุด และถูกข้ามด้วยthread returnที่จุดเริ่มต้นของฟังก์ชัน - crash หลังฉีดโค้ดดูใกล้เคียงกับ การสูญเสียสิทธิ์ App Group มากกว่าการตรวจสอบ framework runtime แยกต่างหาก และทำให้รันบนอุปกรณ์ทั่วไปได้ด้วยการ swizzle ให้
containerURLForSecurityApplicationGroupIdentifier:เปลี่ยนไปใช้ไดเรกทอรีชั่วคราว
แอปวิดเจ็ต iOS ที่ซ้อนกลไกป้องกันหลายชั้น
- เป้าหมายคือแอปวิดเจ็ตบน App Store ซึ่งมี พฤติกรรมป้องกัน ที่แข็งแรงกว่าแอปวิดเจ็ตทั่วไป
- บล็อกการ attach ดีบักเกอร์
- ปิดแอปเมื่อมีการฉีดโค้ด
- หากรันในสภาพแวดล้อม jailbreak โทรศัพท์ทั้งเครื่องจะ soft-reboot/respring
- การใส่กลไกป้องกันอย่างการตรวจจับ jailbreak หรือการทำ code obfuscation ในแอป iOS ไม่ใช่เรื่องแปลก แต่แอปนี้ใช้หลายวิธีร่วมกัน
- พฤติกรรมที่น่าสนใจอื่น ๆ ภายในแอปถูกเก็บไว้เป็นหัวข้อของบทความถัดไป
บล็อกการ attach ดีบักเกอร์ด้วย PT_DENY_ATTACH
- บนอุปกรณ์ jailbreak โดยปกติสามารถเชื่อมต่อด้วย
sshแล้วรันdebugserverจากนั้นเชื่อมต่อจากlldbบนคอมพิวเตอร์อีกเครื่องเพื่อดีบักแอปได้ - เมื่อนำ
debugserverไป attach กับแอปนี้ด้วยวิธีเดียวกัน จะเกิด Segmentation fault และ attach ไม่สำเร็จ - สาเหตุเกี่ยวข้องกับคำขอ
PT_DENY_ATTACHของptraceptraceเป็น private API บน iOS และเป็น public API บน macOSPT_DENY_ATTACHตั้งค่า flag ที่ปฏิเสธการ trace จาก parent process หลังจากนั้น- หากกำลังถูก trace อยู่แล้ว แอปจะจบการทำงานด้วยสถานะ
ENOTSUP - หาก parent พยายาม trace process ที่ตั้งค่า flag นี้ไว้ ฝั่ง parent จะเกิด segmentation violation
- การทำแบบง่ายสามารถเรียก
ptrace(PT_DENY_ATTACH, 0, 0, 0)ได้- เนื่องจากเป็น private API ของ iOS การเรียกจริงต้องใช้
dlopen,dlsymเพื่อค้นหา symbolptraceในlibsystem_kernel.dylib - วิธีนี้เลี่ยงได้ค่อนข้างง่ายด้วยการตั้ง breakpoint ที่
ptraceแล้วใช้thread returnเพื่อข้ามการเรียก
- เนื่องจากเป็น private API ของ iOS การเรียกจริงต้องใช้
ทำไมการเลี่ยงแบบง่ายจึงใช้ไม่ได้
PT_DENY_ATTACHจะกันดีบักเกอร์ได้หลังถูกเรียกเท่านั้น ดังนั้นถ้า attach ก่อนที่โค้ดแอปจะทำงาน ก็สามารถสร้างจุดเลี่ยงได้- หากไม่ attach
debugserverเข้ากับ process โดยตรง แต่รันไว้ก่อน แล้วใช้process attach --name TopWidget --waitforในlldbเพื่อรอการเปิดแอป ก็จะ attach ได้ก่อนที่โค้ดแอปจะทำงาน - แต่ในแอปนี้ breakpoint
b ptraceแรก ๆ ไม่ถูก resolve และแม้ภายหลังจะ resolve ได้ ก็ไม่เคย hit จริงก่อนที่แอปจะจบการทำงาน - เพราะแอปใช้วิธี เรียก system call โดยตรง ที่ให้ผลแบบเดียวกัน แทนการเรียกฟังก์ชัน
ptrace
ค้นหาตำแหน่ง system call โดยตรง
- ใน disassembly ของฟังก์ชัน
ptraceมี system call หลักคือsvc #0x80x0มีค่า31ซึ่งเป็นค่าของPT_DENY_ATTACHx1,x2,x3มี argument ที่ไม่ได้ใช้คือ0x16มีหมายเลข system call ของptraceคือ26
- แอปสามารถไม่เรียกฟังก์ชัน
ptraceแต่ตั้งค่า register แบบเดียวกันด้วย inline assembly แล้วรันsvc #0x80โดยตรงได้- วิธีนี้หลีกเลี่ยงการ lookup private API ที่ดูน่าสงสัยอย่าง
dlopen,dlsym - จับได้ยากด้วยวิธีตั้ง breakpoint ที่
ptraceซึ่งเป็นฟังก์ชันร่วม
- วิธีนี้หลีกเลี่ยงการ lookup private API ที่ดูน่าสงสัยอย่าง
- หากต้องการเลี่ยง ต้องเปิดไบนารีแอปที่ถอดรหัสแล้วด้วย disassembler และหาตำแหน่ง system call ของ
ptrace - เป้าหมายการค้นหาคือ
mov x16, #26หรือmov w16, #26ซึ่งเป็นมุมมอง 32 บิตของ register เดียวกัน- ใช้ armconverter.com เพื่อได้ byte ของ
mov x16, #26คือ50 03 80 D2แล้วนำไปค้นหาในไบนารีได้ mov x16, #26ไม่พบผลลัพธ์ ส่วนการค้นหาmov w16, #26พบ 4 ผลลัพธ์
- ใช้ armconverter.com เพื่อได้ byte ของ
- ในจำนวนนั้น สองผลลัพธ์มีคำสั่งรอบข้างไม่ตรงกับแพตเทิร์นที่คาดไว้ และที่ผลลัพธ์ที่สามพบโค้ดรูปแบบต่อไปนี้
MOV X0, #0x1FMOV X1, #0MOV X2, #0MOV X3, #0MOV W16, #0x1ASVC 0x80
- ผลลัพธ์ที่สี่เป็น branch อื่นของฟังก์ชันเดียวกัน และยืนยันได้ว่าฟังก์ชันนี้คือตำแหน่งที่บล็อกการ attach ดีบักเกอร์
ข้ามคำสั่ง svc
- ที่อยู่
svcที่ยืนยันใน disassembler คือ0x102A2BB14และ0x102A2BB68 - ใน
lldbตั้ง breakpoint โดยใส่-s TopWidgetเพื่อแปลงที่อยู่ตามไบนารีเป็นที่อยู่โหลดจริงbr s -a 0x102A2BB14 -s TopWidgetbr s -a 0x102A2BB68 -s TopWidget
- เมื่อรันต่อ breakpoint จะถูก hit ที่ตำแหน่ง
svc #0x80 - วิธีเลี่ยงที่ง่ายที่สุดคือ jump ไปยังที่อยู่ของคำสั่งถัดไป เพื่อไม่ให้ system call นั้นทำงานเลย
- ในตัวอย่าง รัน
jump *0x10327bb18เพื่อข้ามไปยังที่อยู่ถัดจากคำสั่งปัจจุบันคือ0x10327bb18
- ในตัวอย่าง รัน
- หลังผ่านขั้นตอนนี้ จะสามารถเข้าสู่แอปในสภาพที่ดีบักเกอร์ attach อยู่ได้
พฤติกรรมที่ทำให้โทรศัพท์ soft-reboot
- หลังเลี่ยงการ attach ดีบักเกอร์แล้ว แอปยังคงรันพฤติกรรมป้องกันที่ทำให้โทรศัพท์ soft-reboot/respring
- คราวนี้
lldbยัง attach อยู่ จึงตรวจสอบสถานะที่ process ได้รับSIGKILLและ stacktrace ได้ - ใน stacktrace ปรากฏ flow ที่จับภาพเนื้อหาหน้าจอ
CARenderServerSnapshotของQuartzCore_UISnapshotScreenWindowsRectAfterCommitของUIKitCore- unnamed symbol ใน
TopWidgetซึ่งเป็นฟังก์ชันภายในแอป
- ใช้
lldb image lookupเพื่อแปลง runtime address เป็นที่อยู่ตามไบนารี0x100041898แล้วตรวจสอบฟังก์ชันนั้นใน disassembler - ผล decompile พบว่าฟังก์ชันทำซ้ำเฉพาะงานต่อไปนี้ใน ลูปไม่สิ้นสุด
- เรียก
+[UIScreen mainScreen] - เรียก
snapshotViewAfterScreenUpdates:กับออบเจ็กต์หน้าจอที่ได้คืนมา - ไม่ใช้ผลลัพธ์ และ release ทิ้ง
- เรียก
snapshotViewAfterScreenUpdates:เป็น public API สำหรับสร้าง view snapshot แต่แอปนี้เรียกซ้ำไม่สิ้นสุดซึ่งใช้หน่วยความจำหนัก- ในวิดีโอที่เชื่อมโยงกัน พบว่าต้นทางของการเรียกนี้เกี่ยวข้องกับ notification
com.apple.tw.twrrและหากไม่ผ่าน risk check ของโทรศัพท์ก็จะ respring - สามารถเลี่ยงได้ด้วยการตั้ง breakpoint ที่จุดเริ่มต้นของฟังก์ชันนั้น และเมื่อ hit ให้ใช้
thread returnเพื่อข้ามการทำงานของฟังก์ชัน
crash ที่เกิดเมื่อฉีดโค้ด
- ระหว่างดีบัก สามารถ implement ยูทิลิตีที่ซับซ้อน เช่น การ log ข้อมูล accessibility ของปุ่มบนหน้าจอ ใน framework ที่ฉีดเข้าไปในแอป แล้วเรียกจาก debugger ได้
- แม้ไม่มีอุปกรณ์ jailbreak ก็สามารถฉีด Frida หรือ Flex เพื่อสำรวจแอปเบื้องต้นได้
- โดยทั่วไปการฉีดแบบนี้ทำด้วยเครื่องมือที่ resign แอป ตัวอย่างที่ใช้คือ Sideloadly
- แอปนี้ crash ทันทีเมื่อรันหลัง resign
- เมื่อตรวจที่อยู่ด้านบนของ crash stack คือ
0x1002027D4ใน disassembler พบคำสั่งBRKซึ่งตรงกับสถานการณ์ crash แบบตั้งใจ เช่น nil force-unwrap - ในโค้ดที่ decompile แล้ว การเรียกที่ดูเป็นปัญหาคือ
containerURLForSecurityApplicationGroupIdentifier:- เมธอดนี้คืน URL ของโฟลเดอร์ที่แอปและ extension ใน group เดียวกันเข้าถึงร่วมกันได้
- App Group ถูกกำหนดในกระบวนการ code signing
- เมื่อฉีดโค้ดแล้ว resign ลายเซ็นเดิมของแอปจะถูกทิ้ง และสิทธิ์ App Group ก็หายไปด้วย
- ดังนั้นมีความเป็นไปได้สูงว่าจะคืน
nilแทน URL และแอป crash เมื่อ force-unwrap ค่านี้
- แอปวิดเจ็ตต้องแชร์ App Group กับ extension ที่รับผิดชอบการแสดงวิดเจ็ตบนหน้าจอ Home ดังนั้นปัญหานี้จึงใกล้เคียงกับปัญหา code signing ที่พบบ่อย มากกว่าจะเป็นพฤติกรรมตั้งใจเพื่อป้องกัน
เลี่ยงปัญหา App Group
- วิธีแก้ที่ง่ายที่สุดคือไม่ resign แอป
- บนอุปกรณ์ jailbreak สามารถฉีด framework ด้วย jailbreak tweak ได้โดยไม่ต้อง resign แอป
- การรันโค้ดที่มีลายเซ็นไม่ถูกต้อง หรือเพิ่มแอปที่ต้องการเข้า group ที่ต้องการก็ทำได้เช่นกัน
- หากไม่มีอุปกรณ์ jailbreak และจำเป็นต้อง resign ก็ยังเลี่ยงได้ด้วยการ swizzle เมธอดผ่าน framework ขนาดเล็ก
- โค้ดตัวอย่างแทนที่
containerURLForSecurityApplicationGroupIdentifier:ของNSFileManager- เมื่อเมธอดเดิมถูกเรียก replacement method จะทำงาน
- replacement method จะคืน
temporaryDirectoryแทน shared container
- การแทนที่นี้ไม่เหมือนพฤติกรรมเดิมที่แอปต้องการ
- แอปเดิมต้องการโฟลเดอร์แชร์ที่แอปและ extension เข้าถึงร่วมกันได้
- ไดเรกทอรีชั่วคราวไม่ใช่โฟลเดอร์แชร์แบบนั้น
- แต่ถ้าเป้าหมายคือดูเฉพาะการทำงานของแอปหลัก ก็อาจเพียงพอ
- patch เล็ก ๆ แบบนี้อาจทำให้ app extension เสียหายได้
- หากต้องการเพียงฟังก์ชันพื้นฐานของแอปหลัก ก็อาจไม่เป็นปัญหา
- การเลี่ยงที่ใหญ่กว่านั้นคือสร้าง App Group ใหม่, resign แอปหลักและ extension ทั้งหมดด้วย group นั้น แล้ว swizzle ให้เมธอดที่เกี่ยวข้องใช้ group identifier ใหม่
- เป็นงานที่ใหญ่กว่ามาก และถ้าไม่จำเป็นจริง ๆ การหา jailbroken device อาจดีกว่า
- เมื่อฉีด framework นี้ร่วมกับเครื่องมืออย่าง
Flexแอปจะรันได้ตามปกติแม้บนอุปกรณ์ทั่วไป - บนอุปกรณ์ jailbreak ยังต้องเลี่ยง anti-debugging และการป้องกัน respring ที่กล่าวไปก่อนหน้าอีกครั้ง แต่หลังจากนั้นก็จะเข้าสู่แอปได้ในสภาพที่มี
Flexถูกฉีดอยู่
สถานะสุดท้าย
- ท้ายที่สุด แอปอยู่ในสถานะที่สามารถ attach ดีบักเกอร์, เลี่ยงการตรวจจับ jailbreak และฉีดโค้ดได้ทั้งหมด
- สิ่งที่ต้องการดูจริง ๆ ภายในแอปถูกเก็บไว้เป็นหัวข้อของบทความถัดไป
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
Bryce Bostwick ทำงานที่เจ๋งและสร้างแรงบันดาลใจมากในด้านการดีบักแอปและวิศวกรรมย้อนกลับ
ผมรู้จักเขาจาก YouTube และหลังจากดูวิดีโอที่ดัดแปลง TikTok ให้แสดงเฉพาะวิดีโอแมว (https://youtu.be/YW3jL2gI9IE) ผมก็ลองดัดแปลง Instagram ให้เหลือเฉพาะฟีเจอร์ข้อความที่ผมใช้ แล้วเอาอย่างอื่นออกทั้งหมด
ผมอยากลงลึกเรื่องวิธีดัดแปลง Windows แบบสไตล์ Windhawk (https://windhawk.net/) มานานแล้ว โดยเฉพาะเรื่อง การดัดแปลงและวิศวกรรมย้อนกลับ และ Bryce ก็แนะนำงานแบบนั้นบน iOS ได้ดีมากผ่านวิดีโอสอนทีละขั้นตอนแบบเรียลไทม์
เคยเห็นว่า Revanced ทำอะไรน่าทึ่งได้หลายอย่าง แต่ดูเหมือนไม่ค่อยมีไกด์ดี ๆ ที่บอกว่าจะเริ่มงานแบบนั้นอย่างไร
ถ้ามีใครสรุปขั้นตอนไว้ ผมสนใจ
ผมไม่ชอบที่ต้องเจอ Reels แม้แค่จะอัปโหลดรูปหรือแชตกับเพื่อน
การขัดขวางการดีบัก และแม้แต่เทคนิคที่ขัดขวางการป้องกันการขัดขวางการดีบักนั้นมีมานานแล้วในฝั่ง DOS/Windows
ถ้าดูเอกสารเก่า ๆ เรื่องการแคร็กหรือการแกะแพ็ก จะเห็นว่ามีการพูดถึงเรื่องนี้ในหลายระดับความลึก
ความง่ายที่ผู้ใช้จะควบคุมการทำงานของแอปได้นั้นแปรผกผันกับความเป็นปฏิปักษ์ต่อผู้ใช้ของแพลตฟอร์ม
PT_DENY_ATTACH ดูเหมือนเป็นฟีเจอร์ที่สร้างมาเพื่อความเป็นปฏิปักษ์ต่อผู้ใช้นั้นพอดี
เท่าที่ผมรู้ Windows ไม่มีฟีเจอร์แบบนั้น แต่ใช้เทคนิคให้แอป attach เข้ากับตัวเองแทน
https://www.x86matthew.com/view_post?id=selfdebug
https://anti-debug.checkpoint.com/techniques/interactive.htm...
ค่อนข้างแปลกใจที่การตรวจของ App Store ของ Apple ไม่ปฏิเสธแอปที่ทำ system call โดยตรง
system call บนแพลตฟอร์มของ Apple ไม่ใช่ ABI ที่เสถียร ดังนั้น system call ทั้งหมดควรต้องผ่าน libSystem และแอปที่ทำ system call โดยตรงโดยไม่ผ่าน libSystem ก็เท่ากับกำลังทำสิ่งที่ไม่ควรทำ
เช่นเดียวกัน ผมสงสัยว่าทำไมผู้เขียนถึงมองหา
mov w16, #26ในโค้ด แทนที่จะเป็นsvc 0x80svc 0x80เป็นคำสั่งที่รัน system call ใด ๆ ก็ได้ และการเรียกที่แน่นอนว่าจะรันอะไรนั้นขึ้นอยู่กับ รีจิสเตอร์ x16แอปคงทำ system call ที่ไม่เกี่ยวข้องเป็นจำนวนมาก ดังนั้นการตั้ง breakpoint ตรงนั้นคงไม่ค่อยมีประโยชน์
อย่างน้อยในวิดีโอก็อธิบายไว้แบบนั้น
ด้วยเหตุผลเดียวกัน ถ้าค้นหาคำสั่ง SVC ผลลัพธ์จะออกมาเยอะมาก
ถ้าหา ID ของ system call ที่แน่นอนซึ่งถูกย้ายเข้า X16 ได้ ก็จะเจอทันที
ผมเป็นผู้เขียนบทความเอง ถ้ามีคำถามก็ถามได้ ขอบคุณ xmprt ที่แชร์
ดีใจที่มีเวอร์ชันบทความให้อ่านด้วย
มีบางอย่างอยากถามผู้เขียน: ถ้าเครื่องมือเชิงพาณิชย์ที่มีชื่อเสียงที่สุดคือ Guardsquare จริง ผมสงสัยว่ามันมีอะไรใหม่ ๆ ที่ป้องกันการ disassemble แบบง่าย ๆ อย่างนี้หรือไม่
และก็สงสัยว่า TopWidgets ใช้การป้องกันแบบคล้าย ๆ กันหรือเปล่า หรือเป็นระดับที่ทำขึ้นเอง
ส่วนตัวใช้ Android จึงนำไปใช้ตรง ๆ ในเชิงเทคนิคไม่ได้ แต่ยังได้คุณค่ามากจากการเรียนรู้ว่า การดีบักระดับต่ำ บน iOS ทำงานอย่างไร
วิดีโอด้านบนสุดเป็นหนึ่งในวิดีโอเกี่ยวกับการเขียนโปรแกรมที่ดีที่สุดเท่าที่เคยดูมา
ดำเนินเรื่องเร็ว สมมติพื้นความรู้ได้พอดี และมีเดโมที่ยอดเยี่ยมโดยไม่ทำให้จังหวะของวิดีโอสะดุด
บทความยอดเยี่ยม
ผมสงสัยจริง ๆ ว่านี่เป็นแอปปกติที่หวาดระแวงเกินเหตุ หรือเป็นแอปที่ถูกดีบักเพราะถูกสงสัยว่าเป็นมัลแวร์ตั้งแต่แรก
ถ้าไม่ใช่อย่างหลัง ความพยายามที่ลงไปก็ดูเกินไปพอสมควร
ดูเหมือนพวกเขาทำอะไรค่อนข้างเจ๋งกับวิดเจ็ต เลยพยายามปกป้องสิ่งนั้นไว้ แต่กลยุทธ์พวกนั้นก็เริ่มรั่วไหลออกมาทีละนิดอยู่ดี
ในไบนารีก็มีสิ่งที่น่าสนใจอยู่ด้วย
ครั้งหนึ่งผมพยายามหาคำตอบว่าทำไมถึงเห็นโค้ดที่ดูเหมือนกำลังดาวน์โหลดไฟล์
.isoของ Windows และสุดท้ายมันก็ใช่จริง ๆ โดยถูกใช้ใน วิดเจ็ตทดสอบความเร็วเครือข่ายยังมีโหมดที่ยากกว่า “ข้าม PT_DENY_ATTACH (โหมด Hard)” อีก
สมัยก่อนผมเคย แพตช์เคอร์เนล บน macOS เพื่อทำให้ PT_DENY_ATTACH ไม่ทำอะไรเลย
บน Mac การรันเคอร์เนลที่ถูกแพตช์นั้นจริง ๆ แล้วค่อนข้างง่าย แต่บน iOS น่าจะยุ่งยากกว่ามากเพราะมีสิ่งอย่าง KTRR
แม้ XNU จะเป็นโอเพนซอร์สในเชิงเทคนิค แต่การแพตช์ด้วย hex editor นั้นง่ายกว่าการคอมไพล์ใหม่
และยังอนุญาต code page ที่ไม่ได้เซ็นกับ RWX เพื่อให้ทำ JIT ได้ด้วย
ถ้า “เปิดเจลเบรกแล้วรัน จากนั้นทั้งเครื่องก็แครช” แบบนี้ แจ้งสโตร์ว่าเป็น มัลแวร์ ไม่ได้หรือ?
การทำให้โทรศัพท์แครชเป็นพฤติกรรมของมัลแวร์อย่างชัดเจน และควรสงสัยด้วยว่าอาจมีพฤติกรรมมุ่งร้ายอื่น ๆ ที่พยายามซ่อนอยู่
การกันขยะพวกนี้ไม่ใช่เหตุผลที่ Apple ใช้สนับสนุน ระบบนิเวศแบบปิด หรอกหรือ?
Apple คงไม่ค่อยสนใจนักถ้าแอปแครชบนเครื่องที่เจลเบรกแล้ว
สงสัยมากเรื่องการแจ้งเตือน com.apple.tw.twrr
ทำไมถึงขึ้นต้นด้วย
com.apple?แอปที่เป็นปัญหาตรงนี้ดูเหมือนเป็นแอปชื่อ Top Widgets ไม่ใช่แอปของ Apple
ตามธรรมเนียมจะใช้ “ชื่อเต็ม” แบบนั้นเพื่อเลี่ยงการชนกัน
ในกรณีนี้ดูเหมือนนักพัฒนาเลือก prefix แบบนั้นโดยบังเอิญ
ต่างกันแค่เครื่องมือ แต่นี่ให้ความรู้สึกเหมือนกับการใช้ boot tracing เพื่อแกะระบบป้องกันการคัดลอกของ Apple II ในยุค 1980 เป๊ะ ๆ
บางอย่างก็ไม่เปลี่ยนไป