LINE株式会社は、2023年10月1日にLINEヤフー株式会社になりました。LINEヤフー株式会社の新しいブログはこちらです。 LINEヤフー Tech Blog

Blog


Another One Bites the Apple!

こんにちは。LINEでセキュリティを担当しているチャン・ジュノです。私は、LINEが提供するサービスをハッキングし、そのセキュリティを強化する仕事を担当しています。また、趣味として他社の製品の脆弱性を見つけて報告することで、より安全な世界を実現することに貢献しています。
このようにセキュリティの脆弱性を探すことを、ハッカーの間ではバグハンティング(bug hunting)と言います。ハッカーは、バグバウンティ(bug bounty)で賞金を獲得します。賞金がなくても、ハッカーとして名声を得るために、あるいは純粋に面白くてハッキングします。

この記事では、多くのハッカーがターゲットにするApple製品で、バグハンティングを行った過程を紹介します。 

バグハンティングのターゲット選定

Appleは、MacBook、iMac、Mac Pro、iPhone、iPad、Apple Watchなど、さまざまな製品を販売しています。
これらの製品は、パソコンとモバイルデバイスに分けられます。パソコンではOS(Operating System)としてmacOSを使い、モバイルデバイスではiOSを使用しています。

macOS対iOS

macOSとiOSは、構造が非常に似ています。たとえば、同じ種類のデーモン(daemon)が多数実行されていて、プロセス間通信のプロトコルも似ています。また、OSのカーネル(kernel)の構造も似ています。
ただ、macOSを使用しているパソコンと、iOSを使用しているモバイルデバイスは、内部の仕組みに違いがあります。その違いが原因で、デバイスドライバーや、モバイルデバイスの効率化のためにあるキャッシュ(cache)システムにも違いがあります。

では、ハッカーから見ると、macOSとiOSのどちらがバグハンティングしやすいでしょうか。

iOSのデバッグは、ユーザーレベルのデバッグもカーネルレベルのデバッグも、非常に難しい作業です。最新のiOSでは、脆弱性なしにはカーネルをデバッグできないので、まるでブラックボックステスト(black-box testing)をするような作業になります。

一方、macOSではVM(Virtual Machine)を使うとカーネルデバッグが容易で、fuzzer(脆弱性を発見するプログラム)を開発・実行するのも容易です。
そのため一般には、macOSでバグハンティングして脆弱性を発見し、その脆弱性がiOSに存在するかを確認するケースが多くなっています。もちろんiOSでのみ発生する脆弱性があるため、モバイルデバイスの特性を利用した機能だけを狙っているハッカーもいます。

このような特性を踏まえ、今回はmacOSを対象にバグハンティングをしました。

ハッキングの目標設定

macOS内には、さまざまなハッキング対象があります。ハッカーが主に選択する対象は、WebブラウザーであるSafariや、デーモン、カーネルなどです。
Safariを対象にする場合は、リモートコード実行(Remote Code Execution)を目標にハッキングを試みます。
デーモンやカーネルを対象にする場合は、ローカル権限昇格(Local Privilege Escalation)を目指してハッキングを試みます。
Safariでローカル権限昇格に成功する攻撃コードには、500,000ドル(USD)以上を獲得できるほどの価値があります。そのため、多くのハッカーが虎視眈々と、脆弱性を見つけようとチャンスを狙っています(参照)。

今回は、ローカル権限昇格ができるカーネルの脆弱性を見つけ出すことを目標にしました。

macOSカーネルについて

macOSとiOSでは、XNU(X is Not Unix)という名前のカーネルを使っています。

XNUカーネルは、大きく2つのオープンソースOSを修正して作ったものです。まず、BSD(Berkeley Software Distribution)システムを利用してsystem callファイルシステムを構築しました。次に、Machカーネギーメロン大学Machカーネル)システムを利用してプロセス間通信(IPC: Inter-Process Communication)を実装しました。他のUnix OSとの違いは、Machシステムを使っているところです。

<XNUカーネル>

Machカーネルとは?

