CVE-2021-1782,一个iOS野外漏洞中的凭证问题

访问原始链接 Google 翻译

作者:Ian Beer,Google Project Zero

这篇博客文章是我对2021年初被野外利用并修复的一个漏洞的分析。与上周发布的文章(分析了ASN.1解析器漏洞)类似,本文基于我在分析补丁和试图理解XNU凭证子系统时所做的笔记。我希望这篇文章能够填补凭证子系统内部工作原理及其导致此漏洞的特殊性的文档空白。

CVE-2021-1782在iOS 14.4中修复,正如@s1guza在推特上指出的:

此漏洞于2021年1月26日修复,苹果于2021年5月28日更新了iOS 14.4的发布说明,指出该问题可能已被主动利用:

内核。适用于:iPhone 6s及更新机型、iPad Pro(所有型号)、iPad Air 2及更新机型、iPad第5代及更新机型、iPad mini 4及更新机型、以及iPod touch(第7代)。影响:恶意应用可能能够提升权限。苹果获悉有报告称此问题可能已被主动利用。描述:通过改进锁定解决了竞争条件。CVE-2021-1772:匿名研究人员。条目更新于2021年5月28日

凭证

凭证到底是什么?

内核代码有一个简洁的描述:

凭证是一组指向特定资源管理器属性值的引用计数不可变(一旦创建)索引(这些属性值本身也是引用计数的)。

这个定义在技术上是正确的,但可能本身并不那么有帮助。

要真正理解此漏洞的根本原因和可利用性,需要涵盖大量凭证代码库。XNU的这一部分相当晦涩且复杂。

凭证是一个键值对的引用计数表。所有已创建凭证的指针都存储在全局的ivht_bucket哈希表中。

对于特定的键值对集合,应该只有一个凭证对象。在创建凭证期间,有一个去重阶段,将新凭证与哈希表中的所有现有凭证进行比较,以确保它们保持唯一,如果找到重复项,则返回对现有凭证的引用。

以下是凭证的结构:

struct ipc_voucher {
    iv_index_t     iv_hash;        /* 校验和哈希 */
    iv_index_t     iv_sum;         /* 值的校验和 */
    os_refcnt_t    iv_refs;        /* 引用计数 */
    iv_index_t     iv_table_size;  /* 凭证表的大小 */
    iv_index_t     iv_inline_table[IV_ENTRIES_INLINE];
    iv_entry_t     iv_table;       /* 凭证属性条目表 */
    ipc_port_t     iv_port;        /* 代表凭证的端口 */
    queue_chain_t  iv_hash_link;   /* 哈希链上的链接 */
};

#define IV_ENTRIES_INLINE MACH_VOUCHER_ATTR_KEY_NUM_WELL_KNOWN

凭证代码库以非常通用、可扩展的方式编写,尽管其实际使用和支持的功能集相当有限。

键

凭证中的键不是任意的。键是凭证iv_table的索引;值在iv_table中的位置决定了它存储在哪个"键"下。虽然凭证代码库支持运行时添加新的键类型,但此功能并未使用,只有少量固定的、众所周知的键:

#define MACH_VOUCHER_ATTR_KEY_ALL ((mach_voucher_attr_key_t)~0)
#define MACH_VOUCHER_ATTR_KEY_NONE ((mach_voucher_attr_key_t)0)
/* 其他众所周知的键将在此添加 */
#define MACH_VOUCHER_ATTR_KEY_ATM ((mach_voucher_attr_key_t)1)
#define MACH_VOUCHER_ATTR_KEY_IMPORTANCE ((mach_voucher_attr_key_t)2)
#define MACH_VOUCHER_ATTR_KEY_BANK ((mach_voucher_attr_key_t)3)
#define MACH_VOUCHER_ATTR_KEY_PTHPRIORITY ((mach_voucher_attr_key_t)4)
#define MACH_VOUCHER_ATTR_KEY_USER_DATA ((mach_voucher_attr_key_t)7)
#define MACH_VOUCHER_ATTR_KEY_TEST ((mach_voucher_attr_key_t)8)
#define MACH_VOUCHER_ATTR_KEY_NUM_WELL_KNOWN MACH_VOUCHER_ATTR_KEY_TEST

