Exploitation of a kernel pool overflow from a restrictive chunk size [CVE-2021-31969]

We discussed this vulnerability during Episode 228 on 28 November 2023

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.