Machカーネルは、タスク、スレッド、ポートのようにシステムを構成する基本的な(primitive)機能を定義し、提供しています。
この記事では、カーネルのバグハンティングに向けて、ユーザーレベルのプロセスがどのようにカーネルと通信するかに焦点を合わせます。

ユーザーのスペースで実行されるプロセスは「Mach messages」(以降、Machメッセージ)というプロトコルを利用してカーネルスペースと通信します。このMachメッセージは、RPC(Remote Procedure Call)の一種で、次に説明するMiGというプログラムで生成します。

<ユーザーのスペースとカーネルのスペース間の通信>

MiG(Mach Interface Generator)とは?

MiGは、RPCコードを生成するツールです。MiGを使うと、クライアントコードとサーバーコードを生成できます。カーネルと通信する際は、一般にユーザーがクライアントの役割をし、カーネルがサーバーの役割をします。MiGは、macOSが提供するプログラムのため、macOSのターミナル環境で'man mig'を実行すると、マニュアルを確認できます。

<MiGプログラムのマニュアル>

MiGプログラムにdefinitionファイルを入力すると、クライアントコードとサーバーコードが生成されます。例えば、host_kernel_versionという関数を定義したdefinitionファイルをMiGプログラムに入力すると、以下の画像のようにmach_hostUser.c(クライアントコード)とmach_hostServer.c(サーバーコード)が生成されます。

<MiGが生成したhost_kernel_version関数>

カーネルバージョンの出力プログラム作り

次に、カーネルとの通信を理解するために、現在のカーネルシステムのバージョンを出力するプログラムを作ってみます。

以下のようにコーディングしてコンパイル・実行すると、先ほど作成したhost_kernel_version関数を利用して、macOSのカーネルバージョンを出力できます。

<カーネルバージョンの出力プログラム>

host_kernel_version関数の動作をさらに詳しく見てみましょう。

カーネルとMachメッセージをやりとりするために、クライアントコードとサーバーコードが実行されています。host_kernel_version関数を呼び出すと、ユーザーライブラリーでプロトコルに合わせてMachメッセージを作成し、カーネルハンドラーに送信します。Machメッセージを受信したカーネルハンドラーは、Machメッセージを解析し、カーネルバージョンを取得する関数を実行します。

つまり、ユーザープロセスで使っているさまざまな関数は、このようにMachメッセージを利用してカーネルと通信しているのです。

<Machメッセージによるhost_kernel_version関数の実行例>

ファジング(Fuzzing)、無作為データを入力して脆弱性を検出する技術

ファジング(Fuzzing)は、自動で脆弱性を検出する技術の一つです。対象プログラムで無作為データを入力して異常な動作をするようにしたり、クラッシュの発生で異常終了するようにしたりして、脆弱性を検出します。

ファザー(Fuzzer)は、ファジングを行うツールです。今回は、カーネルを対象に、特にMachメッセージによる通信を対象にファザーを設計しました。 

MiG Fuzzerの設計

ユーザーのプロセスでカーネルを呼び出せる関数を集め、その一部を無作為で選択してファジングを行います。前述のMachメッセージを生成するクライアントコードをMiGで実装し、カーネルにMachメッセージを送信します。

以下の画像でファジングの対象は、赤色で示したカーネルコードです。カーネルに送るMachメッセージを生成する過程で、引数の値の有効性を確認する部分をすべて取り除き、無作為の値を制限なくカーネルに送信できるようにしました。

<iG Fuzzer>

例えば、仮にMiG Fuzzerで無作為で選択した関数がhost_info関数とします。以下の画像のように、関数テーブルのmig_table変数から_call_host_info関数を選択して呼び出します。

<MiG Fuzzerの関数テーブルから関数を一つ選択>

_call_host_info関数では、ファジングを行う任意の引数を設定し、host_info関数を呼び出します

<MiG Fuzzerの_call_host_info関数

host_info関数では、入力してもらった引数の値でMachメッセージを作成しカーネルハンドラーに送信します。

