Analysis and exploitation of the iOS kernel vulnerability CVE-2021-1782

Written by Luca Moro - 10/02/2021 - in Exploit , Reverse-engineering - Download
Two weeks ago, CVE-2021-1782 was fixed by Apple. If the patch for this kernel vulnerability is simple, a way to exploit the bug was still to be discovered. This blog post aims to explain how an exploit is possible while providing a PoC.

TL/DR: You have to race twice to exploit the bug, the PoC is at the end or there.

EDIT: Well it seems that @ModernPwner just published an exploit for this vulnerability, racing us by few hours! Congrats to them! You can find their exploit here.

 

Introduction

A few days ago Apple released iOS 14.4, which mainly fixed security issues.
At first, the release notes described three vulnerabilities that were actively exploited according to the editor, CVE-2021-1782 (Kernel), CVE-2021-1870 and CVE-2021-1870 (WebKit).
The notes were updated later to include more details on the other issues.

Besides being a race condition reported by an anonymous researcher, there is not much details on CVE-2021-1782.
However, because the update was light on new features, finding the kernel bug was doable by binary diffing.
It quickly became public information (@s1guza) that CVE-2021-1782 was due to a lack of locks in in the voucher implementation.

A few days later, the publication of XNU up to dates sources (xnu-7195.81.3) gave the new code for :

We were wondering how this bug was exploitable.
At first it became clear that this section can race with itself and an count can be lost.
This is because the increment is not atomic.
But then, by looking at the code, it is not really obvious how this can be leveraged to reach a potential Use-After-Free situation.

We spent some time figuring this out, and this blog post presents our results and a PoC to trigger the vulnerability.

Mach voucher basics

Vouchers as Mach objects

The Mach vouchers are not the most manifest concept of XNU, so let's start by giving a little introduction to them.
We will not cover everything, but we will try to give enough information to understand why an UaF is not simply reachable.

A mach voucher is a kernel object used to store and represent an immutable resource.
Most of the implementation of the vouchers is located in source file.
In a pure Mach fashion, a voucher can be handled in the userland as a mach port (), while the kernel uses a more complex .
Then, as expected, vouchers can be used in inter process communication (IPC) by sending them in mach messages.

Vouchers attributes

Behind a voucher, various kind of resources can be referenced.
In the voucher lingo, these different resources are referred as attributes.

For now XNU has 4 different attribute types, , , and .
For todays' blog post we will only focus on the type of voucher that is used to store... user data as plain text.

Each attribute type comes with its own identifier, that is a key ().
This key is used to specify which attribute a function should work on, but more on that later.
For instance "bank" attribute is accessed through the .

Moreover, each attribute also comes with its own manager (), which is a set of callbacks to handle the specific data under the voucher.

Last, but not least, a control port () is also linked with each attributes but this is out of this post's scope.
For more details on how attributes manager are registered please see the code of .

With that in mind, we can say that the whole voucher implementation is splitted into two layers:

  • The upper and generic voucher layer, responsible for the bookkeeping (counting and storing the reference) and the IPC (handling the userland/kerneland port translation)
  • The inner layer specific to an attribute and handled by the attribute manager.

Voucher creation

From the userland, one can create a voucher thanks to the mach trap:

So needs a set of one or multiple recipes ().
A recipe explains how the voucher should be constructed by the kernel before producing a reference to it.
A recipe is made of a , an attribute and usually a or a reference to a (but it could be both).

During the voucher creation, is called for each recipe of the set.
It takes into account the and the provided or previous voucher to shape a forming voucher.
The forming voucher pass through each one of the recipes and the resulting voucher is given back to the userland.

For instance, by using a recipe with the command and a previous voucher, we get a new voucher that is a copy of the previous one.
If it seems silly it's because we usually use commands that are specific to an attribute for the voucher creation.
For instance, a voucher can be made with a recipe containing the command.
Here is an example of how to create such voucher:

Later the content of the voucher can be extracted back with the .

Voucher bookkeeping

Within a voucher, a value is stored with a generic :

Here the is an opaque type that depends on the attribute stored.
For instance, when used with the user_data attribute, this field stores a .

and are indexes, used to retrieve the from different tables.
However, the way the vouchers store their entries on that upper level is not really relevant to the study of CVE-2021-1782, so we will pass on it.

More interestingly, we see and .
represents how many live references of the exist, that is to say, a refcount, which tends to fluctuate.
accounts for the number of time a reference is made, so this field only grows.
For the sake of simplicity let's say that most of the time and are incremented together using (the more avid reader can always read to see more nuances).

On important thing to know is that because of the immutability trait of the voucher (think read only), there is no need to store the same value twice (that is the whole concept).
To avoid doing so, deduplication functions are implemented.
Because the voucher layer has no idea how the value is stored by the attribute manager, this feature is usually found on both layers.
For instance see (voucher layer) and (manager layer).
This fact also explain why we get the same port when we create the same voucher twice.

The vulnerability and the voucher release cycle

Now we can come back to the patch of .
This function is the callback of "user_data" attribute manager.
It is used during a voucher creation, to get a from that layer.

When used with the command, a new is created by unless a duplicate already exists.
When used with the command fetches the value from a previous voucher (or from the forming one).
In both cases, the reference is incremented (see ).

With the lack of , we understand that the the vulnerability allows us to race the increment.
Indeed, by issuing two ) and the command , we might be able to "skip" an increment.

So the reference counting of the element might be off on the manager level.
Now comes the question: how can this be freed ?
Well, let's see which is responsible for the release of .