ipc_voucher中的iv_inline_table有8个条目。但其中只有四个实际上被支持并具有任何相关功能。ATM凭证属性已弃用,支持它们的代码已消失,因此只有IMPORTANCE(2)、BANK(3)、PTHPRIORITY(4)和USER_DATA(7)是有效的键。关于何时应该使用术语"键"和何时使用"属性"存在一些混淆(可能是我个人的);我将互换使用它们来指代这些键值及其管理的相应值"类型"。稍后会详细讨论。

值

凭证iv_table中的每个条目都是一个iv_index_t:

typedef natural_t iv_index_t;

每个值又是一个索引;这次是进入每个键的值缓存,抽象为"凭证属性缓存控制对象",由以下结构表示:

struct ipc_voucher_attr_control {
    os_refcnt_t   ivac_refs;
    boolean_t     ivac_is_growing;      /* 表是否正在增长 */
    ivac_entry_t  ivac_table;           /* 凭证属性值条目表 */
    iv_index_t    ivac_table_size;      /* 属性值表的大小 */
    iv_index_t    ivac_init_table_size; /* 属性值表的大小 */
    iv_index_t    ivac_freelist;        /* 第一个空闲元素的索引 */
    ipc_port_t    ivac_port;            /* 用于访问缓存控制的端口 */
    lck_spin_t    ivac_lock_data;
    iv_index_t    ivac_key_index;       /* 此值的键索引 */
};

这些通过另一个全局表间接访问:

static ipc_voucher_global_table_element iv_global_table[MACH_VOUCHER_ATTR_KEY_NUM_WELL_KNOWN];

(同样,代码中的注释表明,未来此表可能会增长,并允许在用户空间管理属性,但目前它只是一个固定大小的数组。)

该表中的每个元素具有以下结构:

typedef struct ipc_voucher_global_table_element {
    ipc_voucher_attr_manager_t      ivgte_manager;
    ipc_voucher_attr_control_t      ivgte_control;
    mach_voucher_attr_key_t         ivgte_key;
} ipc_voucher_global_table_element;

iv_global_table和每个凭证的iv_table都是通过(键-1)索引的,而不是键,因此userdata条目是[6],而不是[7],尽管数组仍然有8个条目。

ipc_voucher_attr_control_t提供了一个管理"值"的抽象接口,而ipc_voucher_attr_manager_t提供了"类型特定"的逻辑来实现每种类型的语义(这里我所说的"类型"指的是"键"或"属性"类型)。让我们更具体地看看这意味着什么。以下是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;
};

ivam_flags是一个包含一些标志的int;其他五个字段是函数指针,定义了特定属性类型的语义。以下是user_data类型的ipc_voucher_attr_manager结构:

const struct ipc_voucher_attr_manager user_data_manager = {
    .ivam_release_value =   user_data_release_value,
    .ivam_get_value =       user_data_get_value,
    .ivam_extract_content = user_data_extract_content,
    .ivam_command =         user_data_command,
    .ivam_release =         user_data_release,
    .ivam_flags =           IVAM_FLAGS_NONE,
};

这五个函数指针是从通用凭证代码到类型特定代码的唯一接口。接口可能看起来简单,但其中有一些棘手的微妙之处;我们稍后会讨论!

让我们回到通用的ipc_voucher_attr_control结构,它以类型无关的方式维护每个键的"值"。最重要的字段是ivac_entry_t ivac_table,它是一个ivac_entry_s的数组。存储在每个凭证iv_table中的就是指向此表的索引。

以下是该表中每个条目的结构:

struct ivac_entry_s {
    iv_value_handle_t ivace_value;
    iv_value_refs_t   ivace_layered:1,   /* 分层有效条目 */
                      ivace_releasing:1, /* 释放进行中 */
                      ivace_free:1,      /* 在空闲列表上 */
                      ivace_persist:1,   /* 持久化条目,不计数made引用 */
                      ivace_refs:28;     /* 引用计数 */
    union {
        iv_value_refs_t ivaceu_made;       /* made计数(非分层) */
        iv_index_t      ivaceu_layer;      /* 下一个有效层(分层) */
    } ivace_u;
    iv_index_t        ivace_next;        /* 哈希或空闲列表 */
    iv_index_t        ivace_index;       /* 哈希头(独立) */
};

ivace_refs是此表索引的引用计数。请注意,此条目是数组中的内联条目;因此,此引用计数变为零不会导致ivac_entry_s被释放回内核分配器(例如区域分配器)。相反,它将此表索引移动到空闲条目的空闲列表上。表可以增长但永远不会缩小。

非空闲的表条目在ivace_value中存储一个类型特定的"句柄"。以下是该类型的typedef链:

iv_value_handle_t ivace_value
typedef mach_voucher_attr_value_handle_t iv_value_handle_t;
typedef uint64_t mach_voucher_attr_value_handle_t;

句柄是一个uint64_t,但实际上属性可以(并且确实)在那里存储指针,隐藏在强制转换后面。

attr_control保证对于特定的ivace_value,永远只有一个(活动的)ivac_entry_s。这意味着每次新的ivace_value需要一个ivac_entry时,都需要搜索attr_control的ivac_table,以查看是否已存在匹配的值。为了加速此过程,正在使用的ivac_entries被链接在哈希桶中,以便可以搜索(希望显著)更短的条目链表,而不是线性扫描整个表。(请注意,这不是指针的链表;链中的每个链接都是表中的一个索引。)

用户数据属性

user_data是四种支持的、已实现的凭证属性类型之一。它的唯一目的是管理任意用户控制数据的缓冲区。由于attr_control仅在ivace_value(这是一个指针)上执行去重,userdata属性管理器负责确保具有相同缓冲区值(匹配长度和字节)的userdata值具有相同的指针。

为此,它维护一个user_data_value_element结构的哈希表,该结构包装了一个可变大小的字节缓冲区:

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[];
};

每个内联的e_data缓冲区最多可达16KB。e_hash_link存储哈希表桶列表指针。

e_made不是一个简单的引用计数。查看代码,你会注意到没有任何地方会递减它。由于在ivace_entry和user_data_value_element之间应该(几乎)总是存在1:1映射,此结构不需要引用计数。然而,有一个非常棘手的竞争条件(这不是导致漏洞的竞争条件!)需要e_made字段。这种竞争条件在某种程度上是有文档记录的,我们最终会讨论到...

配方

host_create_mach_voucher主机端口MIG(Mach Interface Generator)方法是创建凭证的用户空间接口:

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

recipes指向一个填充了打包的可变大小mach_voucher_attr_recipe_data结构序列的缓冲区:

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;

key是我们之前看到的四种支持的凭证属性类型之一(importance、bank、pthread_priority和user_data)或通配符值(MACH_VOUCHER_ATTR_KEY_ALL),表示该命令应应用于所有键。有许多通用命令以及类型特定命令。命令可以通过previous_voucher字段可选地引用现有凭证,该字段应命名一个凭证端口。

以下是凭证创建支持的通用命令:

  • MACH_VOUCHER_ATTR_COPY:从先前的凭证复制属性值。您可以指定通配符键以从先前的凭证复制所有属性值。
  • MACH_VOUCHER_ATTR_REMOVE:从正在构建的凭证中删除指定的属性值。这也可以从正在构建的凭证中删除所有属性(这可以说没有意义)。
  • MACH_VOUCHER_ATTR_SET_VALUE_HANDLE:此命令仅对内核客户端有效;它允许调用者指定任意的ivace_value,这对用户空间没有意义,应该无法访问。
  • MACH_VOUCHER_ATTR_REDEEM:从先前凭证"兑换"属性的语义不由凭证代码定义;这取决于各个管理器来确定这可能意味着什么。

