最後の0day脱獄: Tachy0n
(blog.siguza.net)- tachy0n は、iOS 13.0〜13.5 で動作したカーネル権限昇格 exploit で、2020年5月23日に unc0ver v5.0.0 に 0day として組み込まれ、当時の最新 iOS を直接狙っていた
- 中核は
lio_listioの Lightspeed 競合状態で、kalloc.16オブジェクトを二重解放し、異なるオブジェクトが同じメモリを指す状態を作れた - 同系統のバグは以前、iOS 11 向けの Spice jailbreak/untether でも使われていたが、アプリサンドボックスと
racoon環境では権限・サンドボックス・スプレー手法の制約が大きく異なっていた - unc0ver 向け exploit は
OSDataの重なりを繰り返し確保し、IOBufferMemoryDescriptorとIOAcceleratorFamily2を用いて fake task と fake mach port を構成する - iOS 14 以降、Apple は allocator 分離、sequestering、PAC、オブジェクト単位のハードニングによって exploit 戦略そのものを遮断する方向へ移り、公開されている iOS カーネル exploit の知識は iOS 13 時代より大きく立ち遅れている
tachy0n の公開が異例だった理由
- tachy0n は iOS 13.0 から 13.5 までに影響した古い exploit で、2020年5月23日に unc0ver v5.0.0 に含まれて公開された
- 当時の基準では標準的なカーネルのローカル権限昇格 (kernel LPE) だったが、最新 iOS バージョンに影響する 0day jailbreak として公開された点が珍しかった
- Apple は exploit 公開から約1週間後、このバグ専用の パッチ を配布した
- このバグは iOS 13.5 では 0day だったが、それ以前には 1day としてすでに利用されたことがあった
- Pwn20wnd はアプリサンドボックスから到達可能な 0day を探しており、出発点は既知の 1day に対する 回帰テスト だった
- iOS 12 の SockPuppet は iOS 12.3 で修正されたが、iOS 12.4 で再登場した
- この事例は、その種のバグに対する Apple の回帰テスト不足を示しており、Pwn20wnd はいくつかの既知 1day の回帰テストを実装してヒットを得た
Lightspeed: lio_listio 競合状態
- tachy0n の中核バグは Synacktiv の Lightspeed バグで、CVE-2020-9859 と CVE-2018-4344 の可能性に関連している
- 脆弱性は、非同期またはバッチ型ファイル I/O を実行する
lio_listiosyscall にある - カーネルは送信された I/O ジョブを追跡するために
aio_lio_context構造体を割り当てる- 構造体には
io_waiter、io_issued、io_completedフィールドがある
- 構造体には
- 実際のジョブは別スレッドで実行され、すべての I/O が完了すると
do_aio_completionがそのコンテキストを解放する - 何もジョブがスケジュールされなかった場合は、
lio_listioの現在スレッドがコンテキストを解放しなければならない - 問題は、この判定に 競合状態 があることだ
- 別スレッドにジョブが送られたが、判定時点より前にすでに完了してコンテキストが解放される可能性がある
- このとき
lio_listioは dangling pointer になったlio_contextを再び検査する
double free が exploit につながる流れ
- exploit に必要な順序は次のとおり
lio_listioがlio_contextを割り当てる- ジョブが完了し、
do_aio_completionがlio_contextを解放する - 解放されたメモリを攻撃者制御のオブジェクトで再割り当てし、
lio_context->io_issued == 0のように見せる lio_listioがそれを見て攻撃者オブジェクトを再度解放する- 同じメモリを別のオブジェクトで再割り当てし、異なる2つの割り当てが同じメモリを指すようにする
- 64ビットデバイスでは
lio_contextは最小の zone であるkalloc.16に入る - iOS 14 より前は、オブジェクト種別に関係なくサイズ単位で同じ allocation site を共有していた
- C++ オブジェクト、ポインタ配列、ユーザー提供データバッファが同じサイズ bucket 内で互いのメモリを再利用できた
- この double free は、中間の再割り当てがなければ容易に致命的状態へ進む一般的な double free とは違う動きをする
lio_context->io_issuedは割り当て中には 0 にならない- 解放後、allocator は先頭 8 バイトを canary 値と freelist pointer、またはオブジェクトアドレス XOR 値で上書きする
- したがって 2 回目の free は、中間に再割り当てがあり、かつ 4〜7 バイトが 0 の場合にのみ発生する
- 実際の exploit はこの競合を何度も再試行でき、システム上の別オブジェクトが偶然必要なバイトを 0 にして double free を引き起こすケースは実運用ではまれだった
Spice における以前の活用
- 同じバグは、iOS 11.x を対象にした Spice jailbreak/untether でも使われていた
- Spice は Sparkey、littlelailo とともに Jake Blair チームが開発しており、当時の最新バージョンは iOS 13.x だった
- 目標は、アプリ環境と
racoon環境の両方で mach port forgery を作ることだった- iOS 14 より前のカーネル exploit では、ユーザー提供値を mach port ポインタとして解釈させられれば、その後の段階が決定的に容易になった
- Lightspeed で mach port forgery を作る基本的な流れは次のとおりだった
lio_contextの最初の free を引き起こす- サイズ 1 または 2 の OOL mach ports descriptor を持つ mach message を spray する
- 最初の entry を
MACH_PORT_NULLにしてkalloc.16に入れ、io_issuedが 0 に見えるようにする - 2 回目の free で OOL mach ports 配列を解放する
kalloc.16に制御データを spray して mach ports 配列を fake pointer に置き換える
アプリサンドボックスと racoon の制約の違い
- A7〜A9(X) では PAN がなく、
mmapとmlockだけで userland アドレスをカーネルポインタのように逆参照できた - A10 と A11 もサポートしようとしたが、アプリサンドボックスでは適切なカーネルアドレス leak と制御データ配置先が見つからず完成しなかった
- 活用しようとしていた 1day には、Ian Beer のカーネルスタック情報 leak と backboardd サンドボックス脱出があった
- 計画は共有メモリポインタを leak するか、カーネル
__DATAセグメントにデータを配置する方式だった - 適切な target が見つからず、アプリ経路での A10/A11 サポートは実現しなかった
- 計画は共有メモリポインタを leak するか、カーネル
racoon経路では条件が異なっていた- root で動作するが、一般アプリよりさらに厳しいサンドボックスを持っていた
- IOSurface へのアクセスがなく、一般的な
IOSurface::setValue経由のOSUnserializeXMLspray は使えなかった - その代わり
RootDomainUserClient::secureSleepSystemOptions内のOSUnserializeXML呼び出しを利用し、一部オブジェクトを leak という形で spray できた
racoonはすべての sysctl の読み書きを許可するサンドボックスプロファイルを持ち、root 権限もあった- カーネル slide が分かれば、カーネル
__DATA内の sysctl global を既知アドレスのデータ格納先として使えた - Spice では
vm.swapfileprefixが選ばれた
- カーネル slide が分かれば、カーネル
- カーネル slide の取得には panicall の CVE-2018-4413 が使われた
sysctl_procargsxの情報 leak により、kernel_mapの初期化されていないカーネルメモリをほぼ 1 ページ分 leak できた- これによりカーネルコードと heap pointer を得て、A7〜A11 を処理できた
- アプリサンドボックスでは
sysctl_procargsxがブロックされており、同じ方法は使えなかった
unc0ver 向け tachy0n exploit の構造
- unc0ver の対象は A8〜A13 だったため、A10+ を無視したり userland 逆参照に依存したりはできなかった
- exploit は失敗しうるメモリ corruption 段階を考慮し、二層構造 で設計された
- 下位層は
lio_listioを呼ぶ freerer スレッドと、IOSurface を通じてOSDataを unserialize する racer スレッドを実行する - デフォルト値は freerer 4 個、racer 16 個で、調整可能
- 下位層は
- IOSurface 経由で unserialize されるデータは、複数の
OSDataentry を持つOSDictionaryだったio_issuedに対応する位置は 0 である必要があった0x41414141、0x69696969のような magic value とキー値kが overlap 検出に使われた
- 競合後、すべての
OSData値を検査する- magic value が変わったオブジェクトは、システムの別オブジェクトに取られたと見なし、後で cleanup 対象として印を付ける
- キーとバッファ内部の
k値が一致しなければ、別のOSDataオブジェクトが同じ backing buffer を指す overlap が発生したと判断する
- コード中の
maybe_reyoinkとoverlap関数は、こうした重なり情報を構成して上位層へ渡す - 上位層は重なった
OSDataオブジェクトを使って fake mach port を構成する- 1 つの
OSDataを free する - OOL port descriptor を持つ mach message を spray する
- 別の
OSDataを free する - fake task port ポインタを含む新しい
OSDataで再割り当てする
- 1 つの
既知のカーネルアドレスに制御データを配置する
- exploit は、OOL ports descriptor 配列として再割り当てされた内容を
OSDataとして読み取り、mach port の raw kernel pointer を leak できた - 以降の段階では task port と
IOSurfaceRootservice port のアドレス leak にこれを使うが、核心的な問題は、制御可能な buffer のカーネルアドレスを安定して得ることだった - XNU ソースから見つかった候補は
IOMemoryDescriptorだった_rangesフィールドはIOVirtualRange配列で、単一のIOVirtualRangeはkalloc.16にちょうど収まる- ただし通常の
IOMemoryDescriptorは range が 1 つしかない場合、heap allocation の代わりに_singleRangeを使う
IOBufferMemoryDescriptorだけは例外的に、range 1 個に対してIONew(IOAddressRange, 1)を呼び、heap allocation を行う- これを任意に割り当て、ユーザーアドレス空間にマップできる都合のよい場所が
IOAcceleratorFamily2の AGX インターフェースだったIOGraphicsAccelerator2で type 0 userclient を開くとIOAccelContext2を得られる::clientMemoryForType()で 3 種類の memory descriptor を map できる- type 0 は 0x8000 bytes で、victim descriptor の識別に使われた
- exploit は次のループを使った
IOAccelContext2を開き、重なったOSDataを 2 つ取得する- 片方の
OSDataを free する - 事前に開いておいた
IOAccelSharedUserClient2をIOConnectAddClient()で接続する - 残った
OSDataを読み、先頭 8 バイトが page-aligned な kernel pointer で、次の 8 バイトが0x8000かを確認する - 条件に合わなければ
IOAccelContext2を閉じて繰り返す
pageable memory、fake port、zone_require
- memory descriptor をプロセスにマップし、カーネルアドレスを把握した後も、メモリが
kIOMemoryPageableとして作られるという問題が残っていた - fake mach port と fake task オブジェクトは preemption が無効な状況でアクセスされうるため、カーネル側でその page を fault-in しなければならなかった
IOAccelContext2::processSidebandBufferを間接呼び出しするIOAccelContext2::submit_data_buffersexternal method 2 を 2 回呼び出して、これを処理した- 共有メモリの先頭から 0x10 バイト後ろにある構造体を読む
- 最初の構造体は
tok == 0x100で、ページ全体を覆うように作られており、2 枚目のページまで進む - 2 枚目のページには、その後 fake object データを置ける
- その後の段階は fake task、fake port、OOL descriptor switcheroo、任意読み取り primitive の構築へと続く
zone_requireの回避も必要だった- 当時の
zone_requireはzone_map外のページを許容し、ページ先頭0x20バイトを metadata のように解釈していた - 正しい zone index を入れれば、目的の zone を通過するためのパスのように使えた
- このため task 用ページと mach port 用ページの、合計 2 ページが必要だった
- 当時の
- この exploit は現在 GitHub で公開されている
公開後の分析とパッチ
- 最新署名版向けの完全な 0day exploit 公開は、iOS jailbreak scene の注目を集めた
- 当時 Project Zero にいた Brandon Azad は、exploit 公開後 4 時間以内に脆弱性を特定して Apple に通知した
- その分析は How to unc0ver a 0-day in 4 hours or less にまとめられている
- exploit 公開 6 日後、Synacktiv は 新しい記事 で、iOS 12 の元の fix が memory leak を生み、その leak を修正しようとした試みが元のバグを復活させた可能性を扱った
- exploit 公開 9 日後、Apple はパッチを配布した
- その後 XNU には、このバグに対する 回帰テスト が追加された
- 公開 54 日後、リバースエンジニアリング版の “tardy0n” が Odyssey jailbreak に組み込まれ、対象はやはり iOS 13.0〜13.5 だった
iOS 14 以降で変わった exploit 環境
- iOS 14 は、Apple のカーネルセキュリティ戦略の変化を示している
- iOS 14 より前は、初期 primitive が heap overflow、C++ オブジェクトの over-release、type confusion など何であっても、次の target はたいてい mach port だった
- iOS 14 における最大の変化の 1 つが、allocator である
kallocとzallocだった- zone map を複数の “kheap” 範囲に分割した
- ユーザー制御データとカーネルオブジェクトが別々の heap に入るよう分離した
- カーネルオブジェクトには sequestering を適用し、特定 zone に割り当てられた virtual address page が再起動まで他 zone に再利用されないようにした
- physical memory は解放されても virtual memory range は他オブジェクトに再利用されず、カーネルオブジェクトの type confusion を事実上遮断した
- guard page、起動ごとに変わる zone allocation 開始位置、その後の精緻化も加わり、cross-zone 攻撃の信頼性は大きく低下した
- Apple は単に個別のバグを塞ぐのではなく、exploit 戦略を塞ぐ方式 へと変わった
- exploit が kmsg struct を corruption target に使えば、その構造体は署名される
- pipe buffer を安定したカーネル読み書きインターフェースに使えば、関連ポインタが PAC の対象になる
- 無関係なオブジェクトを victim に使う手法が現れれば、そのオブジェクト型がハードニングされる
- その結果、exploit 開発では初期の memory corruption 0day より exploit strategy のほうが価値を持つ状況が生まれた
公開知識の断絶
- iOS 14 より前は、公開されている iOS セキュリティ研究の知識は非公開知識とほぼ同水準だったと評価されている
- iOS 14 以降は、例外を除けば情報共有が事実上止まった状態になっている
- iOS 19 beta まで数週間という時点でも、iOS 18 または iOS 17 に対する公開カーネル exploit は存在しないとまとめられている
- Apple のセキュリティノートには実地で悪用された脆弱性が時折掲載されるが、公開情報は非公開研究に追いついていない
- tachy0n が公開されてからまだ 5 年しか経っていないという事実は、iOS カーネル exploit 分野がどれほど急速に変化したかを示している
2件のコメント
Appleのハードウェアは素晴らしいですが、ソフトウェアはユーザーを縛り付けようという意図に満ちています。
自分で作ってビルドしたアプリを自分の端末でだけ動かそうとしても、100ドルのサブスクリプションが必要です。
中小規模のオープンソースアプリを使い、自分でビルドして利用する開発者なら、
Appleの端末で脆弱性を突いて脱獄しながらサイドロードするくらいなら、ただAndroidを使うほうが楽です。
Hacker News の意見
1兆ドル企業に勝った方法が、Apple が特に苦手とする単純で退屈な作業、つまり回帰テストだったという点が印象的
iOS 12 で jailbreak に使われた大きな脆弱性の一つである SockPuppet は、Project Zero の Ned Williamson が発見して Apple に報告し、iOS 12.3 でパッチが当てられ、その後 Project Zero のバグトラッカーで公開された
ところが iOS 12.4 で、まるで一度もパッチされていなかったかのように再び現れた。おそらく Apple がそのバージョン向けに XNU を別ブランチへフォークした際、パッチを適用し損ねたのだと思われる
これは、この種の脆弱性に対する回帰テストが存在しないことを強く示すシグナルで、既知の 1-day をいくつか自動化するだけで Pwn がすぐ命中させられた
Linux、FreeBSD、OpenWRT、OpenSSH のようなプロジェクトの新バージョンに対して、過去の脆弱性を継続的に走らせる CI ファームを運用しているところがどれくらいあるのか気になる
20年前の大学時代に Mozilla でボランティア QA をしていたが、レンダリング/レイアウトや JavaScript エンジンのバグを中心に、増え続ける回帰テスト群があった
再現と修正確認のために最小テストケースを作っていたので、それをビルドパイプラインに入れるのも簡単だった
バグは避けられないが、時間とお金をかけて直したバグが復活するのは最悪のシナリオ
品質を重視する組織は確実に回帰テストへ投資するが、多くの組織は QA を尊重せず、そもそもやらないか、最安値の外注に丸投げする
Apple が、歴史的に最も注目度の高いバグの種類である jailbreak に対する回帰テストを備えていなかったというのは本当に奇妙
最近の Mozilla にはさまざまな批判があり得るが、2000年代初頭でも Tinderbox や Bugzilla のようなツールでかなり堅実な QA と CI/CD を運用していた
DevOps が流行してこうしたやり方を一般化したとき、すでにみんなやっていたことではないのかと思っていたが、それは自分の勘違いだった
セキュリティと機能開発の分離が生んだ、コンウェイの法則のような状況
ビルド/リリース手順と成熟した回帰テスト群があったとしても、内部の組織構造上、こうしたセキュリティ問題はその中に入らない可能性が高い
数千個は軽くあって、おそらく SQLite だったと思う
見習うべきやり方
修正をバックポートしないなら、テストもバックポートしない可能性が高そう
kheap separation、task port 緩和、SSV、SPTM といった言葉を見ていると、外国語で友人と普通に会話できていたのに、突然脳外科手術や核物理学の説明に移って理解が崖から落ちるような感覚だった
溶鉱炉の改修についての会話を通訳しようとしたときも似たような感じだった
jailbreak が今では昔ほど活発でないのは残念
jailbreak した iPad で実用的なことをしたことはほとんどないが楽しかったし、今なら tethering アプリと UTM、それに JIT の回避策を入れたい
SideStore も有望そうに見えたが、自分のアカウントは一時期有料の Apple Developer アカウントだったため、期限切れにならない App ID が10個残っていて、そのせいで新しいアカウントを作るか再びお金を払わない限り、UTM のようなアプリをインストールできない
それを失ってから Android に戻ったが、その頃には Android も基本機能の面でかなり追いついていた
Apple は最近 jailbreak に100万ドルを支払うと聞いたが、それが自由市場価格の下限なのだろう
仲介業者を通す必要があるのか、それとも一次サポートに入って埋もれずに済む公開メールアドレスのようなものがあるのか分からない
これが事実なら、Apple は驚くべき戦略を使ったことになる
デバイスで root を得るあらゆる道を塞いでおけば、jailbreak 開発者たちが無料で見つけた脆弱性を Apple がパッチできる
記事によると、非公開コミュニティは今も exploit を持っており、Apple はそれらをパッチしている
公開コミュニティやこの開発者の側だけが、何らかの理由でもはやそうではないようだ
記事全体で一番よかった文は「iOS 13.0 でそのバグを unpatch してくれた誰かにも感謝したい。それも非常に素晴らしい行動だった。」だった
「5年後に自分たちがどこにいるのか想像もつかない」とあるが、私は想像できる
iMessage は今もデバイス、アカウント、データの窃取を可能にしている
tethered なのか untethered なのか記事には書かれていなかった
これが含まれて配布された jailbreak である unc0ver は、おそらく「semi-untethered」だったと記憶している