Read previous part: iOS 6 Kernel Security 4 - Attack Strategies
At the end of their presentation, Mark Dowd and Tarjei Mandt from Azimuth Security analyze techniques for heap overflow in iOS 6 and draw conclusions.
Mark Dowd: The first primitive we can do with this is ‘adjacent disclosure’. You essentially groom the heap such that you’ve got to know all descriptor right after the buffer that you’re overflowing, and you overwrite the size parameter of the ‘vm_map_copy_t’. And then you receive that message on the port that you sent it to, and the overwritten size will actually cause it to copy kernel memory that’s adjacent in memory to your kernel buffer, so you’ll be able to read a whole bunch of adjacent data.What’s really useful about this attack is that size is not passed to ‘kfree()’ or anything like that. If you are unfamiliar with iOS kernel programming, the ‘kfree()’ API takes a size, and that size is used to denote what zone it’s going to put it in out of those ‘kalloc’ zones. So, basically, if you put the wrong size in you can cause some problems, but in this case there’re no side effects whatsoever. The only thing that that size is useful is to determine how much data to copy out of kernel memory.
This is how they look when a data structure is allocated. You’ve got your user data in there – I’ve chosen 256 but obviously it can be anything up to 4096. It fills out the header. Basically when they allocate ‘vm_map_copy_t’ structures like this, they allocate structure plus however much data you have, and then they make the pointer to the data that they’re going to copy back out to user-mode directly under the header. As you can see, if you just overwrite the size to be much larger, they’ll end up copying the adjacent memory on the heap back out to you, which is quite useful.
We are going to take this a step further. As you saw in the previous slide, right after the size it’s a pointer which points directly under it, because that’s where your user data is stored. So, essentially, we can overwrite the size and the pointer of an adjacent ‘vm_map_copy_t’ and we can set that pointer to anything we want and we can read arbitrary kernel memory at this point. We just overwrite those few members, we receive the message, and we can read arbitrary memory. Again, this is really great, particularly because there’re no side effects. The data pointer in that previous slide is never passed to ‘kfree()’ or anything like that, so it’s not going to cause some zone corruption or something like that, and as you know the size doesn’t either. So the only thing that that pointer is useful is to decide which memory to copy back into user-mode. Again, this has no side effects.
Using both of these primitives together is really useful because if you can perform an overflow multiple times, which you probably can, this is a very safe way to read memory back from the kernel and get the data that you need in order to perform a useful overflow. One thing I should also note is that the data pointer is never used for anything else.
We can extend this again and, basically, recreate an overflow in one go. As you saw in one of the previous slides, there’s a ‘kalloc_size’ in the data structure right after the pointer. Now, that ‘kalloc_size’ is actually passed to ‘kfree()’, and like I mentioned before, that size is used to determine which zone the block is going to be entered into when it’s freed. So, what we can do is we can overwrite ‘kalloc_size’ with a larger value than it really is and then read some arbitrary kernel memory. The other thing I was going to mention previously is that, if you are reading arbitrary kernel memory and it’s unmapped – this actually won’t panic the kernel because it will just safely return an error, because essentially they use ‘copyout()’ to perform the copying, as we talked about before. It’s not going to cause a panic, so it’s quite safe.
So you change ‘kalloc_size’ after the data pointer and you make it something bigger, like 256 instead of 128 or whatever, and then you allocate a 256-byte block – and you get this block back that’s really only 128 bytes, and you can do the overflow again. So, essentially, if you overflow one of these data structures it allows you to do a second overflow, but in the process receiving arbitrary kernel data which will be really useful to you in performing some sort of overwrite.
I have some pictures; again, this is the normal one where you overwrite with a larger ‘kalloc_size’, and that’s what’s eventually passed to ‘kfree()’.
The last primitive that I wanted to talk about is finding your own address and doing an overflow, which is basically mix and match of those previous techniques. This is basically one way we want to overflow to adjacent ‘vm_map_copy_t’ structures. In the first one that we overflow, we’re going to overflow the whole thing, which involves overwriting the size, the offset, the kalloc size, and everything. This is going to be where a poisoned block is, right? But then we want to keep overflowing and go a little bit further into an adjacent ‘vm_map_copy_t’ and partially overwrite it, basically just overwriting the low byte of the pointer and changing the size to be something quite large.
Then when we free the second ‘vm_map_copy_t’ structure, it will end up leaking the page that our ‘vm_map_copy_t’ structures are on back to us. For the uncorrupted ones and for the partially corrupted one, we will see the pointers to itself in the data that we receive back. So we’ll know exactly where we are in memory and then we can free the first ‘vm_map_copy_t’ structure, which is where the poisoned block will be, and we’ll know exactly where that is in memory.
So, again, you’ve got a memory layout like this; the overflow block is presumably something that you know you’re going to trigger your bug in. And then we basically overflow over the whole first one, which is where the poisoned ‘kalloc’ block is going to be, and we partially overflow the second one, so we point a little bit backwards on erroring page in memory so we can read back and see exactly where we are.
Using this is really useful because there’s a couple of times for arbitrary write primitives that we wanted to go into but unfortunately we ran out of time. But both the write primitives that we came up with involved basically having one level of pointer indirection, so we had to know where we were in memory. So, using this trick we were able to go ahead and figure out where we were in memory, write pointers to erroring block and then do a second-level dereference and gain arbitrary execution by corrupting some kernel data structures.
So, in conclusion, you can see that iOS 6 added a huge bunch of mitigations. All the previous techniques that have been used are, for the most part, useless. But there’s still room to move and it’s still possible to exploit kernel bugs in iOS 6, but the bar has definitely been raised. So, that’s it. Thanks!
Conference Host: Are there any questions?
Question: Could you use that exploit to unlock the iPhone so you could use another SIM chip?
Mark Dowd: Oh, no, this is just a kernel mode exploit.
Question: Maybe I missed this, but what bug did you use to actually overflow into the other ‘vm_map_copy_t’ structures?
Mark Dowd: It’s non-disclosure, but we’ll publish it when Apple is done with this.
Question: What methodology did you use to analyze the code flow of the kernel?
Mark Dowd: We just used IDA, and the XNU source is available, so, that was a real big help, particularly the Mountain Lion releases.