通常、Apple製品ではファームウェアのインストール時にデジタル署名による厳格な検証が行われるため、Appleが許可していないバージョンのiOSを対象ではないデバイスにインストールすることは不可能とされています。しかし、こうした制約を乗り越えiOS 6をアップデート対象外のiPod touch 3で動作させる技術的な手法をソフトウェア開発者のjohn(@nyan_satan)氏が公開しました。

Running unsupported iOS on deprecated devices

https://nyansatan.github.io/run-unsupported-ios/

◆iOSの構成要素

・iBoot

iBSS・iBEC・LLB・iBoot、4種類のブートローダーが存在。

・カーネルキャッシュ

OSカーネルとドライバを単一のバイナリブロブに統合したもの。

・デバイスツリー

特定のデバイスモデルで使用されるハードウェアの構造化リストと、ソフトウェアの動作を指定するパラメータ。カーネルにジャンプする前にiBootによって大幅に変更可能。

・ユーザースペースファイルシステム

OSインストール専用の小型リストア用RAMディスク、またはiOSの実際のルートファイルシステム。

・各種コプロセッサ用ファームウェア

ベースバンド・Wi-Fi・Bluetooth・マルチタッチなど、メインSoCの内部または外部にあるコプロセッサ用。

◆iPhone 3GSでのテスト

2009年にリリースされたiPhone 3GSは「S5L8920X SoC」、iPod touch 3は「S5L8922X SoC」という非常に似たハードウェアを搭載しており、iPhone 3GSは公式にiOS 6に対応しています。そこでiPod touch 3で作業を行う前に、iPhone 3GSでiOS 6.0をiOS 5.1.1のiBootとデバイスツリーで起動させ、何が壊れるかをテストしました。

◆デバイスツリーの修正

iOS 6では多数の新しいノードとプロパティが追加されていおり、デバイスツリーを自動的に修正するため、2つのデバイスツリーをデコードして差分を計算するPythonスクリプトを作成しました。iBootが実行時に入力する新しいプロパティの1つが、chosenノード内のnvram-proxy-dataです。プロパティには生のNVRAMダンプが含まれている必要があり、空のままにするとカーネルが非常に早い段階でスタックします。iPod touch 3の場合、差分からiPhone固有の要素をクリーンアップしてから、iPodのiOS 5.1.1 デバイスツリーに適用する必要がありました。

◆iBootの変更

iBootは大きな変更を必要としませんでした。通常のImage3署名チェックパッチ・boot-args挿入・debug-enabledパッチを適用し、カーネルがAMFI boot-argsを実際に尊重するようにしました。

重要な点は、通常起動の場合nvram-proxy-dataを動的に入力することです。リストア起動ではDeviceTreeにハードコードされたランダムなNVRAMで問題ありませんが、通常起動ではある時点で同期することを決定した場合、実際のNVRAMがランダムなもので上書きされてしまいます。そこで、UpdateDeviceTree()の呼び出しを独自の小さな関数に置き換え、実際のnvram-proxy-dataとrandom-seedを動的に入力します。boot-argsには常にamfi=0xffを追加してコード署名を無効にしていますが、標準的な手法です。

◆カーネルキャッシュの作成

最も複雑な部分がカーネルキャッシュでした。iPod touch 3は公式にiOS 6を受け取っていませんが、ほぼすべての内部iOS 6ビルドには、スタンドアロンのS5L8922Xカーネルと、iPod touch 3固有のものを含むスタンドアロンのkextが含まれていました。当初、古いMacOS Xのようにブートローダーレベルですべてのkextを動的にロードすることを考えました。戦略は以下の通りです。

・iBootコンテキストで、ファイルシステムからすべてのkextをロードする。

・メモリにレイアウトし、DeviceTreeのchosen/memory-mapノードに対応するエントリを追加する。

・スタンドアロンカーネルを起動し、カーネルがkextとエントリをリンクする。

しかし、結果は失敗でした。カーネルにはすべてのkextを取得するコードは含まれていますが、実際にリンクするコードは含まれていませんでした。そこで、Appleシリコン以前のMacOS Xがすべてのkextを含むカーネルキャッシュを生成していたことに気づき、そのロジックを使用してiOSカーネルキャッシュを構築することを思いつきました。macOS Sierraの/usr/local/bin/kcgenを使用しましたが、最新のMacOSにデフォルトで含まれているkextcacheでも可能なようです。

kcgenコマンドの主要なオプションは以下の通りです。

・-c output.bin

結果のカーネルキャッシュを書き込む出力ファイルを指定。

・-arch armv7

armv7スライスのみをビルド。

・-all-personalities

無関係なIOKitパーソナリティが削除されるのを防ぐ重要なフラグ。「無関係」とは「現在のマシンに無関係」という意味で、iPod touch 3に関連するすべてが削除されることを意味します。

・-strip-symbols

不要なシンボルを削除する。フラグは理論的には省略できるが、結果のカーネルキャッシュを小さくするために保持することを推奨。

・-uncompressed

圧縮を適用しない。後で1つの小さな変更を行う必要があるため、圧縮を再適用する必要があります。

小さな変更として、ファットヘッダーを削除する必要があり、何らかの理由で、単一のスライスを持つファットMach-Oが作成されますが、iBootは認識しないため「lipo -thin armv7 output.bin -o output.thin.bin」を実行してファットヘッダーを削除します。カーネルキャッシュの準備ができたら、圧縮してImage3コンテナにパッケージ化する必要があります。

◆IOKitパーソナリティの変更

今回の特定のケースでは、Wi-Fi kextのInfo.plistファイルを修正する必要がありました。サンプルコードはリポジトリに用意しています。

