Hi traveler, welcome to the second and final blog post in this series. This time, we’re going to move on where we left off, and make this controlled pool overflow a success and eventually get a SYSTEM shell. Get yourself your favorite beverage and let’s get started!

Exploitation Technique

The exploitation technique we are going to use was presented by Corentin Bayet and Paul Fariello of Synacktiv link to paper. It’s a nice technique as it requires just a four byte overflow, it supports a wide range of vulnerable chunk sizes (0x130 - 0xFE0) and it also supports Paged Pool and Non-Paged Pool alike.

Described in one sentence, by using this technique we try to modify the POOL_HEADER of the chunk following the vulnerable chunk to eventually allow the decrementation of two EPROCESS.TOKEN values of our own process for privilege escalation. No worries if that creates a lot of questionmarks above your head, we will dissect this one by one as I feel this will enable you to adopt this technique even faster when you have the given primitive or just to let you grasp the concept of how these kind of issues might be exploited.

The following graphic depicts the layout we are working towards:

Final Setup

The next sections will describe each step we have to take one by one to achieve the layout of this graphic. If you want a deeper understanding it’s highly recommended to follow the steps within the exploit code. Maybe read through this blog once without then with following the code during each step. Also I recommend to you to have the original whitepaper at hand so you can read up anything I missed/skipped or didn’t explain good enough.


Draining and Spraying

The first step is to allocate a lot of chunks of the same size as our vuln object. So we drain every already freed object within the bucket and then allocate a lot of objects we have control over in terms of which and when we want to free them. In my exploit I sprayed 0x20000 times for the draining part and 30*0x80 times for the spraying part. The amout for the draining part is likely way to much for the LFH, but I used that value at one time and after confirming that it works and I get the layout I want most of the time, I left it as it is. So what do we use to spray? The Synacktiv guys figured out that PipeAttributes are put on the Paged Heap and that we have much control over them in terms of size, data control and when to allocate or free them. The structure is not public but they reversed it and it looks like this:

struct PipeAttribute {
    LIST_ENTRY list ;
    char * AttributeName ;
    uint64_t AttributeValueSize ;
    char * AttributeValue ;
    char data [0];
};

AttributeName and AttributeValue are pointers into the data segment. We will abuse these pointers in two different ways in the coming sections.

To allocate such a (populated) structure they also published code:

int set_pipe_attribute(pipe_pair_t* target_pipe, char* data, size_t size)
{
    IO_STATUS_BLOCK status;
    char output[0x100];

    memset(output, 0x42, 0xff);

    NtFsControlFile(target_pipe->write,
        NULL,
        NULL,
        NULL,
        &status,
        0x11003C, //0x11002C for arg of set attribute is 2
        data,
        size,
        output,
        sizeof(output)
    );
    return 1;
}

So you call NtFsControlFile with a control code of 0x11003C to set a pipe attribute, which is then allocated on the Paged Pool, and this is how we spray.

The allocations for the draining part and spraying part will be of size sizeof(vulnerable_object) + sizeof(POOL_HEADER) hence leading to 0x143 + 0x10 in our case. Both will end up in the 0x160 buckets as every bucket is 0x10 bytes aligned. The layout should look like:


Create Gaps

Next, we create some gaps where our vuln object should be allocated into. We have to avoid freeing two adjacent objects. Hence our strategy is to free every third object, this is the same Synacktiv used in their example exploit and did work fine for me.

Just closing the corresponding pipe does trigger the free of the respective PipeAttribute.


Allocate Vulnerable Object

After we created the gaps, we want to allocate our vuln object into a previously freed space. The layout should be, that one of our sprayed PipeAttributes is directly behind (adjacent) to our vuln object.

If you had a bit of luck, the memory layout looks like:


Trigger Vuln to Overwrite Pool Header

With this bug we’re exploiting currently, we don’t have control over when the vulnerable object is allocated, when the vulnerability is triggered and when the object is freed again, as it is both executed through one ntdll api call. Allocation, overflow and deallocation happens within one go.

Dynamically analysing of the adjacent’s chunk POOL_HEADER:

