原文地址:https://worthdoingbadly.com/specialreply/
近期,macOS 11.0/iOS 14.2/iOS 12.4.9修复了一个安全漏洞:因host_request_notification未检查port->ip_specialreply而导致的覆盖ip_sync_inheritor_port的安全漏洞。这貌似可以在遇到区块检查错误时重启系统,但我想知道,除此之外,它还能做些什么。
正如Synacktiv所详细介绍的那样,我们也可以通过BinDiff找到CVE-2020-27932的修复程序。
按照Synacktiv介绍的方法,我们发现这次修复的函数是ffffff0076bb370:host_request_notification。
并且,修正后的函数只是增加了一项检查。
在下面的BinDiff窗口中,显示了两个并排的代码块,并且,右边有一行代码处于突出显示状态
原来的代码:
if (!ip_active(port) || port->ip_tempowner || ip_kotype(port) != IKOT_NONE) {
修改后的代码:
if (!ip_active(port) || port->ip_tempowner || port->ip_specialreply || ip_kotype(port) != IKOT_NONE) {
实际上,这段代码在旧版macOS/iOS系统上运行时并没有什么问题,但在macOS 11.0/10.5.7版本于11月更新至/iOS 14.2/iOS 12.4.9之后后,上述代码就会出现KERN_FAILURE错误。
mach_port_t port = thread_get_special_reply_port();
kern_return_t err = host_request_notification(mach_host_self(), HOST_NOTIFY_CALENDAR_CHANGE, port);
这个函数用于每当macOS/iOS系统上的日期或时间发生变化时获取相应的通知。
调用host_request_notification会将端口添加到将接收日期/时间更改通知的端口的双向链表中。
为了便于从链接列表中删除端口,列表条目还将存储在端口的ip_kobject字段中。
ipc_kobject_set_atomically(port, (ipc_kobject_t)entry, IKOT_HOST_NOTIFY);
它会将ip_kotype(port)设置为IKOT_HOST_NOTIFY, 并将port->ip_kobject设置为条目。
这就是内核如何将一个Mach端口与一个内核对象关联起来的方式。其他代表内核对象的Mach端口,如任务端口或定时器端口,也使用ip_kotype和ip_kobject来存储它们关联的内核对象。
当端口被销毁时,它会调用host_notify_port_destroy,重新读取列表条目,并将其从列表中解除链接。
if (ip_kotype(port) == IKOT_HOST_NOTIFY) {
entry = (host_notify_t)port->ip_kobject;
那么,这些特殊回复端口(special reply port)到底有什么特别之处呢?它们能做哪些其他端口做不到的事情?
在内核源代码中搜索ip_specialreply,我们只找到了29个引用,其中大部分都与QoS和turnstile有关。
在Mach RPC中,端口是单向的,因此,当您向另一个进程发送消息时,同时需要提供一个回复端口。这样的话,远程进程会将它的响应发送回指定的回复端口。
下面的说明,取自OSFMK/mach/message.h:
* The msgh_remote_port field specifies the destination of the message.
* It must specify a valid send or send-once right for a port.
*
* The msgh_local_port field specifies a "reply port". Normally,
* This field carries a send-once right that the receiver will use
* to reply to the message. It may carry the values MACH_PORT_NULL,
* MACH_PORT_DEAD, a send-once right, or a send right.
下面是Mach消息的工作原理图:
Me -----------------------------------> [destination port] other process
{message, reference to reply port}
[reply port] <----------------------------------
{response message}
由于几乎每一个IPC请求都有响应,因此,Mach会让我们创建一个特殊的回复端口,并且内核对这个端口进行了优化,以便在回复过程中获得更好的QoS——例如,避免优先级倒置。为了让这个QoS发挥作用,在发送和接收消息时,回复端口会与目的端口建立相连。
在iOS 11和12中,特殊的回复端口发生了变化。
在iOS 11中,每个任务都有一个回复端口(mach_reply_port),而不是单一的回复端口,现在是通过thread_get_special_reply_port函数按线程创建回复端口。
由于一个进程现在可以拥有多个特殊的回复端口,iOS 增加了布尔值ip_specialreply来指示该端口是否下面是一个特殊的回复端口。
我们可以将xnu-3789.1.32的osfmk/ipc/ipc_port.h与xnu-4570.1.46的比较一下:
- ip_reserved:2,
+ ip_specialreply:1, /* port is a special reply port */
+ ip_link_sync_qos:1, /* link the special reply port to destination port */
在iOS 12中,对回复端口的QoS系统进行了重写,它不仅可以链接到端口,还可以链接到其他QoS对象,如knode和turnstile。下面,我们来比较xnu-4570.1.46和xnu-4903.221.2:
ip_specialreply:1, /* port is a special reply port */
- ip_link_sync_qos:1, /* link the special reply port to destination port */
+ ip_sync_link_state:3, /* link the special reply port to destination port/ Workloop */
因此,在iOS 12中,我们使用以下字段来表示特殊回复及其链接:
ip_specialreply:1, /* port is a special reply port */
ip_sync_link_state:3, /* link the port to destination port/ Workloop */
其中,ip_sync_link_state字段可以取以下值:
/*
* SYNC IPC state flags for special reply port/ rcv right.
*
* PORT_SYNC_LINK_ANY
* Special reply port is not linked to any other port
* or WL and linkage should be allowed.
*
* PORT_SYNC_LINK_PORT
* Special reply port is linked to the port and
* ip_sync_inheritor_port contains the inheritor
* port.
*
* PORT_SYNC_LINK_WORKLOOP_KNOTE
* Special reply port is linked to a WL (via a knote).
* ip_sync_inheritor_knote contains a pointer to the knote
* the port is stashed on.
*
* PORT_SYNC_LINK_WORKLOOP_STASH
* Special reply port is linked to a WL (via a knote stash).
* ip_sync_inheritor_ts contains a pointer to the turnstile with a +1
* the port is stashed on.
*
* PORT_SYNC_LINK_NO_LINKAGE
* Message sent to special reply port, do
* not allow any linkages till receive is
* complete.
*
* PORT_SYNC_LINK_RCV_THREAD
* Receive right copied out as a part of bootstrap check in,
* push on the thread which copied out the port.
*/
那么这些链接是如何建立的呢?
当一个进程通过这个调用链向目标端口发送消息,并附加回复端口时,就会产生一个链接:
mach_msg_overwrite_trap
ipc_kmsg_copyin_header
ipc_kmsg_set_qos
ipc_port_link_special_reply_port.
或者,也可以在目的地接收到消息时通过以下调用链创建它:
mach_msg_overwrite_trap
mach_msg_rcv_link_special_reply_port
ipc_port_link_special_reply_port
在这两种情况下,都会创建如下所示的链接:
/* Check if we need to drop the acquired turnstile ref on dest port */
if (!special_reply_port->ip_specialreply ||
special_reply_port->ip_sync_link_state != PORT_SYNC_LINK_ANY ||
special_reply_port->ip_sync_inheritor_port != IPC_PORT_NULL) {
drop_turnstile_ref = TRUE;
} else {
/* take a reference on dest_port */
ip_reference(dest_port);
special_reply_port->ip_sync_inheritor_port = dest_port;
special_reply_port->ip_sync_link_state = PORT_SYNC_LINK_PORT;
}
所以:如果可以链接特殊端口,且还没有继承端口,就可以把回复端口链接到目的端口。
当特殊回复端口需要更新时(例如,在目的端口收到消息后,或者当一个旧的线程特殊端口被新的端口取代时),内核就会调用ipc_port_adjust_special_reply_port_locked,根据特殊回复端口的当前状态来更新其链接对象。
如果端口没有被链接,则不会发生任何事情:
/* Check if the special reply port is marked non-special */
if (special_reply_port->ip_sync_link_state == PORT_SYNC_LINK_ANY) {
not_special:
if (get_turnstile) {
turnstile_complete((uintptr_t)special_reply_port,
port_rcv_turnstile_address(special_reply_port), NULL, TURNSTILE_SYNC_IPC);
}
imq_unlock(&special_reply_port->ip_messages);
ip_unlock(special_reply_port);
if (get_turnstile) {
turnstile_cleanup();
}
return;
}
否则,根据状态和新的所需链接,在端口/knote/turnstile之间进行链接交换。
据我所知,这两个函数是唯一对ip_sync_inheritor_port执行写入操作的函数。此外, ipc_port_adjust_special_reply_port_locked是唯一一个写入另外两个字段(即ip_sync_inheritor_knote和ip_sync_inheritor_ts)的函数。
为什么对这些字段的写入操作非常重要呢?
ip_kobject、ip_sync_inheritor_port、ip_sync_inheritor_knote和ip_sync_inheritor_ts是在一个共用体(union)中声明的!
union {
ipc_kobject_t kobject;
ipc_importance_task_t imp_task;
ipc_port_t sync_inheritor_port;
struct knote *sync_inheritor_knote;
struct turnstile *sync_inheritor_ts;
} kdata;
然而,这些字段中的内容并不是集中存放的:ip_kotype和ip_sync_link_state并不是一起存储的。
这意味着可以通过使用host_request_notification的链表条目来覆盖ip_sync_inheritor_port!
并且,为了实现上述目的,我们有多种方法可以使用,但这里将采用一个最简单的方法,利用这个漏洞让内核崩溃。
首先,我们需要调用thread_get_special_reply_port。
这将为这个线程创建一个新的特殊回复端口。
Reply port
- ip_sync_link_state: PORT_SYNC_LINK_ANY
- {ip_kobject, ip_sync_inheritor_*}: null
- ip_kotype: IKOT_NONE
要改变ip_sync_link_state,我们需要调用ipc_port_link_special_reply_port函数。调用这个函数的最简单的方法,是尝试在特殊回复端口上接收消息,并将目标端口作为通知端口(如内核的单元测试test/kevent_qos.c所示)。
之后,mach_msg_rcv_link_special_reply_port将调用ipc_port_link_special_reply_port,从而将特殊回复端口与目的端口链接起来:
Reply port
- ip_sync_link_state: PORT_SYNC_LINK_PORT
- {ip_kobject, ip_sync_inheritor_*}: destination port
- ip_kotype: IKOT_NONE
当它等待接收消息时,我们在另一个线程中调用host_request_notification,从而在不改变ip_sync_link_state的情况下写入ip_kobject和ip_kotype:
Reply port
- ip_sync_link_state: PORT_SYNC_LINK_PORT
- {ip_kobject, ip_sync_inheritor_*}: host notify link entry (overwritten!!)
- ip_kotype: IKOT_HOST_NOTIFY
当接收超时时,内核会调用ipc_port_adjust_special_reply_port_locked来解除端口的链接。
当函数得到一个链接的列表条目而不是它所期望的端口时,这应该会引起崩溃。
当然,说了这么多,但都是理论上的。
值得庆幸的是,特殊回复端口是内核中为数不多的在tests/kevent_qos.c中有单元测试的部分之一,因此,这里只需为它添加一个host_request_notification 调用。
我目前要做的事情,就是在自编译的macOS 10.15.6内核上触发崩溃。
Receiving message! object=0xffffff80237e8d48
mach_msg_rcv_link_special_reply_port port=0xffffff80237e8d48 dest=f0b
mach_msg_rcv_link_special_reply_port got dest port=0xffffff8023e48618
ipc_port_link_special_reply_port: port=0xffffff80237e8d48 dest=0xffffff8023e48618 sync=no state=0 ip_sync_inheritor_port=0
Take a reference: 0xffffff80237e8d48 -> 0xffffff8023e48618
ipc_port_recv_update_inheritor special port=0xffffff80237e8d48 state=1
<snip>
host_request_notification port 0xffffff80237e8d48 old 0xffffff8023e48618 entry 0xffffff801f206390
panic(cpu 0 caller 0xffffff800630fc8a): "Address not in expected zone for zone_require check (addr: 0xffffff801f206390, zone: ipc ports)"@/
Users/zhuowei/Documents/winprogress/macos11/crashtest/xnubuild/build-xnu/xnu-6153.141.1/osfmk/kern/zalloc.c:662
Backtrace (CPU 0), Frame : Return Address
0xffffff95997758a0 : 0xffffff8006273cee
0xffffff9599775900 : 0xffffff800627349f
0xffffff9599775940 : 0xffffff80064df248
0xffffff9599775990 : 0xffffff80064c7fbe
0xffffff9599775ad0 : 0xffffff80064e7540
0xffffff9599775af0 : 0xffffff8006272d78
0xffffff9599775c40 : 0xffffff8006273916
0xffffff9599775cc0 : 0xffffff8006e7266f
0xffffff9599775d30 : 0xffffff800630fc8a
0xffffff9599775d60 : 0xffffff800623d73d
0xffffff9599775d80 : 0xffffff80062393e3
0xffffff9599775db0 : 0xffffff800624224e
0xffffff9599775df0 : 0xffffff8006242acb
0xffffff9599775e50 : 0xffffff800625b403
0xffffff9599775ea0 : 0xffffff800625ae7b
0xffffff9599775f60 : 0xffffff800625b819
0xffffff9599775f80 : 0xffffff800623abfc
0xffffff9599775fa0 : 0xffffff80064bb72e
也就是说,在我的内核中,
debugger_collect_diagnostics (in kernel.debug.unstripped) (debug.c:1008)
handle_debugger_trap (in kernel.debug.unstripped) (debug.c:0)
kdp_i386_trap (in kernel.debug.unstripped) (kdp_machdep.c:436)
kernel_trap (in kernel.debug.unstripped) (trap.c:785)
trap_from_kernel (in kernel.debug.unstripped) + 38
DebuggerTrapWithState (in kernel.debug.unstripped) (debug.c:555)
panic_trap_to_debugger (in kernel.debug.unstripped) (debug.c:877)
0xffffff8000e7266f (in kernel.debug.unstripped)
zone_require (in kernel.debug.unstripped) (zalloc.c:664)
ipc_object_validate (in kernel.debug.unstripped) (ipc_object.c:500)
imq_lock (in kernel.debug.unstripped) (ipc_mqueue.c:1872)
ipc_port_send_turnstile_complete (in kernel.debug.unstripped) (ipc_port.c:1571)
ipc_port_adjust_special_reply_port_locked (in kernel.debug.unstripped) (ipc_port.c:1867)
mach_msg_receive_results_complete (in kernel.debug.unstripped) (mach_msg.c:719)
mach_msg_receive_results (in kernel.debug.unstripped) (mach_msg.c:334)
mach_msg_receive_continue (in kernel.debug.unstripped) (mach_msg.c:492)
ipc_mqueue_receive_continue (in kernel.debug.unstripped) (ipc_mqueue.c:993)
在iOS 14.1上运行相同的代码,结果:
panic(cpu 1 caller 0xfffffff02667d3f0): Kernel data abort. at pc 0xfffffff025f59d2c, lr 0xddf82e7025f5d4d0 (saved state: 0xffffffe8157d3a40)
除此之外,我们也可以在发送消息时触发内核崩溃。但是,我不知道这样做是否会更方便一些。
相反, 用port/knote/turnstile来覆盖host_notify的链表条目, 则显得更加困难。
如前所述,对于一个带有PORT_SYNC_LINK_ANY的新端口来说, 只能通过ipc_port_link_special_reply_port建链接,而且它还会检查是否存在已有的对象。所以,一旦host_request_notification附加了一个对象,ipc_port_link_special_reply_port就不再起作用了。
不过我想,我们可以先链接一个端口,然后用host_request_notification通过一个表项来覆盖这个端口,再用ipc_port_adjust_special_reply_port_locked通过knote或turnstile来覆盖表项。但是,具体如何操作,我还不是很清楚。
……我完全不知道这怎么会引起安全问题。
实际上,只有少数几种可以使用存储在ip_kobject或ip_sync_inheritor_ *中的值的方法。
host_notify_all:
我们显然不能使用它,因为iOS上的应用不能改变系统时间。
host_notify_port_destroy:
因为ip_kotype被设置为通知,所以当端口被销毁时,host_notify_port_destroy会被调用。
如前所述,我们不能使用ipc_port_link_special_reply_port来覆盖链表条目,因为它在覆盖之前会检查端口是否为空。如果我们想破坏host_notify_port_destroy函数,我们需要想办法让ipc_port_adjust_special_reply_port_locked(唯一一个设置ip_sync_inheritor_*字段的函数)用knote或turnstile来覆盖对象。
即使这样,由于链表在取消链接时存在多种安全检查,导致上述方法仍无法奏效。
ipc_port_adjust_special_reply_port_locked或各种turnstile/QoS方法。
您有没有好办法呢?如何使链表节点与任务端口/knote/turnstile足够接近,从而使这些方法不会立即崩溃?
封装是非常重要的。
随你怎么笑,但你的CS101课本是对的:面向对象的编程本可以避免这种情况。
setSyncInheritorPort方法可以检查是否已经设置了kobject,而setKObject方法可以为链接的端口做同样的检查。
通过把检查放在一个地方,对象的用户就不需要自己去验证对象的状态,也不会像我们这里看到的那样漏掉一个检查。
Mach回复端口的使用方法
如何按照Scott Knight和kernelshaman的指南编译macOS内核以添加调试语句。
CVE-2020-27932如何释放线程的特殊回复端口。
本文作者:mssp299
本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/150492.html