macOS IPC - Inter Process Communication
Last updated
Last updated
Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE) Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Machは、リソースを共有するための最小単位としてタスクを使用し、各タスクは複数のスレッドを含むことができます。これらのタスクとスレッドはPOSIXプロセスとスレッドに1:1でマッピングされています。
タスク間の通信は、Mach Inter-Process Communication (IPC)を介して行われ、一方向の通信チャネルを利用します。メッセージはポート間で転送され、ポートはカーネルによって管理されるメッセージキューのように機能します。
ポートはMach IPCの基本要素です。メッセージを送信し、受信するために使用できます。
各プロセスにはIPCテーブルがあり、そこにはプロセスのmachポートが見つかります。machポートの名前は実際には番号(カーネルオブジェクトへのポインタ)です。
プロセスは、いくつかの権利を持つポート名を別のタスクに送信することもでき、カーネルはこのエントリを他のタスクのIPCテーブルに表示させます。
タスクが実行できる操作を定義するポート権は、この通信の鍵です。可能なポート権は次の通りです(定義はこちらから):
受信権は、ポートに送信されたメッセージを受信することを許可します。MachポートはMPSC(複数のプロデューサー、単一のコンシューマー)キューであり、システム全体で各ポートに対して1つの受信権しか存在できません(パイプとは異なり、複数のプロセスが1つのパイプの読み取り端にファイルディスクリプタを保持できます)。
受信権を持つタスクはメッセージを受信し、送信権を作成することができ、メッセージを送信できます。元々は自分のタスクのみがポートに対して受信権を持っています。
受信権の所有者が死亡するか、それを殺すと、送信権は無効になります(デッドネーム)。
送信権は、ポートにメッセージを送信することを許可します。
送信権はクローン可能であり、送信権を持つタスクはその権利をクローンし、第三のタスクに付与できます。
ポート権はMacメッセージを介しても渡すことができます。
一度だけ送信権は、ポートに1つのメッセージを送信し、その後消失します。
この権利はクローンできませんが、移動することができます。
ポートセット権は、単一のポートではなく、_ポートセット_を示します。ポートセットからメッセージをデキューすると、その中の1つのポートからメッセージがデキューされます。ポートセットは、Unixのselect
/poll
/epoll
/kqueue
のように、複数のポートを同時にリッスンするために使用できます。
デッドネームは実際のポート権ではなく、単なるプレースホルダーです。ポートが破棄されると、そのポートに対するすべての既存のポート権はデッドネームに変わります。
タスクは他のタスクにSEND権を転送でき、それによりメッセージを返送することができます。SEND権もクローン可能であり、タスクはその権利を複製して第三のタスクに与えることができます。これにより、ブートストラップサーバーと呼ばれる仲介プロセスと組み合わせて、タスク間の効果的な通信が可能になります。
ファイルポートは、Macポート内にファイルディスクリプタをカプセル化することを可能にします(Machポート権を使用)。fileport_makeport
を使用して指定されたFDからfileport
を作成し、fileport_makefd
を使用してファイルポートからFDを作成することができます。
前述のように、Machメッセージを使用して権利を送信することは可能ですが、Machメッセージを送信する権利を持っていないと権利を送信することはできません。では、最初の通信はどのように確立されるのでしょうか?
これには、ブートストラップサーバー(macのlaunchd)が関与します。誰でもブートストラップサーバーにSEND権を取得できるため、他のプロセスにメッセージを送信する権利を要求することが可能です:
タスクAが新しいポートを作成し、そのポートに対する受信権を取得します。
タスクAは、受信権の保持者として、ポートの送信権を生成します。
タスクAはブートストラップサーバーとの接続を確立し、最初に生成したポートの送信権を送信します。
誰でもブートストラップサーバーにSEND権を取得できることを忘れないでください。
タスクAは、ブートストラップサーバーにbootstrap_register
メッセージを送信して、与えられたポートをcom.apple.taska
のような名前に関連付けます。
タスクBは、ブートストラップサーバーと対話してサービス名のブートストラップルックアップを実行します(bootstrap_lookup
)。ブートストラップサーバーが応答できるように、タスクBはルックアップメッセージ内で以前に作成したポートへのSEND権を送信します。ルックアップが成功すると、サーバーはタスクAから受け取ったSEND権を複製し、タスクBに送信します。
誰でもブートストラップサーバーにSEND権を取得できることを忘れないでください。
このSEND権を持って、タスクBはタスクAにメッセージを送信することができます。
双方向通信のために、通常タスクBは受信権と送信権を持つ新しいポートを生成し、タスクAに送信権を与えてタスクBにメッセージを送信できるようにします(双方向通信)。
ブートストラップサーバーは、タスクが主張するサービス名を認証できません。これは、タスクが任意のシステムタスクを偽装する可能性があることを意味し、虚偽の認証サービス名を主張してすべてのリクエストを承認することができます。
その後、Appleはシステム提供サービスの名前を安全な構成ファイルに保存し、SIP保護されたディレクトリに配置します:/System/Library/LaunchDaemons
および/System/Library/LaunchAgents
。各サービス名に加えて、関連するバイナリも保存されます。ブートストラップサーバーは、これらのサービス名の受信権を作成し保持します。
これらの事前定義されたサービスについては、ルックアッププロセスがわずかに異なります。サービス名がルックアップされると、launchdはサービスを動的に開始します。新しいワークフローは次のようになります:
タスクBがサービス名のブートストラップルックアップを開始します。
launchdはタスクが実行中かどうかを確認し、実行されていない場合は開始します。
タスクA(サービス)はブートストラップチェックイン(bootstrap_check_in()
)を実行します。ここで、ブートストラップサーバーはSEND権を作成し、それを保持し、受信権をタスクAに転送します。
launchdはSEND権を複製し、タスクBに送信します。
タスクBは受信権と送信権を持つ新しいポートを生成し、タスクA(svc)に送信権を与えてタスクBにメッセージを送信できるようにします(双方向通信)。
ただし、このプロセスは事前定義されたシステムタスクにのみ適用されます。非システムタスクは元の説明のように動作し続け、偽装を許可する可能性があります。
したがって、launchdは決してクラッシュしてはいけません。そうでないと、システム全体がクラッシュします。
mach_msg
関数は、実質的にシステムコールであり、Machメッセージの送信と受信に使用されます。この関数は、最初の引数として送信されるメッセージを必要とします。このメッセージは、mach_msg_header_t
構造体で始まり、その後に実際のメッセージ内容が続きます。この構造体は次のように定義されています:
プロセスが 受信権 を持っている場合、Machポートでメッセージを受信できます。逆に、送信者には 送信 または 一度だけ送信権 が付与されます。一度だけ送信権は、単一のメッセージを送信するためのもので、その後は無効になります。
初期フィールド msgh_bits
はビットマップです:
最初のビット(最も重要なビット)は、メッセージが複雑であることを示すために使用されます(詳細は以下)。
3番目と4番目はカーネルによって使用されます。
2バイト目の5つの最下位ビット は バウチャー に使用できます:キー/値の組み合わせを送信するための別のタイプのポートです。
3バイト目の5つの最下位ビット は ローカルポート に使用できます。
4バイト目の5つの最下位ビット は リモートポート に使用できます。
バウチャー、ローカルポート、リモートポートで指定できるタイプは(mach/message.h から):
例えば、MACH_MSG_TYPE_MAKE_SEND_ONCE
は、このポートに対して送信一回の権利を導出し、転送することを示すために使用できます。また、受信者が返信できないようにするためにMACH_PORT_NULL
を指定することもできます。
簡単な双方向通信を実現するために、プロセスはメッセージヘッダー内のマッチポートを指定できます。これを_返信ポート_(msgh_local_port
)と呼び、メッセージの受信者がこのメッセージに返信を送信できるようにします。
この種の双方向通信は、再生を期待するXPCメッセージで使用されることに注意してください(xpc_connection_send_message_with_reply
およびxpc_connection_send_message_with_reply_sync
)。しかし、通常は異なるポートが作成され、前述のように双方向通信を作成します。
メッセージヘッダーの他のフィールドは次のとおりです:
msgh_size
: パケット全体のサイズ。
msgh_remote_port
: このメッセージが送信されるポート。
msgh_voucher_port
: machバウチャー。
msgh_id
: このメッセージのIDで、受信者によって解釈されます。
machメッセージはmach port
を介して送信されることに注意してください。これは単一受信者、複数送信者の通信チャネルで、machカーネルに組み込まれています。複数のプロセスがmachポートにメッセージを送信できますが、いつでも単一のプロセスのみがそれから読み取ることができます。
メッセージは、mach_msg_header_t
ヘッダーの後に本体、およびトレーラー(ある場合)で構成され、返信の許可を与えることができます。この場合、カーネルは単にメッセージを一つのタスクから別のタスクに渡す必要があります。
トレーラーは、カーネルによってメッセージに追加される情報(ユーザーによって設定できない)で、フラグMACH_RCV_TRAILER_<trailer_opt>
を使用してメッセージ受信時に要求できます(要求できる情報は異なります)。
ただし、追加のポート権を渡したり、メモリを共有したりするような、より複雑なメッセージもあります。この場合、カーネルはこれらのオブジェクトを受信者に送信する必要があります。この場合、ヘッダーの最上位ビットmsgh_bits
が設定されます。
渡す可能な記述子は、mach/message.h
で定義されています:
In 32ビットでは、すべてのディスクリプタは12Bで、ディスクリプタタイプは11番目にあります。64ビットでは、サイズが異なります。
カーネルは、あるタスクから別のタスクにディスクリプタをコピーしますが、最初にカーネルメモリにコピーを作成します。この技術は「風水」として知られ、いくつかのエクスプロイトで悪用され、カーネルがそのメモリにデータをコピーすることを可能にし、プロセスが自分自身にディスクリプタを送信します。その後、プロセスはメッセージを受信できます(カーネルがそれらを解放します)。
また、脆弱なプロセスにポート権を送信することも可能で、ポート権はプロセスに表示されます(たとえそのプロセスがそれらを扱っていなくても)。
ポートはタスクネームスペースに関連付けられているため、ポートを作成または検索するには、タスクネームスペースもクエリされます(mach/mach_port.h
の詳細):
mach_port_allocate
| mach_port_construct
: ポートを作成します。
mach_port_allocate
はポートセットも作成できます:ポートのグループに対する受信権。メッセージが受信されると、どのポートから受信されたかが示されます。
mach_port_allocate_name
: ポートの名前を変更します(デフォルトは32ビット整数)
mach_port_names
: ターゲットからポート名を取得します
mach_port_type
: 名前に対するタスクの権利を取得します
mach_port_rename
: ポートの名前を変更します(FDのdup2のように)
mach_port_allocate
: 新しいRECEIVE、PORT_SETまたはDEAD_NAMEを割り当てます
mach_port_insert_right
: RECEIVE権を持つポートに新しい権利を作成します
mach_port_...
mach_msg
| mach_msg_overwrite
: machメッセージを送受信するために使用される関数。オーバーライトバージョンでは、メッセージ受信のために異なるバッファを指定できます(他のバージョンはそれを再利用します)。
**mach_msg
およびmach_msg_overwrite
**関数はメッセージを送受信するために使用されるため、これらにブレークポイントを設定すると、送信されたメッセージと受信されたメッセージを検査できます。
たとえば、デバッグ可能なアプリケーションをデバッグし始めると、libSystem.B
がロードされ、この関数を使用します。
**mach_msg
**の引数を取得するには、レジスタを確認します。これらが引数です(mach/message.hから):
レジストリから値を取得します:
メッセージヘッダーを検査し、最初の引数を確認します:
そのタイプの mach_msg_bits_t
は、応答を許可するために非常に一般的です。
The nameはポートに与えられたデフォルトの名前です(最初の3バイトでどのように増加しているかを確認してください)。ipc-object
はポートの難読化された一意の識別子です。
また、send
権限のみを持つポートがその所有者を特定している方法にも注意してください(ポート名 + pid)。
さらに、+
を使用して同じポートに接続された他のタスクを示していることにも注意してください。
procesxpを使用して、登録されたサービス名(com.apple.system-task-port
の必要性によりSIPが無効になっている場合)も確認することができます:
You can install this tool in iOS downloading it from http://newosxbook.com/tools/binpack64-256.tar.gz
Note how the sender allocates a port, create a send right for the name org.darlinghq.example
and send it to the bootstrap server while the sender asked for the send right of that name and used it to send a message.
特定の敏感なアクションを実行したり、特定の敏感なデータにアクセスしたりすることを可能にする特別なポートがあります。タスクがそれらに対してSEND権限を持っている場合です。これにより、攻撃者の視点からこれらのポートは非常に興味深いものとなります。なぜなら、機能だけでなく、タスク間でSEND権限を共有できるからです。
これらのポートは番号で表されます。
SEND権利は**host_get_special_port
を呼び出すことで取得でき、RECEIVE権利はhost_set_special_port
を呼び出すことで取得できます。ただし、両方の呼び出しにはhost_priv
ポートが必要で、これはルートのみがアクセスできます。さらに、過去にはルートがhost_set_special_port
**を呼び出して任意のポートをハイジャックでき、例えばHOST_KEXTD_PORT
をハイジャックすることでコード署名をバイパスできました(現在はSIPがこれを防止しています)。
これらは2つのグループに分かれています:最初の7つのポートはカーネルによって所有され、1がHOST_PORT
、2がHOST_PRIV_PORT
、3がHOST_IO_MASTER_PORT
、7がHOST_MAX_SPECIAL_KERNEL_PORT
です。
8から始まる番号のものはシステムデーモンによって所有され、host_special_ports.h
に宣言されています。
ホストポート:このポートに対してSEND権限を持つプロセスは、次のようなルーチンを呼び出すことでシステムに関する情報を取得できます:
host_processor_info
: プロセッサ情報を取得
host_info
: ホスト情報を取得
host_virtual_physical_table_info
: 仮想/物理ページテーブル(MACH_VMDEBUGが必要)
host_statistics
: ホスト統計を取得
mach_memory_info
: カーネルメモリレイアウトを取得
ホスト特権ポート:このポートに対してSEND権限を持つプロセスは、ブートデータを表示したり、カーネル拡張を読み込もうとしたりする特権アクションを実行できます。この権限を取得するにはプロセスがルートである必要があります。
さらに、kext_request
APIを呼び出すには、他の権限**com.apple.private.kext*
**が必要で、これはAppleのバイナリにのみ付与されます。
呼び出すことができる他のルーチンは:
host_get_boot_info
: machine_boot_info()
を取得
host_priv_statistics
: 特権統計を取得
vm_allocate_cpm
: 連続物理メモリを割り当て
host_processors
: ホストプロセッサへの送信権
mach_vm_wire
: メモリを常駐させる
ルートはこの権限にアクセスできるため、host_set_[special/exception]_port[s]
を呼び出してホスト特別または例外ポートをハイジャックできます。
すべてのホスト特別ポートを表示するには、次のコマンドを実行できます:
これらは、よく知られたサービスのために予約されたポートです。task_[get/set]_special_port
を呼び出すことで取得/設定することが可能です。これらはtask_special_ports.h
にあります:
From here:
TASK_KERNEL_PORT[task-self send right]: このタスクを制御するために使用されるポート。タスクに影響を与えるメッセージを送信するために使用されます。これは mach_task_self によって返されるポートです(下記のタスクポートを参照)。
TASK_BOOTSTRAP_PORT[bootstrap send right]: タスクのブートストラップポート。その他のシステムサービスポートの返却を要求するメッセージを送信するために使用されます。
TASK_HOST_NAME_PORT[host-self send right]: 含まれているホストの情報を要求するために使用されるポート。これは mach_host_self によって返されるポートです。
TASK_WIRED_LEDGER_PORT[ledger send right]: このタスクがそのワイヤードカーネルメモリを引き出すソースを示すポート。
TASK_PAGED_LEDGER_PORT[ledger send right]: このタスクがそのデフォルトのメモリ管理メモリを引き出すソースを示すポート。
元々Machには「プロセス」はなく、「タスク」があり、これはスレッドのコンテナのように考えられていました。MachがBSDと統合されたとき、各タスクはBSDプロセスと関連付けられました。したがって、すべてのBSDプロセスはプロセスとして必要な詳細を持ち、すべてのMachタスクもその内部動作を持っています(存在しないpid 0は kernel_task
です)。
これに関連する非常に興味深い関数が2つあります:
task_for_pid(target_task_port, pid, &task_port_of_pid)
: 指定された pid
に関連するタスクのタスクポートのSEND権を取得し、指定された target_task_port
に渡します(通常は mach_task_self()
を使用した呼び出しタスクですが、異なるタスクのSENDポートである可能性もあります)。
pid_for_task(task, &pid)
: タスクへのSEND権を与えられた場合、このタスクが関連するPIDを見つけます。
タスク内でアクションを実行するためには、タスクは mach_task_self()
を呼び出して自分自身への SEND
権を必要としました(これは task_self_trap
(28) を使用します)。この権限を持つことで、タスクは以下のような複数のアクションを実行できます:
task_threads
: タスクのスレッドのすべてのタスクポートに対するSEND権を取得
task_info
: タスクに関する情報を取得
task_suspend/resume
: タスクを一時停止または再開
task_[get/set]_special_port
thread_create
: スレッドを作成
task_[get/set]_state
: タスクの状態を制御
その他の詳細は mach/task.h で見つけることができます。
異なるタスクのタスクポートに対するSEND権を持つことで、異なるタスクに対してそのようなアクションを実行することが可能です。
さらに、task_portは vm_map
ポートでもあり、vm_read()
や vm_write()
などの関数を使用してタスク内のメモリを 読み取りおよび操作 することを可能にします。これは基本的に、異なるタスクのtask_portに対するSEND権を持つタスクがそのタスクに コードを注入する ことができることを意味します。
カーネルもタスクであるため、誰かが kernel_task
に対する SEND権 を取得できれば、カーネルに何でも実行させることができます(脱獄)。
呼び出しタスクのためにこのポートの 名前を取得する ために mach_task_self()
を呼び出します。このポートは exec()
を通じてのみ 継承されます; fork()
で作成された新しいタスクは新しいタスクポートを取得します(特別なケースとして、suidバイナリの exec()
後にタスクも新しいタスクポートを取得します)。タスクを生成し、そのポートを取得する唯一の方法は、fork()
を行いながら "ポートスワップダンス" を実行することです。
これらはポートにアクセスするための制限です(バイナリ AppleMobileFileIntegrity
の macos_task_policy
から):
アプリが com.apple.security.get-task-allow
権限 を持っている場合、同じユーザーのプロセスがタスクポートにアクセスできます(通常はデバッグのためにXcodeによって追加されます)。ノータリゼーション プロセスは、製品リリースではこれを許可しません。
com.apple.system-task-ports
権限を持つアプリは、カーネルを除く 任意の プロセスの タスクポートを取得できます。古いバージョンでは task_for_pid-allow
と呼ばれていました。これはAppleのアプリケーションにのみ付与されます。
ルートは、 ハードニングされた ランタイムでコンパイルされていないアプリケーションのタスクポートにアクセスできます(Apple製ではない)。
タスク名ポート: タスクポート の特権のないバージョン。タスクを参照しますが、制御することはできません。これを通じて利用可能な唯一のものは task_info()
のようです。
スレッドにも関連するポートがあり、これは task_threads
を呼び出すタスクや processor_set_threads
を持つプロセッサから見ることができます。スレッドポートへのSEND権を持つことで、thread_act
サブシステムの関数を使用できます:
thread_terminate
thread_[get/set]_state
act_[get/set]_state
thread_[suspend/resume]
thread_info
...
任意のスレッドは mach_thread_sef
を呼び出すことでこのポートを取得できます。
シェルコードを取得するには:
Introduction to ARM64v8前のプログラムをコンパイルし、同じユーザーでコードを注入できるように権限を追加します(そうでない場合はsudoを使用する必要があります)。