Kinda a cool bug dealing with an improper optimization and the usage of an unexpected object from JS, leading to an out-of-bounds access.
So upfront, I had some issues following this one, so I’ll summarize what I got from it, but I might be misunderstanding something. The issue came down to a combination of two things:
the_hole
object. The hole is a special oddball
type of value in V8. Oddball just meaning it is defined by V8 before any user javascript runs, and can’t be modified by a user. It also, as I understand it is not supposed to really be accessed or user by user javascript. So if one can leak a variable pointing to it and use it, some problems can arise. In this case, when the optimizer is doing a value-range analysis, and the_hole
is part of it, it’ll treat it as undefined. So in the following code:
function test(b) {
let index = Number(b ? the.hole : -1);
index |= 0;
index += 1;
index *= 100
return index
}
Where the.hole
is the_hole
object. When this gets optimized it thinks that the only possible initial value for index
is -1
and will optimize around that fact. When doing array[indexing]
a bounds check is necessarily inserted even if the range analysis indicates its unnecessary, however using array.at(...)
does not have the same bounds check, instead that bounds check is only conditionally inserted. As such because the value-range analysis is incorrect with the hole object, it believes the bounds check is unnecessary and allows for an OOB access.
Honestly, this is a really specific V8 issue, but I thought it was fun in a more general sense too, just the idea of using these sort of edge-case values and not necessarily reasoning about them correctly is an issue any dev can make.
This bug is basically just a failure to properly intercept guest writes to the IA32_HW_FEEDBACK_PTR
Machine State Register (MSR), which the CPU uses to store the physical address to write performance information feedback to upon reset. As this MSR was not intercepted, a guest could write a hypervisor physical address into this MSR on sleep or hibernation (S3/S4) resume and get the CPU to corrupt hypervisor memory.
An integer underflow in calculating the maximum size of a buffer, allows for a far too large maximum and ultimately for data being uncompressed to overflow the allocated buffer.
So there is a bit of complexity around setting up the _REPARSE_DATA_BUFFER
but ultimately these fields are controllable through some FSCTL calls. The structure contains a DataBuffer
field which can basically contain arbitrary data that is defined by the driver that intends to use it. This DataBuffer
contains two fields that are used here: cstmDataSize
and the compressedBuffer
Basically, it contains a LZNT1 compressed buffer, and the size of the data in that buffer. Whats kinda interesting here is that the size is kinda specified twice, we have the cstmDataSize
that is used to actually allocate a buffer to store the data. That is also the size passed in as the maximum size for the decompressor to use in the buffer. The problem is that the code plans to use the first 12 bytes of that buffer for a header, so isntead of passing in the buffer pointer directly it passed in buf+12, and naturally it reduces the buffer size by 12, cstmDataSize - 12
which can underflow if the size set by the attacker is not originally greater than 4 (the size gets a +8 earlier). When a small size is specified it overflow and that maximum size ends up being far larger than the allocated buffer.
It then getsinto parsing the compressedBuffer
the buffer itself being in LZNT1 format, contains size information of the buffer/block being decompressed, and it will decompress all the available data, as long as it is less than that underflowed maximum size. This ends up being a pretty nice primitive compared to many underflows as you can basically decide exactly how much to overflow by, and not have an insanely large copy to block. As there is also a flag that can indicate the data is uncompressed, it basically just works as a memcpy of attacker data at this point.
The major part of the post on exploitaiton of this issue, using a combination of spaying low-size allocations that would end up in the same 0x20 bucket of the Low Fragmentation Heap, while also spraying allocations of _WNF_STATE_DATA
objects, and _TOKEN
objects. The goal being that you can fill up the available space in the LFH bucket, and cause a Variaible Sized subsegment to be allocated in adjacent memory. That subsegment containing the two target objects. Hopefully overflowing from the LFH with repeating writes of 0x1000 to corrupt the _WNF_STATE_DATA
’s AllocatedSize
and DataSize
fields in order to use the objects for relative read/write primitives, and corrupt an adjacent _TOKEN
object.
An iOS bug due to improper handling of the Fault Address or FAR register in XNU on arm64. The FAR register is updated with the faulting address upon certain CPU exceptions, such as instruction or data aborts on invalid addresses, alignment faults, and faulting in pages. The FAR is also copied into the thread structure as part of the core state. The problem is, certain other exceptions such as breakpoint debug instructions will not update the FAR, and the FAR is never cleared from its previous value. By first triggering an exception to page-in physical memory on a freshly allocated buffer, an attacker can cause a kernel pointer to get stored in the FAR. By then triggering a breakpoint debug instruction, that kernel pointer can be infoleaked.