macOS IPC - Inter Process Communication
Last updated
Last updated
学习和实践 AWS 黑客技术:HackTricks 培训 AWS 红队专家 (ARTE) 学习和实践 GCP 黑客技术:HackTricks 培训 GCP 红队专家 (GRTE)
Mach 使用 任务 作为共享资源的 最小单位,每个任务可以包含 多个线程。这些 任务和线程与 POSIX 进程和线程 1:1 映射。
任务之间的通信通过 Mach 进程间通信 (IPC) 进行,利用单向通信通道。消息在端口之间传输,端口充当由内核管理的 消息队列。
端口 是 Mach IPC 的 基本 元素。它可以用来 发送消息和接收 消息。
每个进程都有一个 IPC 表,在其中可以找到 进程的 mach 端口。mach 端口的名称实际上是一个数字(指向内核对象的指针)。
一个进程也可以将一个端口名称和一些权限 发送到不同的任务,内核会在 另一个任务的 IPC 表 中显示这个条目。
端口权限定义了任务可以执行的操作,是这种通信的关键。可能的 端口权限 是(定义来自这里):
接收权限,允许接收发送到端口的消息。Mach 端口是 MPSC(多个生产者,单个消费者)队列,这意味着在整个系统中每个端口只能有 一个接收权限(与管道不同,多个进程可以持有一个管道的读端文件描述符)。
拥有 接收权限 的任务可以接收消息并 创建发送权限,允许其发送消息。最初只有 自己的任务对其端口拥有接收权限。
如果接收权限的拥有者 死亡 或被杀死,发送权限变得无用(死名称)。
发送权限,允许向端口发送消息。
发送权限可以被 克隆,因此拥有发送权限的任务可以克隆该权限并 授予给第三个任务。
注意 端口权限 也可以通过 Mac 消息 传递。
一次性发送权限,允许向端口发送一条消息,然后消失。
该权限 不能 被 克隆,但可以被 移动。
端口集权限,表示一个 端口集 而不是单个端口。从端口集中出队一条消息会从其包含的一个端口中出队一条消息。端口集可以用来同时监听多个端口,类似于 Unix 中的 select
/poll
/epoll
/kqueue
。
死名称,这不是一个实际的端口权限,而只是一个占位符。当一个端口被销毁时,所有现有的端口权限都会变成死名称。
任务可以将发送权限转移给其他任务,使其能够发送消息。发送权限也可以被克隆,因此一个任务可以复制并将权限授予第三个任务。这与一个称为 引导服务器 的中介进程结合,使任务之间的有效通信成为可能。
文件端口允许在 Mac 端口中封装文件描述符(使用 Mach 端口权限)。可以使用 fileport_makeport
从给定的 FD 创建一个 fileport
,并使用 fileport_makefd
从 fileport 创建一个 FD。
如前所述,可以使用 Mach 消息发送权限,然而,您 不能在没有发送 Mach 消息的权限的情况下发送权限。那么,如何建立第一次通信呢?
为此,引导服务器(在 mac 中为 launchd)参与其中,因为 任何人都可以获得引导服务器的发送权限,可以请求它发送消息到另一个进程的权限:
任务 A 创建一个 新端口,获得该端口的 接收权限。
任务 A,作为接收权限的持有者,为该端口生成一个发送权限。
任务 A 与 引导服务器 建立 连接,并 将其为最初生成的端口发送的发送权限 发送给它。
请记住,任何人都可以获得引导服务器的发送权限。
任务 A 向引导服务器发送 bootstrap_register
消息,以 将给定端口与名称关联,如 com.apple.taska
。
任务 B 与 引导服务器 交互以执行服务名称的引导 查找(bootstrap_lookup
)。因此,引导服务器可以响应,任务 B 将在查找消息中发送 发送权限到它之前创建的端口。如果查找成功,服务器复制从任务 A 接收到的发送权限 并 将其传输给任务 B。
请记住,任何人都可以获得引导服务器的发送权限。
通过这个发送权限,任务 B 能够 发送 一条 消息 给任务 A。
对于双向通信,通常任务 B 会生成一个带有 接收 权限和 发送 权限的新端口,并将 发送权限授予任务 A,以便它可以向任务 B 发送消息(双向通信)。
引导服务器 无法验证 任务声称的服务名称。这意味着一个 任务 可能会 冒充任何系统任务,例如虚假 声称一个授权服务名称,然后批准每个请求。
然后,Apple 将 系统提供服务的名称 存储在安全配置文件中,位于 SIP 保护 目录:/System/Library/LaunchDaemons
和 /System/Library/LaunchAgents
。每个服务名称旁边,相关的二进制文件也被存储。引导服务器将为这些服务名称创建并持有 接收权限。
对于这些预定义服务,查找过程略有不同。当查找服务名称时,launchd 动态启动该服务。新的工作流程如下:
任务 B 启动对服务名称的引导 查找。
launchd 检查任务是否正在运行,如果没有,则 启动 它。
任务 A(服务)执行 引导检查(bootstrap_check_in()
)。在这里,引导服务器创建一个发送权限,保留它,并 将接收权限转移给任务 A。
launchd 复制 发送权限并将其发送给任务 B。
任务 B 生成一个带有 接收 权限和 发送 权限的新端口,并将 发送权限授予任务 A(服务),以便它可以向任务 B 发送消息(双向通信)。
然而,这个过程仅适用于预定义的系统任务。非系统任务仍然按照最初描述的方式操作,这可能会导致冒充。
因此,launchd 绝不能崩溃,否则整个系统将崩溃。
mach_msg
函数,基本上是一个系统调用,用于发送和接收 Mach 消息。该函数要求将要发送的消息作为初始参数。该消息必须以 mach_msg_header_t
结构开始,后面跟着实际的消息内容。该结构定义如下:
Processes possessing a receive right can receive messages on a Mach port. Conversely, the senders are granted a send or a send-once right. The send-once right is exclusively for sending a single message, after which it becomes invalid.
The initial field msgh_bits
is a bitmap:
First bit (most significative) is used to indicate that a message is complex (more on this below)
The 3rd and 4th are used by the kernel
The 5 least significant bits of the 2nd byte from can be used for voucher: 另一种发送键/值组合的端口类型。
The 5 least significant bits of the 3rd byte from can be used for local port
The 5 least significant bits of the 4th byte from can be used for remote port
The types that can be specified in the voucher, local and remote ports are (from mach/message.h):
例如,MACH_MSG_TYPE_MAKE_SEND_ONCE
可用于 指示 应该为此端口派生并转移一个 一次性发送权。也可以指定 MACH_PORT_NULL
以防止接收者能够回复。
为了实现简单的 双向通信,进程可以在 mach 消息头 中指定一个 mach 端口,称为 回复端口 (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
中定义:
在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):
您可以通过从 http://newosxbook.com/tools/binpack64-256.tar.gz 下载此工具来安装它。
注意 发送者 如何 分配 一个端口,为名称 org.darlinghq.example
创建一个 发送权限 并将其发送到 引导服务器,同时发送者请求该名称的 发送权限 并使用它来 发送消息。
有一些特殊端口允许在任务对其具有 SEND 权限的情况下 执行某些敏感操作或访问某些敏感数据。这使得这些端口从攻击者的角度来看非常有趣,不仅因为其能力,还因为可以 在任务之间共享 SEND 权限。
这些端口由一个数字表示。
SEND 权限可以通过调用 host_get_special_port
获得,而 RECEIVE 权限则通过调用 host_set_special_port
获得。然而,这两个调用都需要 host_priv
端口,只有 root 可以访问。此外,在过去,root 能够调用 host_set_special_port
并劫持任意端口,例如通过劫持 HOST_KEXTD_PORT
来绕过代码签名(SIP 现在防止了这一点)。
这些端口分为两组:前 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 权限的进程可以执行 特权操作,例如显示启动数据或尝试加载内核扩展。进程需要是 root 才能获得此权限。
此外,为了调用 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
: 使内存常驻
由于 root 可以访问此权限,它可以调用 host_set_[special/exception]_port[s]
来 劫持主机特殊或异常端口。
可以通过运行以下命令 查看所有主机特殊端口:
这些是为知名服务保留的端口。可以通过调用 task_[get/set]_special_port
来获取/设置它们。它们可以在 task_special_ports.h
中找到:
从 这里:
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
)。
与此相关的有两个非常有趣的函数:
task_for_pid(target_task_port, pid, &task_port_of_pid)
: 获取与指定的 pid
相关的任务的任务端口的发送权限,并将其授予指定的 target_task_port
(通常是使用 mach_task_self()
的调用任务,但也可以是不同任务上的发送端口。)
pid_for_task(task, &pid)
: 给定一个任务的发送权限,查找该任务相关的 PID。
为了在任务内执行操作,任务需要对自己调用 mach_task_self()
的 SEND
权限(使用 task_self_trap
(28))。有了这个权限,任务可以执行多个操作,例如:
task_threads
: 获取任务线程的所有任务端口的发送权限
task_info
: 获取有关任务的信息
task_suspend/resume
: 暂停或恢复任务
task_[get/set]_special_port
thread_create
: 创建线程
task_[get/set]_state
: 控制任务状态
更多内容可以在 mach/task.h 中找到
请注意,拥有 不同任务 的任务端口的发送权限,可以对不同任务执行此类操作。
此外,task_port 也是 vm_map
端口,允许使用 vm_read()
和 vm_write()
等函数 读取和操作内存。这基本上意味着,拥有不同任务的 task_port 的发送权限的任务将能够 注入代码到该任务中。
请记住,因为 内核也是一个任务,如果有人设法获得 kernel_task
的 发送权限,它将能够使内核执行任何操作(越狱)。
调用 mach_task_self()
以 获取此端口的名称,用于调用任务。此端口仅在 exec()
之间 继承;使用 fork()
创建的新任务会获得一个新的任务端口(作为特例,任务在 suid 二进制文件中 exec()
后也会获得一个新的任务端口)。生成任务并获取其端口的唯一方法是在执行 fork()
时进行 "port swap dance"。
访问端口的限制(来自二进制文件 AppleMobileFileIntegrity
的 macos_task_policy
):
如果应用具有 com.apple.security.get-task-allow
权限,则来自 同一用户的进程可以访问任务端口(通常由 Xcode 添加用于调试)。公证 过程不允许其用于生产版本。
具有 com.apple.system-task-ports
权限的应用可以获取 任何 进程的任务端口,除了内核。在旧版本中称为 task_for_pid-allow
。这仅授予 Apple 应用。
Root 可以访问未使用 hardened 运行时(且不是来自 Apple)编译的应用的任务端口。
任务名称端口: 一个未特权版本的 任务端口。它引用任务,但不允许控制它。通过它似乎唯一可用的功能是 task_info()
。
线程也有相关的端口,可以从调用 task_threads
的任务和使用 processor_set_threads
的处理器中看到。对线程端口的发送权限允许使用 thread_act
子系统中的函数,例如:
thread_terminate
thread_[get/set]_state
act_[get/set]_state
thread_[suspend/resume]
thread_info
...
任何线程都可以通过调用 mach_thread_sef
获取此端口。
您可以从以下位置获取 shellcode:
Introduction to ARM64v8编译之前的程序并添加权限以便能够以相同用户注入代码(如果没有,您将需要使用sudo)。