136 - A subtle iOS parsing bug and a PHP use-after-free
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.
- A 0 length bitstring, that triggers the bug setting
Data
toNULL
- A small bitstring, that because
Data
isNULL
will result in a small buffer being placed intoData
- A long bitstring, that will write into
Data
assuming it was allocated with enough space.
A bug and exploit that hearkens back to old-school browser exploitation. The bug is a use-after-free in concat_function()
for variable concatenation, which is abused in the PHP engine to escape disable_functions
and open_basedir
sandboxing.
The bug
In PHP it’s possible to concatenate variables like strings using the .=
operator. Internally this invokes concat_function()
, which will try to process the operands and convert things like integers to strings if necessary. Objects however (such as arrays) are not supported for concatenation and trigger an error. In PHP it’s possible to define your own error handlers, which can get invoked in scenarios like this. In an error case, it’ll free and cleanup the first operand (the variable being concatenated to), but it won’t consider the fact that the user-specified error handler can swap out that variable for something else which will implicitly free the variable’s backing zval
, which holds refcounting information and pointers to backing data.
Exploitation
Abusing the bug gets you in a scenario where you can effectively overlap some arbitrary object with the UAF’d zval
that’s getting destroyed. This is used to build two primary primitives, an arbitrary free and an infoleak. The arbitrary free simply involves overlapping a string which will get used as a zval in the cleanup code. The infoleak seems to be later in the cleanup code where a pointer gets written to the overlapped string, which is a string of null bytes.
By using some heap feng shui, objects can be allocated contiguously, and by leaking the pointer of a string, an adjacent object (such as a helper object with some inline properties) pointer can be leaked and free()’d. From here, they use the object properties and the ability to corrupt the backing pointers to derive arbitrary read/write primitives (also similar to browser exploitation).
With arbitrary read, they scan the heap for the basic_functions
table that contains a mapping of functions to handlers, and search for the system()
pointer. Then with arbitrary write, they smash a setup anonymous function handler to set it to the leaked system()
pointer, bypassing the disable_functions
sandboxing.