This function is only called as the callback in , when the representing the value on the upper layer is released.
Here is the relevant and annotated code:

  • In [1] the manager is fetched, that's .
  • In [2] the ivace responsible for our value is fetched.
  • In [3] the release process stops if it's not the last reference.
  • In [4] the is fetched.
  • In [5] the ivac lock is let, this gives room to a race for the ivace->ivace_made modification, but that's another story.
  • In [6] the function is called with our element and as argument.
  • In [7] there is the handling of the eventual race in [5], this is interesting, but out of scope for now.
  • In [8] the ivace is removed from the ivac hash table.

Here is the relevant code for :

The fact that is passed as the argument is quite interesting.
Indeed if does not equals to , is not freed.

Now we realize that both layers keep a made count, that should be synchronized.
The general idea is that under normal operations, should match .
This implementation is made that way so that if a (legitimate) race happens while calling , the manager does not end up freeing the resource.

When we happen to trigger vulnerability and skip an increment, we only get an larger than .
This breaks the synchronization and our hopes of having an used after free.

Well, there must be another way to "re-synchronize" the layers !

The another (legitimate) race

So far, we know that is incremented in .
On the other hand is incremented via when we create a voucher with the or .

To keep everything in sync, we expect both functions to always be called together.
That is the case in , called for most commands during a voucher creation:

In [1] we retrieved the associated to either the forming voucher or the .
Then from that entry we pull the .
At this point, we are sure that can not be freed.
To establish that we must understand the refcounting semantics of the .
Here there are two possibilities:

  • If the considered voucher ( or ) had no value, is NULL. This can happen if we are storing a new value with in a new voucher that is currently empty.
  • If the considered voucher had a value, it could either come from the or the forming voucher. On the first case, had to be passed through a to the kernel, so we got the ivace reference that is kept within the voucher mach port. This case happens for instance when we use or and specify a prev voucher. On the second case, if the forming voucher had value, that means that we already went on an iteration of . When that is the case a reference on the ivace was taken on the previous iteration via (or ).

At [2], we apply the recipe to the manager to get a new value from it ().
With this may create a new or reuse an existing one thanks to deduplication.
With , a is reused.
In both cases, after [2] we incremented .

At [3] we create or find the linked , then we increment and .

At [4] we release the of the previous value of the forming voucher.


The semantics here are quite complex and it took us some time to figure out how this function can be used to exploit the desynchronization brought by CVE-2021-1782.
Indeed, there is another tricky race condition that allows to bring back the sync, between the tempered and its while making the ivac releasable.

Indeed, at [2] we can bump the of a value, without having a reference on the linked To do so, let's  consider the case where the vulnerability was triggered on associated with the on the
We have:


U0.e_content = "AAAA" // chosen value
U0.e_made = N // unknown

IVAC0.ivace_refs = 1
IVAC0.ivace_made = N+1 // thanks to the vulnerability

Then we will try to do the following actions in a race:

  • Thread 1: Destroy the voucher via , this will trigger on
  • Thread 2: Create a new user_data voucher with and the command , using the same content than on ("AAAA").

If everything triggers correctly we might have the sequence:

  1. Thread 2 executes [1], at this point we did not take any reference on IVAC0, as there is no value yet.
  2. Thread 2 executes [2], because of the deduplication U0.e_made is incremented to N+1, we still do not have any reference on
  3. Thread 1 executes , consuming the last reference on it so is called with and matching therefore freeing .
  4. Thread 2 executes [3] creating a new with being used after free.
  5. Thread 2 returns, providing the userland with a new that references a freed

So we get our UaF!

It is worth pointing out that this second race is totally legitimate and not a bug in itself.
We do not think there is a real issue when a prior desynchronization (caused by the vulnerability) is not doable.
In the usual situation, the code handling this race properly is present in (commented out in our extract).
At most, we think that some might never be freed, but that's for the reader to find out.

EXPLOIT!

To illustrate our (complicated) explanations we provide a POC for iOS 13 that leaks kernel data at https://github.com/synacktiv/CVE-2021-1782.

The idea is to spray controlled to cover the freed .
By controlling the field, we can then read back and after our data with .
(Thanks to Brandon Azad (@_bazad) for the helpful 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..... 

// [...]

On iOS 14 (<14.4), because of the allocator mitigation the spraying technique will not work.
However, we can still spray with ools ports.
But, this won't give a leak because the member collides with half an pointer.
This makes the size too big to be retrievable.
That is because there is 5120 bytes maximum (see and ).

You can try to compile the PoC with to demonstrate the vulnerability on iOS 14 (or older) by making the kernel crash.
This is done by incrementing an pointer (instead of ) via and a redeem command.

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

As expected we get the following panic on the mach message reception because the pointer alignment was broken:

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

Conclusion

This concluded our analysis of the patch for CVE-2021-1782.
This journey led us into digging the internals of mach vouchers.
Exploiting the vulnerability required to understand and trigger another (legitimate) race condition.

We suppose that it should be possible to construct a full jailbreak out of CVE-2021-1782 (and as a matter of fact some actor did).
Beside, it turns out that the vulnerability is really stable and quick to trigger.
So feel free to experiment with it.

We hope to have brought some light on the mach vouchers, even tough there is still a lot to cover, and we might be wrong on some aspects.
If you find any mistakes in our post or discover another way to exploit the vulnerability, we would gladly hear from you, so feel free to contact us.

I would like to thanks my colleagues Eloi Benoist-Vanderbeken, Fabien Perigaud and Etienne Helluy-Lafont for their help in the making of this blog post.

POC