// Adjacent's Chunk Pool Header Before Overflow
1: kd> p
Ntfs!NtfsQueryEaUserEaList+0x1ca:
fffff806`3eb5c2ee 41897d00        mov     dword ptr [r13],edi
1: kd> dt nt!_POOL_HEADER ffffc303dce4aa00
   +0x000 PreviousSize     : 0y00000000 (0)
   +0x000 PoolIndex        : 0y00000000 (0)
   +0x002 BlockSize        : 0y00010110 (0x16)
   +0x002 PoolType         : 0y00000011 (0x3)
   +0x000 Ulong1           : 0x3160000
   +0x004 PoolTag          : 0x7441704e
   +0x008 ProcessBilled    : (null) 
   +0x008 AllocatorBackTraceIndex : 0
   +0x00a PoolTagHash      : 0

1: kd> g
Breakpoint 0 hit
Ntfs!NtfsQueryEaUserEaList+0x1c5:
fffff806`3eb5c2e9 e81258f5ff      call    Ntfs!memcpy (fffff806`3eab1b00)
1: kd> p
Ntfs!NtfsQueryEaUserEaList+0x1ca:
fffff806`3eb5c2ee 41897d00        mov     dword ptr [r13],edi

// Adjacent's Chunk Pool Header After Overflow
1: kd> dt nt!_POOL_HEADER ffffc303dce4aa00
   +0x000 PreviousSize     : 0y00010010 (0x12)
   +0x000 PoolIndex        : 0y00000000 (0)
   +0x002 BlockSize        : 0y00000000 (0)
   +0x002 PoolType         : 0y00000100 (0x4)
   +0x000 Ulong1           : 0x4000012
   +0x004 PoolTag          : 0x7441704e
   +0x008 ProcessBilled    : (null) 
   +0x008 AllocatorBackTraceIndex : 0
   +0x00a PoolTagHash      : 0

Let’s dissect the values of our modified POOL_HEADER.

PreviousSize: 0x12 (will be multiplied by 0x10, current_chunk-0x120 will point to the fake pool header we are going to setup)
PoolIndex:    0x0  (can be any value, will not be used)
BlockSize:    0x0  (can be any value, will not be used)
PoolType:    PreviousPoolType | 0x4 (OR 4 to set CacheAligned bit)

As shown in part 1 we have complete control over these values.

Now when we will free the adjacent chunk, the magic we set up with the overwritten values will take effect and the freeing mechanism will look for a “second” POOL_HEADER which points into the data section within the vulnerable chunk due to the CacheAligned bit(have a look into the paper of Synacktiv where they describe said mechanism if you want to know more about how this works, as I would just reiterate what they have said and it would put a toll on the reading experience of this blog entry). But the time for this has yet to come, first we need to put a fake POOL_HEADER into that place.

Zoomed In

Layout Zoomed In

Zoomed Out

Layout Zoomed Out

We will reallocate the space of the vulnerable object to have arbitrary control over this fake POOL_HEADER in the next step.


Fake Pool Header

Having exploited the pool overflow to overwrite the adjacents chunk POOL_HEADER we now want to free and directly reallocate the vulnerable object, so we can create our faked POOL_HEADER with arbitrary values. The freeing of the vulnerable object is done automaticlly due to the nature of the bug. Whenever we now do the reallocation part we can’t just allocate one of these objects we want to land in the correct spot, the chances to make this work would be too small. So how can we increase the chances? Spraying is the answer. We just spray the object a number of times. In the exploit I sprayed 60*0x80 times.

Faked Pool Header

PreviousSize: 0x0 (can be any value, will not be used)
PoolIndex:    0x0 (can be any value, will not be used)
BlockSize:    0x21 (will be multiplied with 0x10 -> hence 0x210)
PoolType:    CacheAligned & PoolQuota bits both unset

The BlockSize is the size of the chunk that will actually be freed. Sizes from 0x200 will use the VS allocator and 0x210 will be the chunk size for these objects. The size is crucial, as for the Dynamic Lookaside list we are going to discuss next, we can’t use the LFH allocator and up to 0x200 bytes (not including) the LFH allocator would be used.

Again, when we free the chunk with the overflowed POOL_HEADER due to the cache aligned bit set, the OS looks back OverwrittenPoolHeader.PreviousSize*0x10 number of bytes and finds our faked POOL_HEADER and hence creates a freed’ chunk of size 0x210 (FakedPoolHeader.BlockSize*0x10)

Zoomed Out

Layout Zoomed Out

Zoomed In

Layout Zoomed In


Enable Dynamic Lookaside List

But before we are going to create the ghost chunk one thing is crucial to our success. We need to enable Dynamic Lookaside list. To do so we spray a number of objects of the respective ghost chunk size which is 0x210 in our case, then wait two seconds, spray a number of objects with the same size again and then sleep for one second. This also enables.

With the Dynamic Lookaside we don’t face as much scrutiny to the faked POOL_HEADER as we would with the normal deallocation algorithms for LFH and VS objects.

This is crucial to the technique, after the Dynamic Lookaside list is enabled, and the chunk to be freed has the CacheAligned bit set within the PoolType the information of the faked pool header will be used and allow us to create a “Ghost Chunk”.

The paper mentions that this can be enabled for the size 0x210 for example by spraying and freeing chunks of that size. In the PoC exploit they shared they do it like that: - Spray 100 times 0x210 chunks - Sleep two seconds - Spray 100 times 0x210 chunks - Sleep one second

Without freeing these chunks afterwards. I did use the same method within my exploit and it did work.


Free overwritten chunk with controlled pool header to create GhostChunk

Now the setup with our faked POOL_HEADER and the Dynamic Lookaside List is done, we can free the adjacent chunk. But we don’t know which one of our sprayed objects it is. So how do we figure that out? We don’t we just free every object of our first round of sprayed 0x160 bytes objects.


Leaking Kernel Pointers - Setup

For making the exploit work in Low Integrity we need a leak, as we need two addresses within the NT kernel (ntoskrnl.exe) and additionally a pointer to the root of the pipe attributes linked list. Why we need them and how we are going to use them will become clearer later on. For now, remember we create this setup to leak kernel pointer.

We free & reallocate the blue chunk (previous space of the vuln chunk) to enable leaking of Ghost Chunks’ Pipe Attribute later on. Reallocate means we need to spray again, just allocating one object/chunk will most likely not suffice.

The following image shows the layout we want to create, the allocated Ghost Chunks data should overlap with the data part of our blue marked PipeAttribute. And the data was rewritten, now the data is the POOL_HEADER and the Ghost Chunks PipeAttribute, these contain the data we’re interested in.

After


Leaking Kernel Pointers - Action

With the pipes sprayed again to hopefully reallocate the space the vulnerable object has left, we are now ready to allocate the ghost chunk. This is done the exact same way we have done so far, with a PipeAttribute object, as we still need an object which is located on the Paged Pool. Besides that, the PipeAttribute does have attributes which will become very helpful down the road ;)