<MiG Fuzzerライブラリーのhost_info関数

このように任意の関数を選択してからその関数の引数を任意の値に設定し、カーネルハンドラーに送信することを繰り返します。その後、macOSカーネルが異常な挙動を見せたり、または異常終了したりすることを確認して、脆弱性を検出します。

ファジングの実行

次は、ファジングを実行する環境をどのように構成するかと脆弱性をどのように判別するかについて説明します。

ファジングを実行するためには、ファザーの開発だけでなく、ファジングを実行する環境の構築も必要です。また、脆弱性がきちんと誘発されたかどうかを判別し、クラッシュが発生したらその原因を自動で分類するなどの機能をもつプログラムも必要です。ファジングが自動で円滑に行われるように、このようなプログラムをまとめた「ファジングフレームワーク」を構成します。

ファジングを実行する環境の構築

ファジングは、無作為でデータを入力するため、できるだけ多くの対象が実行できるように環境を構築すると良いでしょう。私は、MacBook 4台にmacOSのVM(Virtual Machine)をそれぞれ2台ずつインストールし、計8台のファジング環境を構築しました。 

<8 VMs on 4 MacBooks>

ファジングのフレームワークの構成

各VMでMiG Fuzzerを実行し、クラッシュが発生したときに、クラッシュレポートを以下の画像のようにCrash Collectorサーバーに送信するようにしました。Crash Collectorサーバーでは自動でクラッシュレポートを分類するように設定しました。これは、MiG Fuzzer実行後にクラッシュを確認する際、さらに分析が必要な脆弱性なのかどうかを判別するときに役立ちます。

<ファジングのフレームワーク>

ファジングの実行結果

約2週間、ファジングを実行して以下のような結果を得ました。各VMからのクラッシュレポートから重要な内容を抽出し、固定長のMD5ハッシュ値を生成します。このハッシュ値を利用して重複する内容を取り除き、一目で脆弱性が把握できるようにしました。

<ファジングの結果>

クラッシュの分析

ファジングの結果を分析し、脆弱性の可能性が高いクラッシュを分類したら、該当クラッシュを再度発生させられるかを判断します。どのコードが対象カーネルにクラッシュを発生させたか確認するため、クラッシュを再現して分析します。ファジングを行う際には、どのコードを実行したのかを記録しておくと、その後クラッシュの再現に役立ちます。

次に、クラッシュレポートの分析、クラッシュの再現、そしてクラッシュ発生の原因を分析した内容を紹介します。

クラッシュレポートの分析

クラッシュレポートを分析すると、どの関数でクラッシュが発生したかが分かります。以下は、興味深かったクラッシュレポートから一部を抜粋したものです。カーネルのmach_vm_page_range_query関数でvm_map_page_range_info_internal関数を呼び出し、その内部でbzero関数を実行している途中クラッシュが発生したことが分かります。

<クラッシュレポート>

クラッシュの再現

ファジングを行ったときの記録をもとに、クラッシュを発生させるコードを作成しました。このように、クラッシュまたは脆弱性を再現する最も簡単なコードは、PoC(Proof of Concept)と呼ばれます。

<簡単なPoCコード>

このコードを実行するだけで、カーネルでクラッシュが発生して再起動します(テストしてみる方は、VMでの実行をおすすめします)。このコードは、macOS 10.14.4, iOS 12.2を対象にクラッシュが発生します。

クラッシュの発生原因を分析

PoCで入力した各引数を確認し、クラッシュが発生した原因を分析します。XNUカーネルのソースコードは、一部が公開されています。該当コードをもとにクラッシュが発生する関数内のコードを読んでいきます。

PoCコードで、mach_vm_page_range_query関数を呼び出しましたが、2番目の引数はaddress、3番目の引数はsizeの値です。これらの値は、それぞれ0x10と0xffffffffffffffff(16個のf)です。なおsizeの値は、64ビット(8バイト)integerで表現すると、-1と同じです。この値が、そのままカーネル内mach_vm_page_range_query関数の引数として入ります。mach_vm_page_range_query関数内では、ローカル変数startとendが計算され、それぞれ0と0x1000になります。

