1 điểm bởi GN⁺ 2025-02-19 | 1 bình luận | Chia sẻ qua WhatsApp
  • Reverse engineering ứng dụng iOS đòi hỏi khả năng quan sát và thao tác ứng dụng đang chạy, nhưng ứng dụng widget này dùng đồng thời chặn debugger, chặn code injection và phát hiện jailbreak
  • Cơ chế chặn cốt lõi có thể được triển khai bằng PT_DENY_ATTACH của ptrace hoặc một syscall trực tiếp có hiệu ứng tương tự (svc #0x80), nên chỉ đặt breakpoint đơn giản vào ptrace sẽ không bắt được
  • Syscall trực tiếp được vượt qua bằng cách tìm mẫu mov w16, #26 trong binary, đặt breakpoint tại vị trí svc, rồi dùng lldb jump để bỏ qua lệnh đó
  • Hành vi soft-reboot/respring điện thoại sau khi phát hiện jailbreak được liên kết với một hàm gọi snapshotViewAfterScreenUpdates: trong vòng lặp vô hạn, và được bỏ qua bằng thread return tại điểm bắt đầu hàm
  • Crash sau code injection có vẻ gần với việc mất quyền App Group hơn là một kiểm tra framework runtime riêng; bằng cách swizzle containerURLForSecurityApplicationGroupIdentifier: để trỏ tới thư mục tạm, ứng dụng có thể chạy được cả trên thiết bị thông thường

Ứng dụng widget iOS chồng nhiều lớp bảo vệ

  • Đối tượng là một ứng dụng widget trên App Store, có các hành vi phòng vệ mạnh hơn ứng dụng widget thông thường
    • Chặn debugger attach
    • Thoát ứng dụng khi có code injection
    • Nếu chạy trong trạng thái jailbreak, toàn bộ điện thoại sẽ soft-reboot/respring
  • Việc ứng dụng iOS đưa vào các cơ chế bảo vệ như phát hiện jailbreak hay làm rối mã không phải hiếm, nhưng ứng dụng này dùng nhiều cách cùng lúc
  • Các hành vi thú vị khác bên trong ứng dụng được để lại làm chủ đề cho bài viết sau

Chặn debugger attach bằng PT_DENY_ATTACH

  • Trên thiết bị jailbreak, thường có thể kết nối bằng ssh, chạy debugserver, rồi kết nối từ lldb trên một máy tính khác để gỡ lỗi ứng dụng
  • Khi gắn debugserver vào ứng dụng này theo cùng cách, xảy ra Segmentation fault và attach thất bại
  • Nguyên nhân liên quan đến request PT_DENY_ATTACH của ptrace
    • ptrace là private API trên iOS và public API trên macOS
    • PT_DENY_ATTACH đặt một flag từ chối việc trace của tiến trình cha về sau
    • Nếu đang bị trace sẵn, ứng dụng sẽ thoát với trạng thái ENOTSUP
    • Nếu tiến trình cha cố trace một tiến trình đã đặt flag này, phía tiến trình cha sẽ gặp segmentation violation
  • Cách triển khai đơn giản có thể là gọi ptrace(PT_DENY_ATTACH, 0, 0, 0)
    • Vì là iOS private API, khi gọi thực tế cần tìm symbol ptrace trong libsystem_kernel.dylib bằng dlopen, dlsym
    • Cách này tương đối dễ vượt qua bằng cách đặt breakpoint vào ptrace rồi dùng thread return để bỏ qua lời gọi

Vì sao cách vượt đơn giản không hiệu quả

  • PT_DENY_ATTACH chỉ chặn debugger sau khi được gọi, nên nếu attach trước khi code ứng dụng chạy thì có thể tạo điểm vượt qua
  • Thay vì gắn debugserver trực tiếp vào tiến trình, có thể chạy nó trước rồi trong lldb dùng process attach --name TopWidget --waitfor để chờ ứng dụng khởi chạy, nhờ đó attach trước khi code ứng dụng chạy
  • Tuy nhiên trong ứng dụng này, breakpoint b ptrace ban đầu không được resolve, và kể cả sau đó khi được resolve thì ứng dụng vẫn thoát mà breakpoint không hề bị hit
  • Lý do là ứng dụng đã dùng syscall trực tiếp có cùng hiệu ứng thay vì gọi hàm ptrace

Tìm vị trí syscall trực tiếp

  • Trong disassembly của hàm ptrace, về cốt lõi có syscall svc #0x80
    • x0 chứa giá trị 31, tức PT_DENY_ATTACH
    • x1, x2, x3 chứa các tham số không dùng đến là 0
    • x16 chứa số syscall của ptrace, là 26
  • Ứng dụng có thể không gọi hàm ptrace, mà tự thiết lập cùng các giá trị thanh ghi bằng inline assembly rồi thực thi trực tiếp svc #0x80
    • Cách này tránh các lookup private API đáng nghi như dlopen, dlsym
    • Khó bắt bằng cách đặt breakpoint vào hàm chung ptrace
  • Để vượt qua, cần mở binary ứng dụng đã được giải mã bằng disassembler và tìm vị trí syscall ptrace
  • Mục tiêu tìm kiếm là mov x16, #26 hoặc view 32-bit của cùng thanh ghi là mov w16, #26
    • Có thể dùng armconverter.com để lấy byte 50 03 80 D2 của mov x16, #26 và dùng để tìm trong binary
    • mov x16, #26 không cho kết quả, còn tìm mov w16, #26 cho 4 kết quả
  • Trong đó, hai kết quả có các lệnh xung quanh không khớp với mẫu dự kiến; ở kết quả thứ ba, đoạn code dạng sau được xác nhận
    • MOV X0, #0x1F
    • MOV X1, #0
    • MOV X2, #0
    • MOV X3, #0
    • MOV W16, #0x1A
    • SVC 0x80
  • Kết quả thứ tư là một branch khác của cùng hàm, và hàm này được xác nhận là vị trí chặn debugger attach

Bỏ qua lệnh svc

  • Các địa chỉ svc được xác nhận trong disassembler là 0x102A2BB140x102A2BB68
  • Trong lldb, để chuyển địa chỉ theo binary thành địa chỉ load thực tế, đặt breakpoint kèm -s TopWidget
    • br s -a 0x102A2BB14 -s TopWidget
    • br s -a 0x102A2BB68 -s TopWidget
  • Khi tiếp tục chạy, breakpoint sẽ dừng tại vị trí svc #0x80
  • Cách vượt dễ nhất là jump tới địa chỉ lệnh tiếp theo để không thực thi chính syscall
    • Trong ví dụ, chạy jump *0x10327bb18 tới địa chỉ ngay sau lệnh hiện tại là 0x10327bb18
  • Sau quá trình này, có thể đi vào bên trong ứng dụng trong trạng thái debugger đã attach

Hành vi soft-reboot điện thoại

  • Ngay cả sau khi vượt qua việc debugger attach, ứng dụng vẫn thực thi hành vi bảo vệ khiến điện thoại soft-reboot/respring
  • Lần này lldb vẫn đang attach, nên có thể kiểm tra trạng thái tiến trình nhận SIGKILL và stacktrace
  • Stacktrace cho thấy luồng chụp nội dung màn hình
    • CARenderServerSnapshot của QuartzCore
    • _UISnapshotScreenWindowsRectAfterCommit của UIKitCore
    • unnamed symbol trong ứng dụng TopWidget
  • Dùng lldb image lookup để chuyển địa chỉ runtime thành địa chỉ theo binary 0x100041898, rồi kiểm tra hàm tương ứng trong disassembler
  • Kết quả decompile cho thấy hàm chỉ lặp lại các tác vụ sau trong vòng lặp vô hạn
    • Gọi +[UIScreen mainScreen]
    • Gọi snapshotViewAfterScreenUpdates: trên đối tượng màn hình trả về
    • Kết quả không được dùng và bị release
  • snapshotViewAfterScreenUpdates: là public API để tạo view snapshot, nhưng ứng dụng này lặp vô hạn một lời gọi tiêu tốn nhiều bộ nhớ
  • Trong video liên quan, nguồn gốc của lời gọi này được xác nhận là liên quan đến notification com.apple.tw.twrr, và nếu không vượt qua risk check với điện thoại thì sẽ bị respring
  • Có thể vượt qua bằng cách đặt breakpoint tại điểm bắt đầu hàm đó và khi hit thì dùng thread return để bỏ qua việc thực thi hàm

Crash khi code injection

  • Khi gỡ lỗi, có thể triển khai các tiện ích phức tạp như logging thông tin accessibility của các nút trên màn hình trong một framework được inject vào ứng dụng, rồi gọi từ debugger
  • Ngay cả khi không có thiết bị jailbreak, có thể inject Frida hoặc Flex để khảo sát ứng dụng ban đầu
  • Thông thường kiểu injection này được thực hiện bằng các công cụ resign ứng dụng; trong ví dụ, Sideloadly được dùng
  • Ứng dụng này crash ngay khi chạy sau khi resign
  • Nhìn địa chỉ trên cùng của crash stack 0x1002027D4 trong disassembler sẽ thấy lệnh BRK, khớp với tình huống crash có chủ ý như force-unwrap nil
  • Trong code đã decompile, lời gọi có vẻ gây vấn đề là containerURLForSecurityApplicationGroupIdentifier:
    • Method này trả về URL thư mục mà ứng dụng và extension trong cùng group có thể cùng truy cập
    • App Group được định nghĩa trong quá trình ký mã
    • Khi resign sau code injection, chữ ký ứng dụng ban đầu bị loại bỏ và quyền App Group cũng mất
    • Vì vậy có khả năng nó trả về nil thay vì URL, rồi ứng dụng force-unwrap giá trị này và crash
  • Vì ứng dụng widget cần chia sẻ App Group với extension phụ trách hiển thị widget trên màn hình chính, vấn đề này gần với lỗi ký mã phổ biến hơn là một hành vi có chủ đích để phòng vệ

Vượt qua vấn đề App Group

  • Cách giải quyết dễ nhất là không resign ứng dụng
    • Trên thiết bị jailbreak, có thể inject framework bằng jailbreak tweak mà không resign ứng dụng
    • Cũng có thể chạy code có chữ ký không hợp lệ hoặc thêm ứng dụng mong muốn vào group mong muốn
  • Nếu không có thiết bị jailbreak và vẫn cần resign, có thể vượt qua bằng cách swizzle method với một framework nhỏ
  • Code ví dụ thay thế containerURLForSecurityApplicationGroupIdentifier: của NSFileManager
    • Khi method gốc được gọi, replacement method sẽ chạy
    • Replacement method trả về temporaryDirectory thay vì shared container
  • Sự thay thế này không giống với hành vi ứng dụng vốn mong muốn
    • Ứng dụng gốc muốn một thư mục dùng chung mà cả ứng dụng và extension đều có thể truy cập
    • Thư mục tạm không phải là thư mục dùng chung như vậy
  • Tuy nhiên, nếu mục tiêu chỉ là xem xét hoạt động của ứng dụng chính thì có thể là đủ
    • Bản patch nhỏ như vậy có thể làm hỏng app extension
    • Nếu chỉ cần các chức năng cơ bản của ứng dụng chính thì có thể không thành vấn đề
  • Cách vượt lớn hơn là tạo App Group mới, resign ứng dụng chính và tất cả extension vào group đó, rồi swizzle các method liên quan để dùng group identifier mới
    • Đây là công việc lớn hơn nhiều, và nếu không thật sự cần thì có lẽ nên kiếm một thiết bị jailbroken
  • Khi inject framework này cùng một công cụ như Flex, ứng dụng chạy bình thường cả trên thiết bị thông thường
  • Trên thiết bị jailbreak, vẫn cần vượt lại các cơ chế anti-debugging và bảo vệ respring ở trên, nhưng sau đó có thể đi vào trong ứng dụng với Flex đã được inject

Trạng thái cuối cùng

  • Cuối cùng, ứng dụng đạt trạng thái có thể debugger attach, vượt phát hiện jailbreak và code injection
  • Việc thực tế muốn xem gì bên trong ứng dụng được để lại làm chủ đề cho bài viết tiếp theo

1 bình luận

 
GN⁺ 2025-02-19
Ý kiến trên Hacker News
  • Bryce Bostwick làm những việc thật sự rất hay và truyền cảm hứng trong mảng gỡ lỗi ứng dụng và kỹ nghệ đảo ngược
    Tôi biết đến anh ấy trên YouTube, rồi sau khi xem video chỉnh sửa TikTok để chỉ hiện video mèo (https://youtu.be/YW3jL2gI9IE), tôi đã thử chỉnh Instagram để loại bỏ mọi thứ, chỉ giữ lại tính năng nhắn tin mà tôi dùng
    Từ lâu tôi đã muốn đào sâu hơn cách chỉnh sửa Windows theo kiểu Windhawk(https://windhawk.net/), đặc biệt là modding và kỹ nghệ đảo ngược, và Bryce giới thiệu rất tốt những việc kiểu đó trên iOS bằng các video từng bước theo thời gian thực

    • Nếu bên Android cũng có người tương tự thì tôi muốn học thêm
      Tôi đã thấy có thể làm được những thứ đáng kinh ngạc với Revanced, nhưng có vẻ không có nhiều hướng dẫn hay về cách bắt đầu làm những việc đó
    • Tôi sẽ thử tự làm theo những gì được mô tả
      Nếu có ai tổng hợp lại quy trình thì tôi rất quan tâm
      Tôi ghét việc chỉ muốn đăng ảnh hoặc chat với bạn bè mà vẫn bị phơi trước Reels
  • Chống gỡ lỗi, thậm chí cả các kỹ thuật chặn lại việc vô hiệu hóa chống gỡ lỗi, đã phổ biến từ lâu ở phía DOS/Windows
    Các tài liệu cracking hoặc unpacking cũ đề cập đến những nội dung này ở nhiều mức độ sâu khác nhau
    Việc người dùng có thể dễ dàng kiểm soát hành vi của ứng dụng đến đâu tỉ lệ nghịch với mức độ nền tảng đó thù địch với người dùng
    PT_DENY_ATTACH trông đúng như một tính năng được tạo ra vì sự thù địch với người dùng đó
    Theo tôi biết thì Windows không có tính năng như vậy; thay vào đó người ta dùng kỹ thuật khiến ứng dụng tự attach vào chính nó
    https://www.x86matthew.com/view_post?id=selfdebug
    https://anti-debug.checkpoint.com/techniques/interactive.htm...

    • Đúng vậy. PT_DENY_ATTACH trước đây là tính năng Apple tự tay tạo ra theo đúng nghĩa đen như một phần trong giải pháp DRM của iTunes
  • Hơi ngạc nhiên là quy trình kiểm duyệt App Store của Apple không từ chối các ứng dụng thực hiện lời gọi hệ thống trực tiếp
    Trên nền tảng Apple, lời gọi hệ thống không phải ABI ổn định, nên mọi lời gọi hệ thống phải đi qua libSystem; ứng dụng gọi hệ thống trực tiếp mà không qua libSystem về cơ bản là đang làm điều không nên làm
    Tương tự, tôi cũng tò mò vì sao ở đây tác giả lại tìm mov w16, #26 trong mã thay vì svc 0x80

    • svc 0x80 là lệnh thực thi bất kỳ lời gọi hệ thống nào, còn chính xác lời gọi nào được thực thi thì do thanh ghi x16 quyết định
      Ứng dụng sẽ thực hiện rất nhiều lời gọi hệ thống không liên quan, nên đặt breakpoint vào đó sẽ không hữu ích
      Ít nhất trong video, họ giải thích như vậy
    • Trình biên dịch đôi khi có thể inline wrapper lời gọi hệ thống, nên kiểm tra tĩnh không dễ lắm
      Cũng vì lý do đó, nếu tìm lệnh SVC thì sẽ ra cực kỳ nhiều kết quả
      Nếu tìm đúng ID lời gọi hệ thống được chuyển vào X16 thì có thể phát hiện ngay
  • Tôi là tác giả bài viết. Nếu có câu hỏi thì tôi sẽ trả lời. Cảm ơn xmprt đã chia sẻ

    • Tôi đã xem video trên YouTube và thấy rất thú vị
      Cũng rất vui vì có cả phiên bản bài viết
    • Bài viết rất thú vị, và tôi luôn muốn thấy những bài kiểu kỹ nghệ đảo ngược điện thoại ở mức thấp như thế này
      Tôi muốn hỏi tác giả vài điều: nếu công cụ thương mại nổi tiếng nhất là Guardsquare, liệu họ có cung cấp thứ gì mới để ngăn việc disassemble dễ dàng như thế này không
      Tôi cũng tò mò liệu TopWidgets có dùng kiểu bảo vệ tương tự hay chỉ là mức tự làm
    • Các video rất thú vị, tôi ngạc nhiên là chưa có nhiều người xem hoặc đọc bài hơn
      Cá nhân tôi dùng Android nên không áp dụng kỹ thuật trực tiếp được, nhưng vẫn học được rất nhiều giá trị từ việc hiểu gỡ lỗi mức thấp trên iOS hoạt động thế nào
    • Tôi tò mò liệu iOS có tính năng giống PTRACE_SYSCALL để móc vào điểm vào của lời gọi hệ thống nhằm thay đổi giá trị trả về, hoặc phát hiện SVC phát sinh ở đâu không
  • Video ở đầu bài là một trong những video lập trình hay nhất tôi từng xem
    Nhịp trình bày nhanh, giả định đúng mức kiến thức nền cần có, và có các phần demo rất tốt mà không làm đứt mạch video

  • Bài viết xuất sắc
    Tôi thật sự tò mò đây là một ứng dụng bình thường nhưng hoang tưởng quá mức, hay là một ứng dụng vốn đã bị nghi là malware nên mới bị đem ra gỡ lỗi
    Nếu không thì công sức bỏ ra có vẻ khá quá đà

    • Theo đánh giá của tôi, nó gần với một ứng dụng hoang tưởng quá mức
      Nó làm được những thứ khá hay với widget nên có lẽ họ muốn bảo vệ điều đó. Dù vậy, các chiến lược kiểu này rồi cũng bắt đầu rò rỉ dần
      Trong binary cũng có vài thứ thú vị
      Có lúc tôi cố tìm hiểu vì sao mình lại thấy đoạn mã trông như đang tải Windows .iso, và hóa ra đúng là vậy; nó được dùng cho widget kiểm tra tốc độ mạng
    • Hoặc cũng có thể là để chứng minh vi phạm bản quyền nếu chính ứng dụng đó bị biên dịch lại với logo khác, v.v.
  • Còn có chế độ khó hơn cả “Vượt PT_DENY_ATTACH (hard mode)”
    Trước đây trên macOS, tôi từng vá kernel để PT_DENY_ATTACH không làm gì cả
    Trên Mac, chạy kernel đã vá thực ra khá dễ, nhưng trên iOS thì có lẽ rắc rối hơn nhiều vì những thứ như KTRR
    XNU về mặt kỹ thuật là mã nguồn mở, nhưng vá bằng trình hex editor dễ hơn so với biên dịch lại

    • Cũng có thể dùng kernel task port để lật bit trong cấu trúc proc
      Có thể cho phép cả trang mã chưa ký và RWX để hỗ trợ JIT
  • Nếu “bật jailbreak rồi chạy là toàn bộ điện thoại crash”, chẳng phải nên báo cáo lên store là malware sao?
    Làm điện thoại crash rõ ràng là hành vi của malware, và cũng nên nghi ngờ còn hành vi độc hại nào khác đang bị che giấu
    Chẳng phải việc ngăn thứ rác rưởi như vậy là lý do biện minh cho hệ sinh thái đóng của Apple sao?

    • “Bật jailbreak” nghĩa là ở ngoài hệ sinh thái đóng
      Tôi nghĩ Apple sẽ không mấy quan tâm việc ứng dụng crash trên điện thoại đã jailbreak
  • Tôi rất tò mò về thông báo com.apple.tw.twrr
    Vì sao nó bắt đầu bằng com.apple?
    Ứng dụng đang nói đến ở đây có vẻ không phải ứng dụng Apple mà là ứng dụng tên Top Widgets

    • Tên thông báo là chuỗi tùy ý
      Thông lệ là dùng một “tên đầy đủ” kiểu như vậy để tránh xung đột
      Trong trường hợp này có lẽ nhà phát triển tình cờ chọn tiền tố đó
  • Chỉ khác công cụ thôi, chứ cảm giác này giống hệt việc phá bảo vệ sao chép Apple II bằng boot tracing hồi thập niên 1980
    Có những thứ không thay đổi