以下是每种类型凭证创建的属性特定命令:

bank:

  • MACH_VOUCHER_ATTR_BANK_CREATE
  • MACH_VOUCHER_ATTR_BANK_MODIFY_PERSONA
  • MACH_VOUCHER_ATTR_AUTO_REDEEM
  • MACH_VOUCHER_ATTR_SEND_PREPROCESS

importance:

  • MACH_VOUCHER_ATTR_IMPORTANCE_SELF

user_data:

  • MACH_VOUCHER_ATTR_USER_DATA_STORE

pthread_priority:

  • MACH_VOUCHER_ATTR_PTHPRIORITY_CREATE

请注意,还有更多命令可以通过mach_voucher_attr_command MIG方法"针对"凭证执行,该方法调用属性管理器的ivam_command函数指针。这些是:

bank:

  • BANK_ORIGINATOR_PID
  • BANK_PERSONA_TOKEN
  • BANK_PERSONA_ID

importance:

  • MACH_VOUCHER_IMPORTANCE_ATTR_DROP_EXTERNAL

user_data:

  • 无

pthread_priority:

  • 无

让我们看一个创建具有单个user_data属性的凭证的示例配方,该属性由4个字节{0x41, 0x41, 0x41, 0x41}组成:

struct udata_dword_recipe {
    mach_voucher_attr_recipe_data_t recipe;
    uint32_t payload;
};

struct udata_dword_recipe r = {0};
r.recipe.key = MACH_VOUCHER_ATTR_KEY_USER_DATA;
r.recipe.command = MACH_VOUCHER_ATTR_USER_DATA_STORE;
r.recipe.content_size = sizeof(uint32_t);
r.payload = 0x41414141;

让我们详细跟踪此配方的路径。

以下是host_create_mach_voucher中最重要的部分,展示了三个高级阶段:凭证分配、属性创建和凭证去重。此代码不负责查找或分配凭证的mach端口;这是由MIG层代码完成的。

/* 分配新凭证 */
voucher = iv_alloc(ivgt_keys_in_use);
if (IV_NULL == voucher) {
    return KERN_RESOURCE_SHORTAGE;
}

/* 迭代配方项 */
while (0 < recipe_size - recipe_used) {
    ipc_voucher_t prev_iv;

    if (recipe_size - recipe_used < sizeof(*sub_recipe)) {
        kr = KERN_INVALID_ARGUMENT;
        break;
    }

    /* 查找下一个配方 */
    sub_recipe =
        (mach_voucher_attr_recipe_t)(void *)&recipes[recipe_used];
    if (recipe_size - recipe_used - sizeof(*sub_recipe) <
        sub_recipe->content_size) {
        kr = KERN_INVALID_ARGUMENT;
        break;
    }
    recipe_used += sizeof(*sub_recipe) + sub_recipe->content_size;

    /* 将凭证端口名称(当前空间)转换为凭证引用 */
    prev_iv =
        convert_port_name_to_voucher(sub_recipe->previous_voucher);
    if (MACH_PORT_NULL != sub_recipe->previous_voucher &&
        IV_NULL == prev_iv) {
        kr = KERN_INVALID_CAPABILITY;
        break;
    }

    kr = ipc_execute_voucher_recipe_command(
        voucher,
        sub_recipe->key,
        sub_recipe->command,
        prev_iv,
        sub_recipe->content,
        sub_recipe->content_size,
        FALSE);

    ipc_voucher_release(prev_iv);
    if (KERN_SUCCESS != kr) {
        break;
    }
}

if (KERN_SUCCESS == kr) {
    *new_voucher = iv_dedup(voucher);
} else {
    *new_voucher = IV_NULL;
    iv_dealloc(voucher, FALSE);
}