endの値を計算する際に、addressとsizeを足した値をmach_vm_round_page関数に渡しています。ここで、integer overflowが発生します(0x10の値と0xffffffffffffffffの値を足すと、論理的には0x10000000000000009ですが、関数内では64ビットのintergerで表現する範囲に限られ、一番上の桁の1の値を含められないため、実際には0x9の値になります。ここをチェックしなかったのが、クラッシュが発生した原因だと言えます)。この値は、その後mach_vm_roud_page関数で丸められ、endは0x1000になります。

<mach_vm_page_range_query関数の内部分析1>

クラッシュの価値評価

該当クラッシュが脆弱性として価値があるかどうかを判断するために、クラッシュが発生したところまでカーネルソースコードを読んでいきます。

カーネルソースコードの追跡

mach_vm_page_range_query関数内で計算されたstartとendの値(0と0x1000)を利用して、curr_szおよびnum_pagesの値が計算されます(start=0, end=0x1000の場合は、curr_sz=0x1000, num_pages=1)。また、num_pagesの値を利用してkalloc関数を呼び出し、カーネルヒープ(heap)領域のメモリー(カーネルヒープメモリー)を割り当てます。結果的に、info変数には32バイト、local_disp変数には4バイトが割り当てられます。

ここで、処理対象になっているsizeが0xffffffffffffffffと非常に大きい領域にも関わらず、実際に割り当てられているのは32バイトに過ぎないというところが、後々問題を引き起こします。

<mach_vm_page_range_query関数の内部分析2>

続きのコードを見ていきましょう。

while文(繰り返し文)の内部で、vm_map_page_range_info_internal関数を呼び出しています。この関数には、メモリの位置とサイズに関する情報を受け取り、そのメモリをbzero関数を使って0クリアする処理があります。また、この関数は、while文(繰り返し文)の中にあるため、sizeが0になるまで繰り返し呼び出されます。

<mach_vm_page_range_query関数の内部分析3>

このとき注目すべきは、mach_vm_page_range_query関数のcurr_szの値です。

vm_map_page_range_info_internal関数を初めて呼び出すときは、上で説明したとおり、curr_sz = MIN(end - start, MAX_PAGE_RANGE_QUERY) で計算されるため、curr_sz = 0x1000(4096)です。curr_szを、endとstartをもとに計算しているため問題が起きないとも言えるでしょう。

しかし、2回目の呼び出しでは、while文の後半にあるcurr_sz = MIN(mach_vm_round_page(size), MAX_PAGE_RANGE_QUERY) で計算されます。curr_szを、sizeをもとに計算していることが問題です。sizeが非常に大きいためMAX_PAGE_RANGE_QUERY(1GB)で丸められますが、それでもcurr_sz = 1GBになります。これは、問題が起きる大きさです。

次に、vm_map_page_range_info_internal関数で、どのように0クリアするメモリサイズを計算しているか見ていきましょう。vm_map_page_range_info_internal関数では、curr_szの値をもとに計算された値を使って、curr_s_offsetとcurr_e_offsetを求めています。さらに、以下のようにcurr_s_offsetとcurr_e_offsetの値をもとに、bzero関数で0クリアするメモリサイズ(ページ数×ページ情報の大きさ)を決定しています。
bzero関数で0クリアするメモリサイズが、Info変数に割り当てられている32バイト(1ページ分)を超えれば、クラッシュする可能性があります。そして今回の例では、1回目の呼び出しでは0クリアするメモリサイズが32バイト(1ページ分)に収まりますが、2回目では32バイトをはるかに超える8MB(0x3ffffページ分)になり、クラッシュしていました。

<vm_map_page_range_info_internal関数の内部分析>

評価

ローカル権限昇格を目指してハッキングするときは、通常「ファジング」→「クラッシュを多数発見」→「クラッシュの分類」→「有効に見えるクラッシュの分析」→「エクスプロイト(exploit)の実行(クラッシュさせずに、任意のコードを実行させる)」のような順番で作業します。