Through the allocation of the ghost chunk, we overwrite the previous attribute we just allocated in the step before. As the overwritten data now creates a diff in one of the allocated pipe attributes and we are able to query all the pipe attributes, this allows us to “diff out” the now overwritten attribute. We iterate over all the resprayed attributes, read the corresponding attribute through the below function. And for the attribute which contained different data compared to the beginning, this is the pipe attribute overlapping with the ghost attribute.

int get_pipe_attribute(pipe_pair_t* target_pipe, char* out, size_t size)
{
    IO_STATUS_BLOCK status;
    NTSTATUS st;
    char input[ATTRIBUTE_NAME_LEN] = ATTRIBUTE_NAME;

    st = NtFsControlFile(target_pipe->write,
        NULL,
        NULL,
        NULL,
        &status,
        0x110038,
        input,
        ATTRIBUTE_NAME_LEN,
        out,
        size
    );

    if (!NT_SUCCESS(st)) {
        fprintf(stderr, "[-]NtFsControlFile failed !");
        return 0;
    }

    return 1;
}

With the identified attribute, we can immediatly use the values returned from the NtFsControlFile call. We are interested in the PipeAttribute.Next pointer and the PipeAttribute.AttributeName pointer and we’re going to make good use of them later on.


Userland PipeAttribute

By now you understand the drill, we again free and reallocate the attribute which got us our leak in the previous step to overwrite the PipeAttribute.Next pointer of the Ghost Chunk (the ghost chunk is still and will continue to be overlapping with memory we can reallocate, as you can see in the graphics). We will be rewriting the PipeAttribute.Next pointer to point to a fake PipeAttribute in userspace we have set up before. The whole idea of rewriting linked list pointer of kernel objects is a commonly used technique to create full control over an object otherwise only residing in kernel land where we have less control over and often times put a toll on the exploits’ reliability if we are trying to modify it too often. And as Windows does not make use of SMAP in a lot of instances, we can do so happily.