在此代码片段的顶部,通过iv_alloc分配了一个新凭证。然后在循环中调用ipc_execute_voucher_recipe_command,以消耗用户空间提供的任意多个子配方结构。每个子配方可以通过子配方的previous_voucher字段可选地引用现有凭证。请注意,MIG本身不支持包含端口的可变大小结构,因此它作为mach端口名称传递,该名称在调用任务的mach端口命名空间中查找,并通过convert_port_name_to_voucher转换为凭证引用。此处的预期功能是能够引用其他凭证中的属性以复制或"兑换"它们。如前所述,兑换凭证属性的语义不由抽象凭证代码定义,这取决于各个属性管理器来决定其含义。

一旦整个配方被消耗并且所有iv_table条目被填充,iv_dedup然后搜索ivht_bucket哈希表,查看是否存在具有匹配属性集的现有凭证。请记住,存储在凭证中的每个属性值都是属性控制器的属性表的索引;并且这些属性是唯一的,因此只需比较凭证索引数组即可确定所有属性值是否相等。如果找到匹配的凭证,iv_dedup返回对现有凭证的引用,并调用iv_dealloc来释放新创建的凭证。否则,如果未找到现有的匹配凭证,iv_dedup将新创建的凭证添加到ivht_bucket哈希表中。

让我们看看ipc_execute_voucher_recipe_command,它负责填充凭证iv_table中请求的条目。请注意,key和command是任意的、受控的双字。content是指向受控字节缓冲区的指针,content_size是该输入缓冲区的正确大小。MIG层将配方的总体输入大小(这是子配方的集合)限制为5260字节,任何输入内容缓冲区都必须适合其中。

static kern_return_t
ipc_execute_voucher_recipe_command(
    ipc_voucher_t                      voucher,
    mach_voucher_attr_key_t            key,
    mach_voucher_attr_recipe_command_t command,
    ipc_voucher_t                      prev_iv,
    mach_voucher_attr_content_t        content,
    mach_voucher_attr_content_size_t   content_size,
    boolean_t                          key_priv)
{
    iv_index_t prev_val_index;
    iv_index_t val_index;
    kern_return_t kr;

    switch (command) {

MACH_VOUCHER_ATTR_USER_DATA_STORE不是此处switch语句的case值之一,因此代码落入默认情况:

    default:
        kr = ipc_replace_voucher_value(voucher,
                                       key,
                                       command,
                                       prev_iv,
                                       content,
                                       content_size);
        if (KERN_SUCCESS != kr) {
            return kr;
        }
        break;
    }
    return KERN_SUCCESS;

以下是该代码:

static kern_return_t
ipc_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)
{
    ...

    /*
     * 获取此key_index的管理器。
     * 返回对控制的引用。
     */
    key_index = iv_key_to_index(key);
    ivgt_lookup(key_index, TRUE, &ivam, &ivac);
    if (IVAM_NULL == ivam) {
        return KERN_INVALID_ARGUMENT;
    }
    ..

iv_key_to_index只是从键中减去1(假设它是有效的且不是MACH_VOUCHER_ATRR_KEY_ALL):

static inline iv_index_t
iv_key_to_index(mach_voucher_attr_key_t key)
{
    if (MACH_VOUCHER_ATTR_KEY_ALL == key ||
        MACH_VOUCHER_ATTR_KEY_NUM_WELL_KNOWN < key) {
        return IV_UNUSED_KEYINDEX;
    }
    return (iv_index_t)key - 1;
}

ivgt_lookup然后获取该键的属性管理器和属性控制器的引用。管理器实际上只是一堆函数指针,定义了不同"键类型"的实际含义;而控制器存储(并缓存)这些键的值。

让我们继续阅读ipc_replace_voucher_value。以下是下一个语句:

/* 保存在正在形成的凭证中存储的当前值 */
save_val_index = iv_lookup(voucher, key_index);

这一点对于理解凭证代码应该如何工作很重要;配方不仅可以引用其他凭证(通过previous_voucher端口),还可以在创建过程中引用自身。您不必为希望在凭证中具有值的每种属性类型只有一个子配方;您可以为该类型指定多个子配方。这样做实际上有意义吗?好吧,对于安全研究人员来说幸运的是,我们不必担心功能是否真的有意义;对我们来说,这一切只是一个奇怪的机器!(代码中暗示了未来的功能,其中属性值可以"分层"或"链接",但目前此类功能不存在。)

iv_lookup返回特定凭证中给定键的"值索引"。这意味着它只返回给定凭证iv_table中的iv_index_t:

static inline iv_index_t
iv_lookup(ipc_voucher_t iv, iv_index_t key_index)
{
    if (key_index < iv->iv_table_size) {
        return iv->iv_table[key_index];
    }
    return IV_UNUSED_VALINDEX;
}

此值索引唯一标识现有属性值,但您需要向属性的控制器请求实际值。在获取该先前值之前,代码首先确定此子配方是否可能尝试引用此凭证当前存储的值,或者是否显式传递了previous_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,
                    previous_vals, &previous_vals_count);

