深入分析macOS 10.15.7系统中的CVE-2020-27932漏洞

2020-12-29 9,440

深入分析macOS10.15.7系统中的CVE-2020-27932漏洞 

原文地址: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窗口中,显示了两个并排的代码块,并且,右边有一行代码处于突出显示状态

 

image.png 

 

原来的代码: 

 

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);

 

host_request_notification

这个函数用于每当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.

 */

 

那么这些链接是如何建立的呢?

 

链接(Linkage

当一个进程通过这个调用链向目标端口发送消息,并附加回复端口时,就会产生一个链接: 

 

    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_kobjectip_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

Tags:
评论  (0)
快来写下你的想法吧!

mssp299

文章数:51 积分: 662

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号