# 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内のとある書き込みを見て見ましょう。
1
驚くことに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

# Thanks