key_index是我们正在操作的键,在此示例中为MACH_VOUCHER_ATTR_KEY_USER_DATA。此函数称为ivace_lookup_values(注意复数形式)。凭证代码中有一些注释表明,也许将来值本身可以被放入链表中,以便您可以拥有更大的值(或分层/链式值)。但此功能未实现;ivace_lookup_values将始终只返回1个值。

以下是ivace_lookup_values:

static void
ivace_lookup_values(
    iv_index_t                              key_index,
    iv_index_t                              value_index,
    mach_voucher_attr_value_handle_array_t         values,
    mach_voucher_attr_value_handle_array_size_t    *count)
{
    ipc_voucher_attr_control_t ivac;
    ivac_entry_t ivace;

    if (IV_UNUSED_VALINDEX == value_index ||
        MACH_VOUCHER_ATTR_KEY_NUM_WELL_KNOWN <= key_index) {
        *count = 0;
        return;
    }

    ivac = iv_global_table[key_index].ivgte_control;
    assert(IVAC_NULL != ivac);

    /*
     * 获取条目,然后获取链接的值。
     */
    ivac_lock(ivac);
    assert(value_index < ivac->ivac_table_size);
    ivace = &ivac->ivac_table[value_index];

    /*
     * TODO: 支持链式值(用于有效凭证)。
     */
    assert(ivace->ivace_refs > 0);
    values[0] = ivace->ivace_value;
    ivac_unlock(ivac);
    *count = 1;
}

凭证代码中使用的锁定对于最终正确理解底层漏洞非常重要,但目前我略过它,我们将在必要时返回检查相关锁。

让我们讨论ivace_lookup_values代码。它们索引iv_global_table以获取指向属性类型的控制器的指针:

ivac = iv_global_table[key_index].ivgte_control;

它们获取该控制器的锁,然后索引其ivac_table以找到该值的struct ivac_entry_s,并从那里读取ivace_value值:

ivac_lock(ivac);
assert(value_index < ivac->ivac_table_size);
ivace = &ivac->ivac_table[value_index];
assert(ivace->ivace_refs > 0);
values[0] = ivace->ivace_value;
ivac_unlock(ivac);
*count = 1;

让我们回到调用函数(ipc_replace_voucher_value)并继续阅读:

/* 调用资源管理器以获取新值 */
new_value_voucher = IV_NULL;
kr = (ivam->ivam_get_value)(
    ivam, key, command,
    previous_vals, previous_vals_count,
    content, content_size,
    &new_value, &new_flag, &new_value_voucher);
if (KERN_SUCCESS != kr) {
    ivac_release(ivac);
    return kr;
}

