CVE-2021-30737, @xerub's 2021 iOS ASN.1 Vulnerability

We discussed this vulnerability during Episode 136 on 12 April 2022

Rather subtle bug in the ASN.1 parsing state machine that comes down to one area of code being unaware of an edge case in another.

A brief bit of background here, ASN.1 or Abstract Syntax Notation One is basically a Type-Length-Value (TLV) style encoding system. You provide is some bytes reflecting the type of object the data is, the length of it, and finally the values. It parses the value as the respective type. Its a bit more complex as it has the concept of primitive and complex types. A primtive type is a type where the value bytes can be interpreted directly as their value. Like a byte string, the bytes in the value section are just the bytes of the string. Complex types are made up of further items (sub-items) that need to be parsed until it reaches the primitive types. It also supports encoding data when you don’t know the length ahead of time, or indefinite length values.

Its also worth calling out that there are very few bounds checks in this code. Instead the code relies on the fact that ASN.1 doesn’t support compression or any type of data expansion. So if you have some ASN.1 encoded item that has a Length of 10, the result of parsing those 10 bytes will be no larger than 10 bytes. If its a complex type (has sub-items to parse) it’ll actually be smaller once the metadata is removed. And so when it is parsing something with a defined length, it allocates a buffer for at least as many bytes as are in the length value, and then each sub-item will write the parsed value into that buffer.

For indefinite length values it creates a linked list, allocating a new buffer for each sub-item and concatenating them together at the end.

The root of this bug is when parsing sub-bitstrings and it encounters an empty bitstring as one of the sub-items. That is it has a type of bitstring but no actual content. When the parsing engine encounters this it will try to skip ahead in processing a bit, setting the destination’s Data buffer to NULL, and Length to 0. Then fast-forwarding the state to beforeEndOfContents basically just telling the parsing engine to carry on.

It makes sense on the surface, but this actually triggers an edge case when it starts processing the next sub-item. Recall that indefinite length items exist, the way the parsing engine detects that the substring is part of a indefinite length item instead is that Data will be set to NULL and Length to 0, This tells it that it should go ahead an allocate a new buffer just for this sub-item. Except we can trigger this situation from inside the sub-items for a definite length value with an empty bitstring. When we do that, we can trick the parser into allocating a new buffer, sized only for the current sub-item.

Then when it proceeds to process the next sub-item after that, this one will see that Data is not NULL and not recognize that it was allocated by a sibling sub-item, instead assume it was allocated by the parent item, and thus large enough for its content. So this third sub-item can overflow the buffer allocated by the second item.

So the final vulnerability, leading to an out of bounds write it to start the ASN.1 encoded data with defined length bitstring, and three substrings making it up.

  1. A 0 length bitstring, that triggers the bug setting Data to NULL
  2. A small bitstring, that because Data is NULL will result in a small buffer being placed into Data
  3. A long bitstring, that will write into Data assuming it was allocated with enough space.