hacktricks/macos-hardening/macos-security-and-privilege-escalation/macos-proces-abuse/macos-ipc-inter-process-communication/macos-thread-injection-via-task-port.md

11 KiB
Raw Blame History

macOS通过任务端口进行线程注入

☁️ HackTricks云 ☁️ -🐦 推特 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥

本文摘自https://bazad.github.io/2018/10/bypassing-platform-binary-task-threads/(其中包含更多信息)

代码

1. 线程劫持

首先,我们调用任务端口上的**task_threads()来获取远程任务中的线程列表,然后选择其中一个线程进行劫持。与传统的代码注入框架不同,我们无法创建一个新的远程线程**,因为thread_create_running()将被新的防护机制阻塞。

然后,我们可以调用**thread_suspend()**来停止线程的运行。

此时,我们对远程线程的唯一有用的控制是停止它,启动它,获取它的寄存器值,并设置它的寄存器。因此,我们可以通过将远程线程中的寄存器x0x7设置为参数,将**pc设置为要执行的函数,并启动线程来发起远程函数**调用。此时,我们需要检测返回值并确保线程不会崩溃。

有几种方法可以实现这一点。一种方法是使用thread_set_exception_ports()为远程线程注册异常处理程序,并在调用函数之前将返回地址寄存器lr设置为无效地址这样在函数运行后将生成一个异常并向我们的异常端口发送消息此时我们可以检查线程的状态以获取返回值。然而为了简单起见我复制了Ian Beer的triple_fetch漏洞利用中使用的策略即将lr设置为一个会无限循环的指令的地址,然后反复轮询线程的寄存器,直到**pc指向该指令**。

2. 用于通信的Mach端口

下一步是创建Mach端口以便我们可以与远程线程进行通信。这些Mach端口在稍后帮助在任务之间传输任意的发送和接收权限时非常有用。

为了建立双向通信我们需要创建两个Mach接收权限一个在本地任务中,一个在远程任务中。然后,我们需要将一个发送权限传输到另一个任务的每个端口。这样,每个任务都有一种可以发送消息并被另一个任务接收的方法。

首先让我们专注于设置本地端口即本地任务持有接收权限的端口。我们可以像创建其他Mach端口一样调用mach_port_allocate()来创建Mach端口。关键是将发送权限传递到远程任务中。

我们可以使用一种方便的技巧,只使用基本的执行原语将发送权限从当前任务复制到远程任务中,即使用thread_set_special_port()将发送权限存储在远程线程的THREAD_KERNEL_PORT特殊端口中;然后,我们可以使远程线程调用mach_thread_self()来检索发送权限。

接下来,我们将设置远程端口,这与我们刚刚所做的相反。我们可以通过调用mach_reply_port()使远程线程分配一个Mach端口我们不能使用mach_port_allocate(),因为后者将在内存中返回分配的端口名称,而我们还没有读取原语。一旦我们有了一个端口,我们可以通过在远程线程中调用mach_port_insert_right()来创建一个发送权限。然后,我们可以使用thread_set_special_port()将端口存储在内核中。最后,在本地任务中,我们可以通过在远程线程上调用thread_get_special_port()来检索端口,从而获得刚刚在远程任务中分配的Mach端口的发送权限

此时我们已经创建了用于双向通信的Mach端口。

3. 基本内存读写

现在我们将使用执行原语来创建基本的内存读写原语。这些原语并不会用于太多的事情(我们很快将升级到更强大的原语),但它们是帮助我们扩展对远程进程控制的关键步骤。

为了使用我们的执行原语读写内存,我们将寻找这样的函数:

uint64_t read_func(uint64_t *address) {
return *address;
}
void write_func(uint64_t *address, uint64_t value) {
*address = value;
}

它们可能对应以下汇编代码:

_read_func:
ldr     x0, [x0]
ret
_write_func:
str     x1, [x0]
ret

快速扫描一些常见的库,发现了一些很好的候选项。要读取内存,我们可以使用Objective-C运行时库中的property_getName()函数:

const char *property_getName(objc_property_t prop)
{
return prop->name;
}

事实证明,propobjc_property_t的第一个字段,因此与上面的假设的read_func直接对应。我们只需要进行远程函数调用,第一个参数是我们想要读取的地址,返回值将是该地址处的数据。

找到一个现成的用于写入内存的函数稍微困难一些但仍然有很好的选择而且没有不希望的副作用。在libxpc中_xpc_int64_set_value()函数的反汇编代码如下:

__xpc_int64_set_value:
str     x1, [x0, #0x18]
ret

因此,要在地址address执行64位写入操作我们可以执行远程调用

_xpc_int64_set_value(address - 0x18, value)

有了这些基本操作,我们就可以创建共享内存了。

4. 共享内存

我们的下一步是在远程任务和本地任务之间创建共享内存。这将使我们能够更轻松地在进程之间传输数据:有了共享内存区域,任意内存读写只需通过远程调用memcpy()即可完成。此外拥有共享内存区域还可以轻松设置堆栈以便我们可以调用具有超过8个参数的函数。

为了简化操作我们可以重用libxpc的共享内存功能。Libxpc提供了一个XPC对象类型OS_xpc_shmem允许在XPC上建立共享内存区域。通过反向工程libxpc我们确定OS_xpc_shmem基于Mach内存条目这些Mach内存条目是代表虚拟内存区域的Mach端口。由于我们已经展示了如何将Mach端口发送到远程任务因此我们可以使用这个方法轻松地设置自己的共享内存。

首先,我们需要使用mach_vm_allocate()来分配我们将共享的内存。我们需要使用mach_vm_allocate()来使用xpc_shmem_create()为该区域创建一个OS_xpc_shmem对象。xpc_shmem_create()将负责为我们创建Mach内存条目并将Mach发送权限存储在偏移量为0x18的不透明OS_xpc_shmem对象中。

一旦我们获得了内存条目端口,我们将在远程进程中创建一个表示相同内存区域的OS_xpc_shmem对象,从而允许我们调用xpc_shmem_map()来建立共享内存映射。首先,我们通过远程调用malloc()来为OS_xpc_shmem分配内存,并使用我们的基本写入原语将本地OS_xpc_shmem对象的内容复制到其中。不幸的是,结果对象并不完全正确:其偏移量为0x18的Mach内存条目字段包含的是本地任务对内存条目的名称而不是远程任务的名称。为了解决这个问题我们使用thread_set_special_port()技巧将Mach内存条目的发送权限插入到远程任务中并将字段0x18覆盖为远程内存条目的名称。此时,远程的OS_xpc_shmem对象是有效的,并且可以通过远程调用xpc_shmem_remote()来建立内存映射。

5. 完全控制

有了已知地址的共享内存和任意执行原语,我们基本上已经完成了。通过调用memcpy()来实现任意内存读写通过按照调用约定在堆栈上布置超过8个参数的附加参数来执行具有超过8个参数的函数调用。通过在之前建立的端口上发送Mach消息可以在任务之间传输任意Mach端口。我们甚至可以使用文件端口在进程之间传输文件描述符特别感谢Ian Beer在triple_fetch中演示了这种技术

简而言之,我们现在对受害进程拥有完全且轻松的控制。您可以在threadexec库中查看完整的实现和公开的API。

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