ivam->ivam_get_value正在调用属性类型的函数指针,该指针定义了特定类型"get_value"的含义。这里的术语get_value有点令人困惑;我们不是试图存储一个新值吗?(并且没有后续调用像"store_value"这样的方法。)更好地思考get_value语义的方式是,它旨在评估previous_vals(来自previous_voucher的值或此凭证中当前的值)和content(来自此子配方的任意字节缓冲区),并将它们组合/评估以创建值表示。然后由控制器层来存储/缓存该值。(实际上,这个系统中有一个繁琐的问题,我们稍后会讨论,涉及锁定...)

user_data属性类型的ivam_get_value是user_data_get_value:

static kern_return_t
user_data_get_value(
    ipc_voucher_attr_manager_t                  __assert_only manager,
    mach_voucher_attr_key_t                     __assert_only key,
    mach_voucher_attr_recipe_command_t          command,
    mach_voucher_attr_value_handle_array_t      prev_values,
    mach_voucher_attr_value_handle_array_size_t prev_value_count,
    mach_voucher_attr_content_t                 content,
    mach_voucher_attr_content_size_t            content_size,
    mach_voucher_attr_value_handle_t            *out_value,
    mach_voucher_attr_value_flags_t             *out_flags,
    ipc_voucher_t                               *out_value_voucher)
{
    user_data_element_t elem;
    assert(&user_data_manager == manager);
    USER_DATA_ASSERT_KEY(key);

    /* 永远没有输出凭证 */
    *out_value_voucher = IPC_VOUCHER_NULL;
    *out_flags = MACH_VOUCHER_ATTR_VALUE_FLAGS_NONE;

    switch (command) {
    case MACH_VOUCHER_ATTR_REDEEM:
        /* 兑换先前值就是该值 */
        if (0 < prev_value_count) {
            elem = (user_data_element_t)prev_values[0];
            assert(0 < elem->e_made);
            elem->e_made++;
            *out_value = prev_values[0];
            return KERN_SUCCESS;
        }
        /* 兑换默认值就是默认值 */
        *out_value = 0;
        return KERN_SUCCESS;

    case MACH_VOUCHER_ATTR_USER_DATA_STORE:
        if (USER_DATA_MAX_DATA < content_size) {
            return KERN_RESOURCE_SHORTAGE;
        }
        /* 空就是默认值 */
        if (0 == content_size) {
            *out_value = 0;
            return KERN_SUCCESS;
        }
        elem = user_data_dedup(content, content_size);
        *out_value = (mach_voucher_attr_value_handle_t)elem;
        return KERN_SUCCESS;

    default:
        /* 所有其他命令都是未知的 */
        return KERN_INVALID_ARGUMENT;
    }
}

让我们看看MACH_VOUCHER_ATTR_USER_DATA_STORE情况,这是我们放在单个子配方中的命令。(漏洞在上面的MACH_VOUCHER_ATTR_REDEEM代码中,但在讨论之前我们需要更多背景知识。)在MACH_VOUCHER_ATTR_USER_DATA_STORE情况下,输入的任意字节缓冲区被传递给user_data_dedup,然后该返回值作为out_value的值返回。以下是user_data_dedup:

static user_data_element_t
user_data_dedup(
    mach_voucher_attr_content_t                 content,
    mach_voucher_attr_content_size_t            content_size)
{
    iv_index_t sum;
    iv_index_t hash;
    user_data_element_t elem;
    user_data_element_t alloc = NULL;

    sum = user_data_checksum(content, content_size);
    hash = USER_DATA_HASH_BUCKET(sum);

retry:
    user_data_lock();
    queue_iterate(&user_data_bucket[hash], elem, user_data_element_t, e_hash_link) {
        assert(elem->e_hash == hash);
        /* 如果总和匹配... */
        if (elem->e_sum == sum && elem->e_size == content_size) {
            iv_index_t i;
            /* 并且所有数据匹配 */
            for (i = 0; i < content_size; i++) {
                if (elem->e_data[i] != content[i]) {
                    break;
                }
            }
            if (i < content_size) {
                continue;
            }
            /* ... 我们找到了匹配项... */
            elem->e_made++;
            user_data_unlock();
            if (NULL != alloc) {
                kfree(alloc, sizeof(*alloc) + content_size);
            }
            return elem;
        }
    }

    if (NULL == alloc) {
        user_data_unlock();
        alloc = (user_data_element_t)kalloc(sizeof(*alloc) + content_size);
        alloc->e_made = 1;
        alloc->e_size = content_size;
        alloc->e_sum = sum;
        alloc->e_hash = hash;
        memcpy(alloc->e_data, content, content_size);
        goto retry;
    }

    queue_enter(&user_data_bucket[hash], alloc, user_data_element_t, e_hash_link);
    user_data_unlock();
    return alloc;
}

