# Defeat KPP on iOS 9
# Introduction
Appleはarm64 iOS 9で新しいカーネル整合性保護システムとしてWatchTower(別名 KPP)と呼ばれるものを導入しました。これがどのように機能するかについてはxerubのTick (FPU) Tock (IRQ)で詳しく解説されています。
この記事の執筆時点では、Apple A10チップで導入されたKTRRと呼ばれるKPPより優れたカーネル整合性保護をベースとしたOSの整合性保護システムのためにjailbreakのようなものは難しくなっていますが、KTRRと比較した場合、KPPには本質的に欠陥があるほか、iOS 9の時点では多数の見落としが存在するため保護を回避することが可能でした。
# 1, 保護外の重要なデータ
前述の通り、Appleはarm64デバイス上でiOS 9.0 beta 1でKPPを導入しました。KPPは頻繁にカーネルの整合性についてをチェックし、iOS 8以前のようにカーネルにパッチを適用した場合、デバイスはパニックに陥るようになりました。
KPPの整合性保護では、非常に短時間での変更についてを検知することは容易ではないため、攻撃者は実行コードを書き換えた後すぐに元に戻すことで利益を得れる場合があります。しかし実行コード等にパッチを継続的に適用し続けることは不可能であるため、継続的にjailbreakを維持したい場合はこの範囲外のデータ構造等へのパッチで行う必要があります。
iOS 9.0の時点で、カーネルのチェックされる範囲は__TEXT
と__DATA.__const
でした。そのほかにはページテーブルやTTBR1_EL1等のいくつかの重要なシステムレジスタもチェックされます。継続的なjailbreakを行うためにはこの領域を変更することはできません。また、ページテーブルやTTBR1_EL1の値はKPPにチェックされるため、偽のページテーブルを挿入したりして実行コードを置き換えることも不可能です。
しかし、初期のiOS 9のカーネルではMAC policyが非constセクションに存在したためにKPPのチェックを受けずにMACフックを置き換えることが可能でした。例えばPangu 9ではAMFIのMAC policyを置き換えてコード署名を無効にして任意のコード実行を可能にしました。この手法はiOS 9.2でMAC policyを__constセクションに移行することで修正されました。
また、jailbreakでは、カーネルにデバッグフラグを設定することでいくつかの制限を軽減する手法を使用しますが、iOS 9では、PE_i_can_has_debugger
関数とフラグの格納アドレスは共にKPPのチェック下にあります。ではiOS 9のAMFIやSandnoxではこれをどのように変更することができるでしょうか?
AMFI kextではPE_i_can_has_debuggerのstub関数を呼び出します。stub関数のポインタは__DATA.__got
にストアされています。__gotセクションは保護されていないので、これをret1 gadgetへのポインタに置き換えることで簡単にチェックを回避できます。
# 2, 保護外の重要なデータ (2)
iOS 9.2の時点では、Appleは保護外に存在した重要なデータを__constセクションに移行することで重要なデータが改竄されることを防止しました。しかし、まだ保護されていない重要なものがあります。
前項の、AMFIのPE_i_can_has_debuggerの呼び出しを改竄した方法を参照してください。これはiOS 9.0の時点で使用されましたが、AppleはこれをiOS 9.2の修正で保護下に置かなかったため、まだ書き込み可能です。
またSandbox MAC関数は最初の初期化時点でmemsetを呼び出しますが、memsetのstub関数のポインタをshellcodeに置き換えることで任意のコードに置き換えることが可能です。例えば、x0レジスタを0に設定して、LRレジスタをret gadgetに設定することで、MAC関数はmemsetを呼び出した時点でret0を返すようになり、チェックを回避できます。これはPangu 9 for iOS 9.2 - 9.3.3で実証されました。
最終的に、iOS 10.0 beta 1の時点でKTRRのためにカーネルセグメントを再配置したことで__gotは保護下に入り、この手法は機能しなくなりました。また、AppleはiOS 9.3.4で__gotをKPPの保護下に置くことでこのバグを修正しました。
# 3, KPP bypass
前項にてKPPでは実行コードの継続的な変更は不可能になったと説明しましたが、一つだけ罠があります。
KPPはEL3で実装されており、またTZの下にあるため、EL1から変更することは不可能です。しかし、KPP/EL3の「入り口」であるMSR CPACR_EL1, X0
命令をhookすることで、KPPが呼び出される前に任意のコードを実行することができます。
つまり、実際のページに変更を加えるのではなく、偽のTTBR1_EL1をセットアップして、MSR CPACR_EL1, X0
命令をshellcodeへのトランポリンに変更し、MSR CPACR_EL1, X0
コールよりも前にTTBR1_EL1の値をオリジナルの値に戻すことで、チェックが常に合格するようになります。 実際にはCPUコアがsleepから復帰した場合、MMUを再度有効化するために実際の物理ページから再開されるため、スリープ復帰時に任意のTTBR1_EL1を設定する方法を考える必要があります。
# 3-1, 制御フローのハイジャック
XNUにはBootArgs
構造体が存在し、CPUコアが物理メモリから仮想メモリに移行する(MMUを有効にする)際に使用される物理および仮想ベースアドレスを保持しています。iOS 10.2以下ではこれは読み取り専用メモリ(=KPPの保護下)には存在しなかったため、制御フローをハイジャックすることが可能でした。
これは@qwertyoruiopzによってKTRR bypassとしても使用されました。
# 3-2, MMU初期化時のコード実行
CPUコアはリセット時にはreset vectorから再開されます。 この際にCPUのreset handlerはdeepsleep handler(start_cpu)もしくはidlesleep handler(resume_idle_cpu)を指しています。KPPデバイスでは、iOS 11以前ではhandlerが前述の2つのうちのいずれかであることを検証するコードが存在しなかったため、これをshellcodeのアドレスに変更することで、MMU初期化時に任意のTTBR1_EL1を注入することが出来ます。
これは@qwertyoruiopzによってyalu102で使用されました。
# 4, TrustZone内メモリへの書き込み
KPPはEL3で実装されており、TZ1の範囲内にあるため、Non-Secureモードで実行されるEL1から読み書きを行うことはできません。ではTZをセットアップしてロックするのはどこでしょうか?
A8デバイス以下では、デバイスがスリープから復帰する際にはLLBを介してそれを行います。LLBは暗号化された"sleep token"をチェックし、これが正しければTZ0/TZ1/IORVBARをロックしてからKPP/EL3にジャンプします。
# バグの概要
ここで、HackDiffent内のとある書き込みを見て見ましょう。
驚くことにiOS 10.0 beta 1以前のLLBにはバグがあります。バグのあるLLBでは、"sleep token"が存在するが無効な場合、関数はロックダウンなしに終了してエラーを返しますが、戻り値をチェックしなかったために無条件にKPP/EL3にジャンプしていました。
sleep tokenを無効にした後にdeep sleepをトリガーして何が起きるのかを見てみましょう。
私はiOS 9.3.1のiPhone 5sを持っていたため、このデバイスを使用してテストしました。デバイスはsleepから復帰した直後に次のメッセージを表示してパニックに陥ります。
AppleSEP:WARNING: SEP ROM timeout - no response
panic(cpu 1 caller 0xffffff800967c3d0): "SEP Boot Failure: status check 2 failed - 0xe00002d6"
Debugger message: panic
OS version: 13E238
Kernel version: Darwin Kernel Version 15.4.0: Fri Feb 19 13:54:53 PST 2016; root:xnu-3248.41.4~28/RELEASE_ARM64_S5L8960X
iBoot version: pongoOS-2.5.1-9234e72f
secure boot?: YES
Paniclog version: 5
Kernel slide: 0x0000000004600000
Kernel text base: 0xffffff8008604000
Epoch Time: sec usec
Boot : 0x65dfd254 0x00000000
Sleep : 0x65dfd2b9 0x000e65f4
Wake : 0x65dfd2bb 0x000304cf
Calendar: 0x65dfd2c0 0x00045731
Panicked task 0xffffff810a030ee0: 28881 pages, 179 threads: pid 0: kernel_task
panicked thread: 0xffffff810b056000, backtrace: 0xffffff8003233890
lr: 0xffffff8008704404 fp: 0xffffff80032338e0
lr: 0xffffff800862e0d0 fp: 0xffffff8003233940
lr: 0xffffff800967c3d0 fp: 0xffffff80032339d0
lr: 0xffffff8008a3172c fp: 0xffffff8003233a40
lr: 0xffffff8009676e44 fp: 0xffffff8003233a90
lr: 0xffffff8008a3172c fp: 0xffffff8003233b00
lr: 0xffffff80096771ec fp: 0xffffff8003233b50
lr: 0xffffff8008a1c174 fp: 0xffffff8003233bf0
lr: 0xffffff800865b97c fp: 0xffffff8003233ca0
lr: 0xffffff8008701380 fp: 0x0000000000000000
詳しくは調べられていませんが、SEPはTZがアンロックされている状態でのブートに失敗するため、このエラーからTZがunlockされたままxnuに復帰したことが推測できます。
# Exploitation
前述の通り、TZをアンロックされたままにすることはできないため、スリープ復帰から早い段階(SEPに何らかの信号を送信するよりも前)にTZを再ロックする必要があります。
私たちはすでに、sleep復帰時にコードを実行する良い方法を知っています。セクション3-2/yalu102で使用されたように、deepsleep handlerを任意のshellcodeに設定することで、MMUが有効になる前の早期の段階でコード実行をすることが出来ます。TZ1を再ロックする前の状態では、TZ1内メモリはまだEL1からでもアクセス可能であり、KPPにパッチを適用することが出来ます。最後にTZ0/TZ1/IORVBARを再ロックしてxnuに戻ることで、KPPを完全に無効にすることが出来ます。
# References
- Tick (FPU) Tock (IRQ) by xerub
- KTRR by Siguza
- Pangu 9 Internals by Pangu Team
- Jailbreak DIY by FriedAppleTeam
- yaluX/yalu102 by qwertyoruiopz
- Technical Keynote: iOS War Stories by KEEN Security Lab
# Thanks
- Clarity for researching the LLB bug