◆リストア用RAMディスク・ファイルシステム

ここでは、通常通りasrにパッチを適用し、パーティションを適切にレイアウトできるようにoptions.n88.plistをoptions.n18.plistに移動する必要があります。ただし、iBootエクスプロイトもインストールする必要があります。そのため、rc.bootバイナリを再実装しました。

・RAMディスクを再マウントし、元のものと同様にumaskを設定する

・restored_externalを-server引数付きで呼び出し、リストア完了後に再起動しないようにする

・リストアが適切に完了した場合、3番目のパーティションを追加し、そこにエクスプロイトを書き込み、boot-partitionを2に設定する

・デバイスを再起動する

実装コードを参考にしてください。

◆ルートファイルシステムの変更

・SpringBoardのハードウェア機能plistを追加

iOS 5.1.1バージョンをベースにし、iOS 6固有の機能を追加しました。また、iPod touch 3のiOS 5.1.1とiPod touch 4の6.xレイアウトをマージして、元のホーム画面アイコンの順序を維持しました。

・マルチタッチとWi-Fiファームウェアを追加

・Bluetoothファームウェアとスクリプトを追加

5.1.1のBlueToolからファームウェアとスクリプトの両方を抽出し/usr/sbin/BlueToolにハードコードされているものを/etc/bluetoolのファイルで上書きします。

・FairPlayデーモンの制限を解除

FairPlayデーモンはN88AP(iPhone 3GS)に制限されています。LaunchDaemon plistにあるLimitLoadToHardwareキーを削除するだけでiPod touch 3でも動作しますが、iOS 6.1以降では難しくなります。LaunchDaemonsが署名されたキャッシュからロードされるためです。それでも、launchdにパッチを適用したり、launchctlを介して別のplistを強制的にロードするなど、多くの方法でバイパスできます。

・DYLD共有キャッシュパッチ

Product IDマップパッチについて、iOS 6は長いバイト列の形式で「product ID」の概念を導入し、iBootによってDeviceTreeのproductノードに入力します。iPhone 3GSの値(8784AE8D7066B0F0136BE91DCFE632A436FFD6FB)をDeviceTreeに直接ハードコードしました。識別子には短い形式(16ビット整数)もあり、iOS 6以前から存在していました。iPhone 3GSは0x2714、iPod touch 3は0x2715です。そこで0x2714を0x2715と交換しました。

getDeviceVariant()パッチについて、MobileGestaltが再び問題を引き起こしました。デバイスバリアントは文字で、通常は「A」または「B」で正確なデバイスで使用されるWi-Fiトランシーバーベンダーに依存するようです。iOS 6はiPod touch 3のこの値を判定できず、アクティベーションプロセスがクラッシュするので、関数を常に「A」で返すようにパッチしました。

共有キャッシュファイルは通常のMach-Oファイルと同じ署名形式を持っています。アドホックであるため、変更したページのSHA-1ハッシュを再計算して署名をバイナリーエディタで更新するだけです。

◆iBootエクスプロイト

iOS 5のiBootにはHFS+ファイルシステムドライバにバグがあり、ゼロから再実装しました。

◆結論と今後の計画

NyanSatan氏は「当初予想していたよりも簡単でした。ジェイルブレイクについても古いツールは動作しませんが、カーネルにパッチを適用してCydia tarballをファイルシステムにドロップするだけで簡単にできるはずです」と述べています。

Appleがその年にサポートを終了した別のデバイスであるiPad 1についてもすぐに試してみる予定があり「この情報がiPhone 4SでのiOS 4やiPad mini 1でのiOS 5など、他のクレイジーな組み合わせを作成するのに役立つことを期待しています」と述べています。

◆議論

旧デバイスへの対応についての議論が行われています。

・Appleのサポート終了ポリシーへの批判

以前ダウンロードしたことがあるアプリでなければ、互換性のある旧バージョンをダウンロードできないAppleの仕様への不満や批判的な意見。

・開発者側の事情

開発者側からは、ほとんど誰も使用していない古い電話をサポートするために数千時間の開発時間を費やすことは、投資対効果が低いという指摘。

・旧デバイスのオープン化を求める声

公式サポートが終了したデバイスについて、企業がデバイスをオープンにすることを義務付けることで、古いハードウェアをより有効に活用できるとされていますが経済的根拠を示す必要があり、今日の状況では可能性は極めて低いとの見方も示されています。

・Mac向けOpenCore Legacy Patcherとの比較

議論の中で、MacでサポートされていないmacOSを動作させるOpenCore Legacy Patcherへの言及がありました。また、iPhoneにAndroidをインストールする試みとして、Project Sandcastleも紹介されています。

・旧デバイスの実用性についての議論

旧デバイスの実用性についての否定的な意見として、2013年のiPad Airはデュアルコアで1GBのRAMしか搭載しておらず、現在では許容可能なパフォーマンスでウェブを閲覧することすらほとんどできない。サーバーやスマートディスプレイとして使用することは可能だが、一般の利用者はより新しいものに移行しており、ニッチな二次的用途を探していない。

・現実的なアプローチの提案

これらのデバイスは一時的に所有するもの、ほぼリースのようなものとして受け入れる必要がある。2013年のiPad Airのようなデバイスを購入者の5%から10%しか再利用していないという現実を受け入れるべきだという指摘がありました。

・Appleデバイスの寿命に関する議論

Appleが高価なデバイスを7年以内に完全に陳腐化させることは良い事ではないという意見があり、同等の特性やパフォーマンスにおいてAndroidやPCは一般的にかなり安価だという指摘がありました。