QEMUでiPhoneをエミュレーションする
(eshard.com)- eShardは既存のオープンソースのiOSエミュレーション作業を土台に、iOS 14をQEMUで起動し、UIや一部アプリの実行まで可能なエミュレータを目標にした
- QEMU内部にカーネルパッチを直接入れる方式ではなく、PongoOSとcheckra1n KPFを活用してXNUパッチを分離し、Mach-O diffベースのツールでパッチ内容を確認できるようにした
- Apple Silicon GPUのエミュレーションは範囲が大きすぎるため、まずソフトウェアレンダリングを選択し、脱獄済みiPhoneの
QuartzCoreパッチによりUIKit UIが遅いながらも描画できることを確認した - 画面が黒いままになる問題を解決するため、アドレスランダム化の無効化、GDBデバッグ、
lockdowndのペアリング回避、PAC無効化、QEMU 8.2.1への移植、dyld cacheパッチ自動化を進めた - 最終的にQEMU画面上にUIKitのパスコード入力UIを表示し、VNCキーボード入力でテキストボックスを操作できるようになり、
SpringBoard表示に必要な基盤まで整えた
iOSエミュレーションの出発点
- 既存のオープンソースソリューションを検討した結果、alephsecurity/xnu-qemu-arm64は実行経験があったものの、プロジェクトは読み取り専用状態だった
- その後、TrungNguyen1909/qemu-t8030を出発点として使用した
- 2台目の「companion」QEMUを介したUSB接続でiOS復元が可能
- iOS 14の実行をサポート
- より新しいQEMUバージョンがベース
- エミュレータの起動方法を扱うWikiを提供
System/Library/xpc/launchd.plistを修正して、素早くshellとSSHアクセスを確保した- 長期目標は、UIがあり最低限一部アプリを実行できる機能的なiOSエミュレーションだった
PongoOSでカーネルパッチを分離
t8030プロジェクトはQEMU自体にXNUカーネルパッチコードを入れていたが、今後パッチが増える可能性があり、よりクリーンな構成が必要だった- 実際の脱獄iPhoneでの経験をもとに、PongoOSを使ってcheckra1nパッチを適用する方式を検討した
- 一般的な脱獄フローでは、checkmateでpwnした後にPongoOSがSRAMへ注入され、
checkra1n-kpfモジュールがUSB経由で送信される- この作業では初期USB処理を避けるため、エミュレートされたiPhoneのSRAMを拡張し、PongoOSとcheckra1n KPFモジュールを使用した
- PongoOS実行初期には、bootromやiBootが行っていた初期化コードが存在しないため問題が発生した
- double/float命令の前にFPU設定が必要だった
- ARMドキュメントや既存のQEMU関連コードを参考に解決した
- A13以降のデバイス機能はPongoがサポートしておらず、一部パッチのパターンマッチングを壊していた
- Pointer Authentication(PAC)命令である
autda、xpacdが追加された - Appleは別のslideを使っていた
task_for_pid(tfp0)パッチではiPhone XとiPhone 11でアドレスとバイナリパターンの差異が確認された
- Pointer Authentication(PAC)命令である
宣言的なカーネルパッチファイル
- Pongoは複数のiOSバージョン向け既存checkra1nパッチを使えるようにしてくれたが、動的適用方式は読みづらく、修正や共有もしにくかった
- 実際のコードパッチのように扱うため、内部ツールで宣言的パッチファイルを生成した
- 2つの
Mach-Oをdiffして、アセンブリ差分ベースのテキストパッチファイルを生成 - 生成したパッチファイルをバイナリへ適用する別プログラムを作成
- 2つの
- Pongoで起動した後、QEMU monitorを使ってPongoがパッチしたメモリセクションをダンプした
- その後、パッチ済みカーネルを再構成し、すべての変更を含む大きなパッチファイルを生成した
- 大きなパッチを分割してコメントを付けることで、カーネルのどの部分がパッチされるのかをレビューし制御できるようになった
GPUなしで画面を描く戦略
- 最新iPhoneのグラフィックレンダリングは最終的にAppleのMetal APIを通るため、実際のGPUが必要になる
- Apple Silicon GPUをエミュレートするのは複雑すぎると判断し、2つの選択肢を検討した
- 旧iOSで可能だったように
gpu=0bootargを使うソフトウェアレンダリング - 実機iPhoneまたはmacOS搭載MacへMetal呼び出しを転送してレンダリングする方式
- 旧iOSで可能だったように
- iOS 14では、XNUカーネルの
gpu=0bootargオプションは削除されていた QuartzCoreフレームワークをGhidraで解析した結果、ソフトウェアレンダリングはMetalレンダラがない場合のfallbackとして呼ばれる構造になっていた- 実際の脱獄iPhoneで
QuartzCoreをパッチし、ソフトウェアレンダリングが使われることを確認した- UIは大幅に遅くなった
- 一部領域にアーティファクトが出ており、その部分は直接
Metalレンダリングを要求していた可能性がある
- この実験から、
MetalやOpenGLを直接使わない範囲、つまり大半のUIKitアプリでは、QEMUでもソフトウェアレンダリングが可能だと判断した
Metal呼び出しプロキシの実験
- Metal呼び出しをプロキシする代替案も、2台の実機iPhoneで実験した
- LLVMですべてのiOSヘッダをパース
- サーバ上のObjective-Cオブジェクトポインタを、クライアント側ではstubポインタとして表現
- 構造体とポインタ交換コードを自動生成
- すべての関数とメソッドをフック
- すべての呼び出しをサーバへ転送し、実行結果を返却
- Metal初期化における基本的な呼び出し往復は一部成功した
- しかしObjective-C言語と
MetalAPIは複雑で機能も多く、実際に動作させるまでの作業量は非常に大きかった - この方式は後回しにし、制約があってもまずソフトウェアレンダリングで他の問題を解決することにした
- iOSフレームワークには公開ヘッダにないprivate APIも露出しており、それをパースしてヘッダを作る方法はあったが、多くは直接扱いにくく複雑さを増すだけだった
IOSurfaceとframebufferのデバッグ
- ソフトウェアレンダリングを試みた後でも、最低限のframebufferデバイスは必要だったが、元の
t8030QEMUには実装されていなかった - IOMFBサポート作業のあるQEMUAppleSiliconフォークを見つけ、ディスプレイデバッグに使用した
- このバージョンでiOSを復元するとAppleロゴと進行バーは表示されたが、通常起動では画面は完全に黒いままだった
- GhidraでIOMFB kextを見てQEMU framebuffer実装を調べた結果、2つのモードがあるようだった
- 固定ハードウェアアドレスのraw framebuffer
- レジスタで複数planeを設定し、DMAでsurfaceデータを書き込むより複雑なAPI
- raw framebufferには任意のARGB surfaceを表示できたが、起動中のシステムはそのframebufferへ書き込んでいなかった
- 2つ目のdisplayモードでは、カーネルがレジスタでgraphical planeを設定するトレースは見えたが、その後画面出力はなかった
アドレスランダム化の無効化とGDBデバッグ
- SSHアクセスだけでは実行中システムの観察に限界があり、カーネルとuser spaceをGDBでデバッグする必要が生じた
- カーネルアドレスランダム化は
t8030ボード初期化で設定されていたため、完全に無効化できた - userlandには実行ファイルランダム化とdyld cache内の動的ライブラリランダム化があった
- 実行ファイルはカーネルの
_load_machfile関数をパッチして無効化した - dyld cacheライブラリは起動時に一度だけアドレスがランダム化され、その後すべての実行ファイルで同じアドレスにロードされた
- 実行ファイルはカーネルの
/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64eのdyld cacheには、すべてのframeworkライブラリが巨大なバイナリblobとして含まれている- Cツールを作成して、すべてのframeworkライブラリを
dlopenし、_dyld*関数でロード済みイメージとoffsetを列挙した - この方法とGDBアドレスを逆算する手順を組み合わせ、dyld cache内ライブラリをデバッグできるようになった
- 特に注目したのは
IOMFBkext、backboardd、SpringBoard、QuartzCoreだった - その後、カーネルパッチでdyld cacheを無効化する方法も見つけ、ホスト上でdyld cacheの仮想アドレスを直接探すためにGimliプロジェクトのRust製objectライブラリを使用した
- user spaceデバッグにはゲストのGDB serverが必要で、例としてProcursusの
debugserverパッケージを使用した
システムログとlockdownd回避
- GDBでは
backboarddが正常に起動しているように見えたが、実際の状況を把握するにはシステムログが必要だった - 実機iPhoneでは、PCとUSBペアリング後に
idevicesyslogでシステムログを確認できる - ペアリング処理には鍵ペア生成が含まれ、秘密鍵はiPhoneに保存され、
lockdowndがPCの身元を確認する - エミュレーション環境ではUSB相互作用は可能だったが、
lockdowndが正しく動作しなかった - Ghidra解析の結果、
lockdowndは秘密鍵保存にkeybagを使おうとしており、これは存在しないSEPを必要としていた - 既存関数の一部を置き換えるshellcodeを注入し、事前生成した公開鍵/秘密鍵ペアをファイルシステムから読み込み、
lockdowndがkeybagから取得しようとするたびにロードするようにした - 追加のデバッグとパッチにより、ユーザーがPCを信頼し、iPhoneがロック解除済みであるとシミュレートした
- 最終的にcompanion QEMUからエミュレートされたiPhoneとペアリングできた
- ログでは、
QuartzCoreが正常に初期化され、ディスプレイサイズを検出し、ソフトウェアレンダリングfallbackを使っている状態が確認できた - すべて正常に見えたが、それでも画面は表示されなかった
- pixel formatに関する単一のエラーはRGBA固定指定で回避したが、後に削除された
PAC問題とQEMU 8への移植
backboarddのpixel formatエラーを修正しようとしたところ、iOSのセキュリティ機能により追加の問題が見つかった- ロード時と実行時の署名チェックはカーネルパッチで解決したが、変更済み
backboarddの実行中にPointer Authentication失敗が発生した - Pointer AuthenticationはARMv8.3で追加された機能であり、以前使っていたt8015ではなく、エミュレーション中の
t8030ボードで新たに直面した問題だった - 当初は、すべてのPAC命令をNOPまたはPACなしの等価命令に置き換えることを考えた
- その後、ARM64 PACバイナリは2種類の方式でビルドできることが分かった
- ARMv8.3+ CPUでのみ実行される専用PAC命令セットを使用する方式
- ARMv8.3+ではPACとして解釈され、旧ARMではPACなしの等価命令として解釈される「未使用」命令セットを使う方式
- buildrootとARM64 Linuxシステムでこの挙動を検証し、
t8030向けバイナリが後方互換命令セットであるarm64eを使っていることを確認した - QEMUでPAC enforcingだけ無効化すればPACなしコードとして実行されると考えたが、QEMU 7では動作せず、QEMU 8では挙動が異なっていた
- 現在のコードベースをQEMU 8.2.1へ移植した
- Apple専用命令
genter、gexit - GL exception levels処理コード
- QEMU汎用コードへの修正が多く、移植は困難だった
- Apple専用命令
- 複数のXNU panic、カーネルGDBデバッグ、QEMU自体のデバッグ、git bisectを経て、iOSをQEMU 8で再び起動した
- これによりPACを無効化し、任意の実行コードを望む位置で修正できるようになった
黒い画面の原因追跡
- システムログ上では
backboarddが正常動作しているように見えたため、表示されない理由をさらに深く追跡した - raw ARGB frameをアドレスへ直接書き込むと実際のディスプレイは変化し、複数のgraphical planeにも描画できた
- したがってディスプレイ実装自体は正常に見え、残る可能性は3つだった
backboarddが何も書き込んでいない- 誤ったアドレスへ書いている
- 書かれたデータが有効ではない
- QEMU monitorで非連続な物理アドレスを取得し、スクリプトで物理DMAメモリをダンプして1つのファイルに結合した
ffplayでARGB frameとして解釈したが、有意な結果は得られなかった- 次に
iosurface_lockへbreakpointを置き、backboarddメモリにマップされたsurfaceアドレスを取得して調査した - ときどきAppleロゴのように見える奇妙な形が見つかったが、frameの書き方に問題があるようだった
- 同じ作業を実機iPhone 10で行うと、現在画面の完全なraw ARGB frameを容易にダンプできた
- iPhone 11、つまり
t8030以降では、surfaceはGPUが処理可能な圧縮形式で渡されているようだった - iPhone Xの
t8015ではこの現象が起きなかったため、QEMUのDTBでchip-idを8030ではなく8015として渡すよう修正した - その結果、起動後の画面にAppleロゴが表示された
進行バーと認証パッチ
- Appleロゴが表示された後もUIは先へ進まず、システムログには複数のdaemonやライブラリから大量のメッセージが出力された
- どのエラーが実際のUI問題に関係するのかを推測し、1つずつ修正していった
- ユーザー認証関連の問題を確認し、エラーの発生源は
mobileactivationddaemonとSpringBoardFoundationframeworkだった - これらをパッチした後、復元段階で見えるものに似た白い進行バーが表示された
- 進行バーは動いているように見えたが、数時間後も90%で止まっているようだった
dyld cacheとuser spaceパッチの反復改善
- アドレスランダム化を無効化したことで、user spaceとdyld cache frameworkのパッチが可能になった
- カーネルと同様に、各バイナリ/ライブラリごとのテキストパッチファイルを作成し、内部ツールで適用した
- dyld cacheは約2GBあり、直接パッチしたりSSHで繰り返しコピーしたりする方法は現実的ではなかった
- Linux環境で作業していたため、NVMeを直接修正することもできなかった
- 内部diff/patchツールをdyld cache向けに拡張し、frameworkのoffsetをdyld cache blob内で見つけられるようにした
- iPhone上でそのまま適用できる簡単な
ddコマンドとrevertコマンドを生成するオプションも追加した - ファイルシステムをread/writeモードで再マウントした後、
ddコマンドを適用するとdyld cacheの変更を高速に反復できた - 修正反映に必要なのはiOSの再起動だけだった
- この方式を動かすには、カーネルの署名チェックに対する追加パッチが一部必要だった
PreBoard起動とUIKit画面の表示
- 停止した進行バーを解決する前に、システムプロセス
PreBoardを試した PreBoardは、アップデート中断のような問題があるときにのみユーザーへ表示されるようだったSpringBoardのようにbackboardd経由で直接描画するシステムアプリケーションであり、コマンドラインからそのまま起動できた- 実行結果として、「swipe to upgrade」を求める白い画面が表示された
- 過去に実機iPhoneでVNCサーバを使った経験をもとにVNCを追加し、何度も失敗した末に、スワイプではなくキーボードキーで画面をロック解除した
- ロック解除直後、QEMUはiOSがillegal instructionを使ったため実行を停止した
backboarddの解析により、vImageframeworkが_vHorizontal_Scale_ARGB_8888_Accelerateのようなハードウェアアクセラレーション画像処理のため**AMX(Apple Matrix Coprocessor)**命令を使っていることが分かった- AMXはQEMUのエミュレーションARM CPUに実装されていないApple独自命令セットだった
vImageframeworkには汎用ARM命令のみを使う代替ソフトウェア版があり、再びパッチでそちらを使わせた- 最終結果として、実際の
UIKitウィンドウが表示され、パスコード入力画面と動作するテキストボックスが現れた - VNCで注入したキーボードイベントにより、テキストボックスへ入力できた
- この時点で
SpringBoardが正しく表示されるために必要な構成要素は揃っており、起動までは時間の問題に見えた - 次の記事はPart 2へ続く
1件のコメント
Hacker News のコメント
https://github.com/devos50/qemu-ios が iPhone OS 3.x まで対応するように発展して、初期の iPhone アプリをデジタル保存の観点から体験できるようになるとよさそう。
https://github.com/touchHLE/touchHLE も素晴らしいが、ごく基本的なアプリを除くとアプリごとのパッチが必要になる。
iOS 10 で 32 ビットアプリを動かすには、QEMU が iPhone 7 にも対応する必要がある。
以前、QEMU で NumWorks N0100[1] と HP Prime G1[2] をエミュレートして、公式ファームウェアが実際に動くレベルまで作ったことがある。
[1] https://github.com/boricj/qemu/tree/numworks_calculators
[2] https://github.com/boricj/qemu/tree/s3c2416-boricj
このプロジェクトを面白く応用するなら、postmarketOS のハードウェア対応がかなり良いスマートフォンに最小構成の pmOS イメージとこの QEMU 版を入れて、Android 端末で iOS を起動できそう。
QEMU をさらにカスタマイズして、モデムや Bluetooth などのスマートフォンのハードウェアを iOS 仮想マシンに渡すことも可能かもしれない。
iOS/Android エミュレーションなしでそれだけできても満足できそう。
アーカイブ版: https://archive.ph/l1CwO
これで Apple ハードウェアなしでも Linux システム上で Safari テストや iOS 向けコンパイルのようなことができるという意味なのだろうか?
https://github.com/ChefKissInc/QEMUAppleSilicon
QEMU でエミュレートされる Apple Silicon デバイスで、現在は iPhone 11 のみ対応。
デモ動画: https://nitter.poast.org/eshard/status/1908162866609311962
実行手順を試してみた: https://github.com/TrungNguyen1909/qemu-t8030/wiki/Bringing-...
認めたくないくらい何度もクラッシュしたが、かなり格好いい。
ネットワーク接続についての言及がない。Wi-Fi やセルラーモデムのチップセットはエミュレートしていないようだ。
このエミュレートされたデバイスをどうやってインターネットに接続するのか気になる。USB 経由の Ethernet のような方式かもしれない。
Apple が マルチプラットフォームの iOS 開発を受け入れるには何が必要だろうか?
Apple の観点では、そのような開発モデルを許可して得るものがない。
こうしたことが起きるには、Apple の世界の見方そのものが変わる必要があり、Microsoft が WSL や Linux 向け .NET で Linux をある程度受け入れたのと同じくらい大きな変化になりそうだ。
これを再現できるリポジトリはあるのだろうか?