攻撃が成功する前にカーネルクラッシュが発生するとOSが再起動するため、カーネルクラッシュを発生させずに攻撃を成功させる必要があります。上記の脆弱性では、32バイトで割り当てられたカーネルヒープメモリーに8MBの大きさを上書き(overwrite)することに成功しています。カーネルヒープメモリーには、さまざまなオブジェクトの値とpageを構成するメタデータの値が存在します。この値が書き変わると、カーネルクラッシュを起こさずに攻撃を続けることが難しくなります。そのため、ハッキングを成功させるには、カーネルクラッシュを誘発せずに攻撃できる新しい技術を研究するか、または他の脆弱性を探す必要があります。

カーネルソースコードの分析中に発見された他の脆弱性を誘発するポイント

前述のmach_vm_page_range_query関数とvm_map_page_range_info_internal関数の内部を分析している中で、原因は同じですが違う脆弱性を誘発するポイントを見つけました。address変数の値を0xfffffffffffee010のように大きな値に設定する場合、bzero関数を実行せず、local_disp変数が指しているカーネルヒープメモリーに値を上書きします。 

<mach_vm_page_range_query関数の内部分析4>

しかしこの脆弱性も、4バイトの大きさで割り当てたカーネルヒープメモリーに1MBの値で上書きを行うため、前述の脆弱性と同じ問題があり、現時点ではハッキングが成功していません。

<ヒープ・バッファ・オーバーフロー>

脆弱性の活用方法の研究

現在、仕事の合間を縫って、この脆弱性を活用できる方法を研究しています。一つ思い浮かんだアイデアは、下記で説明する「カーネルヒープfeng-shui」技術を活用することです。

カーネルヒープfeng-shuiとは?

「カーネルヒープfeng-shui」は、風水という言葉から由来した用語で、カーネルヒープメモリーをハッカーが希望する形にして攻撃する方法の一つです。この技術を活用し、local_disp変数が指しているヒープメモリーのアドレスから1MB程度を不要な値で上書きし、 カーネルクラッシュが誘発できないようにすると、脆弱性を十分に活用できると思います。

<カーネルヒープfeng-shui>

まず、上の画像のようにカーネルヒープメモリーを構成します。そして、1MB目以後のところに参照カウント(reference counting)を持つ特定オブジェクトを割り当てることができれば、1MBを上書きします。その後、参照カウントを0にすると「Use After Free」の脆弱性に変形できます。Use after freeの脆弱性に変形すれば、多くのハッカーが該当の脆弱性を活用してカーネルの権限を獲得し、ローカル権限昇格ができます。

終わりに

当該の脆弱性は、2019年5月14日に公開されたmacOS 14.4.5、iOS 12.3のアップデートでパッチされました(脆弱性識別番号:CVE-2019-8576)。
発見した脆弱性をパッチするため、Appleが提示する方法を参照して脆弱性についてできるだけ詳細に書きました。そして、それを専用のメールアドレス(product-security@apple.com)に送信しました。メールで報告したら、まずAppleから自動で番号を付与した返信があり、しばらくしてから、当該脆弱性の担当者から連絡がありました。パッチのスケジュールの概要が伝えられ、セキュリティアップデートに残す名前を聞かれました。
Appleでは、さまざまな脆弱性の報告を受け、定期的にセキュリティアップデートを行います。今回は、脆弱性の報告からパッチまで約3か月かかりました。セキュリティアップデートのサイクルによって報告してからパッチまで、通常1~3か月程度かかるようです。

さて、この記事では、カーネルのバグハンティングの方法や、発見した脆弱性を分析した内容を紹介しました。発見した脆弱性をうまく活用できていないように見えますが、カーネルヒープメモリーをさらにうまく扱う方法がわかれば、脆弱性を十分に活用できるでしょう。

バグハンティングに関心のある方々と一緒にアイデアを共有し、研究を進めたいと思います。長文を読んでいただき、ありがとうございました。