user_data属性只是唯一的缓冲区指针。每个缓冲区由一个user_data_value_element结构表示,该结构具有一个元数据头,后跟一个包含任意字节数据的可变大小内联缓冲区:

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[];
};

指向这些元素的指针存储在user_data_bucket哈希表中。

user_data_dedup搜索user_data_bucket哈希表,查看是否已存在匹配的user_data_value_element。如果没有,则分配一个并将其添加到哈希表中。请注意,在调用kalloc()时不允许持有锁,因此代码必须先释放user_data锁,分配一个user_data_value_element,然后再次获取锁,并第二次检查哈希表,以确保在锁被释放时没有其他线程也分配并插入匹配的user_data_value_element。

user_data_value_element的e_made字段对我们最终要讨论的漏洞至关重要,因此让我们在此检查其使用。

如果创建了新的user_data_value_element,则其e_made字段初始化为1。如果找到与请求的内容缓冲区匹配的现有user_data_value_element,则在返回指向该user_data_value_element的指针之前递增e_made字段。兑换user_data_value_element(通过MACH_VOUCHER_ATTR_REDEEM命令)也只是在返回之前递增要兑换的元素的e_made。e_made字段的类型是mach_voucher_attr_value_reference_t,因此很容易认为此字段是引用计数。但现实比这更微妙。

e_made不完全是一个引用计数的第一个提示是,如果你在XNU中搜索e_made,你会注意到它从未被递减。也没有任何地方将该结构的指针强制转换为另一种类型,将第一个双字视为引用计数。e_made只能增加(好吧,从技术上讲,也没有任何东西阻止它溢出,所以它也可以在每2^32次递增中减少1...)

让我们回到user_data_get_value的调用者ipc_replace_voucher_value:

下一部分又是未使用功能的代码。当前的凭证属性类型实现都没有返回new_value_voucher,因此此条件永远不会为真:

/* TODO: 从返回的凭证中插入值 */
if (IV_NULL != new_value_voucher) {
    iv_release(new_value_voucher);
}

接下来,代码需要将new_value包装在ivace_entry中,并确定该ivace_entry在控制器的值表中的索引。这是通过ivace_reference_by_value完成的:

/*
 * 查找或创建与此属性值关联的表中的槽。
 * ivac引用被转移到新值,或者如果我们找到匹配的现有值则被消耗。
 */
val_index = ivace_reference_by_value(ivac, new_value, new_flag);
iv_set(voucher, key_index, val_index);
/*
 * 查找给定<键,索引>对的值。
 *
 * 消耗传递的凭证控制的引用。
 * 要么将其捐赠给新创建的值缓存,要么将其释放(如果我们附加到现有的值缓存条目)。
 */
static iv_index_t
ivace_reference_by_value(
    ipc_voucher_attr_control_t      ivac,
    mach_voucher_attr_value_handle_t        value,
    mach_voucher_attr_value_flags_t          flag)
{
    ivac_entry_t ivace = IVACE_NULL;
    iv_index_t hash_index;
    iv_index_t index;

    if (IVAC_NULL == ivac) {
        return IV_UNUSED_VALINDEX;
    }

    ivac_lock(ivac);
restart:
    hash_index = IV_HASH_VAL(ivac->ivac_init_table_size, value);
    index = ivac->ivac