Mô phỏng iPhone bằng QEMU
(eshard.com)- eShard đặt mục tiêu xây dựng một trình giả lập có thể khởi động iOS 14 trên QEMU, hiển thị UI và chạy được một số ứng dụng, dựa trên các nỗ lực mô phỏng iOS mã nguồn mở trước đây
- Thay vì đưa trực tiếp các bản vá kernel vào bên trong QEMU, họ dùng PongoOS và checkra1n KPF để tách riêng các bản vá XNU, đồng thời tạo công cụ dựa trên diff Mach-O để có thể rà soát nội dung bản vá
- Vì phạm vi mô phỏng GPU Apple Silicon quá lớn, họ trước tiên chọn render bằng phần mềm, và xác nhận qua bản vá
QuartzCoretrên iPhone jailbreak rằng UI UIKit có thể được vẽ, dù chậm - Để giải quyết vấn đề màn hình vẫn đen, họ tiếp tục tắt ngẫu nhiên hóa địa chỉ, debug bằng GDB, vượt qua cơ chế pairing của
lockdownd, tắt PAC, port sang QEMU 8.2.1 và tự động hóa việc vá dyld cache - Cuối cùng, họ hiển thị được UI nhập mật mã của UIKit trên màn hình QEMU và thao tác textbox bằng đầu vào bàn phím qua VNC, đồng thời có được nền tảng cần thiết để hiển thị
SpringBoard
Điểm khởi đầu của mô phỏng iOS
- Khi xem xét các giải pháp mã nguồn mở hiện có, alephsecurity/xnu-qemu-arm64 từng chạy được trong trải nghiệm thực tế, nhưng dự án đã ở trạng thái chỉ đọc
- Sau đó, TrungNguyen1909/qemu-t8030 được dùng làm điểm xuất phát
- Có thể khôi phục iOS thông qua kết nối USB bằng QEMU “companion” thứ hai
- Hỗ trợ chạy iOS 14
- Dựa trên phiên bản QEMU mới hơn
- Cung cấp wiki hướng dẫn cách chạy trình giả lập
- Bằng cách chỉnh sửa
System/Library/xpc/launchd.plist, họ nhanh chóng có được quyền truy cập shell và SSH - Mục tiêu dài hạn là mô phỏng iOS có tính chức năng, có UI và có thể chạy tối thiểu một số ứng dụng
Tách bản vá kernel bằng PongoOS
- Dự án
t8030đưa mã vá kernel XNU vào chính QEMU, nhưng vì số lượng bản vá có thể tăng trong tương lai, cần một cấu trúc gọn gàng hơn - Dựa trên kinh nghiệm với iPhone jailbreak thật, họ xem xét cách dùng PongoOS để áp dụng các bản vá checkra1n
- Trong luồng jailbreak thông thường, sau khi bị pwn bằng checkmate, PongoOS được inject vào SRAM và module
checkra1n-kpfđược truyền qua USB- Trong công việc này, để tránh xử lý USB ban đầu, họ tăng SRAM của iPhone được mô phỏng và dùng PongoOS cùng module checkra1n KPF
- Ở giai đoạn đầu khi chạy PongoOS, phát sinh vấn đề vì không có mã khởi tạo mà bootrom hoặc iBoot thường thực hiện
- Cần thiết lập FPU trước các lệnh double/float
- Họ giải quyết bằng cách tham khảo tài liệu ARM và mã liên quan đến QEMU hiện có
- Pongo không hỗ trợ các tính năng của thiết bị từ A13 trở đi, làm hỏng việc khớp mẫu của một số bản vá
- Có thêm các lệnh Pointer Authentication (PAC) như
autda,xpacd - Apple dùng slide khác
- Ở bản vá
task_for_pid(tfp0), xác nhận có khác biệt về địa chỉ và mẫu nhị phân giữa iPhone X và iPhone 11
- Có thêm các lệnh Pointer Authentication (PAC) như
Tệp vá kernel dạng khai báo
- Pongo cho phép dùng các bản vá checkra1n hiện có cho nhiều phiên bản iOS, nhưng cách áp dụng động khó đọc, khó sửa và khó chia sẻ
- Để xử lý như các bản vá mã thực sự, họ tạo tệp bản vá dạng khai báo bằng công cụ nội bộ
- Diff hai
Mach-Ođể tạo tệp vá dạng văn bản dựa trên khác biệt assembly - Viết một chương trình riêng để áp dụng tệp vá đã tạo vào binary
- Diff hai
- Sau khi boot bằng Pongo, họ dùng QEMU monitor để dump các vùng bộ nhớ mà Pongo đã vá
- Sau đó, họ tái dựng kernel đã vá và tạo một tệp vá lớn chứa toàn bộ thay đổi
- Bằng cách chia nhỏ tệp vá lớn và thêm chú thích, họ có thể rà soát và kiểm soát phần nào của kernel được vá
Chiến lược vẽ màn hình khi không có GPU
- Việc render đồ họa trên iPhone đời mới cuối cùng đều đi qua API Metal của Apple và cần GPU thật
- Họ cho rằng mô phỏng GPU Apple Silicon là quá phức tạp, nên xem xét hai lựa chọn
- Render bằng phần mềm dùng bootarg
gpu=0như từng khả thi trên các bản iOS cũ - Chuyển tiếp các lời gọi Metal sang iPhone thật hoặc máy Mac chạy macOS để thực hiện render
- Render bằng phần mềm dùng bootarg
- Trên iOS 14, tùy chọn bootarg
gpu=0của kernel XNU đã không còn - Phân tích framework
QuartzCorebằng Ghidra cho thấy render phần mềm được gọi như cơ chế fallback khi không có rendererMetal - Trên iPhone jailbreak thật, họ vá
QuartzCorevà xác nhận việc sử dụng render phần mềm- UI chậm hơn nhiều
- Một số vùng xuất hiện artifact, có thể vì các phần đó yêu cầu render trực tiếp bằng
Metal
- Thử nghiệm này cho thấy trong phạm vi không dùng trực tiếp
MetalhayOpenGL, tức phần lớn ứng dụng UIKit, QEMU cũng có thể render bằng phần mềm
Thử nghiệm proxy lời gọi Metal
- Họ cũng thử phương án proxy lời gọi Metal với hai iPhone vật lý
- Parse toàn bộ header iOS bằng LLVM
- Biểu diễn con trỏ đối tượng Objective-C trên server thành con trỏ stub ở client
- Tự động tạo mã trao đổi struct và pointer
- Hook toàn bộ hàm và method
- Chuyển tiếp mọi lời gọi sang server và trả về kết quả thực thi
- Một số vòng gọi cơ bản trong quá trình khởi tạo Metal đã thành công
- Tuy nhiên, ngôn ngữ Objective-C và API
Metalphức tạp, nhiều tính năng, nên khối lượng công việc để chạy thực sự là rất lớn - Họ tạm hoãn cách này và quyết định giải quyết các vấn đề khác trước bằng render phần mềm, dù có giới hạn
- Các framework iOS cũng lộ ra private API không có trong header công khai; tuy có cách parse để tạo header, đa số rất khó dùng trực tiếp và làm tăng độ phức tạp
Debug IOSurface và framebuffer
- Ngay cả sau khi thử render phần mềm, vẫn cần ít nhất một thiết bị framebuffer tối thiểu, nhưng QEMU
t8030gốc chưa triển khai phần này - Họ tìm thấy fork QEMUAppleSilicon có công việc hỗ trợ IOMFB và dùng nó để debug hiển thị
- Khi khôi phục iOS bằng phiên bản này, logo Apple và thanh tiến trình xuất hiện, nhưng khi boot bình thường thì màn hình vẫn hoàn toàn đen
- Sau khi xem kext IOMFB bằng Ghidra và kiểm tra triển khai framebuffer của QEMU, có vẻ tồn tại hai chế độ
- Raw framebuffer tại một địa chỉ phần cứng cố định
- API phức tạp hơn, thiết lập nhiều plane bằng register và ghi dữ liệu surface bằng DMA
- Có thể hiển thị một ARGB surface tùy ý bằng raw framebuffer, nhưng trong quá trình boot hệ thống không ghi vào framebuffer đó
- Ở chế độ display thứ hai, trace cho thấy kernel thiết lập graphical plane bằng register, nhưng sau đó không có đầu ra màn hình
Tắt ngẫu nhiên hóa địa chỉ và debug bằng GDB
- Chỉ có SSH thì việc quan sát hệ thống đang chạy bị giới hạn, nên cần debug kernel và user space bằng GDB
- Ngẫu nhiên hóa địa chỉ kernel được thiết lập trong phần khởi tạo board
t8030, nên có thể tắt hoàn toàn - Userland có ngẫu nhiên hóa file thực thi và ngẫu nhiên hóa thư viện động bên trong dyld cache
- File thực thi được tắt bằng cách vá hàm
_load_machfilecủa kernel - Các thư viện trong dyld cache được ngẫu nhiên hóa địa chỉ một lần khi boot, rồi được nạp ở cùng địa chỉ cho mọi file thực thi
- File thực thi được tắt bằng cách vá hàm
- dyld cache tại
/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64echứa toàn bộ thư viện framework dưới dạng một blob nhị phân lớn - Họ viết công cụ C để
dlopentoàn bộ thư viện framework và liệt kê các image đã load cùng offset bằng các hàm_dyld* - Kết hợp cách này với quá trình đảo ngược địa chỉ GDB, họ có thể debug các thư viện trong dyld cache
- Các đối tượng đặc biệt được quan tâm là kext
IOMFB,backboardd,SpringBoard,QuartzCore - Sau đó, họ cũng tìm được cách tắt dyld cache bằng bản vá kernel, và dùng thư viện Rust object của dự án Gimli để tìm trực tiếp địa chỉ ảo của dyld cache trên host
- Việc debug user space cần GDB server trong guest; ví dụ, họ dùng gói
debugservercủa Procursus
Log hệ thống và vượt qua lockdownd
- Qua GDB,
backboarddcó vẻ khởi động bình thường, nhưng để hiểu tình hình thực tế cần log hệ thống - Trên iPhone thật, sau khi pairing USB với máy tính, có thể xem log hệ thống bằng
idevicesyslog - Quá trình pairing bao gồm tạo cặp khóa, private key được lưu trên iPhone và
lockdowndxác minh danh tính máy tính - Trong môi trường mô phỏng, tương tác USB khả dụng nhưng
lockdowndkhông hoạt động đúng - Phân tích bằng Ghidra cho thấy
lockdowndcố dùngkeybagđể lưu private key, việc này yêu cầu SEP không tồn tại - Họ inject shellcode thay thế một số hàm hiện có để đọc cặp public/private key đã tạo sẵn từ filesystem và nạp lại mỗi khi
lockdowndcố lấy từkeybag - Bằng debug và vá bổ sung, họ mô phỏng việc người dùng đã tin cậy máy tính và iPhone đã được mở khóa
- Cuối cùng, có thể pairing với iPhone được mô phỏng từ companion QEMU
- Log cho thấy
QuartzCorekhởi tạo bình thường, phát hiện kích thước màn hình và sử dụng fallback render phần mềm - Mọi thứ trông như bình thường, nhưng màn hình vẫn không hiển thị
- Một lỗi đơn lẻ liên quan đến pixel format được vượt qua bằng cách ép chỉ định RGBA, nhưng về sau cách này được gỡ bỏ
Vấn đề PAC và port sang QEMU 8
- Khi cố sửa lỗi pixel format của
backboardd, các vấn đề bổ sung do tính năng bảo mật của iOS lộ ra - Kiểm tra chữ ký tại thời điểm load và runtime đã được giải quyết bằng bản vá kernel, nhưng khi chạy
backboarddđã chỉnh sửa, xảy ra lỗi Pointer Authentication - Pointer Authentication là tính năng được thêm trong ARMv8.3, và đây là vấn đề mới gặp trên board
t8030đang được mô phỏng, thay vì t8015 từng dùng trước đó - Ban đầu, họ nghĩ đến cách thay tất cả lệnh PAC bằng NOP hoặc lệnh tương đương không PAC
- Sau đó, họ xác nhận binary ARM64 PAC có thể được build theo hai cách
- Dùng tập lệnh PAC chuyên biệt chỉ chạy trên CPU ARMv8.3+
- Dùng tập lệnh “chưa sử dụng”, được diễn giải là PAC trên ARMv8.3+ và là lệnh tương đương không PAC trên ARM cũ
- Họ kiểm chứng hành vi này trên buildroot và hệ thống ARM64 Linux, đồng thời xác nhận binary cho
t8030dùng arm64e, tập lệnh tương thích ngược - Họ cho rằng chỉ cần tắt PAC enforcing trong QEMU thì mã sẽ chạy như mã không PAC, nhưng QEMU 7 không hoạt động như vậy, còn QEMU 8 có hành vi khác
- Họ port codebase hiện tại sang QEMU 8.2.1
- Các lệnh riêng của Apple
genter,gexit - Mã xử lý GL exception levels
- Việc port khó vì có nhiều chỉnh sửa vào generic code của QEMU
- Các lệnh riêng của Apple
- Sau nhiều lần XNU panic, debug kernel bằng GDB, debug chính QEMU và git bisect, họ boot lại được iOS trên QEMU 8
- Nhờ đó có thể tắt PAC và sửa bất kỳ đoạn mã thực thi nào tại vị trí mong muốn
Truy tìm nguyên nhân màn hình đen
- Vì log hệ thống cho thấy
backboarddcó vẻ hoạt động bình thường, họ truy sâu hơn lý do không hiển thị - Nếu ghi trực tiếp raw ARGB frame vào địa chỉ, màn hình thật sự thay đổi, và có thể vẽ lên nhiều graphical plane
- Do đó bản thân triển khai display có vẻ bình thường, còn lại ba khả năng
backboarddkhông ghi gì cả- Ghi vào địa chỉ không đúng
- Dữ liệu được ghi không hợp lệ
- Họ dùng QEMU monitor để lấy các địa chỉ vật lý không liên tục, rồi dùng script dump bộ nhớ DMA vật lý và ghép thành một file
- Dù diễn giải bằng
ffplaynhư ARGB frame, họ không thu được kết quả có ý nghĩa - Tiếp theo, họ đặt breakpoint tại
iosurface_lockđể lấy địa chỉ surface được map vào bộ nhớbackboarddvà kiểm tra - Đôi khi phát hiện hình dạng kỳ lạ trông giống logo Apple, nhưng có vẻ có vấn đề với cách ghi frame
- Khi thực hiện cùng thao tác trên iPhone 10 thật, họ dễ dàng dump được raw ARGB frame đầy đủ của màn hình hiện tại
- Trên iPhone 11, tức từ
t8030trở đi, surface có vẻ được truyền ở dạng nén mà GPU có thể xử lý - Vì điều này không xảy ra trên
t8015của iPhone X, họ sửa DTB của QEMU để truyềnchip-idlà 8015 thay vì 8030 - Kết quả là sau khi boot, logo Apple được hiển thị trên màn hình
Thanh tiến trình và bản vá xác thực
- Ngay cả sau khi logo Apple hiển thị, UI vẫn không tiến xa hơn, và log hệ thống in ra rất nhiều thông điệp từ nhiều daemon và thư viện
- Họ tiến hành bằng cách đoán lỗi nào thực sự liên quan đến vấn đề UI rồi sửa từng lỗi một
- Họ xác nhận vấn đề liên quan đến xác thực người dùng, nguồn lỗi là daemon
mobileactivationdvà frameworkSpringBoardFoundation - Sau khi vá chúng, một thanh tiến trình màu trắng tương tự như trong giai đoạn khôi phục được hiển thị
- Thanh tiến trình trông như đang chạy, nhưng sau vài giờ có vẻ dừng ở 90%
Cải thiện vòng lặp vá dyld cache và user space
- Nhờ tắt ngẫu nhiên hóa địa chỉ, việc vá user space và các framework trong dyld cache trở nên khả thi
- Tương tự kernel, họ tạo tệp vá dạng văn bản cho từng binary/thư viện và áp dụng bằng công cụ nội bộ
- dyld cache khoảng 2GB, nên vá trực tiếp hoặc lặp lại việc sao chép qua SSH là không thực tế
- Vì làm việc trong môi trường Linux, họ cũng không thể sửa trực tiếp NVMe
- Họ mở rộng công cụ diff/patch nội bộ để tìm offset của framework bên trong blob dyld cache
- Thêm tùy chọn tạo các lệnh
ddđơn giản có thể áp dụng ngay trên iPhone, cùng lệnh revert - Sau khi remount filesystem ở chế độ đọc/ghi, có thể áp dụng lệnh
ddđể lặp nhanh các chỉnh sửa dyld cache - Để thay đổi có hiệu lực, chỉ cần reboot iOS
- Để cách này hoạt động, cần thêm một số bản vá cho kiểm tra chữ ký của kernel
Chạy PreBoard và hiển thị màn hình UIKit
- Trước khi xử lý thanh tiến trình bị kẹt, họ thử nghiệm tiến trình hệ thống
PreBoard PreBoarddường như chỉ được hiển thị cho người dùng khi có vấn đề như gián đoạn cập nhật- Đây là ứng dụng hệ thống vẽ trực tiếp qua
backboarddgiốngSpringBoard, nên có thể khởi chạy ngay từ dòng lệnh - Kết quả là một màn hình trắng yêu cầu “swipe to upgrade” được hiển thị
- Dựa trên kinh nghiệm từng dùng VNC server trên iPhone vật lý, họ thêm VNC, và sau nhiều lần thất bại, mở khóa màn hình không phải bằng thao tác vuốt mà bằng phím bàn phím
- Ngay sau khi mở khóa, QEMU dừng thực thi vì iOS dùng illegal instruction
- Phân tích
backboarddcho thấy frameworkvImagedùng lệnh AMX (Apple Matrix Coprocessor) cho các phép toán đồ họa tăng tốc phần cứng như_vHorizontal_Scale_ARGB_8888_Accelerate - AMX là tập lệnh riêng của Apple chưa được triển khai trong CPU ARM mô phỏng của QEMU
- Framework
vImagecó phiên bản phần mềm thay thế chỉ dùng lệnh ARM generic, nên họ lại vá để dùng phiên bản này - Kết quả cuối cùng là một cửa sổ
UIKitthật sự được hiển thị, cùng màn hình nhập mật mã và textbox hoạt động - Có thể nhập vào textbox thông qua sự kiện bàn phím được inject bằng VNC
- Tại thời điểm này, các thành phần cần thiết để
SpringBoardhiển thị đúng đã sẵn sàng, và việc khởi chạy chỉ còn là vấn đề thời gian - Bài tiếp theo tiếp tục ở Part 2
1 bình luận
Ý kiến trên Hacker News
Hy vọng https://github.com/devos50/qemu-ios sẽ phát triển để hỗ trợ đến iPhone OS 3.x, để có thể trải nghiệm các ứng dụng iPhone đời đầu ở góc độ bảo tồn số
https://github.com/touchHLE/touchHLE cũng rất tuyệt, nhưng ngoài các ứng dụng cực kỳ cơ bản thì cần bản vá riêng cho từng ứng dụng
Để chạy ứng dụng 32-bit trên iOS 10, QEMU cũng cần hỗ trợ iPhone 7
Trước đây tôi từng giả lập NumWorks N0100[1] và HP Prime G1[2] bằng QEMU, đến mức firmware chính thức thực sự chạy được
[1] https://github.com/boricj/qemu/tree/numworks_calculators
[2] https://github.com/boricj/qemu/tree/s3c2416-boricj
Nếu ứng dụng dự án này theo hướng thú vị, có lẽ có thể cài một image pmOS tối giản cùng phiên bản QEMU này lên một điện thoại có hỗ trợ phần cứng postmarketOS khá tốt, rồi boot iOS trên điện thoại Android
Cũng có thể tùy biến QEMU thêm để chuyển tiếp phần cứng điện thoại như modem, Bluetooth vào máy ảo iOS
Chỉ riêng việc đó mà làm được, chưa cần giả lập iOS/Android, thì cũng đã hài lòng rồi
Phiên bản lưu trữ: https://archive.ph/l1CwO
Điều này có nghĩa là giờ có thể làm những việc như kiểm thử Safari hay biên dịch cho iOS trên hệ thống Linux mà không cần phần cứng Apple sao?
https://github.com/ChefKissInc/QEMUAppleSilicon
Đây là thiết bị Apple Silicon được giả lập trong QEMU, hiện chỉ hỗ trợ iPhone 11
Video demo: https://nitter.poast.org/eshard/status/1908162866609311962
Tôi đã thử làm theo hướng dẫn chạy: https://github.com/TrungNguyen1909/qemu-t8030/wiki/Bringing-...
Nó crash nhiều lần đến mức tôi không muốn thừa nhận, nhưng quả là khá hay
Không thấy nhắc gì đến kết nối mạng. Có vẻ họ không giả lập Wi-Fi hay chipset modem di động
Tôi tò mò không biết sẽ kết nối thiết bị giả lập này với Internet bằng cách nào. Có thể là kiểu Ethernet qua USB chẳng hạn
Cần điều gì để Apple chấp nhận phát triển iOS đa nền tảng?
Từ góc nhìn của Apple, họ chẳng được gì khi cho phép mô hình phát triển như thế
Để chuyện như vậy xảy ra, chính cách Apple nhìn thế giới phải thay đổi; có lẽ đó sẽ là một thay đổi lớn cỡ việc Microsoft phần nào chấp nhận Linux với WSL và .NET cho Linux
Có repository nào có thể tái hiện việc này không?