You might ask “Hey why don’t we just modify the PipeAttribute in said space?” This is due to the fact, that when we rewrite data within the PipeAttribute (through normal winapi interaction), the kernel does a reallocation and does not modify the object in place.

Userland PipeAttribute Setup

The same way we leaked the data (int get_pipe_attribute(pipe_pair_t* target_pipe, char* out, size_t size)) we can now do arbitrary reads, by setting the ValueSize and the AttributeValue (pointer) within the fake Userland PipeAttribute. Here again the PipeAttribute structure for reference:

struct PipeAttribute {
    LIST_ENTRY list ;
    char * AttributeName ;
    uint64_t AttributeValueSize;    // we modify this
    char * AttributeValue ;         // and this for arbitrary reads
    char data [0];
};

One detail which is crucial is the fact, that when we call get_pipe_attribute on the ghost chunk we normally would just read the data inside the PipeAttributes ghost chunk. In other words, it would not follow the Next pointer we set up so carefully. The Synacktiv guys figured out, that if it does not find the correct attribute by name, it follows the Next pointer. This is a small but crucial fact to make the arbitrary read possible with a fake Userland PipeAttribute.


First Arbitrary Reads -> Kernel Base

Of course, everything has worked out perfect ;P and we are able to do arbitrary reads on absolute addresses. We can hear the Kernel ask “What do you wanna know my dear friend?”, and he’s right, what the hell do we want to know?

Remember the leaked pointers? We leaked the PipeAttribute.List.Next pointer and the PipeAttribute.AttributeName pointer, these now will come in handy. The linked list Next pointer will allow us to traverse the list of PipeAttributes, the leak of the AttributeName pointer leaks to us the address of the Ghost Chunk.

First, lets get the kernels (nt) base address and take things from there.

As the graphic illustrates, we use the arbitrary leak to go from the leaked Next ptr of the Ghost Chunk to the address of nt!ExAllocatePoolWithTag which is in the Import Address Table of the npfs.sys driver. Then using a hardcoded offset (nt!ExAllocatePoolWithTag-nt) or a dynamically calculated one, we eventually know the base of the kernel by following all the layed out pointers.


Kernel Base -> Data of Interest

The NT kernel (ntoskrnl.exe) stores two values which are of interest to us at this time. These also come with symbols.

The first one is PsInitialSystemProcess, this is a pointer to the _EPROCESS/_KPROCESS structure of the first process. If you don’t know, this structure contains a doubly-linked list of all running processes. (If you would have an arbitrary read and write bug, you can for example just traverse all processes and steal the _EPROCESS.Token of a privileged process and write the value into your own process for LPE, this method is often times used in PoC exploits for said vuln).

0: kd> dq nt!PsInitialSystemProcess l1
fffff803`094fb420  ffff9b8e`89c82040
0: kd> ?nt!PsInitialSystemProcess-nt
Evaluate expression: 13612064 = 00000000`00cfb420

and

0: kd> ?nt!ExpPoolQuotaCookie-nt
Evaluate expression: 13613520 = 00000000`00cfb9d0

Note: These offsets do differ for different Windows OS versions, so you either hardcode them to fit your specific OS version, have a set of different OS versions supported (with hardcoded values for each) or have a method to dynamically identify the respective offset.

We store the ExpPoolQuotaCookie for the next step, then traverse the whole _EPROCESS linked list to get the _EPROCESS address of our own process.

0: kd> dt nt!_EPROCESS ActiveProcessLinks
   +0x448 ActiveProcessLinks : _LIST_ENTRY

Remember, we want to modify the _EPROCESS.Token value of our own process. To identify that the current _EPROCESS we are iterating over is ours, we can compare the _EPROCESS.UniqueProcessId value with the one we get from _getpid() for example.

0: kd> dt nt!_EPROCESS UniqueProcessId
   +0x440 UniqueProcessId : Ptr64 Void

While iterating we also store the pid of a privileged process in this case winlogon. The ExpPoolQuotaCookie becomes critical to our goal of the arbitrary decrement, I’ll break it down in the next section.


Arbitrary Decrementation for LPE - Setup

The whole idea of the decrementation of an arbitrary value allowing for LPE stems from Tarjei Mandt’s paper Kernel Pool Exploitation on Windows 7. During that time you could set the ProcessBilled within a POOL_HEADER and then set the PoolQuota flag (0x8) within the PoolType (with an overflow like ours), trigger a free of said chunk which then led to the execution of the following logic:

*(ProcessBilled->QuotaBlock) -= BlockSize*0x10

The ProcessBilled was expected to be a _EPROCESS structure.

0: kd> dt nt!_EPROCESS QuotaBlock
   +0x568 QuotaBlock : Ptr64 _EPROCESS_QUOTA_BLOCK

However things changed, and now before dereferencing the ProcessBilled pointer in this situation, it expects the value to be the result of following equation:

ProcessBilled = _EPROCESS/_KPROCESS ^ ExpPoolQuotaCookie ^ ChunkAddr

_EPROCESS/_KPROCESS start addresses can be used interchangebly as _KPROCESS is the first element within _EPROCESS.

0: kd> dt nt!_EPROCESS 
   +0x000 Pcb              : _KPROCESS

Hence when writing the ProcessBilled pointer to a fake _EPROCESS structure we need to take this calculation into account and write the value accordingly. The ChunkAddr and the ExpPoolQuotaCookie is already known to us, the _EPROCESS address is the one we want to point to our fake _EPROCESS

Also these fake _EPROCESSs have to reside in kernel address space. How do we get them into there and where can we retrieve the address of said allocation? Meet Windows Kernel Hackers favorite PipeQueueEntry, first publicly discussed to my knowledge by Alex Ionescu in Sheep Year Kernel Heap Fengshui: Spraying in the Big Kids’ Pool.

It gives us great control over the data within the allocation and also of the size of the allocation. We can just use WriteFile into the GhostChunks pipe with two faked _EPROCESS structures in one go, we need one _EPROCESS for each decrement we want to achieve. Why we need two will become clear within the next sections, don’t worry.

Remember the leaked RootPipeQueueEntry when leaking the GhostChunk Attributes? This pointer holds the address of the PipeQueueEntry containing our two freshly faked _EPROCESS structures (at an offset)


Arbitrary Decrementation for LPE - Action

So the POOL_HEADER struct looks like this:

struct POOL_HEADER
{
    char PreviousSize ;
    char PoolIndex ;
    char BlockSize ;
    char PoolType ;
    int PoolTag ;
    Ptr64 ProcessBilled ;
};

This time we need to write the ProcessBilled pointer within our ghost chunk, as discussed. And to control this value we again, free and reallocate the object in the original space of the vulnerable object, with the data part of the attribute overwriting the original values of the Ghust Chunks POOL_HEADER.

In the end we want to modify the EPROCESS.Token.Present and EPROCESS.Token.Enabled within the EPROCESS structure of our own process, the exploit exe.

struct _TOKEN
{
    struct _TOKEN_SOURCE TokenSource;                                       //0x0
    struct _LUID TokenId;                                                   //0x10
    struct _LUID AuthenticationId;                                          //0x18
    struct _LUID ParentTokenId;                                             //0x20
    union _LARGE_INTEGER ExpirationTime;                                    //0x28
    struct _ERESOURCE* TokenLock;                                           //0x30
    struct _LUID ModifiedId;                                                //0x38
    struct _SEP_TOKEN_PRIVILEGES Privileges;                                //0x40
struct _SEP_TOKEN_PRIVILEGES
{
    ULONGLONG Present;                                                      //0x0
    ULONGLONG Enabled;                                                      //0x8
    ULONGLONG EnabledByDefault;                                             //0x10
}; 

In pseudo code we want to do:

decrement(OurEprocess.Token.Present);
decrement(OurEprocess.Token.Enabled);

The Enabled and Present fields can be thought of as bitmaps which represent the privileges the process has or can achieve. Through modifying it we raise the privileges of our process and can later on acquire the SeDebugPrivilege privilege to inject shellcode into a process with High Integrity.

Now freeing the ghost chunk leads to the first decrementation.


Second Decrementation

For the second decrement we just have to do the same steps more or less again:

  1. Allocate Ghost Chunk
  2. Free & Reallocate the object within the space of the original vulnerable object to rewrite the Ghost Chunks POOL_HEADER
  3. Free Ghost Chunk to trigger the decrementation of the Present field within our processes’ EPROCESS.Token structure.

The last step is to acquire SeDebugPrivilege with common winapi functions and inject into a High Integrity process. The injected shellcode then just continues to spawn a cmd.exe as SYSTEM user for demonstration purposes. If you ever done some process injection, it’s pretty much that.

As some final notes about this or any exploitation technique, for me it is crucial to take this step-by-step. Not getting discouraged by all the further steps, the potentially unfavourable amount of stars which might have to be aligned to make this a success. Also the most important thing here is to understand the step you are doing currently and how to confirm that it does work before moving on, where do I set breakpoints, where do I use getchar() within my exploit to not continue directly, what do I need to confirm within memory.

Exploit in Action

Below you can see the exploit in action, it fails the first two times but finally succeeds and we are able to pop a SYSTEM shell. I decided to use that video instead of trying to record a perfect run as (at least my version) does rely a bit on luck to get all the spraying right ;)

You can find the final exploit here

Steps Summarized

  1. Drain and spray objects of same size as vulnerable object
  2. Free some objects (ever third for example) to create holes for the vulnerable object
  3. Trigger bug to overwrite adjacent pool header
  4. Respray to reallocate into vuln objects space with fake pool header at correct offset
  5. Enable Dynamic Lookaside list
  6. Free pipes from step 1 to make use of fake pool header and create a free object in the Dynamic lookaside list
  7. Allocate ghost chunk (now overlapping with one pipe attribute from step 4)
  8. Leak Ghost Chunk Data (through iterating over all pipe attributes from step 4)
  9. Create fake userland PipeAttribute
  10. Free and reallocate of the leaking chunk found in step 8. Let GhostChunkPipeAttribute.Next point to the fake userland PipeAttribute of step 9.
  11. Use now setup arbitrary read to get the kernel base and the cookie value for the ProcessBilled
  12. Use arbitrary read to get kernel addr of our EPROCESS and the one of a privileged process
  13. Setup fake EPROCESSs (two) which are used for the decrement.
  14. Setup free and respray of the overlapping pipe attribute to rewrite POOL_HEADER.ProcessBilled within the Ghost Chunk
  15. Free Ghost Chunk to trigger the decrementation
  16. Goto 14 again for second decrement.
  17. Raise privileges through winapi to SeDebugPrivilege and inject into High Integrity process for cmd.exe as SYSTEM

Reliability

I haven’t measured the overall reliablity of the exploit in a detailed manner. Sometimes it works first try (measured after clear boot) and more than ten times in a row, sometimes you need more than five (as stated, maybe even more) attempts to get one successful run. Around every ten to twenenty attempts we run into a BSOD. For this time, it was OK for me. Next time I’ll sure try to improve on this score but for this exercise the main goal was getting a somehow stable exploit done.

Further Improvement Ideas

  • Paged Pool layout
  • Amount of object spraying
  • Testing which sizes work best (are used less by regular OS activity)
  • Better cleanup to avoid BSODs

Getting Started

If you want to get started with Windows Kernel Exploitation, here is my suggestion:

  1. Don’t be scared, it’s easier than one might think.

  2. Read Windows Drivers Reverse Engineering Methodology
  3. Setup two VMs Windows 10/11, 1 2
  4. Find/See Vulnerabilities “easy mode” -> Write Exploits:
    • Exploit the Stack Overflow of HEVD with a tutorial
    • Exploit the Arbitrary Read/Write of HEVD with a tutorial
    • Exploit Arbitrary Read/Write within dbutil_2_3.sys (giyf)
  5. Do the courses Debuggers 3011: Advanced WinDbg and Exploitation 4011: Windows Kernel Exploitation: Race Condition + UAF in KTM

While doing the above, the more you do on your own and don’t rely on writeups the more skilled you will get. It’s more time consuming and can be a toll, but it’s totally worth it!

Thanks

In no particular order :P

  • Corentin Bayet, Paul Fariello and Synacktiv for the research and publication of the exploitation technique.
  • My employer CODE WHITE who paid for the Advanced Windows Exploitation training which set me off for up2date Windows exploitation.
  • Cedric Halbronn for the awesome Windows Kernel Exploitation: Race Condition + UAF in KTM course.
  • Friends and Colleagues who listened to my explanations even when there was no graphical illustration ;)
  • Everyone who helped push the public knowledge of Windows (Kernel) exploitation <3
  • You who took the time and read the blog post

<
Previous Post
Windows Kernel Pool Exploitation CVE-2021-31956 - Part 1
>
Blog Archive
Archive of all previous blog posts